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) | 11.41K ops/s | — | new |
| core/player/src/tests/player.bench.ts > Player data set > set bound data triggers re-resolve | 179.62K ops/s | — | new |
| core/player/src/tests/string-resolver.bench.ts > resolveExpressionsInString > single expression | 1.07M ops/s | — | new |
| core/player/src/tests/string-resolver.bench.ts > resolveExpressionsInString > embedded expressions | 449.34K ops/s | — | new |
| core/player/src/tests/string-resolver.bench.ts > resolveDataRefsInString > single ref | 774.99K ops/s | — | new |
| core/player/src/tests/string-resolver.bench.ts > resolveDataRefsInString > multiple refs | 272.75K ops/s | — | new |
| core/player/src/tests/string-resolver.bench.ts > resolveDataRefsInString > no refs (fast exit) | 5.58M ops/s | — | new |
| core/player/src/tests/string-resolver.bench.ts > resolveDataRefs object > flat (30 keys) | 45.72K ops/s | — | new |
| core/player/src/tests/string-resolver.bench.ts > resolveDataRefs object > nested (depth 3) | 125.47K ops/s | — | new |
| core/player/src/binding/tests/binding.bench.ts > BindingInstance construction > from array (numeric + string segments) | 1.53M ops/s | — | new |
| core/player/src/binding/tests/binding.bench.ts > BindingInstance construction > from string | 1.31M ops/s | — | new |
| core/player/src/binding/tests/binding.bench.ts > BindingParser.parse (simple) > cache hit | 1.61M ops/s | — | new |
| core/player/src/binding/tests/binding.bench.ts > BindingParser.parse (simple) > cache cold (fast-path) | 929.22K ops/s | — | new |
| core/player/src/binding/tests/binding.bench.ts > BindingParser.parse (complex query) > foo.pets[name='frodo'].type | 382.03K ops/s | — | new |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.bar |
947.43K ops/s | 879.70K ops/s | +7.7% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets.1.name |
586.44K ops/s | 567.31K ops/s | +3.4% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets.01.name |
566.52K ops/s | 548.71K ops/s | +3.2% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets['01'].name |
493.78K ops/s | 489.72K ops/s | +0.8% |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets[01].name |
545.79K ops/s | 512.39K ops/s | +6.5% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets[name = "frodo"].type |
323.16K ops/s | 301.85K ops/s | +7.1% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets["name" = "sprinkles"].type |
251.38K ops/s | 239.32K ops/s | +5.0% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets["isDog" = false].type |
322.23K ops/s | 305.91K ops/s | +5.3% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > parser benchmarks > Resolving binding: foo.pets["isDog" = true].type |
323.72K ops/s | 310.29K ops/s | +4.3% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.bar |
634.80K ops/s | 624.19K ops/s | +1.7% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets.1.name |
402.47K ops/s | 404.21K ops/s | -0.4% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets.01.name |
407.87K ops/s | 387.24K ops/s | +5.3% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets['01'].name |
356.55K ops/s | 360.02K ops/s | -1.0% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets[01].name |
401.74K ops/s | 389.29K ops/s | +3.2% |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets[name = "frodo"].type |
254.08K ops/s | 240.57K ops/s | +5.6% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets["name" = "sprinkles"].type |
205.45K ops/s | 192.56K ops/s | +6.7% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets["isDog" = false].type |
256.00K ops/s | 226.09K ops/s | +13.2% ✅ |
core/player/src/binding/__tests__/parser.bench.ts > binding creation benchmarks > Resolving binding: foo.pets["isDog" = true].type |
259.72K ops/s | 235.14K ops/s | +10.5% ✅ |
| core/player/src/data/tests/local-model.bench.ts > LocalModel get > get shallow | 3.43M ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel get > get deep | 2.92M ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel get > get array index | 3.03M ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel set > set existing shallow | 735.68K ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel set > set deep | 450.16K ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel set > set new key | 565.27K ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel set > set batch (15) | 44.02K ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel delete > delete object key | 124.41K ops/s | — | new |
| core/player/src/data/tests/local-model.bench.ts > LocalModel delete > delete array index | 381.49K ops/s | — | new |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = 1 + 3 (sync) |
518.99K ops/s | 441.41K ops/s | +17.6% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = 1 + 3 (async) |
402.25K ops/s | 369.56K ops/s | +8.8% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: conditional(true, true, false) (sync) |
519.47K ops/s | 460.66K ops/s | +12.8% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: conditional(true, true, false) (async) |
453.77K ops/s | 398.91K ops/s | +13.8% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional({{bar}} > 0, true, false) (sync) |
236.04K ops/s | 195.52K ops/s | +20.7% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional({{bar}} > 0, true, false) (async) |
216.27K ops/s | 170.11K ops/s | +27.1% ✅ |
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) |
174.59K ops/s | 161.81K ops/s | +7.9% ✅ |
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) |
168.71K ops/s | 146.70K ops/s | +15.0% ✅ |
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) |
295.16K ops/s | 259.21K ops/s | +13.9% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = asyncTestFunction(1) (sync) |
368.45K ops/s | 300.53K ops/s | +22.6% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = asyncTestFunction(1) (async) |
326.89K ops/s | 287.64K ops/s | +13.6% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: asyncTestFunction(1) (sync) |
938.56K ops/s | 849.70K ops/s | +10.5% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: asyncTestFunction(1) (async) |
731.01K ops/s | 664.21K ops/s | +10.1% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional(!{{bar}} == false, await(asyncTestFunction(1)), false) (sync) |
203.76K ops/s | 179.69K ops/s | +13.4% ✅ |
core/player/src/expressions/__tests__/performance.bench.ts > Expression Parsing/Execution Benchmark > Parsing: {{foo}} = conditional(!{{bar}} == false, await(asyncTestFunction(1)), false) (async) |
186.94K ops/s | 160.62K ops/s | +16.4% ✅ |
| core/player/src/schema/tests/schema.bench.ts > SchemaController getType (uncached binding) > shallow binding | 473.93K ops/s | — | new |
| core/player/src/schema/tests/schema.bench.ts > SchemaController getType (uncached binding) > deep binding (array+record, 6 segments) | 162.06K ops/s | — | new |
| core/player/src/schema/tests/schema.bench.ts > SchemaController getApparentType (cached binding) > repeat same binding | 3.05M ops/s | — | new |
| core/player/src/controllers/data/tests/controller.bench.ts > DataController set > set no-op | 1.09M ops/s | — | new |
| core/player/src/controllers/data/tests/controller.bench.ts > DataController set > set changed scalar | 505.05K ops/s | — | new |
| core/player/src/controllers/data/tests/controller.bench.ts > DataController set > set changed object | 451.51K ops/s | — | new |
| core/player/src/controllers/data/tests/controller.bench.ts > DataController set > set batch (15, all changed) | 21.57K ops/s | — | new |
| core/player/src/controllers/data/tests/controller.bench.ts > DataController get > get hot | 2.77M ops/s | — | new |
core/player/src/view/resolver/__tests__/index.bench.ts > resolver benchmarks > initial resolve |
663.49 ops/s | 627.37 ops/s | +5.8% ✅ |
core/player/src/view/resolver/__tests__/index.bench.ts > resolver benchmarks > Resolving from cache |
20.92K ops/s | 18.15K ops/s | +15.3% ✅ |
core/player/src/view/resolver/__tests__/index.bench.ts > resolver benchmarks > data changes |
3.07K ops/s | 2.97K ops/s | +3.4% |
core/player/src/view/resolver/__tests__/index.bench.ts > resolver benchmarks > data changes slow |
649.58 ops/s | 583.21 ops/s | +11.4% ✅ |
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 |
14.55K ops/s | 4.92K ops/s | +195.7% ✅ |
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 5 times |
14.50K ops/s | 5.74K ops/s | +152.3% ✅ |
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 10 times |
10.30K ops/s | 4.09K ops/s | +151.7% ✅ |
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 50 times |
3.39K ops/s | 1.48K ops/s | +129.9% ✅ |
plugins/async-node/core/src/__tests__/index.bench.ts > async node benchmarks > Resolve Async Node 100 times |
1.72K ops/s | 897.89 ops/s | +92.1% ✅ |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 1 times |
7.35K ops/s | 2.93K ops/s | +150.9% ✅ |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 5 times |
8.29K ops/s | 3.12K ops/s | +165.4% ✅ |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 10 times |
7.15K ops/s | 2.68K ops/s | +167.1% ✅ |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 50 times |
2.91K ops/s | 1.31K ops/s | +122.8% ✅ |
plugins/async-node/core/src/__tests__/transform.bench.ts > async transform benchmarks > Resolve Async Node 100 times |
1.66K ops/s | 767.69 ops/s | +115.7% ✅ |
react/player
| Benchmark | Current | Baseline | Change |
|---|---|---|---|
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 1 ReactAssets |
625.58 ops/s | 346.43 ops/s | +80.6% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 1 ReactAssets |
1.13K ops/s | 421.19 ops/s | +168.1% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 5 ReactAssets |
617.85 ops/s | 298.87 ops/s | +106.7% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 5 ReactAssets |
1.11K ops/s | 424.04 ops/s | +161.4% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 10 ReactAssets |
626.49 ops/s | 323.13 ops/s | +93.9% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 10 ReactAssets |
885.06 ops/s | 355.28 ops/s | +149.1% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 50 ReactAssets |
504.47 ops/s | 262.79 ops/s | +92.0% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 50 ReactAssets |
282.74 ops/s | 125.72 ops/s | +124.9% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Render asset nested in 100 ReactAssets |
321.64 ops/s | 199.87 ops/s | +60.9% ✅ |
react/player/src/asset/__tests__/index.bench.tsx > ReactAsset benchmarks > Bubble errors nested in 100 ReactAssets |
120.88 ops/s | 54.91 ops/s | +120.2% ✅ |
Bundle ReportChanges will increase total bundle size by 42.84kB (0.76%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: plugins/markdown/coreAssets Changed:
view changes for bundle: plugins/reference-assets/coreAssets Changed:
view changes for bundle: plugins/common-types/coreAssets Changed:
view changes for bundle: plugins/async-node/coreAssets Changed:
view changes for bundle: core/playerAssets Changed:
view changes for bundle: plugins/metrics/coreAssets Changed:
view changes for bundle: plugins/check-path/coreAssets Changed:
view changes for bundle: plugins/common-expressions/coreAssets Changed:
view changes for bundle: plugins/stage-revert-data/coreAssets Changed:
view changes for bundle: plugins/beacon/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%
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.