Skip to content

gpui: Retain clean entity views - #30

Open
huacnlee wants to merge 10 commits into
mainfrom
gpui-benchmark-small-updates
Open

gpui: Retain clean entity views#30
huacnlee wants to merge 10 commits into
mainfrom
gpui-benchmark-small-updates

Conversation

@huacnlee

@huacnlee huacnlee commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • Retain clean, definite-sized Entity and AnyView subtrees by default.
  • Add a versioned retained(id, version, build) boundary for unchanged subtrees inside an entity that must rerender, such as static input contents beside a blinking cursor.
  • Reuse layout, prepaint, and paint output only when size and inherited drawing context still match.
  • Bypass retained-state dependency tracking on subsequent clean frames after a View is known to require intrinsic measurement.
  • Let ListState, ScrollHandle, and UniformListScrollHandle invalidate their owning view when element-local state changes outside the Entity notification graph.
  • Add focused synthetic and production Editor benchmarks for cursor-sized updates and scrolling.

Root cause

A GPUI notification reruns the changed Entity's Render implementation. Before this change, ordinary ViewElements did not use dirty_views to prune clean sibling Entities, so layout preparation, prepaint, text work, and paint were repeated across unrelated visible subtrees even when only a cursor or scroll offset changed.

The production Editor phase benchmark confirms that Taffy layout is not the main cost for these small updates. A no-op Editor notification spends about 457.5 µs total, including about 248 µs in prepaint and 133 µs in paint, while layout computation is below 1 µs.

A benchmark that toggles the real BlinkManager visibility state confirms that a cursor blink and a no-op Editor notification take the same full redraw path. In an adjacent run, the cursor blink was 2.5% faster than the no-op notification, with no statistically significant difference. Their phase distributions were also equivalent: prepaint dominated, paint was second, and request plus computed layout remained below 0.005 ms. These thermally adjacent measurements establish path equivalence; their absolute values are not mixed with the cold before/after table below.

Incremental rendering model

Ordinary Entity<T> and AnyView elements now retain their previous subtree automatically when the backing Entity and every Entity accessed while rendering it remain clean. Reuse is limited to roots with definite width and height; intrinsic-sized roots continue through normal rendering.

Once a clean View is known to have an intrinsic root, later parent redraws use the ordinary render path directly instead of repeating retained dependency detection. A notification on that View rechecks eligibility, so a root that changes from intrinsic to definite-sized can still begin retaining.

The cache is also invalidated by changes to bounds, content mask, inherited text style, rem size, scale factor, explicit window refresh, and Inspector picking.

Some GPUI elements keep mutable state outside an Entity. ListState and scroll handles now capture a weak, view-scoped invalidator during prepaint. Their public mutation methods mark that View dirty without emitting an Entity observer notification. Mutations made while the current frame is already being built do not enqueue another invalidation.

The versioned retained element remains available for finer boundaries inside one dirty Entity:

retained("contents", contents_version, move || build_contents())

Its stable ID identifies the boundary and its version must cover every captured value that affects the subtree.

Benchmarks

Apple Silicon Mac, production Editor with 1,000 lines and an unchanged 1,000-row sibling Entity. Criterion used 100 samples for the final default-path runs.

Scenario Before Default retained Entities Change
Editor no-op notify + clean sibling 2.768 ms 0.859 ms -69.0%
One-line Editor scroll + clean sibling 2.981 ms 1.187 ms -60.2%

Criterion's stored-baseline comparisons reported -69.8% for the no-op update and -59.7% for scrolling.

Production Editor + unchanged 1,000-row sibling

no-op notify before     2.768 ms  ████████████████████████████
no-op notify after      0.859 ms  █████████

one-line scroll before  2.981 ms  ██████████████████████████████
one-line scroll after   1.187 ms  ████████████

The synthetic same-Entity benchmark isolates the finer-grained case:

Scenario Time Change
Cursor and 1,000 rows in one Entity 1.6505 ms Baseline
Versioned retained rows 159.18 µs -90.4%
Retained rows invalidated every frame 1.6733 ms +1.4%
Empty root 89.170 µs Baseline
Empty root with retained boundary 89.216 µs No measurable fixed overhead

Correctness boundaries

  • Only concrete Entity<T> and AnyView conversions opt into automatic retention. A custom View that combines parent props with an Entity identity remains uncached unless it explicitly chooses otherwise.
  • Intrinsic-sized roots fall back because their layout depends on measuring contents.
  • Nested Entity dependencies are recorded and invalidate the retained parent.
  • List and scroll handle mutations invalidate the View that last rendered the handle.
  • Window refresh and Inspector picking bypass reuse.

An earlier attempt to retain all clean Entities failed five list remeasurement/follow-tail tests because element-local state could change without Context::notify. The view invalidator closes that gap; the complete GPUI suite now passes with automatic retention enabled.

Test Plan

  • cargo test -p gpui --features test-support --lib (223 passed)
  • cargo test -p workspace --lib (237 passed)
  • ./script/clippy -p gpui --features test-support
  • ./script/clippy -p workspace
  • cargo bench -p gpui --bench small_updates --features bench --no-run
  • cargo bench -p benchmarks --bench editor_render --no-run
  • cargo bench -p benchmarks --bench editor_render -- editor_noop_notify_with_static_sibling
  • cargo bench -p benchmarks --bench editor_render -- editor_cursor_blink
  • cargo bench -p benchmarks --bench editor_render -- editor_scroll_with_static_sibling

Suggested .rules additions

When adding mutable state handles to GPUI elements, ensure mutations made outside rendering can invalidate the Entity View that last rendered the handle. Entity cleanliness alone is not sufficient if an element owns independently mutable state.

Release Notes:

  • Improved rendering performance during animations, scrolling, and small visual updates by reusing unchanged GPUI Entity subtrees.

huacnlee and others added 2 commits August 2, 2026 03:21
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Codex <codex@openai.com>
@huacnlee huacnlee changed the title gpui: Add small-update rendering benchmarks gpui: Retain clean definite-sized views Aug 1, 2026
Co-authored-by: Codex <codex@openai.com>
@huacnlee huacnlee changed the title gpui: Retain clean definite-sized views gpui: Retain unchanged element subtrees Aug 1, 2026
huacnlee and others added 5 commits August 2, 2026 05:38
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Codex <codex@openai.com>
@huacnlee huacnlee changed the title gpui: Retain unchanged element subtrees gpui: Retain clean entity views Aug 2, 2026
huacnlee and others added 2 commits August 2, 2026 08:13
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Codex <codex@openai.com>
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.

1 participant