Skip to content

Fix/container gc retention#9918

Merged
kirangadhave merged 6 commits into
marimo-team:mainfrom
edwardwkrohne:fix/container-gc-retention
Jul 21, 2026
Merged

Fix/container gc retention#9918
kirangadhave merged 6 commits into
marimo-team:mainfrom
edwardwkrohne:fix/container-gc-retention

Conversation

@edwardwkrohne

@edwardwkrohne edwardwkrohne commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

📝 Summary

Closes #9880

Widgets created as

get_state, set_state = mo.state(False)
wgt = mo.ui.checkbox(on_change=set_state).style()
wgt

would never fire their set_state methods. The issue is that style returns

    return Html(h.div(children=as_dom_node(item).text, style=style_str))

which drops the actual python object item, preventing the corresponding frontend ui object from actually interacting with the backend.

Now, style is a class that derives from a new class ContainerHtml, which explicitly contains strong references to a list of children to prevent them from being garbage collected. The offending code survives as

    def _build_text(self) -> str:
        return h.div(children=self._children[0].text, style=self._style_str)

which uses the explicitly stored child self._children[0] instead of a local. Similar treatment has been given to accordion, callout, carousel, tabs, and sidebar, all of which could potentially drop their children by converting to text and losing the original.

tabs was slightly different in that it was already a UIElement, which would have been incompatible with the implementation of the new ContainerHtml class, so it was modified to explicitly store its own children.

sidebar was reparented to ContainerHtml and stores its children that way.

_BlockWrapped was also reparented to ContainerHtml.

📋 Pre-Review Checklist

  • For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on Discord, or the community discussions (Please provide a link if applicable).
  • Any AI generated code has been reviewed line-by-line by the human PR author, who stands by it.
  • Video or media evidence is provided for any visual changes (optional).

✅ Merge Checklist

  • I have read the contributor guidelines.
  • Documentation has been updated where applicable, including docstrings for API changes.
  • Tests have been added for the changes made.

Review in cubic

@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment Jul 21, 2026 8:22am

Request Review

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@edwardwkrohne

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a widget interactivity regression caused by container-like outputs “freezing” child HTML and dropping strong references to UI elements, allowing them to be garbage collected (and thereby removed from the weak UI registry).

Changes:

  • Introduces ContainerHtml as a base Html wrapper that retains strong references to child Html objects and re-renders children live via .text.
  • Refactors style, accordion, callout, carousel, and sidebar to use ContainerHtml (and updates _BlockWrapped), preventing dropped-child GC retention bugs.
  • Adds regression tests that explicitly verify UI registry retention and live re-render behavior across the affected components.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
marimo/_output/hypertext.py Adds ContainerHtml and re-parents _BlockWrapped to retain strong child refs and render live.
marimo/_plugins/stateless/style.py Refactors .style() implementation to a ContainerHtml wrapper so it keeps the wrapped object alive.
marimo/_plugins/stateless/accordion.py Refactors accordion to retain strong refs to rendered tab content/labels and render live.
marimo/_plugins/stateless/callout.py Refactors callout to a ContainerHtml wrapper that retains its child.
marimo/_plugins/stateless/carousel.py Refactors carousel to retain strong refs to items and render live.
marimo/_plugins/stateless/sidebar.py Re-parents sidebar to ContainerHtml and retains its processed item to prevent GC.
marimo/_plugins/ui/_impl/tabs.py Makes tabs retain strong refs to rendered tab contents to avoid registry reaping.
tests/_output/test_hypertext.py Adds targeted tests for ContainerHtml retention + live rendering behavior.
tests/_plugins/stateless/test_style.py Adds regression coverage that .style() keeps UI elements registered after GC.
tests/_plugins/ui/_impl/test_tabs.py Adds regression coverage that mo.ui.tabs keeps UI elements registered after GC.
tests/_plugins/stateless/test_sidebar.py Updates assertions to use .text and adds multiple GC-retention + live-update regression tests.
tests/_plugins/stateless/test_accordion.py Adds GC-retention + live-update regression tests for accordion (incl. lazy).
tests/_plugins/stateless/test_callout.py Adds GC-retention + live-update regression tests for callout.
tests/_plugins/stateless/test_carousel.py Adds GC-retention + live-update regression tests for carousel.

