| # Trace Processor `report` Subcommand |
| |
| **Authors:** @lalitm |
| |
| **Status:** Draft |
| |
| > **Note:** This document is a thought experiment exploring a possible future |
| > direction for trace_processor_shell. It is NOT a proposal for immediate |
| > implementation. The goal is to capture the design space and solicit feedback. |
| |
| ## Motivation |
| |
| Perfetto traces are rich, multi-dimensional datasets. Today, extracting a |
| useful summary requires either: |
| |
| 1. Loading the trace in the UI and clicking around. |
| 2. Writing ad-hoc SQL queries against trace_processor. |
| 3. Authoring `TraceSummarySpec` textprotos for the `summarize` subcommand. |
| |
| None of these serve the "I just collected a trace, what's in it?" use case |
| well. Users coming from `perf report` expect to point a tool at a data file |
| and immediately see an opinionated, useful summary: no query authoring, no |
| spec files, no UI. |
| |
| This gap is especially felt by: |
| |
| - **CLI power users** who want quick triage without leaving the terminal. |
| - **AI tools** that need structured trace summaries to reason about |
| performance. |
| - **CI pipelines** that want a human-readable (or machine-parseable) trace |
| summary as a build artifact, and to compare traces across builds. |
| |
| ## Decision |
| |
| Pending |
| |
| ## Design |
| |
| ### Relationship to `summarize` |
| |
| The two subcommands split by who decides what is computed: |
| |
| - **`summarize`** is for summaries the user asks for themselves: they |
| author `TraceSummarySpec` protos defining exactly what to compute. |
| - **`report`** is opinionated summaries we give the user: built-in views |
| over the common trace dimensions, with no authoring required. |
| |
| Under the hood both are PerfettoSQL. `summarize` converts query results to |
| protos as directed by the user's spec, with typed schemas and semantics |
| such as higher/lower-is-better; `report` is a presentation layer over the |
| PerfettoSQL standard library, where each view is computed by curated |
| stdlib queries and rendered from a hand-written proto schema |
| (`report.proto`, one message per view). Two things follow: |
| |
| - **Typed output from a fixed proto.** `summarize` derives its output |
| schema from the user's spec because the computation is arbitrary; |
| `report`'s views are a closed, compile-time set, so a hand-written proto |
| gives stable text/JSON output directly. |
| - **Report improvements land in the stdlib.** Every gap a report exposes is |
| fixed in a module all SQL users benefit from, and the queries shown in |
| the SQL drill-down hints are the same ones the tool runs. |
| |
| ### CLI surface |
| |
| ``` |
| tp report [<noun> [<view>]] [FLAGS] (<trace_file> | --remote <addr>) |
| ``` |
| |
| The first positional is always a noun (what data you are looking at); the |
| second is always a view (how to see it), from a closed per-noun set with a |
| default: |
| |
| | Noun | Views (default first) | |
| | --------------- | -------------------------------------------- | |
| | (none) | overview | |
| | `processes` | `table` | |
| | `tracks` | `table` | |
| | `slices` | `table`, `histogram`, `timeline`, `inspect` | |
| | `stack-samples` | `functions`, `top-down`, `bottom-up` | |
| | `heap-profile` | `functions`, `top-down`, `bottom-up` | |
| | `heap-dump` | `types` | |
| | `scheduling` | `summary`, `timeline`, `inspect` | |
| | `counters` | `list`, `timeline`, `histogram`, `inspect` | |
| |
| ``` |
| tp report trace.pftrace # overview |
| tp report stack-samples top-down trace.pftrace |
| tp report slices histogram --name doFrame --remote calm-blue-otter |
| tp report scheduling inspect --thread RenderThread --ts 2.734s trace.pftrace |
| tp report slices --baseline base.pftrace target.pftrace # diff two traces |
| ``` |
| |
| With no noun, the overview covers every noun that has data in the trace, |
| skipping the rest. `processes`, `tracks`, and `counters list` exist for |
| structural discovery: they enumerate the names that the scoping flags and |
| the other views take as input, so a user can orient in an unfamiliar trace |
| before drilling down. Output shape is statically known per (noun, view). |
| |
| The nouns are deliberately generic trace primitives, applicable to any |
| trace regardless of origin. Domain-specific reporting (e.g. Android frame |
| timeline, ANRs, battery) is out of scope for now; it may be added in the |
| future. |
| |
| #### Output format flags |
| |
| ``` |
| --format text|json Output format (default: text). |
| ``` |
| |
| - **`text`**: human-readable tables, similar to `perf report --stdio`. |
| - **`json`**: one stable schema per (noun, view), generated from |
| `report.proto`, with the scope context embedded and richer stats than |
| text (e.g. p50/p95/p99 where text shows min/avg/max). Text output is |
| explicitly not a stable interface; JSON is. |
| |
| ``` |
| --show-sql Print the stdlib queries the view executed. |
| ``` |
| |
| `--show-sql` is what makes the SQL escape hatch concrete: the printed |
| queries are the ones the view ran, and a starting point for going beyond |
| what `report` offers. |
| |
| #### Scoping flags |
| |
| These filter the report to a subset of the trace data: |
| |
| ``` |
| --pid <pid> Scope to a specific process ID. |
| --process <name> Scope to a process by name. |
| --tid <tid> Scope to a specific thread ID. |
| --thread <name> Scope to a thread by name. |
| --track <name> Scope to a track by name. |
| --cpu <cpu> Scope to a specific CPU. |
| --time <start>,<end> Scope to a time range. Accepts raw nanoseconds or |
| human-friendly format (e.g. 2.7s,3.1s). |
| --ts <ts> Scope to a point in time (same formats as --time). |
| Used by the inspect views. |
| ``` |
| |
| Scoping flags are translated into structured filters on the underlying |
| stdlib queries. Every invocation carries its full scope in flags; there is |
| no sticky state, so commands are reproducible, CI-safe, and safe for |
| concurrent clients of one warm session. |
| |
| #### Filtering and aggregation control |
| |
| ``` |
| --name <glob> Row filter, same meaning in every view. In |
| bottom-up, rows are roots, so the callers of |
| function X are: stack-samples bottom-up --name X. |
| Single-series views (histogram, timeline, inspect |
| on slices/counters) require the glob to match |
| exactly one distinct name; if it matches several, |
| the command fails and lists them. Many instances |
| of one name are fine (see inspect). |
| --min-duration <dur> Hide rows below a duration threshold. |
| --min-count <N> Hide rows with fewer instances. |
| --sort <key> Sort key, enumerated per view (e.g. dur|count|max). |
| --top <N> Entries per table section (default: 10). |
| --max-lines <N> Node budget for tree views (default: 100). |
| --buckets <N> Bucket count for timeline/histogram views. |
| --group-by <key> Grouping key where a view supports one |
| (scheduling summary: thread (default) | process). |
| ``` |
| |
| #### Warm sessions |
| |
| ``` |
| --remote <addr> Run against a warm session (see RFC 0031); |
| mutually exclusive with a trace path. |
| ``` |
| |
| RFC 0031's `--remote` subcommand list grows `report`. The interactive |
| drill-down loop (overview -> view -> inspect) is a rapid sequence of |
| invocations against one trace, the pattern warm sessions exist to serve. |
| |
| ### Overview output |
| |
| When invoked without a noun, the overview produces a one-line trace context |
| followed by per-noun highlights, ending with drill-down hints. The overview |
| accepts the scoping flags and `--top` (applied per section); `--name` and |
| `--sort` are rejected, since the sections have disjoint name spaces and |
| sort keys. |
| |
| Example (`--format text`): |
| |
| ``` |
| [trace.pftrace | full trace | 12.3s | Android 14 | Pixel 7 Pro | 12 processes] |
| |
| Slices (12.3M total): |
| Name Count Total dur % of trace Max dur |
| Choreographer#doFrame 83.2k 4.1s 33.2% 128ms @ 2.734s (RenderThread) |
| DrawFrame 83.1k 3.5s 28.4% 96ms @ 5.101s (RenderThread) |
| measure 41.6k 890ms 7.2% 42ms @ 8.913s (main) |
| ... |
| * 210 more slice names below --top 10; rerun with --top 50 |
| |
| Stack Samples (3.2k total): |
| Function Self% Total% Samples |
| art::Thread::RunRootClock 18.2% 42.1% 583 |
| __epoll_pwait 12.1% 12.1% 387 |
| ... |
| |
| Scheduling: |
| Thread CPU time Runnable Sleeping % of trace |
| RenderThread 3.2s 120ms 8.9s 26.0% |
| mali-cmar-backe 1.8s 45ms 10.4s 14.6% |
| ... |
| |
| Heap Profile: (not present in trace) |
| Heap Dump: (not present in trace) |
| |
| Next: tp report processes trace.pftrace |
| tp report slices trace.pftrace --top 50 |
| tp report slices inspect --name "Choreographer#doFrame" --ts 2.734s --thread RenderThread trace.pftrace |
| ``` |
| |
| ### View semantics |
| |
| #### Tree views |
| |
| The tree views (`top-down`, `bottom-up`) are never dumped in full. Nodes |
| are included best-first under the `--max-lines` budget, scored by total% |
| with a depth decay (internal, not user-configurable). Single-child chains |
| are collapsed to the same indent, and pruned subtrees are summarized: |
| |
| ``` |
| └─ ... (5 more children: combined 12.3%, max 4.1%) |
| ``` |
| |
| `functions` and the tree views show both self and total, with self |
| displayed more prominently. |
| |
| #### `timeline` and `histogram` |
| |
| `timeline` buckets the trace (or the `--time` range) and renders one line |
| per bucket: an ASCII bar, the value, and the dominant entries in that |
| bucket. `scheduling timeline` needs no selection (CPU load per bucket, |
| plus the threads that dominate each bucket); `slices timeline` and |
| `counters timeline` take a `--name` and show |
| count and total duration, or average value, per bucket. |
| |
| `histogram` renders the distribution of a single series: durations of a |
| slice name, or values of a counter. The output labels its scale (linear or |
| logarithmic) so bucket widths are not misread; JSON carries percentile |
| stats alongside the buckets. |
| |
| #### `inspect` |
| |
| `inspect` drills from a noun's aggregate into instances. The noun's normal |
| filters select the entity; scoping flags narrow which instances match. The |
| output shape is fixed per noun: aggregate stats over the matched instances |
| plus the instances themselves, rendered in full detail when exactly one |
| matches. `--name` must resolve to one distinct name (usually copy-pasted |
| from report output); many instances of that name are not an error: the |
| output is a wider instance table plus a hint on how to narrow. |
| |
| - `slices inspect --name <n>`: instance count, duration percentiles, top |
| instances by duration (each carrying its ts/thread for narrowing). A |
| unique match shows dur, self dur, args, parent chain, and children. |
| - `counters inspect --name <n>`: value at `--ts`, plus min/avg/max and |
| delta over the scoped range. |
| - `scheduling inspect --thread <t> --ts <ts>`: state, CPU, end state, |
| waker thread, and wakeup latency: "why was this thread not running at T". |
| |
| Adding an inspect view to a noun must pass a two-part test: (1) instances |
| are addressable by name or coordinates, never tool-emitted ids; (2) it |
| answers an instance-level question the aggregate views do not. |
| |
| **Coverage gap:** `stack-samples`, `heap-profile`, and `heap-dump` have no |
| inspect view yet. A per-source function inspect (callers/callees of one |
| function) is tractable under this grammar, since the noun fixes the data |
| source, but is left as future work; heap-graph objects remain id-only. |
| Until then, the tree views plus `--name` and the SQL hints cover these. |
| |
| ### Diffing |
| |
| Comparing two traces of the same workload (before/after a change, across |
| builds in CI) is an explicit goal. Any table view, and the overview, |
| accepts: |
| |
| ``` |
| --baseline <trace|addr> Compare against a baseline trace. Accepts a path |
| or a warm-session address (as with --remote). |
| ``` |
| |
| Diff is a presentation-layer join: the view's queries run independently on |
| each trace, and the two typed results are joined on the view's aggregation |
| key (slice name, function, thread, process, track, or counter name) in the |
| shell. Neither engine ever sees both traces; multi-trace queries are out |
| of scope, both because they are very hard to reason about and because |
| sessions are single-trace (RFC 0031). Because the two runs are |
| independent, target and baseline may each be a path or a warm-session |
| address, in any combination. |
| |
| Semantics: |
| |
| - Value columns become Baseline / Target / Delta with signed formatting. |
| Default sort is the duration delta; `--sort` selects others (e.g. count |
| delta). Rows present on only one side are marked added/removed and rank |
| by absolute delta. |
| - Deltas are raw values. The context header shows both trace durations and |
| a footnote warns when they differ materially; `report` does not attempt |
| to normalize for workload size and makes no claim of statistical |
| significance. |
| - The join is by exact name. Canonicalizing names that embed dynamic |
| values (tids, addresses) is an open question. |
| - Scoping flags apply to both traces identically. |
| |
| ``` |
| [target.pftrace 10.1s | baseline base.pftrace 12.3s | full trace] |
| |
| Slices (vs base.pftrace): |
| Name Baseline dur Target dur Delta |
| Choreographer#doFrame 4.1s 5.0s +900ms |
| inflate 95ms 2ms -93ms |
| BitmapDecode (absent) 310ms +310ms |
| * baseline is 22% longer than target; deltas are not normalized |
| ``` |
| |
| `tp report --baseline base.pftrace target.pftrace` diffs the overview: a |
| per-noun digest of the biggest movements, the intended CI build artifact. |
| Tree and single-series views do not accept `--baseline` initially; |
| structural tree diffs need their own design (see open questions). A flag |
| is easier to miss than a subcommand, so `--baseline` is included in the |
| `report` help text's example block (as above). |
| |
| ### Per-view column definitions |
| |
| #### Slices `table` |
| |
| Aggregation key: slice name. |
| |
| | Column | Description | |
| | ---------- | ------------------------------------------------------- | |
| | Name | Slice name | |
| | Count | Number of instances | |
| | Total dur | Sum of wall durations across all instances | |
| | % of trace | Total duration as percentage of trace duration | |
| | Max dur | Maximum single-instance duration, with the instance's | |
| | | ts/thread (outlier detection, feeds the inspect hint) | |
| |
| The noun covers sync and async slice tracks; instants (zero duration) |
| contribute to Count only. Durations sum per instance without overlap |
| deduplication, so % of trace can exceed 100 on async-heavy traces. |
| |
| #### `stack-samples functions` |
| |
| | Column | Description | |
| | -------- | ------------------------------------------------------- | |
| | Function | Function name (symbol) | |
| | Self% | Samples where this function is at the top of the stack | |
| | Total% | Samples where this function appears anywhere in stack | |
| | Samples | Absolute sample count | |
| |
| #### `heap-profile functions` |
| |
| | Column | Description | |
| | ----------- | ---------------------------------------------------- | |
| | Function | Allocation site / function | |
| | Self bytes | Bytes allocated directly by this function | |
| | Total bytes | Bytes allocated by this function and its callees | |
| | Count | Number of allocations | |
| | Avg size | Average allocation size | |
| |
| #### `heap-dump types` |
| |
| | Column | Description | |
| | ------------- | -------------------------------------------------- | |
| | Type | Type or allocator | |
| | Retained size | Total retained memory | |
| | Live objects | Count of live objects | |
| |
| Retained size needs a dominator-tree computation and the Total%/Total |
| bytes columns need callstack ancestry walks; these are the most expensive |
| views to compute, and a further reason `report` benefits from warm |
| sessions. |
| |
| #### `scheduling summary` |
| |
| | Column | Description | |
| | ---------- | ----------------------------------------------------- | |
| | Name | Thread (or process, with --group-by process) | |
| | CPU time | Total time spent running on a CPU | |
| | Runnable | Total time in runnable state (waiting for CPU) | |
| | Sleeping | Total time sleeping | |
| | % of trace | CPU time as percentage of trace duration | |
| |
| #### `processes` / `tracks` / `counters list` |
| |
| Discovery tables: Process, PID, CPU time, % of trace, top threads; Track, |
| Type, Event count, Process/Thread; Counter name, Track/Process, Sample |
| count, Min, Avg, Max. |
| |
| ### Output contract |
| |
| Every view emits: |
| |
| 1. **A context header** echoing the effective scope, derived purely from |
| the invocation (see the overview example). |
| 2. **Truncation footnotes**: what was hidden and the flag to reveal it. |
| 3. **Next-command hints**: complete, copy-pasteable drill-down invocations |
| (including `--remote` and scope flags), including `tp query` commands |
| for raw data. The intended progression is: overview -> aggregate view |
| -> inspect -> SQL. |
| 4. **Terminal-aware tables**: columns are width-fitted when stdout is a |
| TTY; piped or redirected output is untruncated. |
| 5. **Errors fail fast**: an unknown noun/view combination, a missing |
| required flag (e.g. `--ts` for `scheduling inspect`), or a malformed |
| value exits non-zero with a message listing the valid options; with |
| `--format json` the error is a structured object. |
| |
| ### Testing |
| |
| Text output is locked by trace processor diff tests: one golden per |
| (noun, view) over reference traces, so any output change is explicit in |
| review and documented behavior cannot drift from shipped behavior. JSON is |
| validated against `report.proto` by construction. |
| |
| ## Alternatives considered |
| |
| ### Build on the summarize machinery |
| |
| Pro: |
| |
| - Reuses an existing declarative engine; specs shareable with `summarize`. |
| |
| Con: |
| |
| - Tree views have no representation in the flat-row summary output. |
| - Scoping flags rewrite the computation per invocation, so the |
| "pre-authored spec" is really a template rewritten per invocation. |
| - Typed output does not require the engine: a fixed proto provides it. |
| |
| ### Alternative grammars |
| |
| Two other grammars were drafted: a `--view` flag on a noun positional, and |
| view-first positionals with data-selection flags (`top-down |
| --heap-profile`). Both rejected: noun-then-view matches how users approach |
| a trace (pick the data, then the presentation) and how comparable |
| heterogeneous profiling tools have converged, keeps each view a closed |
| per-noun set, and has in-repo precedent (`tp server http`). |
| |
| ### Sticky server-side session state |
| |
| Holding analysis state (a selected thread, zoom/filter stacks) in a |
| long-lived server makes interactive drill-down terse, but single |
| invocations become non-reproducible and concurrent clients unsafe. Warm |
| sessions (RFC 0031) already give the real benefit, parse-once performance, |
| so `report` keeps all state in flags. |
| |
| ### Id-based inspect addressing |
| |
| `inspect slice:187432` style handles: rejected. Ids are only knowable if |
| the tool emitted them and are unstable across trace loads and versions; |
| names and coordinates (thread, ts) are legible from any output. Id-precision |
| lookups remain available via the SQL bridge. |
| |
| ### A `--focus <function>` stack filter |
| |
| A pprof-style focus transform (keep stacks containing X, re-root per view): |
| rejected as a second drill-down mechanism with subtle semantics (recursion |
| double-counting, per-view re-rooting). `bottom-up --name` covers the |
| callers workflow; re-rooting is left as an open question. |
| |
| ### A separate `diff` subcommand |
| |
| `tp report diff <base> <target>` was considered. Rejected: it would need to |
| replicate every noun/view under itself, whereas `--baseline` composes with |
| the existing views and the overview for free, and inherits scoping flags. |
| |
| ## Open questions |
| |
| - **Domain-specific reports:** Android frame timeline, ANRs, battery and |
| similar have stdlib modules but are out of scope for now; they may be |
| added in the future. |
| - **Inspect for the callstack/heap nouns:** per-source function inspect |
| (callers/callees) and heap-graph object drill-down (see coverage gap). |
| - **Tree diffs:** delta-annotated call trees under `--baseline`; the |
| rendering and matching rules need their own design. |
| - **Per-trace time windows in diffs:** `--time` applies to both traces; |
| comparing different windows (e.g. startup vs startup at different |
| offsets) may need a `--baseline-time`. |
| - **Diff join canonicalization:** names embedding dynamic values (tids, |
| addresses) break the exact-name join into spurious added/removed pairs. |
| - **Diff noise:** accepting multiple baseline traces (min/median/max) to |
| separate real movement from run-to-run variance. |
| - **`--root <function>` on tree views:** pure re-rooting for "what does X |
| call", if hop-wise navigation proves insufficient. |
| - **A heap-dump retainers view:** retaining paths for a type. |
| - **Cross-sectional view:** "what was happening at time T" across all |
| nouns (the CLI analog of a vertical line in the UI). |
| - **JSON schema discovery:** how consumers find the per-view schemas |
| (help section vs a dedicated subcommand). |