Skip to content

perf: reduce GC pressure — prune teardown, idle yield, squashOutsideCode fast path, deferred hljs highlight, background stream cleanup #82

Description

@jdmanring

Problem

Six targeted GC/memory improvements identified through a full audit of the GC stack (chat.js, chatHistory.js, markdown.js, qt_wrapper.py):

  1. Missing idle GC signal after _evictLive and _pruneBottom_pruneTop already yields with requestIdleCallback(() => {}, { timeout: 3000 }) after detaching subtrees. _evictLive and _pruneBottom create detached subtrees but never yield, leaving V8/Oilpan without the idle hint needed to trigger incremental collection.

  2. Missing timer/renderer teardown in _pruneTop and _pruneBottom_evictLive correctly clears _waveInterval, _elapsedTicker, and _streamRenderer on every removed element and its descendants before .remove(). _pruneTop and _pruneBottom only call hljsDeferForgetNode, leaving live timers on detached nodes.

  3. squashOutsideCode allocates on every render frame — called at ~30 fps during streaming. For the common case (plain text, no code blocks) it allocates a split array and join string that are immediately discarded. A str.includes('```') guard short-circuits the whole allocation.

  4. 7 direct window.hljs.highlightElement calls bypass deferHighlightAlldeferHighlightAll (already imported) uses IntersectionObserver to highlight only visible code blocks. 4 of the 7 sites highlight off-screen blocks synchronously, blocking the main thread on large responses.

  5. _purgeStaleBackgroundStreams called only on chat submit — completed/error background stream Map entries persist across session switches and accumulate until the next submit. checkBackgroundStream is invoked on every session switch and is the correct place to run the purge.

Fix

  • A: Add requestIdleCallback(() => {}, { timeout: 3000 }) at the end of _evictLive and inside the if (removed > 0) block of _pruneBottom.
  • A+: Add full timer/renderer teardown (_waveInterval, _elapsedTicker, _streamRenderer + descendant walk) before each .remove() call in _pruneTop and _pruneBottom, mirroring the pattern already in _evictLive.
  • B: Add if (!str.includes('```')) { return str.replace(...); } fast path to squashOutsideCode in markdown.js.
  • C: Replace 7 window.hljs.highlightElement forEach loops in chat.js with deferHighlightAll(container).
  • F: Call _purgeStaleBackgroundStreams() at the top of checkBackgroundStream in chat.js.

Files

  • static/js/chatHistory.js — Changes A, A+
  • static/js/markdown.js — Change B
  • static/js/chat.js — Changes C, F

Tests

15 new static-analysis tests across 4 files:

  • tests/test_chat_history_js.py (+4)
  • tests/test_markdown_squash_js.py (new, 3)
  • tests/test_chat_hljs_defer_js.py (new, 3)
  • tests/test_chat_gc_hint_js.py (+1)

Note: V8/Chromium flag changes (--initial-old-space-size, --optimize-for-size, --minor-mc, --renderer-process-limit=1, --disable-extensions) are tracked separately on the Qt wrapper branch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions