Skip to content

perf(ml): M5 variable-height VirtualList — measured extents + anchor drift - #216

Merged
tiensonqin merged 19 commits into
feat/ml-backendfrom
perf/ml-m5-variable-vlist
Oct 13, 2026
Merged

tiensonqin merged 19 commits into
feat/ml-backendfrom
perf/ml-m5-variable-vlist

Conversation

@tiensonqin

Copy link
Copy Markdown
Collaborator

Summary

Stacked on #215 (M3). Milestone 5: variable-height rows in the virtual list.

  • Measured extents: each mounted row's real main extent feeds the Fenwick extent model (stride = extent + gap); index↔offset stays O(log n) as real heights replace the estimate, and the leaf's virtual extent re-patches + relayouts when the total moves.
  • Anchor correction: when overscan rows above the viewport top measure off-estimate, the anchored item's prefix delta is recorded as drift; virtual_take_drift (consume-on-read) lets the driver fold it back into the scroll offset so the top row does not visibly jump. populate now returns the correction's actions for dispatch.
  • Memo-hit fixpoint: the remount path runs a second mount pass after the extent patch so rows the post-measure window reaches mount the same frame.
  • Perf: new vlist-var-100k scenario (every 7th row 60px vs 28px estimate) exercises measurement + drift; the harness applies drift like the driver.

Test plan

  • dune build clean, dune runtest all green
  • New tests: vlist mounted children slice, vlist anchor drift (mid-list jump yields exactly +160 correction, consume-on-read, corrected item positions)

Bench (this branch)

scenario frame layout p50 frame paint p50 frame total p50 / p95 mounted rows
vlist-100k 0.03ms 0.07ms 3.68 / 4.54ms 38
vlist-var-100k 0.04ms 0.07ms 3.55 / 3.99ms 41

Mixed-height scroll is as fast as fixed-height; mounted set stays ~40 rows at 100k items.

Generated with Devin

tiensonqin and others added 5 commits October 13, 2026 05:10
- root_ids now reads an incrementally maintained roots set instead of
  folding every node + probing each parent per call — a paint over a
  100k-node store no longer pays O(n) just to enumerate roots.
- child_count / child_id_at / child_index / children_range expose
  indexed slices of the child vector so virtualized containers can
  materialize a mounted window without building the full child list.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…paint and hit-test (Milestone 3)

A virtual-list's store children stay the logical model; the layout
engine materializes flex trees only for the mounted window of item
indices (visible range + overscan):

- Lui_layout: per-list extent model (Fenwick tree over stride =
  measured extent + gap) giving O(log n) index<->offset; the list is
  a leaf in the main mirror whose intrinsic main size is the virtual
  content extent; mounted row subtrees live in a side forest and get
  composed absolute rects in t.rects. Sync feeds a ?vport accessor
  (content-space visible range per list); on a memo hit only the rows
  that slid are remounted — scroll frames pay no mirror rebuild.
- Config rides data-attrs (no schema change): data-item-extent,
  data-overscan, data-track-visible-range, data-scroll-target/
  token/anchor.
- Lui_paint: hooks.mounted_range slices the child walk to the mounted
  window; the paint pass no longer touches unmounted rows.
- Lui_host: forwards the vport feed into every engine sync and
  exposes the virtual-list query surface (mounted range, virtual
  extent, item rects incl. unmounted indices, key/id -> index).
- Ui/main: hit-test mirror and scroll caps slice to the mounted
  window and span the virtual extent; scroll-target resolves keys
  through the model so unmounted rows are reachable; visible-range
  reports answer via the fed viewport + binary search; the
  scrollable-id list is memoized on store revision so populate does
  not re-walk all rows per scroll frame.
- Lui_elements.virtual_list gains ?overscan / ?item_extent.

Tests: layout window mount/move/measured/gap/unwindowed, paint
mounted-range slicing, Ui vlist scroll-request.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Harness feeds each virtual-list's content-space visible range into
  layout sync and wires mounted_range into paint hooks — the same
  composition the window driver makes.
- vlist scenarios report "mounted rows"; the perf test asserts the
  milestone bound (10k rows mount <= 200).

Results: vlist-100k scroll frame p50 = 7.3ms (paint 0.13ms), mounted
rows = 38 at 10k/50k/100k alike.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Conflicts resolved keeping both sides:
- lui_store: upstream intrusive child list + typed dirty masks +
  change journal, plus the incremental roots set; indexed child
  access reimplemented on the linked list (nearer-end walk).
