#1389·serena

Memory leak in dashboard frontend

Author: opcode81Created Apr 21, 2026Updated Sep 17, 2026

Follow-up: full-memory dump analysis of two Serena-hosted WebView2 renderers

I ran a comparison between a "small" (fresh) and a "big" (~22 h old) Serena dashboard renderer on this box, using full-memory dumps captured with procdump -ma. No dump data is attached (project code could be in the address space). Only aggregate counts and inferences below, all reproducible from a fresh install.

Targets

Both processes are Serena-hosted WebView2 renderers identified by --webview-exe-name=python.exe + tmpXXXX\EBWebView user-data-dir:

Process Working set Private bytes Full dump size Age at capture
Fresh renderer 122 MB 97 MB 528 MB ~38 min
Aged renderer 1,867 MB 2,152 MB 2,650 MB ~22.4 h

String-content comparison

Extracted with Sysinternals strings64 -a -n 20:

Metric Fresh Aged Δ
String count (≥ 20 chars) 312,332 724,114 2.3×
Total text bytes 22 MB 261 MB 11.9×
Mean string length 74 B 370 B 5.0×

The aged renderer holds ~5× longer strings on average — the signature of accumulated long JSON/log payloads rather than just more short tokens.

What actually accumulated

Top recurring long strings in the aged dump (count × exact response body):

Count Response body Endpoint (inferred)
36,884 {"last_execution":null,"status":"success"} /last_execution
34,458 {"active_project":{...},"active_tools":[...],"available_contexts":[...],...} (~4 KB) /get_config_overview
11,987 {"queued_executions":[],"status":"success"} /queued_task_executions (empty)
~7,457 unique task_id values, each appearing dozens of times {"queued_executions":[{"finished_successfully":false,...,"task_id":N}],...} /queued_task_executions (non-empty)

URL string counts in the aged dump (polling surface):

1222  http://127.0.0.1:24286/heartbeat
 593  http://127.0.0.1:24286/dashboard/index
 309  http://127.0.0.1:24286/last_execution
 306  http://127.0.0.1:24286/queued_task_executions
  88  http://127.0.0.1:24286/heartbeatfstatus
   8  http://127.0.0.1:24286/get_config_overview

The fresh renderer shows the same pattern at lower counts — 1,006 /last_execution bodies, 597 config blobs, 803 empty-queue bodies. The leak is present from the first few minutes of life and grows linearly.

Poll-rate inference

From 36,884 /last_execution bodies over ~80,700 seconds of uptime: ~1 poll every 2.2 s for that endpoint. The config-overview endpoint shows ~1 poll every 3.8 s from the same math applied to its count. Matches the URL counts above as a sanity check.

Memory math

  • Directly attributable retained response bodies: ~34K × 4 KB (config blob) + ~37K × 42 B (last_execution) + ~7.5K × 280 B (task_id variants, ~5 copies each) ≈ ~150 MB of serialized JSON
  • Renderer growth observed: ~1.7 GB (122 → 1,867 MB WS)
  • Typical Chromium/V8 heap multiplier on parsed JSON (hidden classes, string interning, object overhead): 5–10×
  • 150 MB serialized × ~10× multiplier ≈ 1.5 GB → matches the 1.7 GB growth almost exactly

The renderer appears to be parsing every polled response into the V8 heap and retaining references so the parsed objects never get garbage-collected.

What this means for this issue

The process-lifecycle leak that this issue (#1387) originally describes is the amplifier, not the sole root cause. Even a fully cleaned-up dashboard — where orphans are reliably terminated on client disconnect — would still grow at approximately 30–100 MB of renderer RAM per hour of active dashboard life, purely from JS-side retention of polled HTTP responses.

Two distinct bugs compound in the observed behavior:

  1. Frontend retention bug (new). The dashboard's JS retains every response from at least /last_execution, /get_config_overview, and /queued_task_executions. This is most likely a state-store / closure / DOM-append pattern in the dashboard app that never prunes. Fix is frontend-side.

  2. Process lifecycle bug (the original scope of this issue). Orphaned Serena Python processes keep their pywebview subprocesses alive, which keep polling and growing indefinitely. Fix is in the Serena agent / _start_dashboard_viewer cleanup.

Suggested debugging hints for maintainers (I won't prescribe fixes)

  • Open the dashboard in a plain browser (no pywebview), leave it open for 10 minutes, and check Chrome DevTools → Memory → Heap snapshot. Expect a long list of polling response objects retained under the page root.
  • The polled endpoints all respond with JSON that's safe to retain nothing from (they're status queries). A simple fix would be to discard the response after updating the UI, and ensure the UI renders by replace rather than append.
  • If the frontend is using a reactive framework with a state store (Vue/Svelte/React/Alpine), check whether responses are being pushed into a list-shaped state field. The duplicate-count pattern (identical strings appearing tens of thousands of times) is consistent with a growing array of historical responses.

Offer

If a Chrome DevTools heap snapshot from a fresh dashboard session would help narrow this down, I can capture one with web_dashboard: true reenabled temporarily and share the heap-snapshot JSON (no project data, just dashboard state). Let me know.

Originally posted by @scarson in #1387