Skip to content

feat(perf): report Apple xctrace hangs, hitches, and hotspots at perf trace stop - #3925

Closed
thymikee wants to merge 1 commit into
mainfrom
perf/perf-trace-report-apple
Closed

thymikee wants to merge 1 commit into
mainfrom
perf/perf-trace-report-apple

Conversation

@thymikee

@thymikee thymikee commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

What

perf trace stop (Apple xctrace) now interprets the finished .trace and attaches a summary to the stop result: hangs (start offset, duration, thread, top symbol, running/blocked split), animation hitches, CPU hotspots, and rule-based nextSteps. Also makes the default perf trace template "Time Profiler" so the hang evidence exists in the capture (the old default, "Animation Hitches", produced malformed traces on simulators under Xcode 27.1).

How

  • After stop, export the TOC-present hang-risks, hitches, and time-profile tables via xctrace export under one overall analysis deadline.
  • Hangs are derived from contiguous main-thread time-profile samples (gap bridge = 4x sample weight, 250ms threshold matching the Instruments hangs default). The main thread is identified by the Main Thread fmt label, so no thread-info export is needed. The running/blocked split and top symbol accumulate per run from exactly the samples that formed it, counting only each sample's uncovered exposure, so runningMs + blockedMs <= durationMs holds by construction.
  • potential-hangs is deliberately not parsed: no capture in this environment (iOS simulator or macOS host, Xcode 27.1) ever fills it — zero rows even for 4s busy hangs — and we don't ship parsers tested only against invented fixtures. A follow-up can add it once a physical-device capture provides a real fixture.
  • Hitches rows go through parseAppleHitchRows (perf-frame) and only count rows of the attached process (pid or process-name match, incl. bundle-id-shaped targets), so other processes' rows in a shared export are dropped.
  • nextSteps come from a static rule table in contracts (no LLM): running-dominant + symbol -> inspect symbol + CPU profile; blocked-dominant -> move waiting work off main.
  • Analysis is best-effort: any failure keeps the capture result intact and reports a typed unavailable reason (toc-export-failed, table-export-failed, no-time-profile-table, no-process-samples, analysis-timeout, analysis-failed). table-export-failed names the failed export (the hint does not send the agent to re-record with a template the trace already used); no-time-profile-table is kept only for a TOC that truly lacks the table. No new CLI actions or flags.
  • Contracts hold the shared PerfHang shape, the PerfTraceAnalysis stop-summary envelope ({available:true, tracePath, hangs, hangCount, hitches?, hotspots, notes} | {available:false, reason, message, hint}) that feat(perf): interpret Android Perfetto traces into main-thread hangs at perf trace stop #3926 builds on, and the static rule table.
  • The analyzer loads behind a function-scoped await import so the perf-facade eager closure stays at its merge-base budget.

Output bound

CLI text prints at most 5 hangs/hotspots plus a "see the JSON summary" overflow line; --json carries the full list.

Live device evidence

Built CLI at 729dc234 (heads dc9f004e and 2e8b2f4d add only review-pass fixes: monotonic run end, stop-wait-derived analysis budget, dropped redundant matcher, and tests; the printed shape and values cannot change, gate-green at 2e8b2f4d), iOS simulator iPhone 17e (iOS 27.0, Xcode 27.1) with a hang app inducing busy main-thread hangs:

$ agent-device perf trace start --kind xctrace   # default template now Time Profiler
$ agent-device perf trace stop --kind xctrace --out live-l1.trace
/private/tmp/perf-report-app/live-l1.trace
Perf trace: stopped
Template: Time Profiler
Hangs (3):
- +6s 4s Main Thread - protocol witness for Collection.formIndex(after:) in conformance <> Range<A> - running 4s / blocked 0ms
- +778ms 3s Main Thread - protocol witness for Collection.formIndex(after:) in conformance <> Range<A> - running 3s / blocked 0ms
- +364ms 358ms Main Thread - protocol witness for Collection.formIndex(after:) in conformance <> Range<A> - running 358ms / blocked 0ms
Hotspots:
- 630ms protocol witness for Collection.formIndex(after:) in conformance <> Range<A>
- 460ms IndexingIterator.next()
- 389ms _xzm_free_tc
- 389ms _xzm_malloc_tc
- 378ms swift_getAssociatedTypeWitness
Next steps:
- Inspect protocol witness for Collection.formIndex(after:) in conformance <> Range<A> on the main thread; it holds the most samples in this window.
- Run perf cpu profile around the interaction for a symbol-level breakdown.

