Skip to content

feat(system-perf): expand popover with heap-space + stress signals + on-demand snapshot#86

Merged
Juliusolsson05 merged 1 commit into
mainfrom
feat/perf-deep-dive
May 13, 2026
Merged

feat(system-perf): expand popover with heap-space + stress signals + on-demand snapshot#86
Juliusolsson05 merged 1 commit into
mainfrom
feat/perf-deep-dive

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Summary

The badge popover used to show one chart (heap + RSS over time) and three rows of aggregate numbers. That answered "is the heap growing?" but not "WHICH part of the heap?" or "where do I look next?" — the 20 MB → 600 MB jumps on heavy multi-agent days were visible but not attributable.

This PR adds — visible only when the popover is expanded — the diagnostic vocabulary you'd otherwise need Chrome DevTools for. The header badge itself is unchanged.

What gets added

  1. Per-space heap breakdown (v8.getHeapSpaceStatistics()) — the top six v8 spaces sorted by used size with proportional bars. old_space rising = retention (real leak); large_object_space rising = giant buffers/strings (typical agent-heavy smoking gun); new_space rising = churn but usually fine.

  2. Detached + native contextsdetachedContexts > 0 is one of the strongest leak smoke alarms v8 exposes (colored red when nonzero). Free signal — already in the getHeapStatistics() call we make.

  3. Event-loop p99 delay — dedicated monitorEventLoopDelay() owned by the IPC handler so its 1 Hz reset window is independent of PerformanceService's 5 s probe. Color thresholds at 16 ms (60 fps budget) and 50 ms ("user notices").

  4. Renderer-process heapwindow.performance.memory read synchronously in the poller (no IPC bounce; renderer reads its own JS heap directly). Lets you tell which process is leaking when main says fine but the app feels slow.

  5. On-demand heap snapshot buttonv8.writeHeapSnapshot() to STATE_DIR/heap-snapshots/manual-… with a "Reveal in Finder" follow-up. Same artifact the watchdog writes at 75 % of cap, but user-initiated so you can capture WHILE growth is happening. Multi-second STW pause + 100 MB-to-3 GB file → explicit-gesture only.

What this doesn't do (and why)

  • Per-allocation attribution ("this session caused these MB") — not possible without AsyncLocalStorage tagging of every async chain, which is infinitely intrusive. Heap snapshot is the right tool for retainer-chain analysis.
  • GC pause frequency / counts — left for a follow-up; would need a PerformanceObserver for 'gc' entries.
  • Continuous statistical sampling profiler (inspector.Session HeapProfiler.startSampling) — Tier 3 from the original sketch; bigger commitment, save for if Tier 1 + snapshot isn't enough.

Implementation surface

  • src/shared/performance/types.ts — extended SystemPerformanceStats with heapSpaces, detachedContexts, nativeContexts, eventLoopDelay, optional rendererHeap. Thick WHY-comments on each field per repo policy.
  • src/main/ipc/performance.ts — extended performance:system-stats to collect everything in one shot (still well under 1 ms/tick). New performance:write-heap-snapshot + performance:reveal-path handlers. Owns a local monitorEventLoopDelay.
  • src/preload/api/performance.ts — exposes the two new IPC methods.
  • src/renderer/src/features/system-perf/useSystemPerfPoller.ts — reads window.performance.memory per tick. Typed null-safe accessor for environments where the Chromium API is absent.
  • src/renderer/src/features/system-perf/ui/SystemPerfPopover.tsx — widened to 520 px, four new sections + capture button. Reuses the shared formatBytes helper from PR Trim dead exports and consolidate duplicated helpers #85.

Test plan

  • npm run test:review-fixes passes
  • npm run build:app succeeds
  • Manual: AGENT_CODE_PERF=1 npm run dev, open badge popover, verify heap-space rows render, detached-contexts shows 0, event-loop p99 has a real number, renderer-heap row appears, capture button writes a file and reveal works.

Stats

5 files changed, +551/-37.

🤖 Generated with Claude Code

…on-demand snapshot

The badge popover used to show one chart (heap + RSS over time) and
three rows of aggregate numbers. That answered "is the heap growing?"
but not "WHICH part of the heap?" or "where do I look next?". The
20→600 MB jumps you see on heavy multi-agent days were observable in
the chart but not attributable.

This PR adds — visible only when the popover is expanded — the
diagnostic vocabulary you'd normally need to drop into Chrome DevTools
for: per-space heap breakdown, leak smoke alarms, main-thread stall
signal, renderer-process heap, and an on-demand snapshot capture.

What was added, with the reasoning:

1. **Per-space heap breakdown** (`v8.getHeapSpaceStatistics()`) shown
   as the top six spaces sorted by used size, with a proportional
   bar. Tells you which v8 space is growing — `old_space` rising =
   retention (a real leak); `new_space` rising = churn but usually
   fine; `large_object_space` rising = giant buffers/strings (often
   the smoking gun on agent-heavy workflows).

2. **Detached + native contexts** from the same `getHeapStatistics()`
   call we already use. `detachedContexts > 0` is one of the
   strongest leak smoke alarms in v8 — colored red when nonzero.
   `nativeContexts` should track BrowserWindow count; surfaced for
   completeness.

3. **Event-loop p99 delay** sampled by a dedicated
   `monitorEventLoopDelay()` owned by the IPC handler. Reset on each
   1 Hz poll so the value is a per-second window, independent of
   PerformanceService's own 5 s probe. Colored green/amber/red at
   16 ms / 50 ms thresholds (the 60 fps budget and a "user notices"
   stall respectively).

4. **Renderer-process heap** via `window.performance.memory`, read
   synchronously in the poller (no IPC bounce — the renderer can
   read its own JS heap directly). Lets you tell which process is
   actually leaking when the main badge is fine but the app feels
   slow.

5. **On-demand heap snapshot button** that calls
   `v8.writeHeapSnapshot()` to STATE_DIR/heap-snapshots/manual-…
   and offers a "Reveal in Finder" follow-up. Same artifact the
   watchdog auto-writes at 75 % of cap, but user-initiated for the
   case you want to inspect the heap WHILE the growth is happening,
   not after it crosses the watchdog threshold. Multi-second STW
   pause and a 100 MB-to-3 GB file, so explicit-user-gesture only.

Implementation surface:
- `src/shared/performance/types.ts` — extended `SystemPerformanceStats`
  with `heapSpaces`, `detachedContexts`, `nativeContexts`,
  `eventLoopDelay`, optional `rendererHeap`. Thick WHY-comments on
  each field per repo policy.
- `src/main/ipc/performance.ts` — extended `performance:system-stats`
  to collect the new fields in one shot (still well under 1 ms per
  tick). Added `performance:write-heap-snapshot` and
  `performance:reveal-path` handlers. Owns a local
  `monitorEventLoopDelay` so the IPC view of p99 isn't disturbed by
  PerformanceService's probe.
- `src/preload/api/performance.ts` — exposes the two new IPC methods.
- `src/renderer/src/features/system-perf/useSystemPerfPoller.ts` —
  reads `window.performance.memory` per tick and stamps it onto each
  buffer entry. Typed accessor returns null when the Chromium API is
  absent (e.g. tests) so the popover hides the row instead of showing
  zeros.
- `src/renderer/src/features/system-perf/ui/SystemPerfPopover.tsx` —
  widened to 520 px, added four new sections + the capture button.
  Reuses the shared `formatBytes` helper landed in PR #85.

Net: 5 files changed, +568/-36. Tests pass; build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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