Skip to content

fix(gui): stop detail-pane render storm that froze traffic exploration - #5

Open
plutack wants to merge 2 commits into
mainfrom
fix/gui-detail-render-storm
Open

fix(gui): stop detail-pane render storm that froze traffic exploration#5
plutack wants to merge 2 commits into
mainfrom
fix/gui-detail-render-storm

Conversation

@plutack

@plutack plutack commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Problem

Exploring captures — rapidly clicking rows in the All-traffic view (e.g. capture 5 → 6 → 5 → 5) — froze the GUI, and large bodies eventually crashed the WebKit web process (JS heap exhaustion; confirmed by a user-session WebKitWebProcess SIGABRT coredump with 71 MB core).

Root causes (four, compounding)

  1. Poll re-rendered everything: the 2s poll loop installed fresh array identities into state unconditionally, re-rendering the whole tree — including any open detail pane — even when nothing changed.
  2. BodyViewer redid heavy work per render: prettyBody (JSON.parse + stringify), regex JSON highlighting, and a full innerHTML swap ran on every render (the dangerouslySetInnerHTML object identity was new each time). For an N-byte body this rebuilt ~4× N of strings/DOM every 2 seconds.
  3. Auto-export per click: the ExportSnippet panel embedded in every capture detail fired a snippet conversion on each selection change — each spinning a goja JS VM + httpsnippet eval on the Go side. Rapid clicks piled these up.
  4. No click coalescing: every rapid click fetched the full body payload and rendered each response on arrival; bodies were also shipped twice (UTF-8 + base64).

Fixes

  • Loaders bail out via functional setState when a row fingerprint is unchanged (webhooks by project/seq, captures by id:status, sessions by id:count:ended) — Preact skips the re-render entirely.
  • BodyViewer memoizes all transforms; pretty-print/highlight are skipped above 1 MiB with an explanatory notice; innerHTML prop identity is stable.
  • ExportSnippet conversion debounced 250 ms; snippet <pre> uses a memoized HTML object.
  • Detail fetches deduplicated per row while in flight; superseded responses dropped (only the last clicked row renders). GetCapture ships base64 only.

Validation (live, Linux/webkit2gtk)

  • A 5 MiB JSON body that killed the app outright on the current release now opens fine and stays interactive.
  • Idle polling with a detail open: RSS dead-flat between real data changes (previously a 30–60 MB sawtooth every 2 s with a mere 265 KB body).
  • Hammer: 12 rapid clicks across 1 MiB-body rows — transient RSS spike, full GC recovery, clicks processed throughout, no crash.

Exploring captures (rapidly clicking rows in the All-traffic view)
froze the UI, and large bodies eventually crashed the WebKit web
process (JS heap exhaustion; confirmed by a user-session WebKitWeb-
Process SIGABRT coredump). Four compounding causes, all fixed:

- The 2s poll loop always installed fresh array identities into
  state, re-rendering the entire tree — including any open detail
  pane — even when nothing changed. Loaders now bail out via
  functional setState when a row fingerprint is unchanged.

- BodyViewer re-ran prettyBody (JSON.parse + stringify), regex
  syntax highlighting, and a full innerHTML swap on every render,
  because dangerouslySetInnerHTML got a new object identity each
  time. All transforms are now memoized, pretty-print/highlight
  are skipped above 1 MiB (with an explanatory notice), and the
  innerHTML prop identity is stable.

- Every capture detail embedded an ExportSnippet that auto-fired a
  snippet conversion on each selection change — one goja JS VM +
  httpsnippet eval per click. Conversion is now debounced 250ms and
  the snippet <pre> no longer re-assigns innerHTML per poll.

- Rapid row clicks each fetched the full body payload and rendered
  every response on arrival. Detail fetches are now deduplicated
  per row while in flight, and superseded responses are dropped.
  GetCapture also stopped shipping bodies twice (UTF-8 + base64);
  base64 alone halves the response the webview has to parse.

Validated live on Linux/webkit2gtk: a 5 MiB body that killed the
app outright now opens fine, and 12 rapid clicks across 1 MiB-body
rows settle cleanly (transient RSS spike, full GC recovery, UI
responsive throughout). Idle polling no longer rebuilds the detail
DOM: RSS is flat between real data changes.
Memoization removed the repeated cost but the FIRST render of a
multi-megabyte body still blocked the webview for 0.5-2s: the whole
body was base64-decoded per byte, escaped with three regex passes,
and materialized as DOM text. Measured on a 4.8 MB JSON body, one
open plus a few row switches cost the shipped v0.2.0 ~7.1s of
webview CPU and ~1 GB of RSS growth; the same interaction now costs
~0.5s with flat RSS.

The viewer now decodes and renders at most DISPLAY_LIMIT (256 KiB)
up front, with a notice and a 'Show entire body' escalation. Copy,
Save, and Image still operate on the full payload, decoded on
demand. escapeHTML is a single pass, and the base64 prefix is cut on
a 4-character boundary so the slice always decodes cleanly.
@plutack

plutack commented Aug 22, 2026

Copy link
Copy Markdown
Owner Author

The memoization killed the repeated cost, but opening a multi-MB body still blocked the webview for 0.5–2s per open — the entire body was base64-decoded byte-by-byte, escaped with three regex passes, and materialized as DOM text. Rapid row switching = that cost per distinct row.

7068df8 windows the preview: at most 256 KiB is decoded and rendered up front, with a notice and a Show entire body escalation; Copy/Save/Image decode the full payload on demand; escapeHTML is single-pass; the base64 prefix is cut on a 4-char boundary.

Measured, shipped v0.2.0 vs this branch — same 4.8 MB JSON body, same click program (open big row + rapid row switching)

build webview CPU RSS
v0.2.0 ~7.1 s 241 MB → 1.2 GB
this PR ~0.5 s flat (226 MB)

The 1 GB RSS balloon is the sluggishness (GC/swap thrash) and the direct precursor to the WebKit heap-exhaustion crashes.

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