Every hang's running + blocked now sits inside its own duration (the old 294ms/334ms inconsistency is fixed by the per-run accumulation).

Honest simulator notes: the derived-samples path above is the one the live evidence exercises (it works on simulators and devices alike); the hitches table is absent from Time Profiler traces and reports undefined (no evidence) in this run.

Tests

  • Fixtures are real excerpts from live captures (TOC + the time-profile window covering one induced hang; a real "Animation Hitches" export for the hitch table).
  • perf-trace-analysis.test.ts: derived hang windows + the runningMs + blockedMs <= durationMs invariant on the real fixture, hitches counted only for the attached process (real export rows plus a foreign-process row in the identical cell encoding — every multi-process export attempt on this host crashed xctrace, so the foreign row itself is constructed, documented in the fixture header), typed unavailable paths incl. table-export-failed, target matching.
  • perf-xctrace-trace-stop.test.ts: stop attaches interpreted hangs through the table mapping (pins a real bug found in live verification: table keys must map to analyzer params), and table-export-failed + no-template-suggesting hint when the TOC lists the table but the export fails.
  • nextSteps rule table and CLI formatter tests.

Validation

pnpm check:affected --run --head 2e8b2f4d8a74a67a7bd728c1b585f3737c92b763 -> passed (format, lint, typecheck, layering, di-seams, fallow, mcp-metadata, build, package, integration-node, macos-coverage, vitest-related, repo guards; receipt exactCommit=true). Prior heads 729dc234 and dc9f004e were also gate-green and CI-complete.

🤖 Generated with Claude Code

@thymikee
thymikee added this pull request to stack #3927 October 23, 2026 08:30
@thymikee thymikee changed the title perf/perf trace report apple feat(perf): report Apple xctrace hangs, hitches, and hotspots at perf trace stop Oct 23, 2026
@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.58 MB 5.59 MB +10.6 kB
Package (unpacked) 5.58 MB 5.59 MB +10.6 kB
Package (download) 1.69 MB 1.69 MB +3.6 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 30.4 ms 31.6 ms +1.2 ms
CLI --help 58.3 ms 62.3 ms +3.9 ms

@thymikee
thymikee force-pushed the perf/perf-trace-report-apple branch 2 times, most recently from 8c42c68 to 3b3eb44 Compare October 23, 2026 09:27
@thymikee

Copy link
Copy Markdown
Member Author

I reviewed 3b3eb44 and found three problems to fix before merge. CI is green (22/22 checks pass) and I know of no conflicts. I did not re-run the live simulator capture; I relied on the PR transcript, and its code matches head.

The unavailable reason can name the wrong cause. In perf-trace-analysis.ts, any time-profile export failure, including the 15s export timeout on a long trace, becomes undefined. The result is then no-time-profile-table with the hint to record with --template "Time Profiler", even though the trace already used Time Profiler. An agent will re-record with the same template and loop. The reason should name the observed cause: use a typed table-export-failed reason when the TOC lists the table but its export failed, and keep no-time-profile-table only for a TOC that lacks it. exportAppleTraceTablesPresent is the place that knows both. Please update the 'reports a typed unavailable summary when table exports fail' test to assert it.

The hang breakdown can be inconsistent. classifyAppleHangWindow rescans all samples within 50ms of the run, so neighbouring work is counted as part of the hang. The PR's own output shows a 294ms hang with "running 334ms / blocked 0ms", and topSymbol and nextSteps can point at adjacent work. A hang's running/blocked split and top symbol should come only from the samples that formed the run. Please accumulate weights per run in deriveAppleHangsFromSamples, drop the slack rescan, and assert runningMs + blockedMs <= durationMs on the real fixture.

Hitch rows are not filtered by process. analyzeAppleHitches counts every row in the hitches table, but that table also carries other processes' rows. perf-frame.ts already filters them with parseAppleHitchRows and matchesAppleFrameProcess. With --template "Animation Hitches", which the new help recommends, SpringBoard or other apps' hitches would count as the app's and trigger hitch next steps. Only rows for the attached app should count, so please route hitches through those existing helpers and delete the new row walk. Please cover it with a real exported hitches fixture that includes a foreign-process row. I could not observe such an export here, since Time Profiler traces have no hitches table.

