perf(table-core): write-epoch fast path for memoized APIs - #6570
perf(table-core): write-epoch fast path for memoized APIs#6570KevinVandy wants to merge 1 commit into
Conversation
Add `table._epoch`, bumped at value-resolution time on every state, options, and external-atom write (the patched atom `set` wraps the updater and bumps after the user updater runs, immediately before the store assigns and notifies, so epoch and value change atomically for updaters and synchronous listeners alike). `memo()` gains an `epochSource`: when the epoch and depArgs identity are unchanged since the last validation, the cached result returns without running `memoDeps`, collapsing the nested dependency cascade that high-fanin table memos (`getAllLeafColumns`, `getRowModel`, selection bounds) otherwise re-run once per consumer call. Opt-in per reactivity binding via `TableReactivityBindings.supportsWriteEpoch`: enabled for store, react/preact/lit/octane (render-phase preset), vue, angular, alpine (each verified by its full e2e subset). Solid and svelte stay off: their read-tracking reactivity subscribes to whatever a memoized call reads, and the fast path registers no dependencies. Ember stays off: its live options getter changes options without any set. Both have a documented epoch-as-signal path in. Contract change: memoized APIs revalidate once per table write epoch, so a manual structural mutation (e.g. `row.subRows = ...` outside any write) surfaces after the next write. Full bench campaign vs 9.1.2 (with the shape refactor): all 165 row-model cases -27% geomean (grouping -35%, selection -46%, filtering -30%); grouping sum 100k 62.1 -> 27.0 ms; Node steady reads -52%, worst case (write before every read pass) at parity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
View your CI Pipeline Execution ↗ for commit 74f824b
☁️ Nx Cloud last updated this comment at |
🚀 Changeset Version PreviewNo changeset entries found. Merging this PR will not cause a version bump for any packages. |
Summary
Stacked on #6569 (shape-stable instances). Adds a table-wide write epoch that lets memoized APIs skip their dependency check entirely when nothing could have changed.
table._epochis bumped on every state, options, and external-atom write. The patched atomsetwraps the updater and bumps at value-resolution time: after the user updater runs (updater reads still see the pre-write epoch) and immediately before the store assigns and notifies (synchronous listeners see new value and new epoch together). Bumping before or aftersetinstead leaves a window where epoch and value disagree for code running inside the write; the react cell-selection per-row<Subscribe>selectors caught exactly that.memo()gainsepochSource: unchanged epoch + same depArgs identity returns the cached result without runningmemoDeps. The win is not per-instance memos on a state-change render (those revalidate anyway, measured at parity); it is the high-fanin table memos (getAllLeafColumns,getRowModel,getRowsInDisplayOrder, selection bounds) that every row's deps call once per row, and any read pass without an intervening write.TableReactivityBindings.supportsWriteEpoch, enabled where verified by the adapter's full e2e subset: store (vanilla), react/preact/lit/octane (render-phase preset), vue, angular, alpine. Off for solid and svelte (read-tracking reactivity subscribes to whatever a memoized call reads during execution; the fast path registers no dependencies, so templates never invalidate) and ember (live pull-based options getter). Their path in is an epoch-as-signal provided by the binding, which is also the shape a transactional/async state model wants (epoch = transaction version, bumped per commit).row.subRows = ...outside any write surfaces after the next write (constructRow.testupdated).Results
Full benchmark campaign against 9.1.2 (shape PR + this PR; v8 re-measured in the same runs):
Node: steady-state reads -52%, same-epoch repeats -57%, worst case (a write before every read pass) at parity. Full monorepo CI and all 388 example e2e projects pass; each adapter subset was verified individually with the flag in its final state.
Notes for review
useTablereturns a spread copy of the table (react-compiler workaround), so_epoch/optionson the returned object are frozen snapshots. Probe the real table viarow.table._epoch(mock tables in tests) and bindings without the flag always run the plain dependency check.🤖 Generated with Claude Code