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
Follow-up to #4500, extending the same analysis to the Cube/View subsystem. Every row a View produces gets a ViewRowData whose field values are installed via dynamic keyed-store loops - the same construction route that demotes objects to V8 dictionary ("slow") mode. Class instances get slightly more headroom than plain objects: demotion sets in between 20 and 30 view fields (F=20 stays fast, F=30 is dictionary-mode for every instance in steady state). Wide views in data-intensive apps - exactly the ones built over large cubes - pay full price.
Measured on the same harness as #4500: a per-View codegen'd named-property assigner keeps ViewRowData instances in fast-properties mode with shared hidden classes - ~5-6x smaller rows and order-of-magnitude faster row construction, with instanceof, prototype getters, and the entire updates machinery untouched.
These rows are heavily retained - View._rowCache, _leafMap, result.rows, and as StoreRecord.raw in every View-bound store - so for a 50k-leaf, 30-field view this is ~83MB β ~17MB of ViewRowData per view, before counting aggregate rows. Dashboard-style apps run several views. Note #4500 already covers the Cube's internal Store and the bound stores' own data objects; the ViewRowData population is what remains.
Current implementation
data/cube/row/LeafRow.ts - ctor: view.fields.forEach(({name}) => this.data[name] = rawRecord.data[name]) - one leaf per cube record in the view.
data/cube/row/BaseRow.ts - initAggregate() also builds canAggregate per aggregate row via keyed reduce into {} - same dictionary fate at β₯~30 fields (smaller population, worth fixing in passing).
Per-row shape: id + cubeRowType/cubeLabel/cubeDimension + F fields + children (+ cubeBuckets when bucketed, _cubeLeafChildren with provideLeaves).
Value-rewrite paths (computeAggregates, applyLeafDataUpdate, applyDataUpdate) only overwrite existing properties - rewrites never demote, so the updates machinery needs no changes.
V8 mechanics
Background and citations in #4500. The specific mechanism here, verified by probe: V8's fast-properties limit applies to keyed stores that create new map transitions; constant-key (named) stores are exempt. A new Function-compiled assigner with literal property keys (d["fieldA"]=v[0];...) therefore keeps class instances fast with a shared hidden class through ~120 total properties.
A notable probe artifact worth preserving: keyed stores that follow existing transitions do not demote - once one object completes the shape fast, subsequent identically-shaped objects can stay fast even via keyed loops. Real Views never get this benefit (no instance ever completes the chain fast), but it cleanly confirms the mechanism.
Measurements
Node 24 / V8 13.6, 50k leaf-style rows, forced-GC heap deltas, fresh field-name chains per experiment (mode verified via %HasFastProperties on first/mid/last instance):
view fields
today (dict) B/row
assigner (fast) B/row
ratio
build 50k rows
30
1,658
337
4.9x
76 β 5ms
57
3,192
553
5.8x
107 β 18ms
Aggregation-style reads (per-field sweeps over rows, the SumAggregator pattern) run ~1.55x faster against fast-mode rows (800 β 516ms, 10 sweeps x 30 fields x 50k rows). Read gains are smaller than #4500's grid case because cube reads are keyed-by-variable-name (megamorphic) rather than fixed-name monomorphic.
Steady-state mode by construction route (all 50k instances):
route
F=20
F=30
F=50
F=80
ViewRowData + keyed loop (today)
FAST
dict
dict
dict
+ codegen'd named assigner
FAST
FAST
FAST
FAST (to ~120)
Proposed change
Build on #4500's RecordDataFactory (same module, shared guards):
Add RecordDataFactory.createAssigner(fieldNames) - compiles (target, values) => void with literal keys; returns null under the same guards (field count over threshold, __proto__ field name, no unsafe-eval, internal kill switch).
View compiles one assigner per query (recompiled on updateQuery when fields change) and exposes it internally to its rows.
LeafRow ctor and BaseRow.initAggregate() use the assigner when present - each keeps its current loop as the fallback; initAggregate seeds values as appliedDimensions.hasOwnProperty(name) ? appliedDimensions[name] : null, folding away the separate Object.assign.
canAggregate built via the same assigner (boolean values).
Late named adds (children, cubeBuckets, _cubeLeafChildren) unchanged - probe-verified shape-safe.
No public API changes. ViewRowData remains a class with its getters and index signature; updates flow untouched.
Summary
Follow-up to #4500, extending the same analysis to the Cube/View subsystem. Every row a
Viewproduces gets aViewRowDatawhose field values are installed via dynamic keyed-store loops - the same construction route that demotes objects to V8 dictionary ("slow") mode. Class instances get slightly more headroom than plain objects: demotion sets in between 20 and 30 view fields (F=20 stays fast, F=30 is dictionary-mode for every instance in steady state). Wide views in data-intensive apps - exactly the ones built over large cubes - pay full price.Measured on the same harness as #4500: a per-View codegen'd named-property assigner keeps
ViewRowDatainstances in fast-properties mode with shared hidden classes - ~5-6x smaller rows and order-of-magnitude faster row construction, withinstanceof, prototype getters, and the entire updates machinery untouched.These rows are heavily retained -
View._rowCache,_leafMap,result.rows, and asStoreRecord.rawin every View-bound store - so for a 50k-leaf, 30-field view this is ~83MB β ~17MB ofViewRowDataper view, before counting aggregate rows. Dashboard-style apps run several views. Note #4500 already covers the Cube's internalStoreand the bound stores' owndataobjects; theViewRowDatapopulation is what remains.Current implementation
data/cube/row/LeafRow.ts- ctor:view.fields.forEach(({name}) => this.data[name] = rawRecord.data[name])- one leaf per cube record in the view.data/cube/row/BaseRow.ts-initAggregate()(Aggregate + Bucket rows):view.fields.forEach(({name}) => data[name] = null)+Object.assign(data, appliedDimensions), thencomputeAggregates()rewrites values in place.data/cube/row/BaseRow.ts-initAggregate()also buildscanAggregateper aggregate row via keyedreduceinto{}- same dictionary fate at β₯~30 fields (smaller population, worth fixing in passing).id+cubeRowType/cubeLabel/cubeDimension+ F fields +children(+cubeBucketswhen bucketed,_cubeLeafChildrenwithprovideLeaves).computeAggregates,applyLeafDataUpdate,applyDataUpdate) only overwrite existing properties - rewrites never demote, so the updates machinery needs no changes.V8 mechanics
Background and citations in #4500. The specific mechanism here, verified by probe: V8's fast-properties limit applies to keyed stores that create new map transitions; constant-key (named) stores are exempt. A
new Function-compiled assigner with literal property keys (d["fieldA"]=v[0];...) therefore keeps class instances fast with a shared hidden class through ~120 total properties.A notable probe artifact worth preserving: keyed stores that follow existing transitions do not demote - once one object completes the shape fast, subsequent identically-shaped objects can stay fast even via keyed loops. Real Views never get this benefit (no instance ever completes the chain fast), but it cleanly confirms the mechanism.
Measurements
Node 24 / V8 13.6, 50k leaf-style rows, forced-GC heap deltas, fresh field-name chains per experiment (mode verified via
%HasFastPropertieson first/mid/last instance):Aggregation-style reads (per-field sweeps over rows, the
SumAggregatorpattern) run ~1.55x faster against fast-mode rows (800 β 516ms, 10 sweeps x 30 fields x 50k rows). Read gains are smaller than #4500's grid case because cube reads are keyed-by-variable-name (megamorphic) rather than fixed-name monomorphic.Steady-state mode by construction route (all 50k instances):
ViewRowData+ keyed loop (today)Proposed change
Build on #4500's
RecordDataFactory(same module, shared guards):RecordDataFactory.createAssigner(fieldNames)- compiles(target, values) => voidwith literal keys; returns null under the same guards (field count over threshold,__proto__field name, nounsafe-eval, internal kill switch).Viewcompiles one assigner per query (recompiled onupdateQuerywhen fields change) and exposes it internally to its rows.LeafRowctor andBaseRow.initAggregate()use the assigner when present - each keeps its current loop as the fallback;initAggregateseeds values asappliedDimensions.hasOwnProperty(name) ? appliedDimensions[name] : null, folding away the separateObject.assign.canAggregatebuilt via the same assigner (boolean values).children,cubeBuckets,_cubeLeafChildren) unchanged - probe-verified shape-safe.No public API changes.
ViewRowDataremains a class with its getters and index signature; updates flow untouched.Validation plan
dataobjects fall into V8 dictionary mode β per-store codegen'd literal factory measures 6-8Γ smaller records at typical widthsΒ #4500's probe suite - assigner-built rows FAST + shared maps at 30-120 fields (incl. post-children/cubeBucketsadds), value parity vs the legacy loops, guard behavior (threshold/CSP/__proto__).updateDataAsyncdeltas flowing up through aggregates, bound-store loads,getDimensionValues.omitFn/lockFnpaths,provideLeaves, row-cache reuse across repeated queries,updateQuerywith changed field sets (assigner recompile).