Skip to content

fix(tui): keep render owner subscriptions weak - #220

Merged
Kushalkhemka merged 1 commit into
opensec-git:pi-boltfrom
Shrub24:fix/weak-render-owners
Oct 31, 2026
Merged

Kushalkhemka merged 1 commit into
opensec-git:pi-boltfrom
Shrub24:fix/weak-render-owners

Conversation

@Shrub24

@Shrub24 Shrub24 commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

What and why

After a long session, /new can leave old rendered message trees in memory even after GC. A shared component that survives the session switch retains its former rendering parents through renderOwners, including their children and cached output. Repeated render-tree rebuilds can therefore accumulate objects that are no longer mounted.

This also affects extension-provided UI if it reuses a child component across discarded parents. It is not a general leak of session-scoped extension state; the fix removes this specific render-registry retaining path. In one observed session an 800mb heap peak of retained renderer components accumulated through multiple new sessions; with this fix it returns to baseline on /new.

The mechanism:
renderOwners uses weak keys but strong owner values. As long as a shared child remains live, it retains discarded parents and their cached render trees. For example, repeatedly rendering a cached Text inside new Box instances keeps those boxes alive after the application drops them.

Store both singleton and multiple-owner subscriptions as WeakRefs, pruning dead registrations during registration, removal and change notification. Live parents still receive updates; discarded parents no longer need explicit teardown to become collectible. The public API is unchanged.

The TUI README documents the ownership rule. This leaves the separate Container.clear() render-cache behavior unchanged.

How it was tested

  • An isolated Node process with --expose-gc verifies collection of one singleton owner and 100 owners of a shared child after an event-loop turn. It also checks that a surviving cached ancestor receives the next change. The collection test fails with the original implementation and passes with this fix.
  • A shared-child test checks updates through multiple live parents and their ancestors.
  • All 47 focused render-owner, render-memo and frame-report tests pass on the rebased branch. The full TUI suite passed before the rebase; upstream made no TUI changes between the two bases.
  • npm run check passes on the rebased branch.

The full monorepo suite has not been rerun for this change. No interleaved bench/ speed or memory measurements have been collected; the regression measures collectibility, not RSS savings or rendering overhead.

Checklist

  • npm run check is clean and ./test.sh passes (check passes; full suite not rerun)
  • A test fails without this change
  • Speed or memory may change: before/after numbers from bench/ are above
  • Engine or build change: patches regenerated, tests/aot/run.sh and tests/pi/run.sh pass on a build with it (not applicable)
  • No CHANGELOG.md edits

A long-lived shared child retained every discarded parent through the change-notification registry, including their rendered subtrees. Store all owners weakly and prune expired registrations without changing notification behavior. Explicit teardown alone cannot handle parents discarded without detaching their children.
@Kushalkhemka
Kushalkhemka merged commit 7720456 into opensec-git:pi-bolt Oct 31, 2026
27 of 29 checks passed
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.

2 participants