Not blocking, and fine to take or leave: (1) the .catch(() => undefined) at perf-xctrace.ts:207 drops analyzer throws silently, so mapping them to an available:false result with a typed reason would match the "typed unavailable reason" claim; (2) the tid == pid fallback and its comment in isAppleMainThread are false on Darwin (fixture tid 246712434, pid 64549) and can be deleted; (3) PERF_HANG_JS_THREAD_PATTERN in perf-hang-next-steps.ts misses the real names com.facebook.react.JavaScript and mqt_js, and Apple hangs only come from main-thread samples, so the rule is unreachable here and could be dropped from this PR; (4) output.ts reads data.hangs and summary.traceAnalysis, which nothing in this PR produces, so keeping only the Apple shape would be simpler; (5) up to four sequential 15s exports run after the 45s+5s stop wait inside the 90s request envelope, so one overall analysis deadline with a typed analysis-timeout would be safer; (6) the hitches test is named 'real hitches schema rows' but uses inline XML with only the app's own row, so it cannot catch the missing filter; (7) in commands.md, "Every entry includes bounded nextSteps guidance" is not true (hotspots have none, and CLI text prints only hang next steps), and the docs should say that perf trace now defaults to Time Profiler, so hitch tables need --template "Animation Hitches".

Could this be smaller? Production net is 707 lines. One shared shape in contracts would remove the extra readers: PerfTraceAnalysis = {available:true, hangs, hotspots, hitches?, notes} | {available:false, reason, message, hint}, placed at the stop result summary for every platform. Together with per-run cause accumulation, the existing hitch helpers and dropping the unreachable JS-thread rule, that would cut roughly 120-150 lines and fix the three issues above by construction. What would have to change first is agreeing that envelope in contracts before #3926 lands its separate summary.traceAnalysis shape. Does that fit your plan?

Before merge, please fix the export-failure reason, the hang split and the hitch filter, and answer the shared-envelope question so #3926 can build on it. I did not exercise physical-device behavior or measure whether real time-profile exports pass the 15s timeout.

@thymikee
thymikee force-pushed the perf/perf-trace-report-apple branch 2 times, most recently from f309086 to 729dc23 Compare October 23, 2026 12:39
@thymikee

Copy link
Copy Markdown
Member Author

All findings fixed at 729dc234 (restacked on current main; pnpm check:affected --run --head 729dc2349a755f1aa522eb658b7704946f3fa67f passed, receipt exactCommit=true). Item by item:

Blocking 1 — wrong unavailable cause on export failure. Fixed in exportAppleTraceTablesPresent, which now returns {tables, failedTables}. analyzeAppleTraceTables reports table-export-failed when the TOC lists time-profile but its export failed; the hint names the failed export and no longer suggests a template the trace already used. no-time-profile-table remains only for a TOC that lacks the table. The 'reports a typed unavailable summary when table exports fail' stop test now asserts table-export-failed + the no-template hint, plus a unit case for the TOC-lists-but-export-fails path.

Blocking 2 — inconsistent hang breakdown. classifyAppleHangWindow and the 50ms slack rescan are deleted. deriveAppleHangsFromSamples accumulates running/blocked and per-symbol weights per run from exactly its own samples, counting only each sample's uncovered exposure (a sample's weight may overlap its predecessor's; only the newly exposed part counts), so runningMs + blockedMs <= durationMs holds by construction. The analysis test asserts that invariant on the real fixture. The fresh live capture in the PR body shows it in printed output (358ms/358ms on the 358ms hang; previously 334ms on a 294ms hang).

