Skip to content

feat(perf): interpret Android Perfetto traces into main-thread hangs at perf trace stop - #3926

Closed
thymikee wants to merge 1 commit into
perf/perf-trace-report-applefrom
perf/perf-trace-report-android
Closed

thymikee wants to merge 1 commit into
perf/perf-trace-report-applefrom
perf/perf-trace-report-android

Conversation

@thymikee

@thymikee thymikee commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

What

perf trace stop (Android Perfetto) now interprets the finished .perfetto-trace on the host and spreads the trace-analysis envelope onto the stop summary: hangs (start offset, duration, running/blocked split, rule-based nextSteps), hangCount, and notes — the same placement Apple's stop summary uses. Interpretation uses the host trace_processor tool (trace_processor_shell preferred on PATH); without it the capture result stays intact and the summary carries the typed unavailable envelope. Builds on the shared contracts shapes from #3925.

How

  • One fixed SQL statement owns the window rule: a hang is a maximal run of consecutive main-thread thread_state rows whose state is non-idle. Idle sleep (S) and unknown states close a window and never count toward duration or blocked time. Only Running counts as running; R, R+, D count as blocked. Windows below 250ms (the Time Profiler "Hangs" default, matching Apple's threshold) are filtered in SQL, so the threshold has exactly one site. trace_bounds rides along in the same CSV block for start offsets.
  • The main thread is resolved by thread JOIN process USING (upid) WHERE thread.tid = p.pid AND process.name = <package> — restart-safe, no arbitrary-pid scalar subquery.
  • The frame-timeline (freeze) pass is dropped: it produced no app rows on the API 35 emulator, carried no state split (it faked running = duration, blocked = 0), and double-counted rows overlapping a run. Symbol attribution stays simpleperf's job; Android hotspots are an empty list in the envelope.
  • The PerfTraceAnalysis envelope spreads onto the stop summary exactly like Apple's (notes: [frameNote, ...envelope.notes]), so hangs, hangCount, hotspots, and the unavailable reason/message/hint each appear once at summary level. There is no summary.traceAnalysis field, no snapshot mirror, and the formatter's only unavailable read is summary.available === false — shared with Apple.
  • Unexpected analyzer throws (e.g. a missing host binding) map to the typed trace-processor-failed reason instead of vanishing through .catch(() => undefined).
  • trace_processor is never bundled; runTraceProcessorQuery runs a temp SQL file through the host-kit command seam (mockable), with a fixed timeout and typed failure detail from stderr.
  • The analyzer loads behind a dynamic import so the Android mechanics closure stays at its merge-base budget.
  • Docs note the S semantics: idle covers lock/binder/GC waits (those stretches count as idle), and a busy hang with a brief S splits into two windows.

Live device evidence

At head e00de8e1 (current head ea1013b6 only appends the install hint to the unavailable CLI line; the quoted runs are unchanged by it): API 35 emulator (perfReport35, google_apis arm64), daemon started with trace_processor_shell on PATH, hang app inducing three 4s busy main-thread hangs at +3/+10/+17s.

Idle app (settled, traced while idle):

$ agent-device perf trace start --kind perfetto
Perf start: perfetto trace state=running
$ agent-device perf trace stop --kind perfetto --out live-idle-r3.perfetto-trace
Perf stop: perfetto trace state=stopped
/private/tmp/perf-report-android/live-idle-r3.perfetto-trace (1.4MB)
Trace frame health: dropped 0% (0/0 frames)

No Hangs ( section — the old rule reported a 20s false hang here.

Hang app (fresh relaunch, all three induced hangs inside the window):

$ agent-device perf trace stop --kind perfetto --out live-hang-r3.perfetto-trace
Perf stop: perfetto trace state=stopped
/private/tmp/perf-report-android/live-hang-r3.perfetto-trace (1.9MB)
Trace frame health: dropped 0% (0/0 frames)
Hangs (4):
- +3s 4s main (com.example.hangapp) - running 4s / blocked 1ms
- +10s 4s main (com.example.hangapp) - running 4s / blocked 1ms
- +17s 3s main (com.example.hangapp) - running 3s / blocked 0ms
- +20s 604ms main (com.example.hangapp) - running 604ms / blocked 0ms
Next steps:
- Run perf cpu profile around the interaction to attribute CPU work inside this window.

The three induced hangs come out as separate ~4s running-dominant windows. The 4th row is real scheduler behavior, not a merge artifact: the third spin loop's final stretch contains a short idle moment, so the window correctly splits at ~+20s (the old rule would have swallowed it into one trace-long run).

JSON shape at this head (idle-window stop): summary keys available, capture, frameHealth, hangCount, hangs, hotspots, notes, tracePath — the envelope spread, no summary.traceAnalysis, notes = [frame note, ...envelope notes].

Tests

  • The window rule is pinned directly: buildAndroidTraceHangsSql runs against an in-memory node:sqlite database shaped like Perfetto's process/thread/thread_state/trace_bounds, asserting that S closes and never stretches windows, only Running counts as running, and sub-threshold windows drop — so a reintroduced merged-window rule fails this test even though the committed CSVs are inputs, not SQL outputs, to it.
  • Two real fixtures, each the production SQL's own trace_processor (v57) CSV output over fresh captures on this emulator: perf-trace-processor-idle.csv (settled app -> bounds row only -> zero hangs) and perf-trace-processor-hangs.csv (three induced 4s hangs -> three separate windows, running-dominant, ~4000ms each). The old fixture/test that pinned the merged 18s run are replaced.
  • SQL text tests pin the window rule pieces (non-idle state set, LAG-based window start, join-based main-thread lookup, single threshold site, no frame-timeline table).
  • runTraceProcessorQuery coverage through the host-kit command seam (no production test seam): tool resolution order (trace_processor_shell preferred when both resolve), SQL file content, query args, timeout option, typed failure on non-zero exit / throw / setup failure (temp dir cleaned), malformed CSV tolerated while staying available.
  • Summary wiring: envelope spread onto the summary (available: hangs/hangCount once + notes; unavailable: available:false + typed reason), analyzer throw -> trace-processor-failed, non-trace modes unaffected.

Validation

pnpm check:affected --run --head ea1013b6f3612adf291655345179dfb350926047 -> passed (format, lint, typecheck, layering, di-seams, fallow, mcp-metadata, build, package, integration-node, vitest-related, repo guards; receipt exactCommit=true). Prior heads 48b0e1c2 and e00de8e1 were also gate-green; CI ran 19/19 green on 48b0e1c2 (an earlier batch there was cancelled, not failed).

Depends on #3925 (shared contracts shapes); restacked on the force-pushed base.

🤖 Generated with Claude Code

@thymikee
thymikee added this pull request to stack #3927 October 23, 2026 08:30
@thymikee
thymikee marked this pull request as ready for review October 23, 2026 08:31
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-23 18:14 UTC

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.59 MB 5.59 MB +4.6 kB
Package (unpacked) 5.59 MB 5.59 MB +4.6 kB
Package (download) 1.69 MB 1.69 MB +1.6 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 32.0 ms 31.8 ms -0.1 ms
CLI --help 65.4 ms 65.1 ms -0.2 ms

@thymikee
thymikee force-pushed the perf/perf-trace-report-android branch from 7199739 to 1776316 Compare October 23, 2026 09:27
@thymikee

Copy link
Copy Markdown
Member Author

Thanks for the work here. At 7199739 the hang detection reports a false, trace-long hang, so this needs a fix before merge.

The run CTE gap-merges every thread_state row of the main thread. Perfetto's thread_state is contiguous per thread and includes state S, the idle Looper sleep. So the gap between busy and idle periods stays near zero, and one run spans the whole time the main thread was observed. Any trace of an app seen for 250ms or more yields one hang about as long as the trace. For an idle app, running time is near zero and S counts as blocked, so the next step says to move waiting work off the main thread. The committed fixture shows this: one 18s run with 6s "blocked" in a 26s trace, and the live output shows one 20s hang. The fixture test asserts the merged 18s run, so it would still pass if a correct fix were reverted. Users would see a false hang on every Android perfetto trace stop when trace_processor is installed. The rule the code must satisfy: a hang is a contiguous window where the main thread does not go back to idle. Idle sleep (S) must close a window and never count toward duration or blocked time. Only Running counts as running, and R, R+ and D count as blocked. Please add one fixture where an idle main thread produces zero hangs, and one where the induced hangs come out as separate rows.

The only live run with the tool installed shows the merged window. No run shows an idle trace yielding zero hangs, or induced hangs reported as separate windows. After the fix, please run perf trace start/stop --kind perfetto on the emulator twice with trace_processor_shell on the daemon PATH. For an idle app, the output must have no Hangs ( section. For the hang app with K induced busy hangs, the output must show Hangs (K) with durations near the induced ones and running-dominant splits. Please paste both outputs.

Not blocking, and you can take or leave these: frame-timeline rows at line 224 carry no state split, yet buildAndroidTraceHangs sets running to the full duration and blocked to 0, so output claims a split that is unknown and always picks the CPU-profile step, and rows that overlap a run are counted twice, so drop that pass or list those freezes separately with perfHitchNextSteps; Android emits hangs three times (summary.hangs, summary.traceAnalysis.hangs, and the snapshot mirror) while Apple emits them once, so keep only summary.hangs and drop the mirror, the duplicate and the data.hangs fallback in output.ts; runTraceProcessorQuery has no test and only a test-only resolveTool parameter as a seam, so bind a fake host through bindAndroidAdbHost with a temp-dir script on PATH, assert trace-processor-failed and the parsed hangs, and delete resolveTool; the main-thread lookup uses a scalar subquery on the process name, which picks one arbitrary pid if the app restarted during the trace, so join thread to process on upid with thread.tid = process.pid; the doc comment at line 123 lists a blocked row kind the SQL never emits, the 250ms threshold appears twice, and .catch(() => undefined) in perf-native-summary.ts swallows a missing-host-binding throw, so remove the stale mention, keep one threshold site and map unexpected throws to trace-processor-failed; and the unavailable shape at line 22 copies Apple's AppleTraceAnalysisUnavailable, so move a shared PerfTraceAnalysisUnavailable<Reason> next to PerfHang in contracts and give Android the same placement as Apple.

Could this PR be smaller? Its own commit is about 340 production lines. One SQL statement for main-thread non-idle windows (exclude S, merge Running/R/D, 250ms threshold) plus bounds would be correct, and would remove the double count and the fake split. The frame-timeline pass produced no app rows on the API 35 emulator, so would it be safe to drop? What would have to change first is a rebase onto the force-pushed base perf/perf-trace-report-apple@8c42c68. The PR still carries the superseded #3925 commit 0d5b4eb, and the Android commit touches no Apple files.

At 7199739 the PR had merge conflicts with its base, and the rebase above should clear them. The Coverage check is likely related to this PR: it flags runTraceProcessorQuery at 0% coverage, which this PR adds, and the bound fake host test above should fix it. I could not run trace_processor here, so the thread_state behavior comes from Perfetto's documented table semantics and the fixture and live output in the PR. I did not run the typecheck or test gates locally. Before merge, please fix the idle-window merge with the new fixtures, paste the fresh live runs, rebase, and cover runTraceProcessorQuery.

A newer head, 1776316, arrived while I was reviewing. It adds tests in perf-trace-processor.test.ts and does not change the run CTE in perf-trace-processor.ts, so the hang-window finding above still applies there. I will look at that head on its own in a later pass.

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed 1776316: the main-thread hang rule still reports false hangs, so this needs one more fix. CI is green (6 checks, none failing), and the earlier Coverage CRAP failure on runTraceProcessorQuery is covered by the new mocked-runCmd tests. There are no conflicts.

The window rule in perf-trace-processor.ts:129 is unchanged from my earlier comment. It merges every main-thread thread_state row, including idle Looper sleep (S), with a 150ms gap rule. Because thread_state is contiguous per thread, one run spans the whole trace and sleep counts as blocked. Every perf trace stop --kind perfetto with trace_processor on PATH then reports one trace-long false hang (the fixture shows an 18s run with 6s "blocked") and points users at work that does not exist. A hang must be a contiguous main-thread window that does not return to idle: S closes a window and never counts toward duration or blocked, only Running counts as running, and R/R+/D count as blocked. Please fix this in the one SQL statement that owns the window rule.

The tests at perf-trace-processor.test.ts:76 add route coverage, but the existing test still asserts the merged 18s run (17,997.8ms, 6,002.1ms blocked) as a correct hang. It pins the defect and would fail on the right fix. Please replace it with two fixtures: an idle main thread that yields no hangs, and induced busy hangs that yield K separate rows with running-dominant splits.

This is a device-facing capture path, and the only live run predates any fix and shows the merged 20s window. After the SQL fix, please run perf trace start/stop --kind perfetto on the emulator twice with trace_processor_shell on the daemon PATH and paste both outputs. The idle app should show no Hangs ( section. The hang app should show Hangs (K) at durations near the induced ones, with running-dominant splits.

Could a single SQL statement do all of this? It would yield main-thread non-idle windows (exclude S, merge Running/R/R+/D, 250ms threshold, Running vs blocked split) plus trace_bounds. That would let you drop the frame-timeline pass (no app rows on API 35, and it fakes a split), resolveTool, the traceAnalysis.hangs duplicate, the snapshot mirror and the data.hangs fallback, and share a PerfTraceAnalysisUnavailable<Reason> type with Apple in contracts. Nothing needs to change first beyond the rebase this head already has; the shared unavailable type would sit next to PerfHang.

Not blocking, take or leave: drop the test-only resolveTool parameter at perf-trace-processor.ts:39 and set the whichCmd mock in the 7 tests that still pass it; the earlier notes at line 224 still apply (fake running=dur/blocked=0 frame rows, hangs emitted three times, the scalar main-thread subquery, the stale blocked doc comment, the 250ms threshold repeated at lines 144/149/202, and the unavailable shape copied from Apple); and the test comment at line 165 says PATH lookup must prefer trace_processor_shell, but only plain trace_processor is resolvable, so make both resolvable and assert the source, or drop the claim.

I could not run trace_processor or the typecheck and test gates here. This review rests on Perfetto's documented contiguous thread_state semantics, the committed fixture and live output, and a read of the tests. Before merge, please land the SQL fix, the two fixtures and the two emulator runs, and answer the single-statement question above.

thymikee added a commit that referenced this pull request Oct 23, 2026
…at perf trace stop

One fixed SQL statement owns the window rule: a hang is a maximal run of
non-idle main-thread thread_state rows (S and unknown states close a
window and never count), Running is running time, R/R+/D is blocked time,
and the 250ms threshold is filtered in SQL. trace_processor_shell is
preferred over trace_processor on PATH; without the tool the stop result
keeps the artifact and adds the typed install note.

Review follow-ups from #3926:
- idle main threads now yield zero hangs; induced 4s hangs yield separate
  running-dominant rows (real fixture CSVs from the production query)
- frame-timeline pass dropped (no app rows on API 35, fake split, double count)
- hangs emitted once on summary.hangs; snapshot mirror and traceAnalysis
  duplication removed
- resolveTool seam deleted; runTraceProcessorQuery covered through the
  host-kit command seam
- main thread resolved by joining thread to process (restart-safe)
- unexpected analyzer throws map to typed trace-processor-failed
- Android summary uses the shared PerfTraceAnalysis envelope
@thymikee
thymikee force-pushed the perf/perf-trace-report-android branch from 1776316 to 48b0e1c Compare October 23, 2026 12:39
@thymikee

Copy link
Copy Markdown
Member Author

Fixed at 48b0e1c2 (restacked on the force-pushed base perf/perf-trace-report-apple@729dc23; the superseded #3925 commit is gone — this layer touches no Apple files). pnpm check:affected --run --head 48b0e1c2f9708cfeb00d3e1548b540f21810746e passed, receipt exactCommit=true. Item by item:

Blocking — false trace-long hang. The window rule now lives in the one SQL statement: thread_state rows are marked with LAG(state); a non-idle row whose predecessor was idle (S, empty, or absent) opens a new window; only Running/R/R+/D rows enter windows at all, so S closes windows and never counts toward duration or blocked time. Running = Running rows; blocked = R/R+/D; threshold filtered in SQL. The old gap-merge run CTE and MAIN_THREAD_RUN_GAP_NS are deleted.

Fixtures. The merged-18s fixture and its test are replaced, not kept. Two new fixtures are the production query's own CSV output, produced by running the exact SQL emitted by buildAndroidTraceHangsSql (extracted from the built module) with trace_processor v57 over fresh emulator captures: perf-trace-processor-idle.csv — settled app, bounds row only, test asserts zero hangs; perf-trace-processor-hangs.csv — three induced 4s busy hangs, test asserts exactly three separate windows, each ~4000ms, running-dominant, runningMs + blockedMs <= durationMs, starts >3s apart. The hang-fixture tests fail if the merge rule is reintroduced.

Live runs (both pasted in the PR body, head 48b0e1c2). perf trace start/stop --kind perfetto twice on an API 35 emulator with trace_processor_shell on the daemon PATH. Idle app: no Hangs ( section. Hang app (3 induced hangs): three separate ~4s running-dominant windows. Honest detail: the hang run printed Hangs (4) — the third spin loop's tail contains a real short idle moment, so the rule splits it into a 3s window plus a 654ms tail. That is the rule doing its job (the old one merged everything); it is a scheduler artifact of this app on this emulator, not a missing hang. The idle run's open had briefly re-created the activity and re-fired the hang schedule inside the first attempt's window; the quoted run settles the app inside the session first. Both runs' pulled traces were also queried directly with the production SQL and match the reported output. The committed fixtures come from companion raw perfetto captures of the same app on the same emulator (hang: three ~4s windows; idle: bounds row, zero runs), run through the same production SQL.

Single-SQL question — yes, done. One statement (WITH ... SELECT ... UNION ALL SELECT ... FROM trace_bounds ORDER BY kind, ts) yields non-idle windows + bounds. That let this layer drop, as you listed: the frame-timeline pass (no app rows on API 35, faked split, double count — dropped rather than relisted as hitches, since it never produced rows here), resolveTool, the traceAnalysis.hangs duplicate, the snapshot mirror, and the data.hangs fallback.

Non-blocking, all taken:

  • Frame-timeline/freeze path deleted; extra column and freeze rows gone; perfHangNextSteps now sees a real split or blocked-dominant windows only.
  • Hangs emitted once on summary.hangs; the snapshot mirror line in the capture host is deleted; the available result no longer nests under traceAnalysis — only the typed unavailable half lands there when analysis fails. The formatter reads summary.hangs and the two envelope placements (Apple spreads the envelope on the summary; Android keeps capture metadata beside it) with a code comment naming that difference.
  • resolveTool deleted; the 7 tests that passed it now set the whichCmd mock. runTraceProcessorQuery is covered through the host-kit command seam (success, non-zero exit, throw mid-run incl. temp-dir cleanup, setup failure, malformed CSV) — this is also the CRAP-coverage fix.
  • Main thread via thread t JOIN process p USING (upid) WHERE t.tid = p.pid AND p.name = ... (SQL test pins the join).
  • Stale blocked doc-comment kind removed (doc now lists exactly run + bounds); threshold stated once (ANDROID_HANG_THRESHOLD_MS interpolated into the single HAVING, SQL test asserts 250000000 appears exactly once).
  • .catch(() => undefined) in perf-native-summary.ts maps unexpected throws to typed trace-processor-failed (new test: analyzer reject -> summary carries the reason, capture survives).
  • Shared PerfTraceAnalysisUnavailable<Reason> (and the PerfTraceAnalysis envelope) now sit next to PerfHang in contracts; Android's result is PerfTraceAnalysis<PerfHang, PerfTraceHotspot, AndroidTraceUnavailableReason> — the copied unavailable shape is gone.
  • PATH-preference test fixed as asked: both trace_processor_shell and trace_processor resolve, and the test asserts trace_processor_shell ran and the plain tool did not.

@thymikee thymikee changed the title feat(perf): interpret Android Perfetto traces into freezes at perf trace stop feat(perf): interpret Android Perfetto traces into main-thread hangs at perf trace stop Oct 23, 2026
@thymikee

Copy link
Copy Markdown
Member Author

I found no code problems in 48b0e1c, and I could not confirm it end to end. The earlier findings from #3926 (comment) are fixed, and the Coverage CRAP failure on runTraceProcessorQuery is now addressed by the mocked runCmd tests.

I could not run trace_processor, so I could not reproduce the committed CSV fixtures or check the SQL output against a real trace. I also could not check the extra +20s 654ms hang row without the trace file. I did not run the typecheck, test or check:affected gates.

The checks on this commit were cancelled, not failed, so no job has produced a result for it. The diff touches Lint/Typecheck, Coverage (CRAP of touched functions) and Integration routes. Please re-run the checks and get them green before merge. There are no conflicts.

One design question I would like answered before merge: Apple's stop summary spreads the PerfTraceAnalysis envelope, but Android copies only hangs and a separate summary.traceAnalysis field, which adds a second shape in output.ts. The Android hangCount and the Perfetto UI note are computed and never printed. Could you spread the envelope onto the Android summary as Apple does, with notes: [frameNote, ...envelope.notes], and drop that field and branch? The existing summary.available === false path already prints the unavailable case.

Not blocking, and you can take or leave these: (1) The idle and hangs fixtures in perf-trace-processor.test.ts would also pass with the old merged-window SQL, so only the substring assertions pin the window rule. Running buildAndroidTraceHangsSql against a small in-memory node:sqlite database would test the rule itself. (2) thread_state S also covers lock, binder and GC waits, so those are not reported, and a busy hang with a brief S splits into two windows, as in your third induced hang. A line in the docs would be enough for now. (3) The doc comment on buildAndroidTraceHangsSql repeats its first line at line 121.

thymikee added a commit that referenced this pull request Oct 23, 2026
…at perf trace stop

One fixed SQL statement owns the window rule: a hang is a maximal run of
non-idle main-thread thread_state rows (S and unknown states close a
window and never count), Running is running time, R/R+/D is blocked time,
and the 250ms threshold is filtered in SQL. trace_processor_shell is
preferred over trace_processor on PATH; without the tool the stop result
keeps the artifact and adds the typed install note.

Review follow-ups from #3926:
- idle main threads now yield zero hangs; induced 4s hangs yield separate
  running-dominant rows (real fixture CSVs from the production query)
- frame-timeline pass dropped (no app rows on API 35, fake split, double count)
- hangs emitted once on summary.hangs; snapshot mirror and traceAnalysis
  duplication removed
- resolveTool seam deleted; runTraceProcessorQuery covered through the
  host-kit command seam
- main thread resolved by joining thread to process (restart-safe)
- unexpected analyzer throws map to typed trace-processor-failed
- Android summary uses the shared PerfTraceAnalysis envelope
@thymikee
thymikee force-pushed the perf/perf-trace-report-android branch from 48b0e1c to e00de8e Compare October 23, 2026 14:11
@thymikee

Copy link
Copy Markdown
Member Author

Second-pass items handled at e00de8e1bba4010842b5ee97a47e37f22ff11f29 (restacked on the force-pushed #3925 head dc9f004e; pnpm check:affected --run passed at this head, receipt exactCommit=true). On the checks: the batch you saw was cancelled, not failed — CI has since run green (19/19) on 48b0e1c2, including Coverage/CRAP and Lint/Typecheck, and this head passed the same local gate set.

Design question — done as suggested. The PerfTraceAnalysis envelope now spreads onto the Android stop summary exactly like Apple's: available/hangs/hangCount/hotspots/notes when available, available:false/reason/message/hint when not, with notes: [frameNote, ...envelope.notes] (the frame-health note leads, then the Perfetto UI note — both printed; hangCount is on the summary and in JSON). summary.traceAnalysis is deleted from the wire, the type, and output.ts: the formatter's only unavailable read is summary.available === false, shared with Apple. Verified live at this head — the JSON summary's keys are available, capture, frameHealth, hangCount, hangs, hotspots, notes, tracePath (no traceAnalysis), quoted in the body.

Non-blocking (1) — fixtures now discriminate against the old merged-window SQL. Agreed that the CSV fixtures are inputs to the interpreter, so only the substring assertions pinned the rule. There is now a rule-level test that runs buildAndroidTraceHangsSql itself against an in-memory node:sqlite database shaped like Perfetto's process/thread/thread_state/trace_bounds: two 4s busy windows separated by a 6s S sleep, a sub-threshold D window, and a Running+D window. It asserts three separate windows at exact offsets/durations (S neither stretches nor merges), running_ns counting only Running, and the threshold filter. Reintroducing the old gap-merge rule fails this test. (node:sqlite is fine in the test lanes: the CI setup action defaults to Node 24.13; the 22.12 job is the package floor check and does not run unit tests.)

Non-blocking (2) — S semantics documented. debugging-profiling.md now states that the idle state also covers lock, binder, and GC waits (those stretches count as idle) and that a busy hang with a brief S can split into two windows.

Non-blocking (3) — duplicated doc-comment line removed from buildAndroidTraceHangsSql.

End-to-end re-verified at this head (since you could not confirm it): both emulator runs re-run after the envelope spread, outputs quoted in the PR body at e00de8e1 — idle app: no Hangs ( section; hang app: the three induced ~4s running-dominant windows (plus the honest 4th row: the third spin's tail really does contain a brief S, and the new docs line explains exactly that split). The committed CSV fixtures come from companion raw perfetto captures of the same app/emulator through the same production SQL; the CLI-captured traces above print through the same path (perf trace stop runs the same query through trace_processor).

@thymikee

Copy link
Copy Markdown
Member Author

This PR is ready at e00de8e. The envelope is now adopted, and summary.traceAnalysis and its output branch are gone. All checks pass at this commit, including Coverage, Lint/Typecheck & Package, Integration Tests and the three Smoke lanes. I could not run trace_processor or an Android emulator, so the live idle and hang runs and the JSON key list are the PR body's evidence, which I checked only against the code shape. I did not run the node:sqlite rule test against the old gap-merge SQL. I judge from its exact-window assertions that it would fail. Not blocking, and fine to take or leave: when trace_processor is missing, the install steps now live only in JSON summary.hint, while CLI text prints just Trace analysis: unavailable - trace-processor-missing (https://github.com/callstack/agent-device/blob/e00de8e/website/docs/docs/debugging-profiling.md#L268 still says the result "adds a note with install steps", and the help line at cli-help-workflows.ts:161 says it "prints an install hint"). Every doc line about the unavailable case should match what the shared unavailable formatter prints. The smallest fix is to append readString(summary.hint) in formatTraceAnalysisUnavailable in output.ts, which makes both doc lines true for Apple and Android. Rewording the doc to say the JSON summary carries a typed reason and an install hint also works.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 23, 2026
…at perf trace stop

One fixed SQL statement owns the window rule: a hang is a maximal run of
non-idle main-thread thread_state rows (S and unknown states close a
window and never count), Running is running time, R/R+/D is blocked time,
and the 250ms threshold is filtered in SQL. trace_processor_shell is
preferred over trace_processor on PATH; without the tool the stop result
keeps the artifact and adds the typed install note.

Review follow-ups from #3926:
- idle main threads now yield zero hangs; induced 4s hangs yield separate
  running-dominant rows (real fixture CSVs from the production query)
- frame-timeline pass dropped (no app rows on API 35, fake split, double count)
- hangs emitted once on summary.hangs; snapshot mirror and traceAnalysis
  duplication removed
- resolveTool seam deleted; runTraceProcessorQuery covered through the
  host-kit command seam
- main thread resolved by joining thread to process (restart-safe)
- unexpected analyzer throws map to typed trace-processor-failed
- Android summary uses the shared PerfTraceAnalysis envelope
@thymikee
thymikee force-pushed the perf/perf-trace-report-android branch from e00de8e to ea1013b Compare October 23, 2026 17:05
@thymikee

Copy link
Copy Markdown
Member Author

Taken at ea1013b6 (restacked on the current #3925 head 2e8b2f4d; content-identical to e00de8e1 plus this change). pnpm check:affected --run --head ea1013b6f3612adf291655345179dfb350926047 passed, receipt exactCommit=true.

Unavailable CLI line now prints the hint. formatTraceAnalysisUnavailable appends readString(summary.hint) when present, so with trace_processor missing the CLI text reads Trace analysis: unavailable - trace-processor-missing Install the Perfetto trace processor on PATH (curl -LO https://get.perfetto.dev/trace_processor && chmod +x ./trace_processor && mv ./trace_processor /usr/local/bin/) to get hangs and next steps at perf trace stop. — the install steps from summary.hint are printed, not just JSON-only. The output test pins the hint text. Since the formatter is shared, Apple's unavailable lines print their hints too (for example the Instruments/retry hint), matching what JSON already carried. Both doc lines you cited — debugging-profiling.md "adds a note with install steps" and the help line "prints an install hint" — now match what the text prints; no doc wording changed because neither statement is now false.

Live evidence in the body stays at e00de8e1: this commit only appends text on the unavailable path, which the quoted runs did not exercise (trace_processor was installed); the new output test pins the changed line.

@thymikee

Copy link
Copy Markdown
Member Author

Closing this one. Interpreting a local Perfetto trace is something a capable agent can already do with trace_processor, and this depends on that same host tool. So it adds a heuristic layer without reaching anything an agent couldn't reach itself. We're putting debugging work into what agents can't do on their own, mainly remote and cloud devices.

The branch stays up for reference. The thread_state window SQL and the emulator fixtures are the parts worth reusing if we come back to this.

@thymikee thymikee closed this Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant