perf: reduce memory footprint across real workflows (idle agent reclaim, poll churn, worker pools)#3149
Conversation
Adds scripts/bench-memory.mjs (process-tree RSS sampler with per-category breakdown + renderer heap over CDP) and scripts/bench-memory-run.mjs (full lifecycle: launch, drive a thread workflow with cheap agent turns via playwright-core over CDP, sample idle + post-workflow, tear down). bootstrap.ts now honors POSTHOG_CODE_CDP_PORT (matching electron-cdp.mjs) so the bench can run beside a Chrome that owns :9222. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each pooled @pierre/diffs worker is a full V8 isolate with shiki grammars loaded (~12MB RSS each, eagerly spawned per provider). Benchmarked -67MB total app RSS with no visible highlighting latency change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A mounted task view heartbeated agent.recordActivity every 5 minutes forever, and reconcileLocalConnection auto-respawned any idle-killed session immediately — so the visible task always pinned a live claude-cli process (~340MB RSS) plus its stdio MCP servers, even overnight. Gate both on user presence (any window input in the last 10 minutes): when the user walks away the heartbeat stops, the existing workspace-server 15-minute idle kill reclaims the agent process, and the existing idle-kill reconcile path restores the session seconds after the user returns. Agents mid-prompt or with in-flight MCP calls are never killed (killIdleSession already refuses). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- agentChatStore: retain at most 2000 AcpMessages per live chat (a streaming chat appends one per chunk, unbounded on long sessions) - freeformChatStore: retain at most 50 edit-history versions (each holds a full copy of the canvas source + context) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…free parked terminal GPU contexts - useUserPresence: input only counts while the window has focus (a stray mouse-over of an unfocused window is not use); VITE_PRESENCE_IDLE_MS override and pause/resume logging so the suspend/reclaim/reconnect loop is verifiable in minutes. - AgentService: POSTHOG_CODE_AGENT_IDLE_TIMEOUT_MS override (dev/test). - TerminalManager: dispose the WebGL renderer when a terminal is parked offscreen and reload it on attach — a parked terminal otherwise holds a live GPU context indefinitely (Chromium drops the oldest past 16). Verified end-to-end with 60s presence / 180s server idle: agent process reclaimed at t+177s (total RSS 1500MB -> 1168MB, node children 0), and focus + click auto-reconnected the session with a fresh agent in ~15s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Idle-network profiling (scripts/bench-memory-run.mjs --scenario switch) showed task polling was 96% of idle traffic (~5.7MB/min): full task payloads (descriptions + latest_run blobs) refetched every 30s by three pollers. Slack-origin decoration (icons + thread links) polled 2.2MB responses for data that is immutable per task. - full task lists: 30s -> 60s (lookup consumers; sidebar freshness comes from the slim summaries poll, which stays at 30s) - slack-origin list: 30s -> 5min (origin never changes; new tasks appear within the window or on window focus) - bench harness: add --scenario switch|longout|thread drivers Halves idle churn in show-all-users profiles (~3.1MB/min) and cuts it ~6x for default profiles (~0.9MB/min). Renderer RSS growth after visiting 6 tasks dropped from ~19MB/min to ~9MB/min. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--scenario switch|longout|thread; thread/longout navigate to the dedicated bench task by id and refuse to send turns into any other task. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
|
Reduces PostHog Code's memory footprint in real user workflows, driven by an autonomous measure → change → verify loop against the live dev app. Users report the app hogging memory and CPU; this PR attacks the verified causes rather than guesses.
TL;DR results
claude-cli+ ~75MB MCP servers forever (activity heartbeat + auto-reconnect never let the existing 15-min idle kill fire)useUserPresence)latest_runblobs every 30s ×3 pollers), driving renderer RSS up ~19MB/min with flat JS heap — also a big slice of the reported CPU drain@pierre/diffsspawns 8 shiki highlighter workers per pool by default (each a full V8 isolate with grammars loaded); two pools mountpoolSize: 2at both providersAcpMessageper stream chunk), canvas edit history (full source copy per version)Research method
Everything was measured on the real dev app (live tRPC, workspace-server, real agent backend) — no synthetic fixtures.
Instrumentation (included in this PR):
scripts/bench-memory.mjs— samples RSS across the app's entire process tree via the CDP listener pid, classifies each process (main / renderer / GPU / utility / workspace-server / agent children), and reads renderer JS heap over CDP. Emits median steady-state totals.scripts/bench-memory-run.mjs— owns the full benchmark lifecycle for comparability: fresh app launch per run (POSTHOG_CODE_CDP_PORT, added inbootstrap.ts, since Chrome typically owns :9222), drives a scenario over CDP withplaywright-core, samples idle + post-workflow, tears down. Scenarios:thread(N cheap real agent turns — "reply with exactly "),switch(visit 6 sidebar tasks with dwell — the realest daily workflow),longout(one turn with a ~40KB bash tool output). Thread scenarios navigate to a dedicated bench task by id and refuse to send turns into any other task.VITE_PRESENCE_IDLE_MS(renderer) andPOSTHOG_CODE_AGENT_IDLE_TIMEOUT_MS(workspace-server) — the full suspend → reclaim → return → reconnect cycle is verifiable in ~4 minutes with the window defocused instead of ~25.Protocol: pinned metric (post-workflow steady-state total RSS, median over 30s), one change per cycle, keep/discard by measurement plus guards (
pnpm typecheck, scoped vitest suites,biome lint packages/core, host-boundary check). Measured noise floor: ±35–50MB run-to-run (each bench run also grows the bench thread's transcript, drifting the baseline upward — treated sub-noise deltas as neutral).Key diagnostics that found the issues:
--resumeand ~340MB — before any user action.created_by+ 2.2MB slack-origin/all-users, each every 30s,limit=500— ~630KB of which is 205KB task descriptions + 320KBlatest_runblobs per response).Structural floors (measured, not worth chasing client-side): Electron main ~310MB RSS with only ~60MB JS heap; GPU ~130MB; network/utility ~78MB; renderer ~590MB dev / ~340–380MB prod with ~145MB JS heap.
Verification
@posthog/uifull suite green (incl. 7 new presence tests),@posthog/core1970/1971 (the 1 failure is a locale-dependent billing test that fails identically onmain), workspace-server agent suite 63/63 (archive integration failures identical tomain),check-host-boundariesclean.Follow-ups (found, not implemented — product calls)
/tasks/summaries/lacksorigin_product/creator fields; adding them cuts the heaviest remaining poll ~20×.claude-cli --resumeat every launch before any user action (~340MB for up to 25 min; booting on a cloud task vs a local task swings idle RSS by ~450MB). Needs a "cold session" state that paints the transcript from logs and connects on first prompt.sessionStore.eventsgrows unbounded for the live viewed session during long streaming runs (residency eviction only covers backgrounded sessions).🤖 Generated with Claude Code
https://claude.ai/code/session_014cVSjw1BsiioCeA5Fhhavz