Skip to content

Commit cfb1623

Browse files
Merge pull request #64 from RtlZeroMemory/layout-stability-polish
Audit layout stability signatures, measure cache, and perf invariants
2 parents 33dadd6 + 9d6cd1d commit cfb1623

5 files changed

Lines changed: 1097 additions & 89 deletions

File tree

docs/guide/layout.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,51 @@ These behaviors are guaranteed by the current layout engine and validation pipel
214214
- If both `width` and `height` are already resolved, `aspectRatio` does not override them.
215215
- After derivation, min/max constraints clamp the chosen size, then final size is capped by available bounds and non-negative clamping.
216216

217+
## Layout Stability & Caching
218+
219+
### Stability signatures (commit-time relayout checks)
220+
221+
- On commit turns with `checkLayoutStability: true`, the renderer compares per-instance layout signatures against the previous committed tree.
222+
- Signature coverage is currently limited to: `text`, `button`, `input`, `spacer`, `divider`, `row`, `column`, and `box`.
223+
- `row`/`column` signatures include layout constraints, spacing, `pad`, `gap`, `align`, `justify`, `items`, and child order (child instance ID sequence).
224+
- `box` signatures include layout constraints, spacing, border/title props, and child order.
225+
- `text`/`button`/`input` signatures track intrinsic width inputs (`text` + `maxWidth`, button `label` + `px`, input `value`).
226+
227+
Intentionally excluded:
228+
229+
- Render-only/style props are excluded from signature checks.
230+
- Routing/identity-only props that do not affect geometry (for example `button` `id`) are excluded.
231+
- Text is tracked by width impact, not full content identity; equal-width edits do not force relayout.
232+
233+
Conservative fallback:
234+
235+
- Unsupported widget kinds or unhashable tracked prop values force relayout for that turn.
236+
- On fallback, cached signature maps are cleared before continuing.
237+
238+
### Measure cache design
239+
240+
- `layout(...)` accepts an optional shared measure cache: `WeakMap<VNode, unknown>`.
241+
- Cache identity key is the VNode object; lookups are then bucketed by `(axis, maxW, maxH)`.
242+
- Internally this is: `VNode -> (row|column) -> maxW -> maxH -> LayoutResult<Size>`.
243+
- Same VNode identity + same axis + same constraints hits; changing any of those misses.
244+
- Structurally equal but distinct VNode objects do not share entries.
245+
- On commit+layout turns, the renderer resets its shared WeakMap to avoid stale cross-identity reuse.
246+
247+
### Performance characteristics
248+
249+
- Full layout computation is O(N) in visited nodes.
250+
- Signature comparison is O(N) when enabled.
251+
- When signatures are stable, the expensive `layout(...)` pass is skipped; the skip decision and reuse of the prior layout tree are O(1).
252+
- After a commit with skipped layout, damage-rect indexes are refreshed by walking runtime nodes.
253+
254+
### Clarified invariants and edge cases
255+
256+
- The first signature pass against an empty previous map reports changed (bootstrap relayout).
257+
- Child add/remove/reorder in `row`/`column`/`box` is always detected as layout-relevant.
258+
- Style-only changes on `text`/`row`/`box` do not trigger relayout.
259+
- `button` `id`-only changes do not trigger relayout; label/padding changes do.
260+
- If any committed node is outside signature coverage, relayout is forced conservatively.
261+
217262
## Borders
218263

219264
`ui.box` can draw a border around its content:

packages/core/src/app/widgetRenderer/submitFramePipeline.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ const STACK_LAYOUT_KEYS: readonly string[] = Object.freeze([
2828
"m",
2929
"mx",
3030
"my",
31+
"mt",
32+
"mr",
33+
"mb",
34+
"ml",
3135
"pad",
3236
"gap",
3337
"align",
@@ -54,8 +58,16 @@ const BOX_LAYOUT_KEYS: readonly string[] = Object.freeze([
5458
"m",
5559
"mx",
5660
"my",
61+
"mt",
62+
"mr",
63+
"mb",
64+
"ml",
5765
"pad",
5866
"border",
67+
"borderTop",
68+
"borderRight",
69+
"borderBottom",
70+
"borderLeft",
5971
"title",
6072
"titleAlign",
6173
]);

0 commit comments

Comments
 (0)