Skip to content

perf: improve GC scheduling for agent sessions — missed-GC catch-up #80

Description

@jdmanring

Problem

QtWebEngineProcess (and any embedded Chromium) accumulates Oilpan/Blink detached DOM
nodes during agent sessions with rapid tool-call rounds, even with the 2.5s post-response
gc() call in place.

Root cause: _gcPending lockout in rapid agent loops

The 5000ms lockout window means that when 4 rapid tool responses complete in succession,
only the first gets a GC cycle. Responses 2–4 accumulate Oilpan garbage with no
collection:

T=0s   Response 1 finishes → gc() dispatched → _gcPending = true
T=2s   Response 2 finishes → gc() BLOCKED (_gcPending)
T=4s   Response 3 finishes → gc() BLOCKED
T=6s   Response 4 finishes → gc() BLOCKED
T=5s   _gcPending = false  ← no catch-up fires; 3 responses' garbage stranded

There is no mechanism to detect that GC was blocked or to collect the missed cycles.

Secondary: element-node in-place patch uses innerHTML unnecessarily

streamingRenderer.js in-place patch uses _tailNodes[i].innerHTML = newNodes[i].innerHTML
for element-node updates, which destroys and recreates child nodes even when both old and
new have a single text child. The simpler firstChild.data = assignment suffices and
avoids one detach+create cycle per token for plain-text elements.

Solution

A: Missed-GC catch-up flag — reduce lockout 5000ms → 3000ms; add _gcMissed flag;
when GC completes and _gcMissed is true, immediately dispatch one catch-up GC cycle and
log [GC] catch-up dispatched. A run of N rapid responses gets at most 2 GC cycles
(primary + catch-up) rather than 1.

B: Element-node text-data patch — for element nodes where both live and new have a
single text-node child, use firstChild.data = instead of innerHTML =. Adds _rtSaved
counter logged in finalize() alongside the existing _rtCalls/_rtFast metrics.

Context

This primarily affects embedded Chromium (QtWebEngine, Electron) where OS memory pressure
signals are not wired to Oilpan's coordinator, so Oilpan never spontaneously runs a major
collection. gc({ type: 'major', execution: 'async' }) via --expose-gc is the only
reliable mechanism — making it fire reliably between agent rounds is critical for
long-running sessions.

[GC] blocked — catch-up queued and [GC] catch-up dispatched log lines route through
javaScriptConsoleMessage into wrapper_system.log for diagnostic visibility.

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