Comment on lines +41 to +52
# Initialize combined_style with style dict if provided,
# otherwise empty dict
combined_style = style or {}

# Add kwargs to combined_style, converting snake_case to kebab-case
for key, value in kwargs.items():
kebab_key = key.replace("_", "-")
combined_style[kebab_key] = value

self._style_str = ";".join(
[f"{key}:{value}" for key, value in combined_style.items()]
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address this

@kirangadhave kirangadhave added the bug Something isn't working label Jun 30, 2026
@kirangadhave

kirangadhave commented Jun 30, 2026

Copy link
Copy Markdown
Member

@edwardwkrohne Please read and sign the CLA https://github.com/marimo-team/marimo/blob/main/CONTRIBUTING.md#your-first-pr

nvm you already did



class _BlockWrapped(Html):
class ContainerHtml(Html):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is identical to

class _FlexContainerHtml(Html):
"""Html produced by hstack/vstack; used to preserve justify in nested stacks.
When this is a child of another stack with widths/heights, the wrapper
is made a flex container so the nested stack's flex and justify work.
Non-flex children use a block wrapper so they fill the space.
Children are stored as live references so that mutable Html elements
(e.g. mo.status.spinner) are re-rendered on every flush rather than
being frozen at construction time.
"""
def __init__(
self,
style: str | None,
live_children: list[Html],
child_flexes: Sequence[float | None] | None,
) -> None:
self._style = style
self._live_children = live_children
self._child_flexes = child_flexes
super().__init__(self._build_text())
def _build_text(self) -> str:
def _item_style(idx: int) -> str | None:
if self._child_flexes is None:
return ""
child_flex = self._child_flexes[idx]
if child_flex is None:
return ""
item_style: dict[str, str | int | float | None] = {
"flex": f"{child_flex}"
}
# Only make the wrapper a flex container for nested stacks so their
# flex: 1 and justify work. Leaf content (e.g. mo.stat) fills the
# wrapper when it is a block.
if isinstance(self._live_children[idx], _FlexContainerHtml):
item_style["display"] = "flex"
item_style["min-width"] = "0"
item_style["min-height"] = "0"
return create_style(item_style)
if self._child_flexes is None:
grid_items = [c.text for c in self._live_children]
else:
grid_items = [
h.div(c.text, style=_item_style(i))
for i, c in enumerate(self._live_children)
]
return h.div(grid_items, style=self._style or "")
@property
def text(self) -> str: # type: ignore[override]
"""Re-render children live on every access."""
return self._build_text()

I will consolidate both classes in a follow up. @edwardwkrohne you can ignore this comment.

Comment on lines +41 to +52
# Initialize combined_style with style dict if provided,
# otherwise empty dict
combined_style = style or {}

# Add kwargs to combined_style, converting snake_case to kebab-case
for key, value in kwargs.items():
kebab_key = key.replace("_", "-")
combined_style[kebab_key] = value

self._style_str = ";".join(
[f"{key}:{value}" for key, value in combined_style.items()]
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address this

Comment thread marimo/_plugins/stateless/accordion.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accordion is not a class not a function, so the Returns: part of doc string doesn't make sense here. Please remove this.

Also verify on all functions -> class conversions in the PR.
callout, style, carousel, accordion,

…out, carousel, and sidebar, all of which are now classes instead of functions.

@kirangadhave kirangadhave left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 65.9kB (0.26%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
marimo-esm 25.4MB 65.9kB (0.26%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: marimo-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/cells-*.js 7.85kB 724.76kB 1.09%
assets/JsonOutput-*.js 1.34kB 573.11kB 0.23%
assets/index-*.js -1.13kB 431.87kB -0.26%
assets/index-*.css -158 bytes 368.03kB -0.04%
assets/dist-*.js -127 bytes 276 bytes -31.51%
assets/dist-*.js -152 bytes 183 bytes -45.37%
assets/dist-*.js -87 bytes 169 bytes -33.98%
assets/dist-*.js 79 bytes 183 bytes 75.96% ⚠️
assets/dist-*.js -82 bytes 177 bytes -31.66%
assets/dist-*.js -40 bytes 137 bytes -22.6%
assets/dist-*.js 243 bytes 403 bytes 151.88% ⚠️
assets/dist-*.js -16 bytes 160 bytes -9.09%
assets/dist-*.js -283 bytes 104 bytes -73.13%
assets/dist-*.js -81 bytes 102 bytes -44.26%
assets/dist-*.js 250 bytes 387 bytes 182.48% ⚠️
assets/dist-*.js 74 bytes 176 bytes 72.55% ⚠️
assets/dist-*.js 166 bytes 335 bytes 98.22% ⚠️
assets/dist-*.js -17 bytes 259 bytes -6.16%
assets/dist-*.js 16 bytes 1.47kB 1.1%
assets/dist-*.js 92 bytes 256 bytes 56.1% ⚠️
assets/dist-*.js 60 bytes 164 bytes 57.69% ⚠️
assets/dist-*.js -79 bytes 104 bytes -43.17%
assets/edit-*.js 3.87kB 332.1kB 1.18%
assets/ai-*.js 9.28kB 286.26kB 3.35%
assets/glide-*.js 4.81kB 255.88kB 1.92%
assets/reveal-*.js 518 bytes 250.97kB 0.21%
assets/layout-*.js 1.79kB 205.01kB 0.88%
assets/add-*.js 2.56kB 204.79kB 1.27%
assets/add-*.js 255 bytes 56.02kB 0.46%
assets/cell-*.js -11.23kB 173.48kB -6.08%
assets/dependency-*.js -2 bytes 156.73kB -0.0%
assets/agent-*.js -3.42kB 154.9kB -2.16%
assets/file-*.js 3.29kB 51.6kB 6.82% ⚠️
assets/worker-*.js 1.5kB 90.61kB 1.68%
assets/save-*.js 1.5kB 86.62kB 1.76%
assets/reveal-*.css 1 bytes 54.54kB 0.0%
assets/panels-*.js 4.34kB 50.17kB 9.47% ⚠️
assets/chat-*.js 650 bytes 15.91kB 4.26%
assets/chat-*.js 219 bytes 33.84kB 0.65%
assets/chat-*.js 43 bytes 15.27kB 0.28%
assets/state-*.js 129 bytes 2.98kB 4.52%
assets/state-*.js 7.8kB 32.59kB 31.47% ⚠️
assets/state-*.js 17 bytes 763 bytes 2.28%
assets/MarimoErrorOutput-*.js 4.25kB 30.88kB 15.94% ⚠️
assets/dagre-*.js -114 bytes 29.62kB -0.38%
assets/session-*.js 2.46kB 28.54kB 9.44% ⚠️
assets/utils-*.js 138 bytes 6.37kB 2.21%
assets/react-*.browser.esm-CLyGMTsX.js (New) 25.64kB 25.64kB 100.0% 🚀
assets/vega-*.browser-D8dnjtEq.js (New) 24.8kB 24.8kB 100.0% 🚀
assets/useNotebookActions-*.js 853 bytes 23.75kB 3.73%
assets/readonly-*.js 13.92kB 18.6kB 297.31% ⚠️
assets/readonly-*.js (New) 303 bytes 303 bytes 100.0% 🚀
assets/command-*.js 12 bytes 18.36kB 0.07%
assets/command-*.js 43 bytes 9.91kB 0.44%
assets/click-*.js 444 bytes 18.16kB 2.51%
assets/packages-*.js -225 bytes 15.74kB -1.41%
assets/swiper-*.css 1 bytes 15.12kB 0.01%
assets/ImageComparisonComponent-*.js 1.09kB 14.45kB 8.16% ⚠️
assets/useEventListener-*.js 358 bytes 13.2kB 2.79%
assets/alert-*.js -3 bytes 1.96kB -0.15%
assets/tracing-*.js 1 bytes 11.45kB 0.01%
assets/CellStatus-*.js -166 bytes 11.06kB -1.48%
assets/switch-*.js 146 bytes 10.27kB 1.44%
assets/run-*.js 16 bytes 9.75kB 0.16%
assets/config-*.js 2.8kB 9.17kB 43.91% ⚠️
assets/config-*.js (New) 6.65kB 6.65kB 100.0% 🚀
assets/cells-*.css 8 bytes 9.1kB 0.09%
assets/scratchpad-*.js 43 bytes 8.45kB 0.51%
assets/react-*.esm-D4GMDkIf.js (New) 8.37kB 8.37kB 100.0% 🚀
assets/glide-*.css -182 bytes 7.97kB -2.23%
assets/snippets-*.js 96 bytes 7.94kB 1.22%
assets/pair-*.js 176 bytes 7.92kB 2.27%
assets/column-*.js -32 bytes 6.46kB -0.49%
assets/RenderHTML-*.js 195 bytes 5.95kB 3.39%
assets/markdown-*.js 4 bytes 5.28kB 0.08%
assets/datasource-*.js 463 bytes 5.13kB 9.91% ⚠️
assets/ttcn-*.js -7 bytes 57 bytes -10.94%
assets/ttcn-*.js 7 bytes 64 bytes 12.28% ⚠️
assets/emotion-*.esm-lG8j6oqk.js (New) 4.37kB 4.37kB 100.0% 🚀
assets/data-*.js 154 bytes 3.85kB 4.17%
assets/cell-*.css 312 bytes 3.58kB 9.54% ⚠️
assets/table-*.js -6 bytes 3.35kB -0.18%
assets/components-*.js -3 bytes 2.59kB -0.12%
assets/Inputs-*.css -1 bytes 2.45kB -0.04%
assets/code-*.js (New) 162 bytes 162 bytes 100.0% 🚀
assets/mermaid-*.core-D70sHJeJ.js (New) 2.38kB 2.38kB 100.0% 🚀
assets/useTheme-*.js 183 bytes 1.78kB 11.44% ⚠️
assets/useHotkey-*.js 141 bytes 1.46kB 10.69% ⚠️
assets/youtube-*.js 584 bytes 1.46kB 66.97% ⚠️
assets/scroll-*.js (New) 1.41kB 1.41kB 100.0% 🚀
assets/JsonOutput-*.css -1 bytes 1.29kB -0.08%
assets/github-*.js 818 bytes 1.23kB 200.98% ⚠️
assets/errors-*.js 110 bytes 1.21kB 10.01% ⚠️
assets/empty-*.js -5 bytes 1.02kB -0.49%
assets/focus-*.js 6 bytes 1.01kB 0.6%
assets/snippets-*.css -1 bytes 709 bytes -0.14%
assets/multi-*.js (Deleted) -8.25kB 0 bytes -100.0% 🗑️
assets/constants-*.js 14 bytes 651 bytes 2.2%
assets/hasIn-*.js -220 bytes 594 bytes -27.03%
assets/multi-*.css -1 bytes 503 bytes -0.2%
assets/outline-*.css -1 bytes 430 bytes -0.23%
assets/hat-*.js (New) 391 bytes 391 bytes 100.0% 🚀
assets/_hasPath-*.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/panels-*.css -1 bytes 304 bytes -0.33%
assets/has-*.js (New) 197 bytes 197 bytes 100.0% 🚀
assets/__vite-*.js 5 bytes 98 bytes 5.38% ⚠️
assets/__vite-*.js -5 bytes 93 bytes -5.1%
assets/react-*.browser.esm-CV8-hvjx.js (Deleted) -25.64kB 0 bytes -100.0% 🗑️
assets/vega-*.browser-C8wT63Va.js (Deleted) -24.8kB 0 bytes -100.0% 🗑️
assets/react-*.esm-BNzu6e7h.js (Deleted) -8.37kB 0 bytes -100.0% 🗑️
assets/emotion-*.esm-C59xfSYt.js (Deleted) -4.37kB 0 bytes -100.0% 🗑️
assets/mermaid-*.core-B73Gp-Wo.js (Deleted) -2.38kB 0 bytes -100.0% 🗑️

@kirangadhave

Copy link
Copy Markdown
Member

Thanks @edwardwkrohne

I'll merge this

@kirangadhave
kirangadhave merged commit 593ba29 into marimo-team:main Jul 21, 2026
39 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Development release published. You may be able to view the changes at https://marimo.app?v=0.23.15-dev44

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multiple instances of potential garbage collection of live widgets

3 participants