- lui_layout: sync memo keys on layout_rev (upstream) with ?vport
  feed preserved; the vlist keeps its own kid-id array snapshot so
  per-row window work stays O(1) despite the linked child list.
- paint/hit-test slicing switched from a (first,last) range +
  store-level indexed access to engine-provided mounted_children id
  lists (O(window), no linked-list walk).
- lui_window/main: upstream caret_ops + vlist hook and unmounted-row
  content_rect fallback both kept.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ck positions, anchor drift (Milestone 5)

- Per-row measured main extents feed the Fenwick tree already in the
  vlist model (stride = extent + gap): index→offset and offset→index
  stay O(log n) as real row heights replace the estimate; the leaf's
  virtual extent repatches and relayouts when measurements move the
  total.
- Anchor correction: when freshly mounted overscan rows above the
  viewport top measure off-estimate, the model records the anchor
  item's prefix delta as drift; the driver consumes it each populate
  (virtual_take_drift → Ui.scroll_set) so the top row keeps its
  screen position instead of visibly jumping. populate now returns
  the correction's actions for dispatch.
- Memo-hit remount runs a second mount pass after the extent patch
  so rows the post-measure window now reaches are covered the same
  frame.
- Perf: vlist-var-100k scenario (every 7th row 60px vs 28px estimate)
  exercises the measurement path; harness applies drift like the
  driver.

Tests: vlist mounted_children slice, anchor drift (+160 correction on
a mid-list jump, consume-on-read), corrected item positions.

Bench: vlist-var-100k scroll frame p50 = 3.55 ms, mounted rows = 41 —
as flat as the fixed-height runs.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
tiensonqin and others added 12 commits October 13, 2026 05:58
# Conflicts:
#	platform/native/lui_layout/lui_layout.ml
#	platform/native/lui_layout/test/lui_layout_test.ml
Resolve conflicts with M4 retained paint / rect-change tracking:
- host: keep both virtual-list surface and take_rect_changes
- paint: adopt shared stacking walk; walk takes ?mounted so virtual
  lists only walk the mounted window; restore resolve_in
- retained: kids lists honour mounted_children and rebuild when a
  list's mounted window slides (host passes vlists)
- layout: vlist row rects and purged rows feed rect_changed
- store: keep incrementally maintained root set
- window: hit-test visit without anc_clip, over mounted children
- host: keep virtual_take_drift/ldrift and take_rect_changes/lchanges
- main.ml: adopt frame scheduler tick; Present branch applies
  populate's anchor-drift actions from Assembly.repaint
- tests: keep both sides' cases
Resolve conflicts keeping both features — virtual lists on top of
retained scene fragments, frame scheduler, subtree culling, text
input session and pixel snapping:

- host: keep both the virtual-list engine surface and
  take_rect_changes; repaint hands the vlist ids to paint_retained
- paint: adopt the shared stacking walk; walk takes ?mounted so
  virtual lists enumerate only the mounted window slice; restore
  resolve_in
- retained: kids lists honour mounted_children and rebuild when a
  list's mounted window slides (vwin snapshot + ?vlists channel)
- layout: vlist row rects go through the snapped scene_rect and feed
  rect_changed; purged rows mark rect_changed too
- store: keep the incrementally maintained root set
- window: hit-test visits the mounted children without anc_clip

Adds a retained test that scrolls a virtual list and cross-checks
the painted scene against a fresh full paint on every frame.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…-215

Same-parent merge: the remote tip already carries the feat/ml-backend
merge; this keeps our resolutions on top — vlist row rects snapped
through scene_rect and the retained vlist-scroll cross-check test.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…-216

Brings the snapped vlist row rects and the retained vlist-scroll
cross-check test onto the variable-height list branch; keeps the
variable engine's virtual_take_drift surface.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@tiensonqin
tiensonqin changed the base branch from perf/ml-m3-virtual-list to feat/ml-backend October 13, 2026 01:04
Base tree equals the already-merged M3 tip; conflicts are squash
artifacts, resolved to this branch's side.
lui_window_linux/main.ml: keep drift-action apply from Assembly.repaint,
adopt renderer-ladder gl_live ().
@tiensonqin
tiensonqin merged commit 72a8646 into feat/ml-backend Oct 13, 2026
8 checks passed
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