blob: 5de0837ee7cedb2fd273523996693ffe1dd127e1 [file] [view] [edit]
# Cookbook: Linux Tracing Recipes
This page collects **end-to-end recipes** for profiling and tracing your own
programs on Linux: how to build so that traces can be symbolized, how to record
the most common kinds of trace, and how to turn raw addresses into function
names afterwards.
It is aimed at developers profiling native binaries on a Linux host or embedded
Linux target (including Yocto, QNX and similar). Each recipe is self-contained
and includes the commands to download the tools it needs. For the full reference
on each topic, follow the links into the deeper guides:
- [Recording system traces](/docs/getting-started/system-tracing.md)
- [CPU profiling and perf counters](/docs/getting-started/cpu-profiling.md)
- [Native heap profiling](/docs/data-sources/native-heap-profiler.md)
- [Kernel function graph tracing](/docs/data-sources/funcgraph.md)
- [Symbolization and deobfuscation](/docs/learning-more/symbolization.md)
## Setup: tools and permissions {#setup}
Two tools cover everything on this page. Both are single self-contained
downloads:
- **`tracebox`**: the recording engine. It bundles `traced`, `traced_probes` and
all data source implementations into one statically linked binary.
```bash
curl -LO https://get.perfetto.dev/tracebox
chmod +x tracebox
```
- **`traceconv`**: a host-side toolkit for converting and, importantly here,
symbolizing traces. It is a thin Python wrapper that downloads the right native
binary for your platform on first use.
```bash
curl -LO https://get.perfetto.dev/traceconv
chmod +x traceconv
```
Recording from ftrace and `perf_event_open` needs elevated privileges. The
simplest option is to run `tracebox` as root (`sudo ./tracebox ...`).
Alternatively, grant the specific permissions once per boot:
```bash
# ftrace-based data sources (scheduling, function_graph, ...).
sudo chown -R $USER /sys/kernel/tracing
# perf / callstack sampling (linux.perf).
echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid
# Resolve KERNEL symbol names (kallsyms). Needed for kernel frames in
# callstacks and for the function_graph recipe below.
echo 0 | sudo tee /proc/sys/kernel/kptr_restrict
```
## Building binaries Perfetto can symbolize {#building-with-symbols}
Perfetto records raw instruction **addresses** for native callstacks (from the
CPU profiler and the heap profiler). To turn those addresses into function names,
files and line numbers, the host doing the symbolization needs unstripped ELF
binaries whose **Build ID matches** the ones that ran on the target. Do this
before you record.
**1. Compile with debug info.** Add `-g`. This does not change the generated
code, only the DWARF debug info attached to it:
```bash
gcc -g -O2 -o myapp myapp.c # or clang, same flags
```
`-O2 -g` is the recommended combination for profiling: optimised code (so you
profile what you actually ship) with enough debug info to map addresses back to
source lines.
**2. Keep a Build ID.** Modern toolchains emit a GNU Build ID by default. Confirm
it with:
```bash
readelf -n ./myapp | grep -A1 'Build ID'
```
The Build ID is how Perfetto matches a binary on disk to the mapping recorded in
the trace. Two different builds have different Build IDs, and Perfetto will
refuse to apply mismatched symbols (this is a feature, it prevents wrong
symbolization).
**3. (Optional) Ship stripped, keep the symbols.** You do not need to deploy
debug info to the target. Split it into a sidecar file and strip the deployed
binary; matching is by Build ID, so the filenames need not line up:
```bash
gcc -g -O2 -o myapp myapp.c
# Split debug info into a sidecar, linked back by Build ID.
objcopy --only-keep-debug myapp myapp.debug
objcopy --strip-debug --add-gnu-debuglink=myapp.debug myapp
# Deploy the small, stripped `myapp` to the target.
# Keep `myapp.debug` (or the original unstripped binary) on your host.
```
On distributions that package debug info separately (`-dbg` / `-dbgsym` /
`debuginfo` packages), installing those on your host gives the same result:
unstripped symbols under `/usr/lib/debug`.
## Recipe: CPU profiling with full symbols {#cpu-profiling}
Goal: a flamegraph of where a process spends CPU time, with real function names.
This is the end-to-end version of the
[CPU profiling guide](/docs/getting-started/cpu-profiling.md).
**1. Build with symbols** as described [above](#building-with-symbols).
**2. Write a config.** This samples callstacks 100 times per second per CPU,
unwinding only when your process is on-CPU, and adds scheduling context. Save it
as `cpu.cfg` and change `target_cmdline` to a substring of your process name:
```protobuf
duration_ms: 10000
buffers {
size_kb: 65536
fill_policy: DISCARD
}
# Periodic callstack sampling, scoped to one process.
data_sources {
config {
name: "linux.perf"
perf_event_config {
timebase {
counter: SW_CPU_CLOCK
frequency: 100
timestamp_clock: PERF_CLOCK_MONOTONIC
}
callstack_sampling {
scope {
target_cmdline: "myapp"
}
# Also unwind into the kernel. Needs kptr_restrict lowered (see Setup).
kernel_frames: true
}
}
}
}
# Scheduling context on the same timeline.
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "sched/sched_waking"
}
}
}
# Process and thread names.
data_sources {
config {
name: "linux.process_stats"
process_stats_config {
scan_all_processes_on_start: true
}
}
}
```
**3. Record** (see [Setup](#setup) for the `tracebox` download and permissions):
```bash
sudo ./tracebox -c cpu.cfg --txt -o /tmp/trace.pftrace
```
At this point **kernel** frames are already symbolized (resolved on-device from
kallsyms), but **userspace** frames are still raw addresses.
**4. Bake in userspace symbols** with `traceconv bundle`. It auto-discovers the
binaries that were loaded (using the absolute paths recorded in the trace, which
works well when you profiled on the same machine), and writes a single
self-contained trace:
```bash
# llvm-symbolizer must be on $PATH, e.g. `sudo apt install llvm`.
./traceconv bundle /tmp/trace.pftrace /tmp/trace.bundle
```
If your symbols live elsewhere (a build host, a `.debug` directory, an embedded
sysroot), point `bundle` at them:
```bash
./traceconv bundle \
--symbol-paths /path/to/sysroot/usr/lib/debug,/path/to/build/out \
--verbose \
/tmp/trace.pftrace /tmp/trace.bundle
```
**5. View.** Open `/tmp/trace.bundle` in the
[Perfetto UI](https://ui.perfetto.dev); select a time range over the samples to
get a flamegraph. The Build-ID lookup order and "could not find library"
troubleshooting are documented in the
[symbolization guide](/docs/learning-more/symbolization.md#callstacks).
To instead produce aggregated [pprof](https://github.com/google/pprof) profiles:
```bash
./traceconv profile --perf /tmp/trace.pftrace
```
## Recipe: Native heap (memory) profiling {#heap-profiling}
Goal: see which callstacks allocated the most native (malloc) memory. On Linux
the `heap_profile` helper drives this end to end, including launching your binary
with the profiler preloaded.
Download the helper script:
```bash
curl -LO https://raw.githubusercontent.com/google/perfetto/main/tools/heap_profile
chmod +x heap_profile
```
Run your binary under it:
```bash
python3 heap_profile host -- ./myapp --some-flag
```
The script auto-downloads `tracebox` and the `libheapprofd_glibc_preload.so`
preload library, runs your binary with heapprofd attached, and on exit (or
`Ctrl-C`) writes a `raw-trace` plus per-process pprof files to a `/tmp` directory
it prints. Because you profiled locally, the matching binaries are present, so
symbols resolve automatically.
Open the `raw-trace` file in the [Perfetto UI](https://ui.perfetto.dev) to see
the allocation flamegraph. See
[native heap profiler: Linux support](/docs/data-sources/native-heap-profiler.md#non-android-linux-support)
for the full set of options (custom preload libraries, sampling interval, etc.).
## Recipe: Kernel function graph tracing {#funcgraph}
Goal: see exactly which kernel functions ran, and for how long, as nested slices.
The single most common mistake is forgetting `symbolize_ksyms`, which leaves
every function as a hex address.
Save as `funcgraph.cfg` (this traces `__schedule` and everything it calls):
```protobuf
duration_ms: 10000
buffers {
size_kb: 65536
fill_policy: DISCARD
}
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
# Without this, functions show as hex addresses.
symbolize_ksyms: true
enable_function_graph: true
function_graph_roots: "__schedule"
function_graph_max_depth: 10
}
}
}
```
Record it (function graph drives the kernel tracer, so root is required):
```bash
sudo ./tracebox -c funcgraph.cfg --txt -o /tmp/funcgraph.pftrace
```
Open `/tmp/funcgraph.pftrace` in the UI; the calls appear as nested slices on a
per-thread `Funcgraph` track. See the dedicated
[function graph data source](/docs/data-sources/funcgraph.md) page for the kernel
requirements (`CONFIG_FUNCTION_GRAPH_TRACER`), the filtering options, and how the
calls are visualised. Note that, unlike the [CPU profile](#cpu-profiling) recipe,
these kernel symbols come from `symbolize_ksyms` and **cannot** be added later
with `traceconv bundle`.
## Recipe: Finding why a thread is blocked {#blocked-thread}
Goal: understand why a thread keeps getting descheduled (lock contention,
priority inversion, blocking syscalls).
On Linux the right tool is **callstack sampling triggered by scheduling events**:
use a `sched/sched_switch` (and `sched/sched_waking`) tracepoint as the perf
`timebase`, so you capture a callstack at the exact moment a thread blocks or is
woken. This is far more precise than time-based sampling for blockage analysis.
WARNING: The Android `blocked_function` field (from the
`sched/sched_blocked_reason` ftrace event used in the
[Android cookbook](/docs/getting-started/android-trace-analysis.md)) is an
Android kernel feature and is generally **not** present on upstream/desktop Linux
kernels. Use the callstack-sampling approach below instead.
Minimal config (save as `blocked.cfg`, adjust the `comm` filters to your
process). The tracepoint `filter` keeps the sampler from being overrun by
unrelated threads:
```protobuf
duration_ms: 10000
buffers {
size_kb: 102400
fill_policy: DISCARD
}
data_sources {
config {
name: "linux.perf"
perf_event_config {
timebase {
period: 1
tracepoint {
name: "sched/sched_switch"
filter: "prev_comm ~ \"*myapp*\" || next_comm ~ \"*myapp*\""
}
timestamp_clock: PERF_CLOCK_MONOTONIC
}
callstack_sampling {
kernel_frames: true
}
ring_buffer_pages: 2048
}
}
}
```
Record and symbolize exactly as in the [CPU profiling recipe](#cpu-profiling)
(`sudo ./tracebox -c blocked.cfg --txt -o ...`, then `./traceconv bundle ...`).
For a full worked example, including filtering on both `sched_switch` and
`sched_waking` and how to reason about the captured callstacks, see the
[scheduling blockages case study](/docs/case-studies/scheduling-blockages.md).