fix: tear down the component context tree on close, freeing it by refcount#52
Merged
Conversation
…count Closures created during rendering (use_state setters, event handlers and the event handler exception wrapper) capture their component context and the render context strongly. They must: a setter can be the only reference keeping its component context alive (cf61378 - weak references lost live state). But the same closures are stored in the hook state and on widget callback dispatchers (where event handlers deliberately stay after close, to avoid syncing _events right before the comm closes), so a closed render tree is one big reference cycle that refcounting cannot free: it waits for a generation-2 gc. On a solara server this shows up as a memory sawtooth whose peaks scale with per-kernel state (measured up to 231MB vs a 174MB peak once the tree is refcount-clean, see solara's docs/memory-usage-inspection.md). Instead of weakening the captures, close() now empties every component context (snapshotted before _remove_element detaches them) and drops the render context's own element/container/root-widget references. Whoever still holds a setter or handler keeps only a small hollow context alive; set_ and force_update are no-ops once _closing is set (set_ checks it first now, before touching the emptied state dict). The containers are replaced rather than cleared, since state_get() hands out the live state dicts (test_state_get re-renders with them after close). The new test asserts a closed tree is freed with gc disabled; 3 of its 4 parametrizations fail without this fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
954fefe to
d0c6765
Compare
Contributor
Author
|
Design changed after checking history (thanks Maarten for the pointer): the first push used weakrefs in the closures, which is exactly what cf61378 reverted in Nov 2023 ("a setter might be the only reference" — weak captures sometimes lost live state in solara). The current version keeps strong captures and instead empties the component context tree at |
Contributor
Author
maartenbreddels
added a commit
to widgetti/solara
that referenced
this pull request
Jul 5, 2026
…tate (#1178) The handbook still described the reacton setter-closure cycle as unfixed and claimed disabling the gc backstop brings the sawtooth back; since widgetti/reacton#52 (context tree teardown on close) both are outdated. Re-measured everything against the merged masters: the kitchen-sink app plateaus at ~168 MB (peak 178) with gc fully disabled, so refcounting alone now frees closed kernels. Also records why weak references were not an option (reacton cf61378: a setter can be the only reference keeping its component context alive). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closed render trees could not be freed by reference counting:
use_statesetters,reacton.ipyvue.use_eventhandlers, and_event_handler_exception_wrapperall capture the_RenderContextandComponentContextstrongly in closures that live in the hook state and on widgetCallbackDispatchers (where event handlers deliberately stay after close, to avoid syncing_eventsright before the comm closes). That makes the whole closed tree one reference cycle —rc → hook state / widgets → closure → rc— which only a generation-2 gc can free.On a solara server this shows up as a memory sawtooth whose peaks scale with per-kernel state: closed kernels (including e.g.
use_memopayloads) pile up until the collector's own schedule kicks in. Measured with a task-heavy test app: idle footprint bounced up to 231 MB; with the tree refcount-clean it converges to a ~170 MB plateau (peak 174 MB). Details, the measurement harness, and the gc-disabled diagnostic that found these exact chains: widgetti/solara#1177 (docs/memory-usage-inspection.md) and widgetti/solara#1176.The fix
Capture both contexts via
weakref.refin the three closure factories. Behavior during the tree's life is unchanged (the render context keeps the tree alive, so the weakrefs always resolve). After the tree is collected the closures become inert:Testing
test_no_reference_cycles_after_close(4 parametrizations, incl. ause_state+on_clickcomponent): asserts a closed tree is freed with gc disabled. Red on master (3/4 fail) → green with the fix. Passes withREACTON_FAST=1too (make_setterand the wrappers are shared with the fast render context).generate_test.py/test_set_state_with_dataframeare pre-existing on master in the same environment (pandas/codegen version drift).reactive/computed/task/use_task+ a 4.5 MBuse_memopayload is fully freed by refcounting after the page closes, with gc disabled.🤖 Generated with Claude Code