Initial Performance Pass#876
Conversation
Benchmark ResultsComparison against baseline from
|
| Benchmark | Current | Baseline | Change |
|---|---|---|---|
| core/player/src/tests/player.bench.ts > Player.start > minimal flow (cold start) | 5.48K ops/s | — | new |
| core/player/src/tests/player.bench.ts > Player data set > set bound data triggers re-resolve | 110.74K ops/s | — | new |
| core/player/src/tests/string-resolver.bench.ts > resolveExpressionsInString > single expression | 717.12K ops/s | — | new |
| core/player/src/tests/string-resolver.bench.ts > resolveExpressionsInString > embedded expressions | 256.32K ops/s | — | new |
| core/player/src/tests/string-resolver.bench.ts > resolveDataRefsInString > single ref | 466.92K ops/s | — | new |
| core/player/src/tests/string-resolver.bench.ts > resolveDataRefsInString > multiple refs | 192.96K ops/s | — | new |
| core/player/src/tests/string-resolver.bench.ts > resolveDataRefsInString > no refs (fast exit) | 3.31M ops/s | — | new |
| core/player/src/tests/string-resolver.bench.ts > resolveDataRefs object > flat (30 keys) | 28.46K ops/s | — | new |
| core/player/src/tests/string-resolver.bench.ts > resolveDataRefs object > nested (depth 3) | 75.46K ops/s | — | new |
| core/player/src/tests/string-resolver.bench.ts > resolveDataRefs object > large nested (8 sections x 10 fields, depth 5) | 1.47K ops/s | — | new |
| core/player/src/binding/tests/binding.bench.ts > BindingInstance construction > from array (numeric + string segments) | 1.15M ops/s | — | new |
| core/player/src/binding/tests/binding.bench.ts > BindingInstance construction > from string | 881.47K ops/s | — | new |
| core/player/src/binding/tests/binding.bench.ts > BindingParser.parse (simple) > cache hit | 1.04M ops/s | — | new |
| core/player/src/binding/tests/binding.bench.ts > BindingParser.parse (simple) > cache cold (fast-path) | 554.81K ops/s | — | new |
| core/player/src/binding/tests/binding.bench.ts > BindingParser.parse (complex query) > foo.pets[name='frodo'].type | 349.77K ops/s | — | new |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.bar |
621.22K ops/s | 792.61K ops/s | -21.6% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets.1.name |
399.69K ops/s | 447.84K ops/s | -10.8% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets.01.name |
462.12K ops/s | 452.53K ops/s | +2.1% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets['01'].name |
342.73K ops/s | 461.73K ops/s | -25.8% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets[01].name |
286.89K ops/s | 464.71K ops/s | -38.3% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets[name = "frodo"].type |
168.14K ops/s | 214.51K ops/s | -21.6% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets["name" = "sprinkles"].type |
218.65K ops/s | 189.71K ops/s | +15.3% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets["isDog" = false].type |
193.69K ops/s | 271.18K ops/s | -28.6% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets["isDog" = true].type |
166.70K ops/s | 228.34K ops/s | -27.0% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.bar |
357.45K ops/s | 484.95K ops/s | -26.3% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets.1.name |
357.58K ops/s | 259.12K ops/s | +38.0% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets.01.name |
384.53K ops/s | 279.20K ops/s | +37.7% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets['01'].name |
314.75K ops/s | 285.38K ops/s | +10.3% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets[01].name |
346.43K ops/s | 363.01K ops/s | -4.6% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets[name = "frodo"].type |
173.96K ops/s | 243.94K ops/s | -28.7% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets["name" = "sprinkles"].type |
131.62K ops/s | 200.91K ops/s | -34.5% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets["isDog" = false].type |
162.05K ops/s | 237.34K ops/s | -31.7% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets["isDog" = true].type |
173.17K ops/s | 230.38K ops/s | -24.8% |
| core/player/src/data/tests/local-model.bench.ts > LocalModel get > get shallow | 2.41M ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel get > get deep | 2.05M ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel get > get array index | 2.10M ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel set > set existing shallow | 378.36K ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel set > set deep | 289.57K ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel set > set new key | 337.81K ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel set > set batch (15) | 28.24K ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel set > set batch (50, shared parent) | 9.71K ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel set > set batch (7, scattered paths) | 50.73K ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel delete > delete object key | 72.02K ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel delete > delete array index | 235.91K ops/s | — | new |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = 1 + 3 (sync) |
291.12K ops/s | 431.98K ops/s | -32.6% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = 1 + 3 (async) |
260.56K ops/s | 256.47K ops/s | +1.6% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: conditional(true, true, false) (sync) |
367.59K ops/s | 446.50K ops/s | -17.7% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: conditional(true, true, false) (async) |
344.86K ops/s | 411.46K ops/s | -16.2% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional({{bar}} > 0, true, false) (sync) |
147.94K ops/s | 187.69K ops/s | -21.2% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional({{bar}} > 0, true, false) (async) |
133.88K ops/s | 176.75K ops/s | -24.3% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional(conditional(true = false, false, true), conditional(false = false, true, false), conditional(true = true, false, true)) (sync) |
109.62K ops/s | 153.94K ops/s | -28.8% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional(conditional(true = false, false, true), conditional(false = false, true, false), conditional(true = true, false, true)) (async) |
97.94K ops/s | 146.44K ops/s | -33.1% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = await(asyncTestFunction(1)) (sync) |
N/A | N/A | N/A |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = await(asyncTestFunction(1)) (async) |
234.46K ops/s | 274.66K ops/s | -14.6% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = asyncTestFunction(1) (sync) |
200.22K ops/s | 304.58K ops/s | -34.3% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = asyncTestFunction(1) (async) |
178.08K ops/s | 250.35K ops/s | -28.9% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: asyncTestFunction(1) (sync) |
499.55K ops/s | 802.33K ops/s | -37.7% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: asyncTestFunction(1) (async) |
410.18K ops/s | 571.23K ops/s | -28.2% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional(!{{bar}} == false, await(asyncTestFunction(1)), false) (sync) |
114.11K ops/s | 155.77K ops/s | -26.7% |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional(!{{bar}} == false, await(asyncTestFunction(1)), false) (async) |
95.94K ops/s | 128.38K ops/s | -25.3% |
| core/player/src/schema/tests/schema.bench.ts > SchemaController getType (uncached binding) > shallow binding | 379.72K ops/s | — | new |
| core/player/src/schema/tests/schema.bench.ts > SchemaController getType (uncached binding) > deep binding (array+record, 6 segments) | 105.38K ops/s | — | new |
| core/player/src/schema/tests/schema.bench.ts > SchemaController getApparentType (cached binding) > repeat same binding | 2.58M ops/s | — | new |
| core/player/src/controllers/data/tests/controller.bench.ts > DataController set > set no-op | 584.24K ops/s | — | new |
| core/player/src/controllers/data/tests/controller.bench.ts > DataController set > set changed scalar | 312.59K ops/s | — | new |
| core/player/src/controllers/data/tests/controller.bench.ts > DataController set > set changed object | 292.20K ops/s | — | new |
| core/player/src/controllers/data/tests/controller.bench.ts > DataController set > set batch (15, all changed) | 11.84K ops/s | — | new |
| core/player/src/controllers/data/tests/controller.bench.ts > DataController set > set batch (50, all changed) | 4.28K ops/s | — | new |
| core/player/src/controllers/data/tests/controller.bench.ts > DataController get > get hot | 1.81M ops/s | — | new |
core/player/src/view/resolver/__tests__/index.bench.ts > resolver benchmarks > initial resolve |
451.17 ops/s | 611.80 ops/s | -26.3% |
core/player/src/view/resolver/__tests__/index.bench.ts > resolver benchmarks > Resolving from cache |
11.03K ops/s | 18.52K ops/s | -40.5% |
core/player/src/view/resolver/__tests__/index.bench.ts > resolver benchmarks > data changes |
2.23K ops/s | 2.99K ops/s | -25.5% |
core/player/src/view/resolver/__tests__/index.bench.ts > resolver benchmarks > data changes slow |
429.98 ops/s | 635.20 ops/s | -32.3% |
| core/player/src/view/resolver/tests/index.bench.ts > resolver wide-node benchmarks > initial resolve (wide: 50 children) | 1.34K ops/s | — | new |
| core/player/src/view/resolver/tests/index.bench.ts > resolver wide-node benchmarks > data change on wide node (50 children) | 2.13K ops/s | — | new |
plugins/async-node/core ⚠️
| Benchmark | Current | Baseline | Change |
|---|---|---|---|
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 1 times |
7.98K ops/s | 14.01K ops/s | -43.0% |
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 5 times |
7.28K ops/s | 12.49K ops/s | -41.7% |
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 10 times |
8.88K ops/s | 10.49K ops/s | -15.3% |
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 50 times |
2.59K ops/s | 3.25K ops/s | -20.3% |
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 100 times |
1.41K ops/s | 1.78K ops/s | -20.8% |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 1 times |
4.73K ops/s | 5.91K ops/s | -19.9% |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 5 times |
5.23K ops/s | 7.91K ops/s | -33.9% |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 10 times |
4.65K ops/s | 6.92K ops/s | -32.8% |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 50 times |
1.62K ops/s | 2.60K ops/s | -37.7% |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 100 times |
1.07K ops/s | 1.50K ops/s | -28.6% |
react/player
| Benchmark | Current | Baseline | Change |
|---|---|---|---|
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 1 ReactAssets |
634.42 ops/s | 532.19 ops/s | +19.2% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 1 ReactAssets |
1.14K ops/s | 742.64 ops/s | +53.5% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 5 ReactAssets |
637.99 ops/s | 529.03 ops/s | +20.6% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 5 ReactAssets |
1.14K ops/s | 787.69 ops/s | +44.4% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 10 ReactAssets |
653.61 ops/s | 530.01 ops/s | +23.3% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 10 ReactAssets |
924.25 ops/s | 701.94 ops/s | +31.7% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 50 ReactAssets |
489.98 ops/s | 426.81 ops/s | +14.8% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 50 ReactAssets |
266.27 ops/s | 237.77 ops/s | +12.0% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 100 ReactAssets |
420.35 ops/s | 329.70 ops/s | +27.5% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 100 ReactAssets |
126.77 ops/s | 78.58 ops/s | +61.3% ✅ |
Bundle ReportChanges will increase total bundle size by 38.68kB (0.75%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: plugins/check-path/coreAssets Changed:
view changes for bundle: plugins/reference-assets/coreAssets Changed:
view changes for bundle: plugins/markdown/coreAssets Changed:
view changes for bundle: core/playerAssets Changed:
view changes for bundle: plugins/external-state/coreAssets Changed:
view changes for bundle: plugins/async-node/coreAssets Changed:
view changes for bundle: plugins/beacon/coreAssets Changed:
view changes for bundle: plugins/metrics/coreAssets Changed:
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #876 +/- ##
===========================
===========================
☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Ran the JVM perf tests locally because we don't have them set up in CI yet and here are the results
|
| ) { | ||
| const split = Array.isArray(raw) ? raw : raw.split("."); | ||
| this.split = split.map((segment) => { | ||
| const normalized: RawBindingSegment[] = new Array(split.length); |
There was a problem hiding this comment.
I'm thinking it might be helpful to drop a comment here that doing the manual for-loop is for better performance. Might help prevent this accidentally getting changed later. Thoughts?
| @@ -0,0 +1,95 @@ | |||
| import { bench, describe } from "vitest"; | |||
There was a problem hiding this comment.
Unrelated to this line; just making reply-able:
-
Is this PR targeting memory performance only? (Not speed?)
-
Are the before and after flipped in your table? Because this table is showing that everything has a significant increase in memory, which seems unintended?
Benchmark Before After Change LocalModel set batch (15) 30.8K 114.7K +272%
There was a problem hiding this comment.
Its only looking at performance right now because we don't currently have a good memory profiling strategy. So the benchmarks are in terms of ops/s so its execution of speedup, not memory.
Optimizes the hot paths in
core/player— data get/set, binding parse/construction,expression evaluation, string/template resolution, and the view-resolution tree walk.
These run on every data change and view render, so per-call costs compound across a flow.
All changes preserve existing behavior and conventions, keep the file layout unchanged,
and are written to stay cheap once transpiled to ES5 for the Hermes mobile runtime
(prefer indexed loops /
Object.assign/ direct property access over spread,for…of,and destructuring, which become heavier helper calls on-device).
What changed
Data model
data/local-model— batchsetwith structural sharing. A multi-bindingtransaction now clones each touched container at most once (tracked in an
ownedWeakSet) instead of re-cloning shared ancestors per binding. Single-binding sets keep
using timm's
setIndirectly. The COW helper lives indata/owned-set-in.tsandmirrors timm's
setInexactly (intermediate creation, no-op short-circuit,symbol-preserving clone, progressive reads), so it's a behavior-preserving drop-in.
controllers/data(DataController.set) —===fast path before the deepdequal, indexed loops instead ofreduce/map/destructuring, Set built without anintermediate array.
Bindings
BindingParser.parsereusesparserOptionsdirectly when no overrides are passed andmerges
updatesin place;BindingInstanceconstructor uses an indexed loop.Expressions
(literals, identifiers, model refs, member access) skip the allocation.
String / template resolution
traverseObjectno longer calls timmsetInper key (which re-cloned the whole objecteach time, ≈O(n²)); it now clones once, lazily, on first change. Hoisted the expression
regex to a module constant and use
match.index.View resolver (
computeTree)resolveduses the same copy-on-writesetIn(shareddata/owned-set-in.ts), cloning shared ancestors once per node instead of once perchild. Guarded to nodes with more than one child — narrow nodes keep plain
setInso the WeakSet bookkeeping never becomes overhead where there's nothing to share. The
fresh-per-node
ownedset guarantees the hook value / reusedpreviousResult.valueisnever mutated (first write always clones), preserving the "unchanged → same reference"
invariant the cache relies on.
so the complete set is just
getDependencies(); dropped the redundant per-node copy +re-merge and hand the (local, discarded-after) set off directly.
Object.assigninstead of spread for the per-node options and children rebuild (cheaperonce transpiled, behavior identical).
Benchmarks
*.bench.tscoverage for the data model, data controller, string-resolver,schema, binding, and an end-to-end
Player.start/ data-set re-resolve.for
traverseObject, larger and scattered-path batch sets, larger data-controllerbatches, and a wide-node resolver view (many sibling children) that exercises the
resolver COW path the existing deep-but-narrow view could not.
Benchmark results
Vitest
bench, ops/sec, measured with controlled before/after A/Bs on the same machine.Only changes above the ~±5% run-to-run noise floor are listed; everything else was
neutral, and there were no regressions.
LocalModel set batch (15)DataController set batch (15, all changed)resolveDataRefs object — flat (30 keys)view resolver — data change on wide node (50 children)resolveDataRefs object — nested (depth 3)BindingParser.parse — foo.pets.1.nameview resolver — initial resolve (wide: 50 children)view resolver — resolving from cacheresolveExpressionsInString — singleexpression eval — {{foo}} = 1 + 3BindingParser.parse — cache hitDataController set — no-op / changed scalarNotes
core/playerunit tests and lint pass; no behavioral changes.setand resolver copy-on-write share one implementation(
data/owned-set-in.ts) that mirrors timm'ssetInsemantics; both are guarded so thecommon single-write path is never penalized.
machine (the CI benchmark job compares each PR against the recorded baseline
automatically).
Change Type (required)
Indicate the type of change your pull request is:
patchminormajorN/ADoes your PR have any documentation updates?
Release Notes
Performance improvements to Player Core. For full explanation see original PR.