report SubcommandAuthors: @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.
Perfetto traces are rich, multi-dimensional datasets. Today, extracting a useful summary requires either:
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:
Pending
summarizeThe 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:
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.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.
--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.
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.
--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).
--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.
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
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 histogramtimeline 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.
inspectinspect 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.
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:
--sort selects others (e.g. count delta). Rows present on only one side are marked added/removed and rank by absolute delta.report does not attempt to normalize for workload size and makes no claim of statistical significance.[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).
tableAggregation 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 listDiscovery tables: Process, PID, CPU time, % of trace, top threads; Track, Type, Event count, Process/Thread; Counter name, Track/Process, Sample count, Min, Avg, Max.
Every view emits:
--remote and scope flags), including tp query commands for raw data. The intended progression is: overview -> aggregate view -> inspect -> SQL.--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.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.
Pro:
summarize.Con:
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).
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.
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.
--focus <function> stack filterA 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.
diff subcommandtp 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.
--baseline; the rendering and matching rules need their own design.--time applies to both traces; comparing different windows (e.g. startup vs startup at different offsets) may need a --baseline-time.--root <function> on tree views: pure re-rooting for “what does X call”, if hop-wise navigation proves insufficient.