You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while researching #452's scaling behaviour. No interaction had ever been measured at target scale. Doing so:
S2 sort, 50,000 rows
pretable
tanstack
status
partial ×3 — "the surface never held still for 2 frames within 48"
completed ×3
latency
unreportable
~33ms
settle
>400ms, never
~100ms
Decomposition
The row model is DOM-free, so the transition can be timed directly in Node (createLocalRowModel + S2 dataset, sort one column, wait for ready), sweeping transitionBudgetMs:
At 3k, the yield tax is ~46% of wall time.DEFAULT_BUDGET_MS = 0.25 (cooperative-transition.ts:75) buys a quarter-millisecond of work per scheduler hop, and the hop costs about as much. Raising the budget to ~4ms nearly halves the sort. This alone closes most of pretable's interaction latency is ~2x TanStack's on S2 sort and filter #452's measured 2x at hypothesis scale.
At 50k, the work itself is ~515ms — ~10µs per row. Per insertRecord (cooperative-transition.ts:556-588): a full queryPlan.evaluate per row (re-runs every active column accessor and re-evaluates ALL filters even when only the sort changed, against a fresh per-plan cache — compiled-query.ts:1298, 1373-1465), a record freeze, a persistent HAMT insert, and an O(log n) AVL insert. TanStack sorts the same 50,000 rows in ~33ms total: rows.slice().sort(compareRows) over cached values (createSortedRowModel.js:81-83, verified synchronous with no chunking anywhere in the installed package).
Note maxUnitsPerSlice = 256 caps slices even at effectively-synchronous budgets (195 hops at 50k regardless), so budget tuning alone cannot fix target scale.
Levers, in order of measured ceiling
Budget/duty-cycle: 0.25ms → ~4ms adaptive. Measured: 41.3 → 21.3ms at 3k. Cheap, low-risk, immediately closes most of the hypothesis-scale gap.
Sort-only metadata reuse: a sort change rebuilds every row's metadata from a cold cache. When filters/groups are unchanged, prior evaluation results (or at least filter verdicts and unchanged sortKeys) should carry over. Bounded by the evaluate share of the 10µs/row.
Bulk rebuild path: sorting 50k precomputed sortKeys with Array.sort and bulk-building the tree bottom-up is O(n log n) with array constants; 50k incremental persistent inserts is the same complexity with tree constants. The persistent structures earn their keep on INCREMENTAL change; a full re-sort is exactly the case where they do not.
Progress publishes: the transition publishes nothing until complete (cooperative-transition.ts:328-331 by design), so at target scale the user sees a frozen sort for half a second. Worth a design discussion — partial sorts are arguably worse than a spinner, but "nothing for 590ms" is worst.
#452's 2x-at-3k is roughly half yield tax (fixable by budget) and half per-row work (levers 2-3). This issue is the same root causes at a scale where they breach the harness window entirely. Fixing the budget alone would NOT fix this — the 50k wall time barely moves across budgets.
Found while researching #452's scaling behaviour. No interaction had ever been measured at
targetscale. Doing so:partial×3 — "the surface never held still for 2 frames within 48"Decomposition
The row model is DOM-free, so the transition can be timed directly in Node (
createLocalRowModel+ S2 dataset, sort one column, wait forready), sweepingtransitionBudgetMs:Two different problems by scale:
DEFAULT_BUDGET_MS = 0.25(cooperative-transition.ts:75) buys a quarter-millisecond of work per scheduler hop, and the hop costs about as much. Raising the budget to ~4ms nearly halves the sort. This alone closes most of pretable's interaction latency is ~2x TanStack's on S2 sort and filter #452's measured 2x at hypothesis scale.insertRecord(cooperative-transition.ts:556-588): a fullqueryPlan.evaluateper row (re-runs every active column accessor and re-evaluates ALL filters even when only the sort changed, against a fresh per-plan cache — compiled-query.ts:1298, 1373-1465), a record freeze, a persistent HAMT insert, and an O(log n) AVL insert. TanStack sorts the same 50,000 rows in ~33ms total:rows.slice().sort(compareRows)over cached values (createSortedRowModel.js:81-83, verified synchronous with no chunking anywhere in the installed package).maxUnitsPerSlice = 256caps slices even at effectively-synchronous budgets (195 hops at 50k regardless), so budget tuning alone cannot fix target scale.Levers, in order of measured ceiling
Array.sortand bulk-building the tree bottom-up is O(n log n) with array constants; 50k incremental persistent inserts is the same complexity with tree constants. The persistent structures earn their keep on INCREMENTAL change; a full re-sort is exactly the case where they do not.Relation to #452
#452's 2x-at-3k is roughly half yield tax (fixable by budget) and half per-row work (levers 2-3). This issue is the same root causes at a scale where they breach the harness window entirely. Fixing the budget alone would NOT fix this — the 50k wall time barely moves across budgets.