Blocking 3 — hitch rows not filtered by process. analyzeAppleHitches now runs the export through parseAppleHitchRows + matchesAppleFrameProcess (perf-frame's pid-or-exact-name filter) with matchesAppleTraceTarget additionally accepting bundle-id-shaped targets; the new row walk is deleted. Fixture provenance, honestly: three rows are byte-exact from a real Animation Hitches export (xctrace, Xcode 27.1, attached app on the macOS host — the template works single-process there); the foreign-process row reuses the real cell encoding because a single-process attach never emitted foreign rows and every multi-process export attempt crashed xctrace export here ("Unexpected internal error"; --all-processes ballooned to 25GB and was killed). This is stated in the fixture header and test comment. The test asserts count 3 (foreign row excluded) at a name-only target so the exclusion cannot come from a pid coincidence.

Non-blocking (1) — silent .catch(() => undefined). Removed. analyzeAppleXctraceTrace never throws: unexpected throws map to typed analysis-failed, and there is no swallow on the stop path.

Non-blocking (2) — tid == pid fallback. Deleted with its comment; the Main Thread/com.apple.main-thread fmt label is the only identifier.

Non-blocking (3) — JS-thread rule. Dropped from contracts along with its test, as suggested.

Non-blocking (4) — output.ts extra readers. data.hangs and summary.traceAnalysis reads removed; hangs read only from summary.hangs, unavailable only from the summary envelope.

Non-blocking (5) — sequential export timeouts. One overall 45s analysis deadline wraps the export + analysis phase (analysis-timeout typed reason; unref'd timer so an idle process still exits).

Non-blocking (6) — hitches test could not catch the missing filter. Rewritten to use the fixture described above (real rows + foreign row), which fails if the filter regresses.

Non-blocking (7) — commands.md. Fixed: the nextSteps claim now says each hang carries bounded guidance (CLI prints at most five per list, JSON carries the full list), and the docs state that perf trace defaults to Time Profiler so hitch tables need --template "Animation Hitches".

Shared envelope question — agreed and adopted. packages/contracts/src/perf-hang-next-steps.ts now exports PerfTraceAnalysis = {available:true, tracePath, hangs, hangCount, hitches?, hotspots, notes} | PerfTraceAnalysisUnavailable<Reason>, attached at the stop-result summary. Apple's AppleTraceAnalysis is that envelope with the Apple reason union (its unavailable branch additionally carries tableSchemas for diagnosis). #3926 was rebased onto this and now emits hangs once on summary.hangs (its summary.traceAnalysis duplication, the snapshot mirror, and the data.hangs fallback are gone), so the envelope question is answered at the contracts owner before layer 2 lands. Net effect near your estimate: the JS-rule, rescan, row-walk, and extra-reader deletions landed; the per-run accumulation and envelope types added back part of that, and the two new typed reasons + deadline are the new surface.

Live simulator evidence re-run at this head; output quoted in the PR body with the sha.

@thymikee

Copy link
Copy Markdown
Member Author

This PR is ready at 729dc23. It fixes the problems from the earlier review, all 22 checks pass, and there are no conflicts. Nothing else must happen before merge. Not blocking, and fine to take or leave: (1) in perf-trace-analysis.ts the run end should never move backwards, so use the max of the old end and the new sample end, which keeps running plus blocked time within the run duration; (2) in perf-xctrace.ts the fixed 45s analysis deadline follows a stop wait of up to 50s, so the 90s command envelope can fire first, and the abandoned exports keep running, so the budget could come from the remaining envelope and be passed into the exports; (3) in perf-trace-analysis.ts matchesAppleTraceTarget already covers every case matchesAppleFrameProcess accepts, so the second matcher and its export can go. I did not reproduce the live simulator run, and I relied on the transcript in the PR body, whose format matches formatTraceHangLine. I did not see a real multi-process Animation Hitches export, so the foreign-process row encoding in the fixture is assumed. I also did not measure non-monotonic Time Profiler sample ends or a stop plus export that runs past 90s.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 23, 2026
@thymikee
thymikee force-pushed the perf/perf-trace-report-apple branch from 729dc23 to dc9f004 Compare October 23, 2026 13:56
@thymikee

Copy link
Copy Markdown
Member Author

All three notes taken, at dc9f004e (pnpm check:affected --run --head dc9f004eb8180e1f51536e97d53029bc5c2332ae -> passed, receipt exactCommit=true). The prior head 729dc23 had all 22 checks green, and this head restacks on the same main base.

  1. Monotonic run end. The merge branch now sets endNs = Math.max(previous.endNs, sample.timeNs + sample.weightNs), so a sample whose end lands before the current run end can never shrink the span the uncovered-exposure accounting is measured against. The running + blocked <= duration invariant stays structural.

  2. Budget from the remaining envelope. stopAppleXctracePerfCapture stamps stopStartedAtMs; before analysis it derives budgetMs = min(45s, 90s - elapsed - 5s margin) (the 90s is the command-registry DEFAULT_TIMEOUT_POLICY envelope the perf stop request runs under, so analysis finishes inside the envelope and the daemon's own result wins the race). analyzeAppleXctraceTrace takes budgetMs: with nothing left it returns analysis-timeout without starting any export, and otherwise passes the budget into the export pass, where every export shares one deadline (exportTimeoutMs() shrinks each export's timeout as the deadline approaches instead of stacking four 15s timeouts). A new stop-path test pins the exhausted-budget short-circuit. Honest note: the envelope constant is mirrored here because the platform layer has no access to the resolved request deadline; the margin keeps the two from drifting silently, and the derivation is named for exactly that (remainingAppleTraceAnalysisBudgetMs).

  3. Redundant matcher dropped. matchesAppleFrameProcess is back to module-private in perf-frame.ts (its export and the second matcher clause in analyzeAppleHitches are gone); matchesAppleTraceTarget alone filters rows by pid, exact name, or bundle-id-shaped target. Fallow is clean at the new head.

Live simulator evidence in the body is from the previous head; the shape the fresh evidence printed is unchanged by these three edits (two are structural guards, one removes a redundant filter).

@thymikee

Copy link
Copy Markdown
Member Author

This PR is ready at dc9f004. It fixes what the earlier review at 729dc23 covered, and the code looks correct. All 22 checks pass, and I know of no conflicts. Live simulator evidence is still from 729dc23, not dc9f004. I accepted it because the change since then only lowers or keeps the analysis budget, changes run end only for non-monotonic samples, and removes a redundant filter, so the output shape cannot change. I did not rerun it, and I did not check perf inside batch end to end.

Not blocking, take or leave: the 90s APPLE_TRACE_STOP_ENVELOPE_MS in perf-xctrace.ts mirrors the daemon request timeout without a real deadline reaching the perf runtime, so a static cap tied to the graceful stop wait would be simpler and gain at most 5s less; the budgetMs: 0 test in perf-xctrace-trace-stop.test.ts should assert no host command ran, and the stop-path budget derivation and the non-monotonic run-end branch have no test; and when the outer deadline wins, runAppleTraceAnalysis still spawns one xcrun export per remaining table because of the 100ms floor, so it should stop issuing exports once the deadline has passed. I did not run the zero-budget test with the guard removed, so that point comes from timer ordering.

Nothing else must happen before merge.

@thymikee
thymikee force-pushed the perf/perf-trace-report-apple branch from dc9f004 to ad3c448 Compare October 23, 2026 16:44
… trace stop

Attach a trace analysis summary to xctrace trace stops: main-thread hangs
derived from contiguous Time Profiler samples, hitch tallies from a real
hitches table counted only for the attached process, CPU hotspots, and
static nextSteps rules. Reasons are typed (table-export-failed names an
export failure instead of sending the agent to re-record; analysis-timeout
and analysis-failed replace silent drops); analysis runs under one overall
deadline and never fails the capture. Contracts hold the shared PerfHang
shape, the PerfTraceAnalysis stop-summary envelope, and the static rule
table that Android reuses.
@thymikee
thymikee force-pushed the perf/perf-trace-report-apple branch from ad3c448 to 2e8b2f4 Compare October 23, 2026 17:05
@thymikee

Copy link
Copy Markdown
Member Author

All four notes taken at 2e8b2f4d (restacked on the current main; commit content identical to ad3c4487). pnpm check:affected --run --head 2e8b2f4d8a74a67a7bd728c1b585f3737c92b763 passed, receipt exactCommit=true.

  1. Mirrored 90s constant gone. APPLE_TRACE_ANALYSIS_DEADLINE_MS is now a static cap tied to the stop path's own waits: APPLE_XCTRACE_STOP_GRACE_TIMEOUT_MS - APPLE_XCTRACE_STOP_FORCE_TIMEOUT_MS (40s), sized so worst-case stop + analysis stays inside the stop request's envelope without mirroring the daemon timeout at runtime. remainingAppleTraceAnalysisBudgetMs(stopStartedAtMs) just shrinks that static cap by the stop time actually spent (floor 0), so a stop wait that consumed the full cap abandons analysis before spawning any export — which is what the new stop-path test below pins. The 90s number and the margin constant are deleted.

  2. budgetMs: 0 test asserts no host command ran. It now asserts runCmd.mock.calls.length === 0 (the mocked host would also throw on any unexpected command) in addition to the typed analysis-timeout.

  3. The two missing branches have tests.

    • Stop-path budget derivation: a new stop test spies Date.now so the entry stamp reads t0 and every later read (budget derivation, export deadline) reads t0 + 41s; the summary must be analysis-timeout and the export handler must never have been entered — proving the derived budget, not just the budgetMs short-circuit.
    • Non-monotonic run end: a new deriveAppleHangsFromSamples test via analyzeAppleTraceTables over a crafted three-sample time-profile table (a 3s sample, a merge-window sample whose time + weight ends 2.9s before the run end, a follow-on sample). With a plain assignment the run would collapse to ~0.1s and drop below the threshold; the test asserts the 3000ms span survives with runningMs capped by exposure and running + blocked <= duration.
  4. No exports after the deadline passes. The 100ms floor is dropped: exportTimeoutMs() can now reach zero, and the export loop checks the remaining budget before each spawn — once it is <= 0 the remaining tables are recorded as failed without running a single command, and the caller's overall deadline resolves the pass as analysis-timeout.

Body's evidence header now names this head and the new receipt; the quoted simulator run stays at 729dc234 for the reason you gave (these edits only lower/cap budgets, guard a branch, and remove a redundant filter).

@thymikee

Copy link
Copy Markdown
Member Author

I reviewed 2e8b2f4 against the earlier review (#3925 (comment)). The new analysis budget is in place, but one timing path can still start an export with no timeout, so this needs one more change before merge.

Dropping the 100ms floor lets exportTimeoutMs() return 0 or less (perf-xctrace.ts#L355). Only the table loop checks for that. The TOC export runs after makeHostTemporaryDirectory and passes the value straight to runXcrun. If a stop ends with a few ms of the 40s cap left, or the temp dir creation outlasts the remaining budget, deadlineAtMs - Date.now() is 0 or less. Then host-kit normalizeTimeoutMs (exec.ts#L851) turns it into undefined, which means no timeout. The earlier code bounded this export at 100ms. Now the outer race returns analysis-timeout, but xcrun xctrace export --toc keeps running in the daemon, and the temp-dir cleanup in runAppleTraceAnalysis waits on it. This is on the normal perf trace stop route. The window is narrow, but it brings back the unbounded host process this change set out to remove.

The rule should be: no analysis export starts without a positive timeout. Please enforce it once in exportAppleXctraceOptionalData, the single owner of exports for this pass, by returning undefined without spawning when timeoutMs <= 0. Then remove the loop-local guard so the TOC and table exports share one check. Please add a test that runs the pass with a tiny budget, or a clock already past the deadline, and asserts runCmd was not called.

Could the delta be simpler? It adds a deadline guard at each call site. If the "positive timeout or do not spawn" rule lives in exportAppleXctraceOptionalData and the cap is a flat 40s, remainingAppleTraceAnalysisBudgetMs, the stopStartedAtMs threading and the loop guard all go away, and the problem above is closed. The earlier size and simplicity question stays closed.

Not blocking, and fine to take or leave: the per-table remainingMs <= 0 guard at L414 has no test, because the new stop-budget test returns at budgetMs <= 0 before runAppleTraceAnalysis runs, and the test above covers it once the guard moves; and since the budget is 40s minus the stop wait (L335), a slow xctrace finalization shortens analysis, and one over 40s always gives analysis-timeout, while a flat 40s cap would still fit the 90s envelope (about 45s for a successful stop plus 40s), so either use the flat cap or say in the help text that a slow stop shortens analysis.

Checks are still running: Smoke Tests is in progress and has not failed. This update changes only the budget and export code in perf-xctrace.ts and its unit tests. The android, help and docs files in the diff come from the rebase onto c48d27a. If Smoke Tests fails, please check whether its route reaches Apple perf trace stop before treating it as caused by this PR. No conflicts are known.

On evidence: the live simulator transcript is still from 729dc23. This update changes only budget timing and export guards on the stop path, not the output shape, so I accepted it as before and did not rerun it. I did not run the tests or the mutations. I judged the new tests by reading them. I did not reproduce the narrow window above. I confirmed the mechanism by reading exportTimeoutMs and normalizeTimeoutMs. I did not re-check the check:affected receipt or that perf stop runs under the 90s default envelope, as in the earlier review.

Before merge, please guard the TOC export against a timeout of 0 or less, ideally in exportAppleXctraceOptionalData, with the test above.

@thymikee thymikee removed the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 23, 2026
@thymikee

Copy link
Copy Markdown
Member Author

Closing this. The analysis runs on a local .trace file, and a capable agent can already explore one with xctrace export. We're putting debugging work into what agents can't do on their own, mainly remote and cloud devices.

One result is worth keeping for later: on the iOS 27 simulator, Instruments' potential-hangs table stayed empty for induced main-thread stalls of about 2 s, while deriving busy windows from time-profile main-thread samples found them. If we revisit trace analysis, start from that derivation and drop the advice rules. The branch stays up for reference.

@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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant