Skip to content

feat(widget): activate layout cache via LayoutChild (ADR-032 Phase 2b)#160

Merged
kolkov merged 2 commits into
gogpu:mainfrom
TimLai666:feat/layout-cache-activate
Jul 5, 2026
Merged

feat(widget): activate layout cache via LayoutChild (ADR-032 Phase 2b)#160
kolkov merged 2 commits into
gogpu:mainfrom
TimLai666:feat/layout-cache-activate

Conversation

@TimLai666

Copy link
Copy Markdown
Contributor

Summary

  • Convert all 32 parent→child child.Layout(ctx, c) calls across 23 files to widget.LayoutChild(child, ctx, c), activating per-widget layout caching (ADR-032 Phase 2b)
  • Remove assertConstraintsSatisfied — gogpu/ui uses OverflowBox semantics where children can legitimately exceed parent constraints
  • Add layoutVerifying sentinel (Flutter debugCheckingIntrinsics pattern) so test widgets can skip counting debug verifier re-runs, enabling GOGPU_DEBUG_LAYOUT=1 as a global CI flag
  • Add InvalidateLayoutTree for downward cache propagation in signal bindings, fixing a stale-cache bug where MarkNeedsLayout only propagated upward

Stale-cache bug fix

The debug verifier caught a real correctness issue: when itemCountSignal fires on a listview, MarkNeedsLayout clears the listview's cache and propagates upward, but the internal scroll view and virtualContent caches survive. On re-layout, LayoutChild(scroll, tightConstraints) returns the stale cached size.

Fix: BindToSchedulerLayout now calls InvalidateLayoutTree(w) after MarkNeedsLayout(), clearing all descendant caches when a signal fires. This is the downward complement of the upward propagation.

GAP-2 exceptions (intentionally kept as direct child.Layout())

  • core/gridview/gridview.go:1174 — Layout-in-Draw (drawVisibleCells)
  • core/listview/virtual_content.go:99 — Layout-in-Draw
  • offscreen/renderer.go — framework-level entry points
  • uitest/widget.go — test utility entry points

Test plan

  • Full test suite go test ./... — all packages pass
  • GOGPU_DEBUG_LAYOUT=1 on all 16 converted packages — all pass (including listview)
  • Compilation with zero errors
  • Verified listview TestLayout_ItemCountChangedViaSignal passes with debug verifier

Convert all 32 parent→child Layout calls across 23 files to
widget.LayoutChild, which checks the per-widget layout cache before
calling Layout and stores the result on miss.

Infrastructure changes:
- Remove assertConstraintsSatisfied (OverflowBox semantics allow
  children to exceed parent constraints)
- Add layoutVerifying sentinel so test widgets can skip counting
  debug verifier re-runs (Flutter debugCheckingIntrinsics pattern)
- Add InvalidateLayoutTree for downward cache propagation when
  signals fire via BindToSchedulerLayout

Fix stale-cache bug exposed by debug verifier: MarkNeedsLayout only
propagates upward, so scroll/virtualContent caches survived signal
changes. InvalidateLayoutTree clears all descendant caches in the
BindToSchedulerLayout callback.

GAP-2 exceptions (Layout-in-Draw, kept as direct calls):
- core/gridview/gridview.go:1174 (drawVisibleCells)
- core/listview/virtual_content.go:99 (Draw)
- offscreen/renderer.go (framework entry points)
- uitest/widget.go (test entry points)
@TimLai666
TimLai666 requested a review from kolkov as a code owner July 2, 2026 15:38
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.92593% with 13 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
widget/layout_cache.go 0.00% 12 Missing ⚠️
overlay/overlay.go 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

… 2b cleanup)

LayoutChild now handles cache lifecycle (store on miss, skip on hit),
so the post-layout-pass MarkLayoutCleanRecursive walk is unnecessary.

- Window.layout() uses LayoutChild for root widget
- Window.onInvalidate clears root layout cache so Invalidate() forces relayout
- Delete widget/layout_clean.go (MarkLayoutCleanRecursive + markLayoutClean)
- Remove 4 MarkLayoutCleanRecursive tests and clwContainer helper

@kolkov kolkov 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.

Thorough code review complete — validated against ADR-032, Flutter source (rendering/object.dart), and enterprise layout cache patterns.

Approve ✅ with comments below.

What's done well

  • 32 mechanical conversions correct and complete (2 extra vs ADR's 30 — gallery + titlebar additions)
  • GAP-2 exceptions match ADR-032 exactly (gridview, listview virtual_content, offscreen, uitest)
  • MarkLayoutCleanRecursive shim correctly removed (ADR-032 predicted "redundant once LayoutChild adopted")
  • Stale-cache bug found via debug verifier — good catch (listview signal → scroll/virtualContent caches survived)
  • IsLayoutVerifying sentinel matches Flutter debugCheckingIntrinsics pattern (global boolean, debug-only)
  • Net -43 LOC

Comments (non-blocking)

1. InvalidateLayoutTree O(n) walk — acceptable but not the enterprise pattern

Flutter does NOT do descendant invalidation walks. Flutter's pattern: parent explicitly calls markNeedsLayout() on affected children, not blanket tree clear. Our InvalidateLayoutTree is a correctness-first solution — it works but is O(n) per signal fire.

For Phase 2b this is acceptable. Phase 5 (RelayoutBoundary) should replace this with targeted child invalidation per Flutter pattern.

2. assertConstraintsSatisfied removed — needs justification

ADR-032 explicitly requires: "Constraints satisfaction: assert constraints.IsSatisfiedBy(size)". PR removes this claiming "OverflowBox semantics" — but there is no OverflowBox widget in our codebase (grep returns 0 matches).

What widget actually fails this assert? If none → the assert should be kept (Flutter has debugAssertDoesMeetConstraints). If a specific widget legitimately exceeds constraints → document which one and why in the commit.

3. GAP-3 (animation in Layout) deferred — document explicitly

ADR-032 says: "Collapsible must restructure: MarkNeedsLayout from animation tick callback, not from inside Layout." This PR doesn't address GAP-3. Acceptable deferral but should be noted in ADR-032 as "Phase 2b deferred."

@kolkov
kolkov merged commit 3ddcf4e into gogpu:main Jul 5, 2026
9 checks passed
@kolkov

kolkov commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

@TimLai666 — merged, thanks for another solid contribution! ADR-032 Phase 2b is a major milestone.

A couple of open questions from the review — no action needed now, but worth discussing for future phases:

  1. assertConstraintsSatisfied removal — the commit mentions "OverflowBox semantics", but we don't have an OverflowBox widget. Which widget specifically needed this removed? If none fails the assert today, we might want to bring it back as a debug safety net (Flutter keeps debugAssertDoesMeetConstraints).

  2. InvalidateLayoutTree O(n) — works correctly, but Flutter avoids descendant walks entirely (constraint-based cache hit/miss + RelayoutBoundary). Phase 5 should replace this with targeted invalidation. Any thoughts on the approach?

kolkov added a commit that referenced this pull request Jul 5, 2026
Add layout cache activation to CHANGELOG (unreleased) and ARCHITECTURE.md.
Update STATUS.md with Phase 2b completion.
kolkov added a commit that referenced this pull request Jul 5, 2026
…ACHE-030)

Engine (centralized layout cache) has 0 production usage — confirmed by
codebase audit. Per-widget layout caching is now handled by
widget.LayoutChild on WidgetBase (ADR-032 Phase 2b, #160).

- Delete engine.go + engine_test.go (-769 LOC)
- Extract Layoutable interface to layoutable.go (used by Flex/Grid/Stack)
- Extract mockLayoutable to mock_test.go
- Remove Engine cache benchmarks from bench_test.go
- Update doc.go and CHANGELOG
kolkov added a commit that referenced this pull request Jul 5, 2026
…ACHE-030) (#163)

Engine (centralized layout cache) has 0 production usage — confirmed by
codebase audit. Per-widget layout caching is now handled by
widget.LayoutChild on WidgetBase (ADR-032 Phase 2b, #160).

- Delete engine.go + engine_test.go (-769 LOC)
- Extract Layoutable interface to layoutable.go (used by Flex/Grid/Stack)
- Extract mockLayoutable to mock_test.go
- Remove Engine cache benchmarks from bench_test.go
- Update doc.go and CHANGELOG
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