From fd4e5705a9986fc078d3f8fd563149f19a437984 Mon Sep 17 00:00:00 2001 From: Dima Giesler Date: Sat, 12 Sep 2026 02:53:44 +0200 Subject: [PATCH 01/28] chore(bench): add macos 27 beta webkit bench results --- .../results/apple-macos-27-beta-webkit.json | 6508 ++++++++++++----- 1 file changed, 4851 insertions(+), 1657 deletions(-) diff --git a/packages/exojs-bench/results/apple-macos-27-beta-webkit.json b/packages/exojs-bench/results/apple-macos-27-beta-webkit.json index ed5ca7235..908a3ca9a 100644 --- a/packages/exojs-bench/results/apple-macos-27-beta-webkit.json +++ b/packages/exojs-bench/results/apple-macos-27-beta-webkit.json @@ -1,5 +1,5 @@ { - "schemaVersion": 6, + "schemaVersion": 7, "profile": { "slug": "apple-macos-27-beta-webkit", "gpu": "apple", @@ -12,7 +12,7 @@ "prerelease": true }, "engineVersion": "0.17.0", - "measuredAt": "2026-09-06T08:00:44.330Z", + "measuredAt": "2026-09-12T00:18:12.752Z", "runs": 3 }, "rendering": { @@ -35,11 +35,15 @@ "source": "declared", "evidence": "the runner declared the platform as '27-beta'" }, + "clock": { + "resolutionMs": 0.01999999999998181, + "crossOriginIsolated": true + }, "flags": [], "headless": true, "software": false, "engineVersion": "0.17.0", - "timestamp": "2026-09-06T06:44:56.679Z" + "timestamp": "2026-09-11T23:43:51.148Z" }, { "backend": "webgpu", @@ -57,12 +61,16 @@ "source": "declared", "evidence": "the runner declared the platform as '27-beta'" }, + "clock": { + "resolutionMs": 0.01999999999998181, + "crossOriginIsolated": true + }, "flags": [], "headless": true, "software": false, "slotTier": 16, "engineVersion": "0.17.0", - "timestamp": "2026-09-06T07:05:32.848Z" + "timestamp": "2026-09-11T23:51:08.527Z" } ] }, @@ -84,11 +92,15 @@ "source": "declared", "evidence": "the runner declared the platform as '27-beta'" }, + "clock": { + "resolutionMs": 0.01999999999998181, + "crossOriginIsolated": true + }, "flags": [], "headless": true, "software": false, "engineVersion": "0.17.0", - "timestamp": "2026-09-06T07:12:45.394Z" + "timestamp": "2026-09-11T23:56:01.601Z" }, { "backend": "webgpu", @@ -106,12 +118,16 @@ "source": "declared", "evidence": "the runner declared the platform as '27-beta'" }, + "clock": { + "resolutionMs": 0.01999999999998181, + "crossOriginIsolated": true + }, "flags": [], "headless": true, "software": false, "slotTier": 16, "engineVersion": "0.17.0", - "timestamp": "2026-09-06T07:33:43.038Z" + "timestamp": "2026-09-12T00:03:21.362Z" } ] }, @@ -133,11 +149,15 @@ "source": "declared", "evidence": "the runner declared the platform as '27-beta'" }, + "clock": { + "resolutionMs": 0.01999999999998181, + "crossOriginIsolated": true + }, "flags": [], "headless": true, "software": false, "engineVersion": "0.17.0", - "timestamp": "2026-09-06T07:40:54.750Z" + "timestamp": "2026-09-12T00:08:10.870Z" }, { "backend": "webgpu", @@ -155,12 +175,16 @@ "source": "declared", "evidence": "the runner declared the platform as '27-beta'" }, + "clock": { + "resolutionMs": 0.01999999999998181, + "crossOriginIsolated": true + }, "flags": [], "headless": true, "software": false, "slotTier": 16, "engineVersion": "0.17.0", - "timestamp": "2026-09-06T08:00:44.330Z" + "timestamp": "2026-09-12T00:15:31.445Z" } ] } @@ -182,9 +206,10 @@ "backends": [ { "backend": "webgl2", - "headlineCount": 5000, + "headlineCount": null, "competitors": [ "excalibur", + "phaser", "pixi" ], "sections": [ @@ -194,35 +219,80 @@ { "archetype": "static-heavy", "category": "Node scaling", - "count": 5000, + "count": 1000, + "loadId": "1k", + "unit": "sprites", + "primary": false, "cells": [ { "competitor": "excalibur", - "referenceMs": 0.12000000000034561, - "referenceP95Ms": 0.24000000000023647, + "referenceMs": 0.20000000000015916, + "referenceP95Ms": 0.5199999999999818, + "referenceGpuMs": 16.660000000000082, "referenceOverFrameBudget": false, - "competitorMs": 8.670000000000073, - "competitorP95Ms": 13.039999999999964, + "competitorMs": 2.30000000000004, + "competitorP95Ms": 8.879999999999654, + "competitorGpuMs": 16.660000000000082, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.013840830449866736, - "factor": 72.24999999979252, - "label": "ExoJS leads clearly (72.25x)", + "ratio": 0.08695652173919813, + "factor": 11.499999999991047, + "label": "ExoJS leads clearly (11.50x)", "structural": true }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.20000000000004547, + "maxMs": 0.22000000000002728, + "ratio": 1.0999999999998864 + }, + "competitor": { + "minMs": 2.2200000000000273, + "maxMs": 2.4700000000000273, + "ratio": 1.1126126126126112 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "phaser", + "referenceMs": 0.20000000000015916, + "referenceP95Ms": 0.5199999999999818, + "referenceGpuMs": 16.660000000000082, + "referenceOverFrameBudget": false, + "competitorMs": 1.3099999999998317, + "competitorP95Ms": 1.7999999999999545, + "competitorGpuMs": 16.679999999999836, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.15267175572533195, + "factor": 6.549999999993946, + "label": "ExoJS leads clearly (6.55x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.11999999999989086, - "maxMs": 0.13999999999987267, - "ratio": 1.1666666666666667 + "minMs": 0.20000000000004547, + "maxMs": 0.22000000000002728, + "ratio": 1.0999999999998864 }, "competitor": { - "minMs": 8.519999999999982, - "maxMs": 9.410000000000082, - "ratio": 1.1044600938967255 + "minMs": 1.2999999999999545, + "maxMs": 1.3199999999999363, + "ratio": 1.015384615384602 }, "stable": true, "rungs": [ @@ -234,11 +304,13 @@ }, { "competitor": "pixi", - "referenceMs": 0.12000000000034561, - "referenceP95Ms": 0.24000000000023647, + "referenceMs": 0.20000000000015916, + "referenceP95Ms": 0.5199999999999818, + "referenceGpuMs": 16.660000000000082, "referenceOverFrameBudget": false, - "competitorMs": 0.15999999999985448, - "competitorP95Ms": 0.2600000000002183, + "competitorMs": 0.18000000000006366, + "competitorP95Ms": 0.4599999999999227, + "competitorGpuMs": 16.680000000000007, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -247,23 +319,24 @@ "label": "unstable across runs", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { "runs": 3, "reference": { - "minMs": 0.11999999999989086, - "maxMs": 0.13999999999987267, - "ratio": 1.1666666666666667 + "minMs": 0.20000000000004547, + "maxMs": 0.22000000000002728, + "ratio": 1.0999999999998864 }, "competitor": { - "minMs": 0.13999999999987267, - "maxMs": 0.15999999999985448, - "ratio": 1.1428571428571428 + "minMs": 0.17999999999989313, + "maxMs": 0.2199999999997999, + "ratio": 1.2222222222218362 }, "stable": false, "rungs": [ "level", - "exojs-leads", + "competitor-leads", "level" ] } @@ -271,37 +344,43 @@ ] }, { - "archetype": "dynamic-heavy", + "archetype": "static-heavy", "category": "Node scaling", - "count": 5000, + "count": 10000, + "loadId": "10k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "excalibur", - "referenceMs": 1.0599999999994907, - "referenceP95Ms": 1.5399999999990541, + "referenceMs": 0.13999999999987267, + "referenceP95Ms": 0.23999999999978172, + "referenceGpuMs": 16.66000000000031, "referenceOverFrameBudget": false, - "competitorMs": 10.470000000001164, - "competitorP95Ms": 13.180000000000291, - "competitorOverFrameBudget": false, + "competitorMs": 19.539999999999964, + "competitorP95Ms": 23.559999999999945, + "competitorGpuMs": 20.460000000000036, + "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.10124164278886082, - "factor": 9.877358490571883, - "label": "ExoJS leads clearly (9.88x)", + "ratio": 0.007164790173995544, + "factor": 139.57142857155523, + "label": "ExoJS leads clearly (139.57x)", "structural": true }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { "runs": 3, "reference": { - "minMs": 1.050000000000182, - "maxMs": 1.1000000000003638, - "ratio": 1.0476190476192127 + "minMs": 0.13999999999987267, + "maxMs": 0.15999999999985448, + "ratio": 1.1428571428571428 }, "competitor": { - "minMs": 9.800000000001091, - "maxMs": 10.929999999998472, - "ratio": 1.1153061224486995 + "minMs": 19.26000000000022, + "maxMs": 19.54000000000042, + "ratio": 1.0145379023883798 }, "stable": true, "rungs": [ @@ -312,75 +391,35 @@ } }, { - "competitor": "pixi", - "referenceMs": 1.0599999999994907, - "referenceP95Ms": 1.5399999999990541, - "referenceOverFrameBudget": false, - "competitorMs": 0.7799999999992906, - "competitorP95Ms": 1.1799999999993815, - "competitorOverFrameBudget": false, - "verdict": { - "side": "competitor", - "ratio": 1.358974358974942, - "factor": 1.358974358974942, - "label": "competitor leads (1.36x)", - "structural": false - }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", - "aggregate": { - "runs": 3, - "reference": { - "minMs": 1.050000000000182, - "maxMs": 1.1000000000003638, - "ratio": 1.0476190476192127 - }, - "competitor": { - "minMs": 0.680000000000291, - "maxMs": 0.8600000000005821, - "ratio": 1.264705882353256 - }, - "stable": true, - "rungs": [ - "competitor-leads", - "competitor-leads", - "competitor-leads" - ] - } - } - ] - }, - { - "archetype": "deep-hierarchy", - "category": "Node scaling", - "count": 5000, - "cells": [ - { - "competitor": "excalibur", - "referenceMs": 0.37999999999919964, - "referenceP95Ms": 0.5, + "competitor": "phaser", + "referenceMs": 0.13999999999987267, + "referenceP95Ms": 0.23999999999978172, + "referenceGpuMs": 16.66000000000031, "referenceOverFrameBudget": false, - "competitorMs": 12.430000000000291, - "competitorP95Ms": 15.080000000001746, + "competitorMs": 3.2200000000000273, + "competitorP95Ms": 7.440000000000055, + "competitorGpuMs": 16.66000000000031, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.030571198712726528, - "factor": 32.710526315859134, - "label": "ExoJS leads clearly (32.71x)", + "ratio": 0.043478260869525304, + "factor": 23.000000000021114, + "label": "ExoJS leads clearly (23.00x)", "structural": true }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { "runs": 3, "reference": { - "minMs": 0.33000000000083674, - "maxMs": 0.42000000000007276, - "ratio": 1.2727272727242662 + "minMs": 0.13999999999987267, + "maxMs": 0.15999999999985448, + "ratio": 1.1428571428571428 }, "competitor": { - "minMs": 12.37999999999738, - "maxMs": 12.5, - "ratio": 1.0096930533120068 + "minMs": 3.1100000000003547, + "maxMs": 3.2500000000002274, + "ratio": 1.045016077170372 }, "stable": true, "rungs": [ @@ -392,31 +431,34 @@ }, { "competitor": "pixi", - "referenceMs": 0.37999999999919964, - "referenceP95Ms": 0.5, + "referenceMs": 0.13999999999987267, + "referenceP95Ms": 0.23999999999978172, + "referenceGpuMs": 16.66000000000031, "referenceOverFrameBudget": false, - "competitorMs": 0.38000000000010914, - "competitorP95Ms": 0.5799999999999272, + "competitorMs": 0.15000000000009095, + "competitorP95Ms": 0.2600000000002183, + "competitorGpuMs": 16.66000000000031, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9999999999976066, + "ratio": 0.9333333333319186, "factor": 1, "label": "level", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { "runs": 3, "reference": { - "minMs": 0.33000000000083674, - "maxMs": 0.42000000000007276, - "ratio": 1.2727272727242662 + "minMs": 0.13999999999987267, + "maxMs": 0.15999999999985448, + "ratio": 1.1428571428571428 }, "competitor": { - "minMs": 0.38000000000010914, - "maxMs": 0.3999999999996362, - "ratio": 1.0526315789461087 + "minMs": 0.13999999999987267, + "maxMs": 0.15999999999985448, + "ratio": 1.1428571428571428 }, "stable": true, "rungs": [ @@ -429,121 +471,129 @@ ] }, { - "archetype": "lifecycle-churn", + "archetype": "static-heavy", "category": "Node scaling", - "count": 5000, + "count": 100000, + "loadId": "100k", + "unit": "sprites", + "primary": false, "cells": [ { - "competitor": "excalibur", - "referenceMs": 4.009999999994761, - "referenceP95Ms": 6.119999999995343, + "competitor": "phaser", + "referenceMs": 0.06000000000040018, + "referenceP95Ms": 0.2600000000002183, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 25.19000000000233, - "competitorP95Ms": 27.75999999998021, + "competitorMs": 17.629999999999654, + "competitorP95Ms": 22, + "competitorGpuMs": 18.23999999999978, "competitorOverFrameBudget": true, "verdict": { - "side": "exojs", - "ratio": 0.1591901548231199, - "factor": 6.281795511230733, - "label": "ExoJS leads clearly (6.28x)", - "structural": true + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/2 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "competitor issues fewer draw calls (25 vs 7 per frame)", + "timer": "limited", "aggregate": { - "runs": 3, + "runs": 1, "reference": { - "minMs": 3.9100000000034925, - "maxMs": 4.690000000002328, - "ratio": 1.1994884910481174 + "minMs": 0.06000000000040018, + "maxMs": 0.06000000000040018, + "ratio": 1 }, "competitor": { - "minMs": 25.019999999989523, - "maxMs": 25.919999999998254, - "ratio": 1.0359712230219467 + "minMs": 17.629999999999654, + "maxMs": 17.629999999999654, + "ratio": 1 }, - "stable": true, + "stable": false, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", "exojs-leads-clearly" ] } }, { "competitor": "pixi", - "referenceMs": 4.009999999994761, - "referenceP95Ms": 6.119999999995343, + "referenceMs": 0.06000000000040018, + "referenceP95Ms": 0.24000000000069122, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 2.3399999999965075, - "competitorP95Ms": 6.6600000000034925, + "competitorMs": 0.03999999999996362, + "competitorP95Ms": 0.23999999999978172, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 1.7136752136755327, - "factor": 1.7136752136755327, - "label": "competitor leads (1.71x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/2 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "competitor issues fewer draw calls (25 vs 1 per frame)", + "timer": "limited", "aggregate": { "runs": 3, "reference": { - "minMs": 3.9100000000034925, - "maxMs": 4.690000000002328, - "ratio": 1.1994884910481174 + "minMs": 0.06000000000040018, + "maxMs": 0.08999999999969077, + "ratio": 1.4999999999848417 }, "competitor": { - "minMs": 2.2399999999979627, - "maxMs": 2.7599999999947613, - "ratio": 1.232142857141639 + "minMs": 0.03000000000020009, + "maxMs": 0.06000000000040018, + "ratio": 2 }, - "stable": true, + "stable": false, "rungs": [ - "competitor-leads", + "level", "competitor-leads", "competitor-leads" ] } } ] - } - ] - }, - { - "title": "Fill and GPU state", - "rows": [ + }, { - "archetype": "overdraw", - "category": "Fill and GPU state", - "count": 5000, + "archetype": "dynamic-heavy", + "category": "Node scaling", + "count": 10000, + "loadId": "10k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "excalibur", - "referenceMs": 0.020000000000436557, - "referenceP95Ms": 0.040000000000873115, + "referenceMs": 1.75, + "referenceP95Ms": 2.5399999999999636, + "referenceGpuMs": 16.660000000000764, "referenceOverFrameBudget": false, - "competitorMs": 13.380000000004657, - "competitorP95Ms": 15.220000000001164, - "competitorOverFrameBudget": false, + "competitorMs": 25.290000000000873, + "competitorP95Ms": 31.539999999997235, + "competitorGpuMs": 26.279999999998836, + "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.001494768310943916, - "factor": 668.99999998563, - "label": "ExoJS leads clearly (669.00x)", + "ratio": 0.06919731119019136, + "factor": 14.451428571429071, + "label": "ExoJS leads clearly (14.45x)", "structural": true }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.020000000000436557, - "maxMs": 0.020000000000436557, - "ratio": 1 + "minMs": 1.6100000000001273, + "maxMs": 1.9299999999998363, + "ratio": 1.1987577639749587 }, "competitor": { - "minMs": 12.730000000003201, - "maxMs": 14.099999999991269, - "ratio": 1.1076197957570875 + "minMs": 24.969999999997526, + "maxMs": 26.6200000000008, + "ratio": 1.0660792951543228 }, "stable": true, "rungs": [ @@ -554,190 +604,201 @@ } }, { - "competitor": "pixi", - "referenceMs": 0.020000000000436557, - "referenceP95Ms": 0.040000000000873115, + "competitor": "phaser", + "referenceMs": 1.75, + "referenceP95Ms": 2.5399999999999636, + "referenceGpuMs": 16.660000000000764, "referenceOverFrameBudget": false, - "competitorMs": 0.020000000000436557, - "competitorP95Ms": 0.06000000000130967, + "competitorMs": 3.329999999999984, + "competitorP95Ms": 7.6400000000001, + "competitorGpuMs": 16.660000000000082, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": 1, - "factor": 1, - "label": "level", + "side": "exojs", + "ratio": 0.525525525525528, + "factor": 1.902857142857134, + "label": "ExoJS leads (1.90x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.020000000000436557, - "maxMs": 0.020000000000436557, - "ratio": 1 + "minMs": 1.6100000000001273, + "maxMs": 1.9299999999998363, + "ratio": 1.1987577639749587 }, "competitor": { - "minMs": 0.020000000000436557, - "maxMs": 0.020000000000436557, - "ratio": 1 + "minMs": 3.1199999999989814, + "maxMs": 3.3600000000000705, + "ratio": 1.076923076923451 }, "stable": true, "rungs": [ - "level", - "level", - "level" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } - } - ] - }, - { - "archetype": "batch-breaking", - "category": "Fill and GPU state", - "count": 5000, - "cells": [ + }, { - "competitor": "excalibur", - "referenceMs": 1.389999999999418, - "referenceP95Ms": 1.6999999999970896, + "competitor": "pixi", + "referenceMs": 1.75, + "referenceP95Ms": 2.5399999999999636, + "referenceGpuMs": 16.660000000000764, "referenceOverFrameBudget": false, - "competitorMs": 12.789999999999964, - "competitorP95Ms": 14.599999999999909, + "competitorMs": 1.319999999999709, + "competitorP95Ms": 1.9400000000005093, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.10867865519932932, - "factor": 9.20143884892469, - "label": "ExoJS leads clearly (9.20x)", - "structural": true + "side": "competitor", + "ratio": 1.3257575757578681, + "factor": 1.3257575757578681, + "label": "competitor leads (1.33x)", + "structural": false }, - "mechanism": "ExoJS issues fewer buffer uploads (0 vs 313 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 1.3400000000001455, - "maxMs": 1.3999999999941792, - "ratio": 1.0447761193985277 + "minMs": 1.6100000000001273, + "maxMs": 1.9299999999998363, + "ratio": 1.1987577639749587 }, "competitor": { - "minMs": 12.64999999999992, - "maxMs": 18.220000000001164, - "ratio": 1.440316205533698 + "minMs": 1.2199999999998, + "maxMs": 1.4900000000002365, + "ratio": 1.22131147541023 }, "stable": true, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" + "competitor-leads", + "competitor-leads", + "competitor-leads" ] } - }, + } + ] + }, + { + "archetype": "dynamic-all", + "category": "Node scaling", + "count": 1000, + "loadId": "1k", + "unit": "sprites", + "primary": false, + "cells": [ { - "competitor": "pixi", - "referenceMs": 1.389999999999418, - "referenceP95Ms": 1.6999999999970896, + "competitor": "excalibur", + "referenceMs": 1.6199999999998909, + "referenceP95Ms": 2.219999999999345, + "referenceGpuMs": 16.67999999999938, "referenceOverFrameBudget": false, - "competitorMs": 1.2200000000011642, - "competitorP95Ms": 1.9800000000032014, + "competitorMs": 3.2999999999992724, + "competitorP95Ms": 8.220000000001164, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "exojs", + "ratio": 0.49090909090916607, + "factor": 2.037037037036725, + "label": "ExoJS leads (2.04x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 313/5000/0 vs 313/5000/0, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 1.3400000000001455, - "maxMs": 1.3999999999941792, - "ratio": 1.0447761193985277 + "minMs": 1.5300000000002, + "maxMs": 1.6400000000003274, + "ratio": 1.0718954248366752 }, "competitor": { - "minMs": 1.139999999999418, - "maxMs": 1.2400000000016007, - "ratio": 1.0877192982475736 + "minMs": 3.2000000000007276, + "maxMs": 3.4200000000000728, + "ratio": 1.0687499999997798 }, - "stable": false, + "stable": true, "rungs": [ - "competitor-leads", - "level", - "level" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } - } - ] - }, - { - "archetype": "mixed-blend", - "category": "Fill and GPU state", - "count": 5000, - "cells": [ + }, { - "competitor": "excalibur", - "referenceMs": 0.46000000000276486, - "referenceP95Ms": 0.6999999999970896, + "competitor": "phaser", + "referenceMs": 1.6199999999998909, + "referenceP95Ms": 2.219999999999345, + "referenceGpuMs": 16.67999999999938, "referenceOverFrameBudget": false, - "competitorMs": 9.729999999995925, - "competitorP95Ms": 13.339999999996508, + "competitorMs": 1.180000000000291, + "competitorP95Ms": 1.6200000000003456, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.047276464542955546, - "factor": 21.152173912907486, - "label": "ExoJS leads clearly (21.15x)", - "structural": true + "side": "competitor", + "ratio": 1.3728813559317723, + "factor": 1.3728813559317723, + "label": "competitor leads (1.37x)", + "structural": false }, - "mechanism": "competitor issues fewer draw calls (120 vs 1 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.3600000000005821, - "maxMs": 0.5200000000040745, - "ratio": 1.444444444453427 + "minMs": 1.5300000000002, + "maxMs": 1.6400000000003274, + "ratio": 1.0718954248366752 }, "competitor": { - "minMs": 9.709999999999127, - "maxMs": 9.9900000000016, - "ratio": 1.02883625128759 + "minMs": 0.9599999999991269, + "maxMs": 1.2100000000000364, + "ratio": 1.260416666667851 }, "stable": true, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" + "competitor-leads", + "competitor-leads", + "competitor-leads" ] } }, { "competitor": "pixi", - "referenceMs": 0.46000000000276486, - "referenceP95Ms": 0.6999999999970896, + "referenceMs": 1.6199999999998909, + "referenceP95Ms": 2.219999999999345, + "referenceGpuMs": 16.67999999999938, "referenceOverFrameBudget": false, - "competitorMs": 0.26000000000203727, - "competitorP95Ms": 0.3800000000046566, + "competitorMs": 1, + "competitorP95Ms": 1.3800000000001091, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.7692307692275402, - "factor": 1.7692307692275402, - "label": "competitor leads (1.77x)", + "ratio": 1.6199999999998909, + "factor": 1.6199999999998909, + "label": "competitor leads (1.62x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 120/0/0 vs 158/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.3600000000005821, - "maxMs": 0.5200000000040745, - "ratio": 1.444444444453427 + "minMs": 1.5300000000002, + "maxMs": 1.6400000000003274, + "ratio": 1.0718954248366752 }, "competitor": { - "minMs": 0.2400000000052387, - "maxMs": 0.2800000000024738, - "ratio": 1.1666666666515084 + "minMs": 0.9600000000000364, + "maxMs": 1.1700000000000728, + "ratio": 1.2187500000000295 }, "stable": true, "rungs": [ @@ -748,44 +809,45 @@ } } ] - } - ] - }, - { - "title": "Material variety", - "rows": [ + }, { - "archetype": "batch-breaking-atlased", - "category": "Material variety", - "count": 5000, + "archetype": "dynamic-all", + "category": "Node scaling", + "count": 10000, + "loadId": "10k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "excalibur", - "referenceMs": 0.08000000000174623, - "referenceP95Ms": 0.13999999999941792, + "referenceMs": 4.1299999999992, + "referenceP95Ms": 5.300000000001091, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 10.579999999998108, - "competitorP95Ms": 14.62000000000262, - "competitorOverFrameBudget": false, + "competitorMs": 31.69999999999891, + "competitorP95Ms": 35.400000000001455, + "competitorGpuMs": 32.56000000000131, + "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.007561436673134266, - "factor": 132.24999999708962, - "label": "ExoJS leads clearly (132.25x)", + "ratio": 0.13028391167190354, + "factor": 7.675544794190085, + "label": "ExoJS leads clearly (7.68x)", "structural": true }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.08000000000174623, - "maxMs": 0.11999999999534339, - "ratio": 1.4999999999090505 + "minMs": 3.8799999999991996, + "maxMs": 4.300000000001091, + "ratio": 1.1082474226809222 }, "competitor": { - "minMs": 9.630000000001019, - "maxMs": 11.539999999993597, - "ratio": 1.1983385254405374 + "minMs": 30.44000000000051, + "maxMs": 31.859999999998763, + "ratio": 1.0466491458606515 }, "stable": true, "rungs": [ @@ -796,80 +858,248 @@ } }, { - "competitor": "pixi", - "referenceMs": 0.08000000000174623, - "referenceP95Ms": 0.13999999999941792, + "competitor": "phaser", + "referenceMs": 4.1299999999992, + "referenceP95Ms": 5.300000000001091, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 0.09999999999854481, - "competitorP95Ms": 0.19999999999708962, + "competitorMs": 3.1600000000034925, + "competitorP95Ms": 7.800000000000182, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": 0.8000000000291039, - "factor": 1, - "label": "level", + "side": "competitor", + "ratio": 1.306962025314758, + "factor": 1.306962025314758, + "label": "competitor leads (1.31x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.08000000000174623, - "maxMs": 0.11999999999534339, - "ratio": 1.4999999999090505 + "minMs": 3.8799999999991996, + "maxMs": 4.300000000001091, + "ratio": 1.1082474226809222 }, "competitor": { - "minMs": 0.09999999999854481, - "maxMs": 0.11999999999534339, - "ratio": 1.1999999999708961 + "minMs": 3.150000000000091, + "maxMs": 3.1799999999998363, + "ratio": 1.0095238095237284 }, "stable": true, "rungs": [ - "level", - "level", - "level" + "competitor-leads", + "competitor-leads", + "competitor-leads" ] } - } - ] - } - ] - }, - { - "title": "Text", - "rows": [ + }, + { + "competitor": "pixi", + "referenceMs": 4.1299999999992, + "referenceP95Ms": 5.300000000001091, + "referenceGpuMs": 16.659999999999854, + "referenceOverFrameBudget": false, + "competitorMs": 2.719999999999345, + "competitorP95Ms": 9.180000000000291, + "competitorGpuMs": 16.660000000001673, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.5183823529412477, + "factor": 1.5183823529412477, + "label": "competitor leads (1.52x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 3.8799999999991996, + "maxMs": 4.300000000001091, + "ratio": 1.1082474226809222 + }, + "competitor": { + "minMs": 2.6500000000005457, + "maxMs": 2.8000000000010914, + "ratio": 1.0566037735851 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, { - "archetype": "text-static", - "category": "Text", - "count": 5000, + "archetype": "dynamic-all", + "category": "Node scaling", + "count": 100000, + "loadId": "100k", + "unit": "sprites", + "primary": false, + "cells": [ + { + "competitor": "phaser", + "referenceMs": 37.5600000000004, + "referenceP95Ms": 41.960000000000946, + "referenceGpuMs": 38.73999999999978, + "referenceOverFrameBudget": true, + "competitorMs": 21.439999999995052, + "competitorP95Ms": 26.020000000004075, + "competitorGpuMs": 22.15999999998894, + "competitorOverFrameBudget": true, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "competitor issues fewer draw calls (25 vs 7 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 1, + "reference": { + "minMs": 37.5600000000004, + "maxMs": 37.5600000000004, + "ratio": 1 + }, + "competitor": { + "minMs": 21.439999999995052, + "maxMs": 21.439999999995052, + "ratio": 1 + }, + "stable": false, + "rungs": [ + "competitor-leads" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 38.149999999999636, + "referenceP95Ms": 43.54000000000087, + "referenceGpuMs": 39.51999999999862, + "referenceOverFrameBudget": true, + "competitorMs": 28.73999999999978, + "competitorP95Ms": 32.56000000000131, + "competitorGpuMs": 30.040000000000873, + "competitorOverFrameBudget": true, + "verdict": { + "side": "competitor", + "ratio": 1.3274182324286683, + "factor": 1.3274182324286683, + "label": "competitor leads (1.33x)", + "structural": false + }, + "mechanism": "competitor issues fewer draw calls (25 vs 1 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 37.5600000000004, + "maxMs": 42.67999999999938, + "ratio": 1.1363152289669576 + }, + "competitor": { + "minMs": 28.420000000000073, + "maxMs": 28.82000000000062, + "ratio": 1.0140745953554027 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "deep-hierarchy", + "category": "Node scaling", + "count": 10000, + "loadId": "10k", + "unit": "nodes", + "primary": true, "cells": [ { "competitor": "excalibur", - "referenceMs": 0.08000000001629815, - "referenceP95Ms": 0.14000000001396984, + "referenceMs": 0.5599999999994907, + "referenceP95Ms": 0.7799999999988358, + "referenceGpuMs": 16.69999999999891, "referenceOverFrameBudget": false, - "competitorMs": 31.82999999999447, - "competitorP95Ms": 37.419999999998254, + "competitorMs": 38.599999999998545, + "competitorP95Ms": 56.799999999995634, + "competitorGpuMs": 39.86000000000058, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.002513352183987183, - "factor": 397.87499991887313, - "label": "ExoJS leads clearly (397.87x)", + "ratio": 0.014507772020712741, + "factor": 68.92857142863151, + "label": "ExoJS leads clearly (68.93x)", "structural": true }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 3/4/3, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.5399999999990541, + "maxMs": 0.6300000000001091, + "ratio": 1.1666666666689123 + }, + "competitor": { + "minMs": 33.52000000000044, + "maxMs": 41.849999999998545, + "ratio": 1.2485083532218975 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "phaser", + "referenceMs": 0.5599999999994907, + "referenceP95Ms": 0.7799999999988358, + "referenceGpuMs": 16.69999999999891, + "referenceOverFrameBudget": false, + "competitorMs": 3.1300000000000523, + "competitorP95Ms": 7.259999999999991, + "competitorGpuMs": 16.660000000000082, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.17891373801900362, + "factor": 5.589285714290891, + "label": "ExoJS leads clearly (5.59x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.07999999998719431, - "maxMs": 0.1200000000098953, - "ratio": 1.5000000003637979 + "minMs": 0.5399999999990541, + "maxMs": 0.6300000000001091, + "ratio": 1.1666666666689123 }, "competitor": { - "minMs": 30.329999999987194, - "maxMs": 34.83999999999651, - "ratio": 1.1486976590837856 + "minMs": 3.0499999999999545, + "maxMs": 3.1900000000023283, + "ratio": 1.0459016393450413 }, "stable": true, "rungs": [ @@ -881,110 +1111,122 @@ }, { "competitor": "pixi", - "referenceMs": 0.08000000001629815, - "referenceP95Ms": 0.14000000001396984, + "referenceMs": 0.5599999999994907, + "referenceP95Ms": 0.7799999999988358, + "referenceGpuMs": 16.69999999999891, "referenceOverFrameBudget": false, - "competitorMs": 0.09999999999126885, - "competitorP95Ms": 0.20000000001164153, + "competitorMs": 0.5799999999999272, + "competitorP95Ms": 0.7999999999992724, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 0.9655172413785533, + "factor": 1, + "label": "level", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.07999999998719431, - "maxMs": 0.1200000000098953, - "ratio": 1.5000000003637979 + "minMs": 0.5399999999990541, + "maxMs": 0.6300000000001091, + "ratio": 1.1666666666689123 }, "competitor": { - "minMs": 0.09999999999126885, - "maxMs": 0.13999999999941792, - "ratio": 1.4000000001164152 + "minMs": 0.5700000000006185, + "maxMs": 0.6599999999998545, + "ratio": 1.1578947368405936 }, - "stable": false, + "stable": true, "rungs": [ "level", "level", - "exojs-leads" + "level" ] } } ] }, { - "archetype": "text-dynamic", - "category": "Text", + "archetype": "lifecycle-churn", + "category": "Node scaling", "count": 5000, + "loadId": "5k", + "unit": "nodes", + "primary": true, "cells": [ { "competitor": "excalibur", - "referenceMs": 7.460000000006403, - "referenceP95Ms": 8.64000000001397, + "referenceMs": 2.720000000001164, + "referenceP95Ms": 5.459999999999127, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 30.139999999999418, - "competitorP95Ms": 32.76000000000931, + "competitorMs": 25.5199999999968, + "competitorP95Ms": 29.19999999999709, + "competitorGpuMs": 26.779999999998836, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.24751161247533335, - "factor": 4.04021447720825, - "label": "ExoJS leads (4.04x)", - "structural": false + "ratio": 0.10658307210037246, + "factor": 9.382352941171279, + "label": "ExoJS leads clearly (9.38x)", + "structural": true }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 3/4/3, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 7.44999999999709, - "maxMs": 7.540000000008149, - "ratio": 1.012080536914241 + "minMs": 2.399999999997817, + "maxMs": 2.860000000000582, + "ratio": 1.191666666667993 }, "competitor": { - "minMs": 28.929999999993015, - "maxMs": 33.220000000001164, - "ratio": 1.148288973384348 + "minMs": 24.779999999998836, + "maxMs": 26, + "ratio": 1.0492332526231325 }, "stable": true, "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads" + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" ] } }, { - "competitor": "pixi", - "referenceMs": 7.460000000006403, - "referenceP95Ms": 8.64000000001397, + "competitor": "phaser", + "referenceMs": 2.720000000001164, + "referenceP95Ms": 5.459999999999127, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 21.220000000001164, - "competitorP95Ms": 25.880000000004657, - "competitorOverFrameBudget": true, + "competitorMs": 10.589999999996508, + "competitorP95Ms": 11.059999999997672, + "competitorGpuMs": 16.659999999999854, + "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.35155513666380744, - "factor": 2.844504021445436, - "label": "ExoJS leads (2.84x)", + "ratio": 0.2568460812088821, + "factor": 3.893382352938226, + "label": "ExoJS leads (3.89x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 7.44999999999709, - "maxMs": 7.540000000008149, - "ratio": 1.012080536914241 + "minMs": 2.399999999997817, + "maxMs": 2.860000000000582, + "ratio": 1.191666666667993 }, "competitor": { - "minMs": 21.060000000004948, - "maxMs": 21.31999999999971, - "ratio": 1.012345679012094 + "minMs": 10.569999999999709, + "maxMs": 10.739999999997963, + "ratio": 1.0160832544936858 }, "stable": true, "rungs": [ @@ -993,89 +1235,91 @@ "exojs-leads" ] } - } - ] - } - ] - }, - { - "title": "Render targets", - "rows": [ - { - "archetype": "filter-chain-1", - "category": "Render targets", - "count": 5000, - "cells": [ + }, { "competitor": "pixi", - "referenceMs": 0.2400000000197906, - "referenceP95Ms": 0.47999999998137355, + "referenceMs": 2.720000000001164, + "referenceP95Ms": 5.459999999999127, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 2.0800000000017462, - "competitorP95Ms": 3.679999999993015, + "competitorMs": 2.569999999999709, + "competitorP95Ms": 7.6599999999998545, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.11538461539403323, - "factor": 8.666666665959282, - "label": "ExoJS leads clearly (8.67x)", - "structural": true + "side": "neither", + "ratio": 1.0583657587554367, + "factor": 1, + "label": "level", + "structural": false }, - "mechanism": "competitor issues fewer texture binds (9 vs 3 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.2400000000052387, - "maxMs": 0.27999999999883585, - "ratio": 1.16666666663635 + "minMs": 2.399999999997817, + "maxMs": 2.860000000000582, + "ratio": 1.191666666667993 }, "competitor": { - "minMs": 2.040000000000873, + "minMs": 2.5599999999994907, "maxMs": 2.5800000000017462, - "ratio": 1.264705882353256 + "ratio": 1.0078125000008826 }, "stable": true, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" + "level", + "level", + "level" ] } } ] - }, + } + ] + }, + { + "title": "Fill and GPU state", + "rows": [ { - "archetype": "filter-chain-2", - "category": "Render targets", - "count": 5000, + "archetype": "fill-layers", + "category": "Fill and GPU state", + "count": 32, + "loadId": "32", + "unit": "layers", + "primary": true, "cells": [ { - "competitor": "pixi", - "referenceMs": 0.2400000000197906, - "referenceP95Ms": 0.3800000000046566, + "competitor": "excalibur", + "referenceMs": 0.07999999999810825, + "referenceP95Ms": 0.12000000000080036, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 2.3299999999944703, - "competitorP95Ms": 4.419999999998254, + "competitorMs": 0.6399999999994179, + "competitorP95Ms": 0.9000000000014552, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.10300429185423184, - "factor": 9.708333332509737, - "label": "ExoJS leads clearly (9.71x)", + "ratio": 0.12499999999715783, + "factor": 8.000000000181899, + "label": "ExoJS leads clearly (8.00x)", "structural": true }, - "mechanism": "competitor issues fewer texture binds (11 vs 3 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { "runs": 3, "reference": { - "minMs": 0.23999999999068677, - "maxMs": 0.27999999999883585, - "ratio": 1.1666666667070886 + "minMs": 0.06000000000130967, + "maxMs": 0.1000000000003638, + "ratio": 1.66666666663635 }, "competitor": { - "minMs": 2.320000000006985, - "maxMs": 2.9199999999982538, - "ratio": 1.2586206896506305 + "minMs": 0.6399999999994179, + "maxMs": 0.6599999999962165, + "ratio": 1.0312499999950262 }, "stable": true, "rungs": [ @@ -1084,64 +1328,55 @@ "exojs-leads-clearly" ] } - } - ] - }, - { - "archetype": "filter-chain-4", - "category": "Render targets", - "count": 5000, - "cells": [ + }, { - "competitor": "pixi", - "referenceMs": 0.27999999999883585, - "referenceP95Ms": 0.4400000000023283, + "competitor": "phaser", + "referenceMs": 0.07999999999810825, + "referenceP95Ms": 0.12000000000080036, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 2.220000000001164, - "competitorP95Ms": 3.7599999999947613, + "competitorMs": 0.27999999999974534, + "competitorP95Ms": 0.4600000000000364, + "competitorGpuMs": 16.679999999999836, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.1261261261255356, - "factor": 7.928571428608551, - "label": "ExoJS leads clearly (7.93x)", - "structural": true + "ratio": 0.28571428570778934, + "factor": 3.500000000079581, + "label": "ExoJS leads (3.50x)", + "structural": false }, - "mechanism": "competitor issues fewer texture binds (15 vs 7 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { "runs": 3, "reference": { - "minMs": 0.2600000000093132, - "maxMs": 0.3000000000029104, - "ratio": 1.1538461538160167 + "minMs": 0.06000000000130967, + "maxMs": 0.1000000000003638, + "ratio": 1.66666666663635 }, "competitor": { - "minMs": 2.0900000000037835, - "maxMs": 2.789999999993597, - "ratio": 1.3349282296595917 + "minMs": 0.19999999999708962, + "maxMs": 0.2800000000002001, + "ratio": 1.400000000021373 }, "stable": true, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } - } - ] - }, - { - "archetype": "composite", - "category": "Render targets", - "count": 5000, - "cells": [ + }, { "competitor": "pixi", - "referenceMs": 0.33999999999650754, - "referenceP95Ms": 0.6199999999953434, + "referenceMs": 0.07999999999810825, + "referenceP95Ms": 0.12000000000080036, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 0.33999999999650754, - "competitorP95Ms": 0.6200000000098953, + "competitorMs": 0.1000000000003638, + "competitorP95Ms": 0.21999999999934516, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -1150,23 +1385,24 @@ "label": "unstable across runs", "structural": false }, - "mechanism": "ExoJS issues fewer draw calls (5 vs 8 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { "runs": 3, "reference": { - "minMs": 0.3200000000069849, - "maxMs": 0.3799999999901047, - "ratio": 1.1874999999431566 + "minMs": 0.06000000000130967, + "maxMs": 0.1000000000003638, + "ratio": 1.66666666663635 }, "competitor": { - "minMs": 0.33999999999650754, - "maxMs": 0.5, - "ratio": 1.4705882353092234 + "minMs": 0.07999999999992724, + "maxMs": 0.1400000000012369, + "ratio": 1.750000000017053 }, "stable": false, "rungs": [ "exojs-leads", - "level", + "exojs-leads", "level" ] } @@ -1174,691 +1410,3204 @@ ] }, { - "archetype": "mask-clip", - "category": "Render targets", + "archetype": "batch-breaking", + "category": "Fill and GPU state", "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, "cells": [ { - "competitor": "pixi", - "referenceMs": 2.5200000000040745, - "referenceP95Ms": 8.159999999974389, + "competitor": "excalibur", + "referenceMs": 1.4200000000018917, + "referenceP95Ms": 1.7600000000020373, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 0.13999999999941792, - "competitorP95Ms": 0.2600000000093132, - "competitorOverFrameBudget": false, + "competitorMs": 18.860000000000582, + "competitorP95Ms": 22.5, + "competitorGpuMs": 27.840000000003783, + "competitorOverFrameBudget": true, "verdict": { - "side": "competitor", - "ratio": 18.00000000010394, - "factor": 18.00000000010394, - "label": "competitor leads clearly (18.00x)", + "side": "exojs", + "ratio": 0.07529162248154019, + "factor": 13.281690140827786, + "label": "ExoJS leads clearly (13.28x)", "structural": true }, - "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", + "mechanism": "ExoJS issues fewer buffer uploads (0 vs 313 per frame)", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 2.289999999993597, - "maxMs": 2.930000000007567, - "ratio": 1.279475982539633 + "minMs": 1.4099999999998545, + "maxMs": 1.4400000000005093, + "ratio": 1.0212765957451475 }, "competitor": { - "minMs": 0.13999999999941792, - "maxMs": 0.17999999999301508, - "ratio": 1.285714285669739 + "minMs": 14.56000000000131, + "maxMs": 19.830000000001746, + "ratio": 1.3619505494505468 }, "stable": true, "rungs": [ - "competitor-leads-clearly", - "competitor-leads-clearly", - "competitor-leads-clearly" + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" ] } - } - ] - } - ] - }, - { - "title": "Camera and world", - "rows": [ - { - "archetype": "scrolling-world", - "category": "Camera and world", - "count": 5000, - "cells": [ + }, { - "competitor": "pixi", - "referenceMs": 0.14999999999417923, - "referenceP95Ms": 1.3399999999965075, + "competitor": "phaser", + "referenceMs": 1.4200000000018917, + "referenceP95Ms": 1.7600000000020373, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 0.09999999999126885, - "competitorP95Ms": 0.22000000000116415, + "competitorMs": 3.0300000000002, + "competitorP95Ms": 4.700000000000728, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 1.5000000000727596, - "factor": 1.5000000000727596, - "label": "competitor leads (1.50x)", + "side": "exojs", + "ratio": 0.468646864687062, + "factor": 2.1338028168987067, + "label": "ExoJS leads (2.13x)", "structural": false }, - "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 313/5000/0 vs 313/5000/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.12000000002444722, - "maxMs": 0.15999999998894054, - "ratio": 1.3333333329695354 + "minMs": 1.4099999999998545, + "maxMs": 1.4400000000005093, + "ratio": 1.0212765957451475 }, "competitor": { - "minMs": 0.09999999999126885, - "maxMs": 0.1200000000098953, - "ratio": 1.2000000002037268 + "minMs": 3.0199999999895226, + "maxMs": 3.0599999999999454, + "ratio": 1.01324503311608 }, "stable": true, "rungs": [ - "competitor-leads", - "competitor-leads", - "competitor-leads" + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 1.4200000000018917, + "referenceP95Ms": 1.7600000000020373, + "referenceGpuMs": 16.659999999999854, + "referenceOverFrameBudget": false, + "competitorMs": 1.2200000000002547, + "competitorP95Ms": 2.0599999999994907, + "competitorGpuMs": 16.659999999999854, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 313/5000/0 vs 313/5000/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 1.4099999999998545, + "maxMs": 1.4400000000005093, + "ratio": 1.0212765957451475 + }, + "competitor": { + "minMs": 1.180000000000291, + "maxMs": 1.2399999999997817, + "ratio": 1.0508474576266744 + }, + "stable": false, + "rungs": [ + "level", + "level", + "competitor-leads" ] } } ] - } - ] - } - ], - "excluded": [ - { - "archetype": "split-screen", - "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" - }, - { - "archetype": "mixed-material", - "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" - }, - { - "archetype": "instanced-batch", - "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" - }, - { - "archetype": "mixed-sprite-mesh-static", - "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" - }, - { - "archetype": "mixed-sprite-mesh-array", - "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" - }, - { - "archetype": "mixed-material-atlased", - "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" - } - ], - "webgl1": [ - { - "archetype": "static-heavy", - "category": "Node scaling", - "count": 5000, - "cells": [ - { - "competitor": "phaser", - "referenceMs": 0.12000000000034561, - "referenceP95Ms": 0.24000000000023647, - "referenceOverFrameBudget": false, - "competitorMs": 2.2899999999999636, - "competitorP95Ms": 5.739999999999782, - "competitorOverFrameBudget": false, - "verdict": { - "side": "exojs", - "ratio": 0.05240174672504258, - "factor": 19.08333333327807, - "label": "ExoJS leads clearly (19.08x)", - "structural": true - }, - "mechanism": null, - "aggregate": { - "runs": 3, - "reference": { - "minMs": 0.11999999999989086, - "maxMs": 0.13999999999987267, - "ratio": 1.1666666666666667 - }, - "competitor": { - "minMs": 2.209999999999809, - "maxMs": 2.8700000000001182, - "ratio": 1.2986425339368173 - }, - "stable": true, - "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" - ] - } - } - ] - }, - { - "archetype": "dynamic-heavy", - "category": "Node scaling", - "count": 5000, - "cells": [ - { - "competitor": "phaser", - "referenceMs": 1.0599999999994907, - "referenceP95Ms": 1.5399999999990541, - "referenceOverFrameBudget": false, - "competitorMs": 2.3899999999998727, - "competitorP95Ms": 6.359999999999673, - "competitorOverFrameBudget": false, - "verdict": { - "side": "exojs", - "ratio": 0.44351464435127497, - "factor": 2.2547169811330385, - "label": "ExoJS leads (2.25x)", - "structural": false - }, - "mechanism": null, - "aggregate": { - "runs": 3, - "reference": { - "minMs": 1.050000000000182, - "maxMs": 1.1000000000003638, - "ratio": 1.0476190476192127 + }, + { + "archetype": "mixed-blend", + "category": "Fill and GPU state", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "excalibur", + "referenceMs": 0.47999999999956344, + "referenceP95Ms": 0.7000000000007276, + "referenceGpuMs": 16.679999999996653, + "referenceOverFrameBudget": false, + "competitorMs": 9.639999999999418, + "competitorP95Ms": 12.900000000001455, + "competitorGpuMs": 16.660000000003492, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.04979253112028967, + "factor": 20.08333333335039, + "label": "ExoJS leads clearly (20.08x)", + "structural": true + }, + "mechanism": "competitor issues fewer draw calls (120 vs 1 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.4599999999991269, + "maxMs": 0.4899999999997817, + "ratio": 1.065217391305895 + }, + "competitor": { + "minMs": 9.520000000000437, + "maxMs": 10.06000000000131, + "ratio": 1.0567226890757193 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } }, - "competitor": { - "minMs": 2.3400000000001455, - "maxMs": 2.6399999999998727, - "ratio": 1.1282051282050036 + { + "competitor": "phaser", + "referenceMs": 0.47999999999956344, + "referenceP95Ms": 0.7000000000007276, + "referenceGpuMs": 16.679999999996653, + "referenceOverFrameBudget": false, + "competitorMs": 2.6899999999996, + "competitorP95Ms": 4.819999999999709, + "competitorGpuMs": 16.660000000000764, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.17843866170990144, + "factor": 5.604166666670929, + "label": "ExoJS leads clearly (5.60x)", + "structural": true + }, + "mechanism": "competitor issues fewer draw calls (120 vs 1 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.4599999999991269, + "maxMs": 0.4899999999997817, + "ratio": 1.065217391305895 + }, + "competitor": { + "minMs": 2.5300000000133878, + "maxMs": 2.869999999999891, + "ratio": 1.1343873517726102 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } }, - "stable": true, - "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads" - ] - } + { + "competitor": "pixi", + "referenceMs": 0.47999999999956344, + "referenceP95Ms": 0.7000000000007276, + "referenceGpuMs": 16.679999999996653, + "referenceOverFrameBudget": false, + "competitorMs": 0.3000000000029104, + "competitorP95Ms": 0.4599999999991269, + "competitorGpuMs": 16.659999999999854, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.5999999999830228, + "factor": 1.5999999999830228, + "label": "competitor leads (1.60x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 120/0/0 vs 158/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.4599999999991269, + "maxMs": 0.4899999999997817, + "ratio": 1.065217391305895 + }, + "competitor": { + "minMs": 0.2999999999992724, + "maxMs": 0.31999999999970896, + "ratio": 1.0666666666682836 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] } ] }, { - "archetype": "deep-hierarchy", - "category": "Node scaling", - "count": 5000, - "cells": [ - { - "competitor": "phaser", - "referenceMs": 0.37999999999919964, - "referenceP95Ms": 0.5, - "referenceOverFrameBudget": false, - "competitorMs": 2.630000000000109, - "competitorP95Ms": 6.300000000000182, - "competitorOverFrameBudget": false, - "verdict": { - "side": "exojs", - "ratio": 0.1444866920148988, - "factor": 6.921052631593812, - "label": "ExoJS leads clearly (6.92x)", - "structural": true - }, - "mechanism": null, - "aggregate": { - "runs": 3, - "reference": { - "minMs": 0.33000000000083674, - "maxMs": 0.42000000000007276, - "ratio": 1.2727272727242662 + "title": "Material variety", + "rows": [ + { + "archetype": "batch-breaking-atlased", + "category": "Material variety", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "excalibur", + "referenceMs": 0.08000000000174623, + "referenceP95Ms": 0.13999999999941792, + "referenceGpuMs": 16.659999999999854, + "referenceOverFrameBudget": false, + "competitorMs": 10.099999999998545, + "competitorP95Ms": 14, + "competitorGpuMs": 16.660000000003492, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.007920792079381955, + "factor": 126.24999999722606, + "label": "ExoJS leads clearly (126.25x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.07999999999810825, + "maxMs": 0.09999999999854481, + "ratio": 1.2500000000113687 + }, + "competitor": { + "minMs": 9.25, + "maxMs": 10.489999999997963, + "ratio": 1.1340540540538337 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } }, - "competitor": { - "minMs": 2.6100000000001273, - "maxMs": 2.900000000001455, - "ratio": 1.1111111111116145 + { + "competitor": "phaser", + "referenceMs": 0.08000000000174623, + "referenceP95Ms": 0.13999999999941792, + "referenceGpuMs": 16.659999999999854, + "referenceOverFrameBudget": false, + "competitorMs": 2.75, + "competitorP95Ms": 4.540000000000873, + "competitorGpuMs": 16.67999999999938, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.029090909091544083, + "factor": 34.37499999924967, + "label": "ExoJS leads clearly (34.37x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.07999999999810825, + "maxMs": 0.09999999999854481, + "ratio": 1.2500000000113687 + }, + "competitor": { + "minMs": 2.4400000000023283, + "maxMs": 2.9100000000003092, + "ratio": 1.1926229508186608 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } }, - "stable": true, - "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" - ] - } + { + "competitor": "pixi", + "referenceMs": 0.08000000000174623, + "referenceP95Ms": 0.13999999999941792, + "referenceGpuMs": 16.659999999999854, + "referenceOverFrameBudget": false, + "competitorMs": 0.11999999999898137, + "competitorP95Ms": 0.2000000000007276, + "competitorGpuMs": 16.659999999999854, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.07999999999810825, + "maxMs": 0.09999999999854481, + "ratio": 1.2500000000113687 + }, + "competitor": { + "minMs": 0.09000000000014552, + "maxMs": 0.11999999999898137, + "ratio": 1.3333333333198594 + }, + "stable": false, + "rungs": [ + "level", + "exojs-leads", + "level" + ] + } + } + ] } ] }, { - "archetype": "overdraw", - "category": "Fill and GPU state", - "count": 5000, - "cells": [ - { - "competitor": "phaser", - "referenceMs": 0.020000000000436557, - "referenceP95Ms": 0.040000000000873115, - "referenceOverFrameBudget": false, - "competitorMs": 0.8600000000005821, - "competitorP95Ms": 0.9799999999959255, - "competitorOverFrameBudget": false, - "verdict": { - "side": "exojs", - "ratio": 0.023255813953980256, - "factor": 42.999999999090505, - "label": "ExoJS leads clearly (43.00x)", - "structural": true - }, - "mechanism": null, - "aggregate": { - "runs": 3, - "reference": { - "minMs": 0.020000000000436557, - "maxMs": 0.020000000000436557, - "ratio": 1 - }, - "competitor": { - "minMs": 0.7800000000002001, - "maxMs": 0.9100000000034925, - "ratio": 1.166666666670845 + "title": "Text", + "rows": [ + { + "archetype": "text-static", + "category": "Text", + "count": 1000, + "loadId": "1k", + "unit": "labels", + "primary": true, + "cells": [ + { + "competitor": "excalibur", + "referenceMs": 0.09999999999854481, + "referenceP95Ms": 0.19999999999708962, + "referenceGpuMs": 16.68000000000029, + "referenceOverFrameBudget": false, + "competitorMs": 6.470000000001164, + "competitorP95Ms": 9.059999999997672, + "competitorGpuMs": 16.660000000003492, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.015455950540730574, + "factor": 64.70000000095315, + "label": "ExoJS leads clearly (64.70x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.09999999999854481, + "maxMs": 0.13999999999941792, + "ratio": 1.4000000000145518 + }, + "competitor": { + "minMs": 5.860000000000582, + "maxMs": 7.009999999998399, + "ratio": 1.196245733788004 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } }, - "stable": true, - "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" - ] - } - } - ] - }, - { - "archetype": "batch-breaking", - "category": "Fill and GPU state", - "count": 5000, - "cells": [ - { - "competitor": "phaser", - "referenceMs": 1.389999999999418, - "referenceP95Ms": 1.6999999999970896, - "referenceOverFrameBudget": false, - "competitorMs": 2.8399999999965075, - "competitorP95Ms": 6.819999999992433, - "competitorOverFrameBudget": false, - "verdict": { - "side": "exojs", - "ratio": 0.4894366197187068, - "factor": 2.043165467624242, - "label": "ExoJS leads (2.04x)", - "structural": false - }, - "mechanism": null, - "aggregate": { - "runs": 3, - "reference": { - "minMs": 1.3400000000001455, - "maxMs": 1.3999999999941792, - "ratio": 1.0447761193985277 + { + "competitor": "phaser", + "referenceMs": 0.09999999999854481, + "referenceP95Ms": 0.19999999999708962, + "referenceGpuMs": 16.68000000000029, + "referenceOverFrameBudget": false, + "competitorMs": 2.9200000000000728, + "competitorP95Ms": 4.240000000001601, + "competitorGpuMs": 16.660000000003492, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.03424657534196655, + "factor": 29.200000000425643, + "label": "ExoJS leads clearly (29.20x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.09999999999854481, + "maxMs": 0.13999999999941792, + "ratio": 1.4000000000145518 + }, + "competitor": { + "minMs": 2.710000000006403, + "maxMs": 3.040000000000873, + "ratio": 1.121771217709849 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } }, - "competitor": { - "minMs": 2.720000000001164, - "maxMs": 3.0500000000029104, - "ratio": 1.1213235294123547 + { + "competitor": "pixi", + "referenceMs": 0.09999999999854481, + "referenceP95Ms": 0.19999999999708962, + "referenceGpuMs": 16.68000000000029, + "referenceOverFrameBudget": false, + "competitorMs": 0.10000000000218279, + "competitorP95Ms": 0.22000000000116415, + "competitorGpuMs": 16.68000000000029, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 0.9999999999636202, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.09999999999854481, + "maxMs": 0.13999999999941792, + "ratio": 1.4000000000145518 + }, + "competitor": { + "minMs": 0.10000000000218279, + "maxMs": 0.13999999999941792, + "ratio": 1.3999999999636201 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + } + ] + }, + { + "archetype": "text-static", + "category": "Text", + "count": 5000, + "loadId": "5k", + "unit": "labels", + "primary": false, + "cells": [ + { + "competitor": "excalibur", + "referenceMs": 0.09999999999854481, + "referenceP95Ms": 0.18000000000029104, + "referenceGpuMs": 16.68000000000029, + "referenceOverFrameBudget": false, + "competitorMs": 33.38000000000102, + "competitorP95Ms": 37.81999999999971, + "competitorGpuMs": 34.55999999999767, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.0029958058717358227, + "factor": 333.8000000048676, + "label": "ExoJS leads clearly (333.80x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 3/4/3, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.09999999999854481, + "maxMs": 0.11999999999898137, + "ratio": 1.200000000007276 + }, + "competitor": { + "minMs": 28.489999999997963, + "maxMs": 33.400000000001455, + "ratio": 1.1723411723413073 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } }, - "stable": true, - "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads" - ] - } - } - ] - }, - { - "archetype": "mixed-blend", - "category": "Fill and GPU state", - "count": 5000, - "cells": [ - { - "competitor": "phaser", - "referenceMs": 0.46000000000276486, - "referenceP95Ms": 0.6999999999970896, - "referenceOverFrameBudget": false, - "competitorMs": 2.7799999999997453, - "competitorP95Ms": 4.7800000000002, - "competitorOverFrameBudget": false, - "verdict": { - "side": "exojs", - "ratio": 0.1654676259002903, - "factor": 6.0434782608326865, - "label": "ExoJS leads clearly (6.04x)", - "structural": true - }, - "mechanism": null, - "aggregate": { - "runs": 3, - "reference": { - "minMs": 0.3600000000005821, - "maxMs": 0.5200000000040745, - "ratio": 1.444444444453427 + { + "competitor": "phaser", + "referenceMs": 0.09999999999854481, + "referenceP95Ms": 0.18000000000029104, + "referenceGpuMs": 16.68000000000029, + "referenceOverFrameBudget": false, + "competitorMs": 6.64000000001397, + "competitorP95Ms": 7.93999999999869, + "competitorGpuMs": 16.659999999999854, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.01506024096360458, + "factor": 66.40000000110595, + "label": "ExoJS leads clearly (66.40x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 4/0/4, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.09999999999854481, + "maxMs": 0.11999999999898137, + "ratio": 1.200000000007276 + }, + "competitor": { + "minMs": 6.410000000000764, + "maxMs": 6.650000000000546, + "ratio": 1.0374414976598678 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } }, - "competitor": { - "minMs": 2.7500000000002274, - "maxMs": 2.9500000000000455, - "ratio": 1.0727272727272006 + { + "competitor": "pixi", + "referenceMs": 0.09999999999854481, + "referenceP95Ms": 0.18000000000029104, + "referenceGpuMs": 16.68000000000029, + "referenceOverFrameBudget": false, + "competitorMs": 0.11999999999898137, + "competitorP95Ms": 0.23999999999796273, + "competitorGpuMs": 16.659999999999854, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 0.8333333333282806, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.09999999999854481, + "maxMs": 0.11999999999898137, + "ratio": 1.200000000007276 + }, + "competitor": { + "minMs": 0.09999999999854481, + "maxMs": 0.11999999999898137, + "ratio": 1.200000000007276 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + } + ] + }, + { + "archetype": "text-dynamic", + "category": "Text", + "count": 1000, + "loadId": "1k", + "unit": "labels", + "primary": true, + "cells": [ + { + "competitor": "excalibur", + "referenceMs": 2.6199999999989814, + "referenceP95Ms": 7.900000000001455, + "referenceGpuMs": 16.659999999999854, + "referenceOverFrameBudget": false, + "competitorMs": 6.920000000001892, + "competitorP95Ms": 9.739999999997963, + "competitorGpuMs": 16.660000000003492, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.3786127167627551, + "factor": 2.64122137404755, + "label": "ExoJS leads (2.64x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 2.460000000000946, + "maxMs": 2.8000000000010914, + "ratio": 1.138211382113827 + }, + "competitor": { + "minMs": 6.140000000003056, + "maxMs": 6.990000000001601, + "ratio": 1.1384364820843846 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } }, - "stable": true, - "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" - ] - } - } - ] - }, - { - "archetype": "batch-breaking-atlased", - "category": "Material variety", - "count": 5000, - "cells": [ - { - "competitor": "phaser", - "referenceMs": 0.08000000000174623, - "referenceP95Ms": 0.13999999999941792, - "referenceOverFrameBudget": false, - "competitorMs": 2.680000000000291, - "competitorP95Ms": 4.380000000001019, - "competitorOverFrameBudget": false, - "verdict": { - "side": "exojs", - "ratio": 0.02985074626930505, - "factor": 33.499999999272404, - "label": "ExoJS leads clearly (33.50x)", - "structural": true - }, - "mechanism": null, - "aggregate": { - "runs": 3, - "reference": { - "minMs": 0.08000000000174623, - "maxMs": 0.11999999999534339, - "ratio": 1.4999999999090505 + { + "competitor": "phaser", + "referenceMs": 2.6199999999989814, + "referenceP95Ms": 7.900000000001455, + "referenceGpuMs": 16.659999999999854, + "referenceOverFrameBudget": false, + "competitorMs": 2.8900000000003274, + "competitorP95Ms": 5.860000000000582, + "competitorGpuMs": 16.659999999999854, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 0.9065743944632126, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 2.460000000000946, + "maxMs": 2.8000000000010914, + "ratio": 1.138211382113827 + }, + "competitor": { + "minMs": 2.5899999999819556, + "maxMs": 2.8900000000003274, + "ratio": 1.1158301158380162 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } }, - "competitor": { - "minMs": 2.4500000000007276, - "maxMs": 3.139999999999418, - "ratio": 1.2816326530606064 + { + "competitor": "pixi", + "referenceMs": 2.6199999999989814, + "referenceP95Ms": 7.900000000001455, + "referenceGpuMs": 16.659999999999854, + "referenceOverFrameBudget": false, + "competitorMs": 4.309999999999491, + "competitorP95Ms": 5.799999999999272, + "competitorGpuMs": 16.659999999999854, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.6078886310903228, + "factor": 1.6450381679393764, + "label": "ExoJS leads (1.65x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 2.460000000000946, + "maxMs": 2.8000000000010914, + "ratio": 1.138211382113827 + }, + "competitor": { + "minMs": 4.209999999999127, + "maxMs": 4.5, + "ratio": 1.068883610451528 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + }, + { + "archetype": "text-dynamic", + "category": "Text", + "count": 5000, + "loadId": "5k", + "unit": "labels", + "primary": false, + "cells": [ + { + "competitor": "excalibur", + "referenceMs": 7.199999999998909, + "referenceP95Ms": 9.119999999998981, + "referenceGpuMs": 16.659999999999854, + "referenceOverFrameBudget": false, + "competitorMs": 35.17999999999665, + "competitorP95Ms": 42.879999999990105, + "competitorGpuMs": 36.94000000000233, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.20466173962477527, + "factor": 4.886111111111386, + "label": "ExoJS leads (4.89x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 3/4/3, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 7.170000000000073, + "maxMs": 7.3400000000001455, + "ratio": 1.0237099023710001 + }, + "competitor": { + "minMs": 34.86999999999898, + "maxMs": 35.450000000004366, + "ratio": 1.016633209062386 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } }, - "stable": true, - "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" - ] - } + { + "competitor": "phaser", + "referenceMs": 7.199999999998909, + "referenceP95Ms": 9.119999999998981, + "referenceGpuMs": 16.659999999999854, + "referenceOverFrameBudget": false, + "competitorMs": 7.039999999979045, + "competitorP95Ms": 8.3799999999992, + "competitorGpuMs": 16.659999999999854, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 1.022727272730162, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 4/0/4, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 7.170000000000073, + "maxMs": 7.3400000000001455, + "ratio": 1.0237099023710001 + }, + "competitor": { + "minMs": 6.8200000000006185, + "maxMs": 7.039999999999054, + "ratio": 1.0322580645158967 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 7.199999999998909, + "referenceP95Ms": 9.119999999998981, + "referenceGpuMs": 16.659999999999854, + "referenceOverFrameBudget": false, + "competitorMs": 18.8899999999976, + "competitorP95Ms": 22.340000000000146, + "competitorGpuMs": 20.700000000000728, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.38115404976176936, + "factor": 2.6236111111111753, + "label": "ExoJS leads (2.62x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 7.170000000000073, + "maxMs": 7.3400000000001455, + "ratio": 1.0237099023710001 + }, + "competitor": { + "minMs": 18.520000000000437, + "maxMs": 19.220000000001164, + "ratio": 1.037796976241939 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + } + ] + }, + { + "title": "Render targets", + "rows": [ + { + "archetype": "filter-chain-1", + "category": "Render targets", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.38000000000101863, + "referenceP95Ms": 0.7600000000020373, + "referenceGpuMs": 16.659999999999854, + "referenceOverFrameBudget": false, + "competitorMs": 2.100000000000364, + "competitorP95Ms": 4.620000000002619, + "competitorGpuMs": 16.659999999999854, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "competitor issues fewer texture binds (9 vs 3 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.3400000000001455, + "maxMs": 0.42000000000189175, + "ratio": 1.2352941176520942 + }, + "competitor": { + "minMs": 2.069999999999709, + "maxMs": 2.429999999998472, + "ratio": 1.1739130434776879 + }, + "stable": false, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads" + ] + } + } + ] + }, + { + "archetype": "filter-chain-2", + "category": "Render targets", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.3600000000005821, + "referenceP95Ms": 0.5600000000049477, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 2.5600000000013097, + "competitorP95Ms": 5.880000000004657, + "competitorGpuMs": 16.68000000000029, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.14062500000015543, + "factor": 7.111111111103251, + "label": "ExoJS leads clearly (7.11x)", + "structural": true + }, + "mechanism": "competitor issues fewer texture binds (11 vs 3 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.3100000000013097, + "maxMs": 0.4000000000014552, + "ratio": 1.290322580644404 + }, + "competitor": { + "minMs": 2.3099999999976717, + "maxMs": 2.61000000000422, + "ratio": 1.1298701298730955 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + } + ] + }, + { + "archetype": "filter-chain-4", + "category": "Render targets", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.31999999999970896, + "referenceP95Ms": 0.4800000000032014, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 2.5, + "competitorP95Ms": 4.980000000003201, + "competitorGpuMs": 16.660000000003492, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.1279999999998836, + "factor": 7.8125000000071045, + "label": "ExoJS leads clearly (7.81x)", + "structural": true + }, + "mechanism": "competitor issues fewer texture binds (15 vs 7 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.27999999999883585, + "maxMs": 0.32999999999810825, + "ratio": 1.1785714285695725 + }, + "competitor": { + "minMs": 2.349999999998545, + "maxMs": 3.0100000000020373, + "ratio": 1.2808510638314472 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + } + ] + }, + { + "archetype": "composite", + "category": "Render targets", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.31999999999970896, + "referenceP95Ms": 0.5400000000008731, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 0.5299999999988358, + "competitorP95Ms": 0.8000000000029104, + "competitorGpuMs": 16.68000000000029, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.6037735849064374, + "factor": 1.6562499999978686, + "label": "ExoJS leads (1.66x)", + "structural": false + }, + "mechanism": "ExoJS issues fewer draw calls (5 vs 8 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.31999999999970896, + "maxMs": 0.33999999999650754, + "ratio": 1.0624999999900524 + }, + "competitor": { + "minMs": 0.4599999999991269, + "maxMs": 0.6199999999953434, + "ratio": 1.347826086948957 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + }, + { + "archetype": "mask-clip", + "category": "Render targets", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.2400000000016007, + "referenceP95Ms": 0.4400000000023283, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 0.19999999999708962, + "competitorP95Ms": 0.3400000000037835, + "competitorGpuMs": 16.679999999993015, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.23999999999796273, + "maxMs": 0.27000000000043656, + "ratio": 1.1250000000113687 + }, + "competitor": { + "minMs": 0.1599999999962165, + "maxMs": 0.27999999999883585, + "ratio": 1.750000000034106 + }, + "stable": false, + "rungs": [ + "level", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "mask-clip-animated", + "category": "Render targets", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.2599999999947613, + "referenceP95Ms": 0.4800000000032014, + "referenceGpuMs": 16.679999999993015, + "referenceOverFrameBudget": false, + "competitorMs": 0.46000000000276486, + "competitorP95Ms": 0.6999999999970896, + "competitorGpuMs": 16.660000000003492, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.565217391289562, + "factor": 1.7692307692770513, + "label": "ExoJS leads (1.77x)", + "structural": false + }, + "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.2400000000052387, + "maxMs": 0.27999999999883585, + "ratio": 1.16666666663635 + }, + "competitor": { + "minMs": 0.4000000000014552, + "maxMs": 0.5499999999992724, + "ratio": 1.3749999999931788 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + } + ] + }, + { + "title": "Camera and world", + "rows": [ + { + "archetype": "scrolling-world", + "category": "Camera and world", + "count": 10000, + "loadId": "10k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.2000000000007276, + "referenceP95Ms": 1.6000000000021828, + "referenceGpuMs": 16.659999999999854, + "referenceOverFrameBudget": false, + "competitorMs": 0.10000000000218279, + "competitorP95Ms": 0.22000000000116415, + "competitorGpuMs": 16.659999999999854, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.9999999999636202, + "factor": 1.9999999999636202, + "label": "competitor leads (2.00x)", + "structural": false + }, + "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.18000000000029104, + "maxMs": 0.2000000000007276, + "ratio": 1.1111111111133567 + }, + "competitor": { + "minMs": 0.10000000000218279, + "maxMs": 0.11999999999898137, + "ratio": 1.1999999999636202 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + } + ] + }, + { + "title": "Tilemaps", + "rows": [ + { + "archetype": "tilemap-scroll", + "category": "Tilemaps", + "count": 10000, + "loadId": "10k", + "unit": "tiles", + "primary": false, + "cells": [ + { + "competitor": "excalibur", + "referenceMs": 0.20000000000436557, + "referenceP95Ms": 1.2600000000020373, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 20.910000000003492, + "competitorP95Ms": 26.019999999989523, + "competitorGpuMs": 22.59999999999127, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.009564801530575427, + "factor": 104.54999999773536, + "label": "ExoJS leads clearly (104.55x)", + "structural": true + }, + "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.18000000000029104, + "maxMs": 0.21000000000276486, + "ratio": 1.1666666666801406 + }, + "competitor": { + "minMs": 20.859999999993306, + "maxMs": 22.150000000001455, + "ratio": 1.061840843720449 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "phaser", + "referenceMs": 0.20000000000436557, + "referenceP95Ms": 1.2600000000020373, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 0.2000000000007276, + "competitorP95Ms": 0.4000000000014552, + "competitorGpuMs": 16.659999999999854, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 1.00000000001819, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.18000000000029104, + "maxMs": 0.21000000000276486, + "ratio": 1.1666666666801406 + }, + "competitor": { + "minMs": 0.180000000007567, + "maxMs": 0.21999999999752617, + "ratio": 1.222222222157098 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 0.20000000000436557, + "referenceP95Ms": 1.2600000000020373, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 0.26000000000203727, + "competitorP95Ms": 0.5, + "competitorGpuMs": 16.660000000003492, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "ExoJS issues fewer draw calls (90 vs 282 per frame)", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.18000000000029104, + "maxMs": 0.21000000000276486, + "ratio": 1.1666666666801406 + }, + "competitor": { + "minMs": 0.26000000000203727, + "maxMs": 0.3899999999994179, + "ratio": 1.4999999999860079 + }, + "stable": false, + "rungs": [ + "exojs-leads", + "exojs-leads", + "level" + ] + } + } + ] + }, + { + "archetype": "tilemap-scroll", + "category": "Tilemaps", + "count": 100000, + "loadId": "100k", + "unit": "tiles", + "primary": true, + "cells": [ + { + "competitor": "excalibur", + "referenceMs": 0.13999999999941792, + "referenceP95Ms": 0.8600000000005821, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 184.3199999999997, + "competitorP95Ms": 192.36000000000058, + "competitorGpuMs": 192.51999999998952, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.0007595486111079544, + "factor": 1316.5714285769004, + "label": "ExoJS leads clearly (1316.57x)", + "structural": true + }, + "mechanism": "competitor issues fewer draw calls (30 vs 5 per frame)", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.13000000000101863, + "maxMs": 0.13999999999941792, + "ratio": 1.076923076910161 + }, + "competitor": { + "minMs": 181.1800000000003, + "maxMs": 189.13999999999942, + "ratio": 1.0439342090738444 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "phaser", + "referenceMs": 0.13999999999941792, + "referenceP95Ms": 0.8600000000005821, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 0.18000000000029104, + "competitorP95Ms": 0.3600000000005821, + "competitorGpuMs": 16.659999999999854, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.7777777777732865, + "factor": 1.28571428572171, + "label": "ExoJS leads (1.29x)", + "structural": false + }, + "mechanism": "competitor issues fewer draw calls (30 vs 1 per frame)", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.13000000000101863, + "maxMs": 0.13999999999941792, + "ratio": 1.076923076910161 + }, + "competitor": { + "minMs": 0.17999999999301508, + "maxMs": 0.18999999999869033, + "ratio": 1.0555555555892406 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 0.13999999999941792, + "referenceP95Ms": 0.8600000000005821, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 0.27999999999883585, + "competitorP95Ms": 0.4400000000023283, + "competitorGpuMs": 16.68000000000029, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.5, + "factor": 2, + "label": "ExoJS leads (2.00x)", + "structural": false + }, + "mechanism": "competitor issues fewer draw calls (30 vs 2 per frame)", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.13000000000101863, + "maxMs": 0.13999999999941792, + "ratio": 1.076923076910161 + }, + "competitor": { + "minMs": 0.20999999999912689, + "maxMs": 0.4800000000032014, + "ratio": 2.285714285739034 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + }, + { + "archetype": "tilemap-edit", + "category": "Tilemaps", + "count": 100000, + "loadId": "100k", + "unit": "tiles", + "primary": true, + "cells": [ + { + "competitor": "phaser", + "referenceMs": 0.680000000000291, + "referenceP95Ms": 1.2399999999979627, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 1.680000000000291, + "competitorP95Ms": 7.059999999997672, + "competitorGpuMs": 16.659999999999854, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.4047619047620079, + "factor": 2.470588235293488, + "label": "ExoJS leads (2.47x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/3/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.5800000000017462, + "maxMs": 0.819999999999709, + "ratio": 1.4137931034435176 + }, + "competitor": { + "minMs": 1.5599999999976717, + "maxMs": 2.4799999999995634, + "ratio": 1.5897435897456826 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 0.680000000000291, + "referenceP95Ms": 1.2399999999979627, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 1.209999999995489, + "competitorP95Ms": 1.819999999999709, + "competitorGpuMs": 16.68000000000029, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.5619834710767159, + "factor": 1.7794117646984868, + "label": "ExoJS leads (1.78x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 2/0/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.5800000000017462, + "maxMs": 0.819999999999709, + "ratio": 1.4137931034435176 + }, + "competitor": { + "minMs": 1.069999999999709, + "maxMs": 1.319999999999709, + "ratio": 1.2336448598131478 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + } + ] + }, + { + "title": "Particles", + "rows": [ + { + "archetype": "particles-draw", + "category": "Particles", + "count": 1000, + "loadId": "1k", + "unit": "particles", + "primary": false, + "cells": [ + { + "competitor": "excalibur", + "referenceMs": 0.26000000000203727, + "referenceP95Ms": 0.41999999999825377, + "referenceGpuMs": 16.68000000000029, + "referenceOverFrameBudget": false, + "competitorMs": 2.4499999999970896, + "competitorP95Ms": 8.740000000005239, + "competitorGpuMs": 16.660000000003492, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.10612244898054944, + "factor": 9.423076922991893, + "label": "ExoJS leads clearly (9.42x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.2599999999947613, + "maxMs": 0.3000000000029104, + "ratio": 1.1538461538805962 + }, + "competitor": { + "minMs": 2.4400000000096043, + "maxMs": 2.639999999999418, + "ratio": 1.0819672131102567 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "phaser", + "referenceMs": 0.26000000000203727, + "referenceP95Ms": 0.41999999999825377, + "referenceGpuMs": 16.68000000000029, + "referenceOverFrameBudget": false, + "competitorMs": 0.8400000000001455, + "competitorP95Ms": 1.0600000000013097, + "competitorGpuMs": 16.679999999996653, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.30952380952618125, + "factor": 3.230769230744475, + "label": "ExoJS leads (3.23x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.2599999999947613, + "maxMs": 0.3000000000029104, + "ratio": 1.1538461538805962 + }, + "competitor": { + "minMs": 0.7799999999988358, + "maxMs": 0.8600000000005821, + "ratio": 1.1025641025664945 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 0.26000000000203727, + "referenceP95Ms": 0.41999999999825377, + "referenceGpuMs": 16.68000000000029, + "referenceOverFrameBudget": false, + "competitorMs": 0.18000000000029104, + "competitorP95Ms": 0.33999999999650754, + "competitorGpuMs": 16.660000000003492, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.2599999999947613, + "maxMs": 0.3000000000029104, + "ratio": 1.1538461538805962 + }, + "competitor": { + "minMs": 0.16000000000349246, + "maxMs": 0.26000000000203727, + "ratio": 1.6249999999772626 + }, + "stable": false, + "rungs": [ + "level", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "particles-draw", + "category": "Particles", + "count": 10000, + "loadId": "10k", + "unit": "particles", + "primary": true, + "cells": [ + { + "competitor": "excalibur", + "referenceMs": 0.47000000000116415, + "referenceP95Ms": 0.6199999999953434, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 25.010000000002037, + "competitorP95Ms": 28.339999999996508, + "competitorGpuMs": 25.79999999998836, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.018792483006842296, + "factor": 53.21276595731934, + "label": "ExoJS leads clearly (53.21x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.4599999999991269, + "maxMs": 0.4800000000032014, + "ratio": 1.0434782608785054 + }, + "competitor": { + "minMs": 23.720000000001164, + "maxMs": 26.5, + "ratio": 1.1172006745362015 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "phaser", + "referenceMs": 0.47000000000116415, + "referenceP95Ms": 0.6199999999953434, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 3.069999999999709, + "competitorP95Ms": 3.820000000003347, + "competitorGpuMs": 16.660000000003492, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.15309446254111034, + "factor": 6.531914893600223, + "label": "ExoJS leads clearly (6.53x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.4599999999991269, + "maxMs": 0.4800000000032014, + "ratio": 1.0434782608785054 + }, + "competitor": { + "minMs": 2.8300000000017462, + "maxMs": 3.1700000000000728, + "ratio": 1.1201413427555182 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 0.47000000000116415, + "referenceP95Ms": 0.6199999999953434, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 0.1599999999962165, + "competitorP95Ms": 0.3000000000029104, + "competitorGpuMs": 16.68000000000029, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 2.9375000000767386, + "factor": 2.9375000000767386, + "label": "competitor leads (2.94x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.4599999999991269, + "maxMs": 0.4800000000032014, + "ratio": 1.0434782608785054 + }, + "competitor": { + "minMs": 0.13999999999941792, + "maxMs": 0.19999999999708962, + "ratio": 1.4285714285565796 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "particles-draw", + "category": "Particles", + "count": 100000, + "loadId": "100k", + "unit": "particles", + "primary": false, + "cells": [ + { + "competitor": "phaser", + "referenceMs": 1.639999999999418, + "referenceP95Ms": 2.4199999999982538, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 9.530000000013388, + "competitorP95Ms": 10.679999999996653, + "competitorGpuMs": 16.659999999999854, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.17208814270693745, + "factor": 5.810975609766324, + "label": "ExoJS leads clearly (5.81x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 7/0/7, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 1.3799999999973807, + "maxMs": 1.7999999999992724, + "ratio": 1.304347826088905 + }, + "competitor": { + "minMs": 9.43000000000211, + "maxMs": 9.569999999999709, + "ratio": 1.014846235418618 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 1.639999999999418, + "referenceP95Ms": 2.4199999999982538, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 0.1500000000014552, + "competitorP95Ms": 0.37999999999738066, + "competitorGpuMs": 16.660000000003492, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 10.933333333223386, + "factor": 10.933333333223386, + "label": "competitor leads clearly (10.93x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 1.3799999999973807, + "maxMs": 1.7999999999992724, + "ratio": 1.304347826088905 + }, + "competitor": { + "minMs": 0.1499999999978172, + "maxMs": 0.22000000000116415, + "ratio": 1.4666666666957704 + }, + "stable": true, + "rungs": [ + "competitor-leads-clearly", + "competitor-leads-clearly", + "competitor-leads-clearly" + ] + } + } + ] + }, + { + "archetype": "particles-lifecycle", + "category": "Particles", + "count": 10000, + "loadId": "10k", + "unit": "particles", + "primary": true, + "cells": [ + { + "competitor": "excalibur", + "referenceMs": 1.430000000000291, + "referenceP95Ms": 2.279999999998836, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 25.67000000000553, + "competitorP95Ms": 27.75999999999476, + "competitorGpuMs": 26.919999999998254, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.055707051032332804, + "factor": 17.951048951049163, + "label": "ExoJS leads clearly (17.95x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 1.360000000000582, + "maxMs": 1.4799999999959255, + "ratio": 1.0882352941141853 + }, + "competitor": { + "minMs": 23.220000000001164, + "maxMs": 26.159999999996217, + "ratio": 1.1266149870798838 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "phaser", + "referenceMs": 1.430000000000291, + "referenceP95Ms": 2.279999999998836, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 3.2600000000002183, + "competitorP95Ms": 6.380000000001019, + "competitorGpuMs": 16.659999999999854, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.4386503067485262, + "factor": 2.2797202797199683, + "label": "ExoJS leads (2.28x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 1.360000000000582, + "maxMs": 1.4799999999959255, + "ratio": 1.0882352941141853 + }, + "competitor": { + "minMs": 3.1000000000058208, + "maxMs": 3.470000000001164, + "ratio": 1.1193548387079513 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 1.430000000000291, + "referenceP95Ms": 2.279999999998836, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 0.9200000000018917, + "competitorP95Ms": 1.139999999999418, + "competitorGpuMs": 16.679999999993015, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.5543478260840768, + "factor": 1.5543478260840768, + "label": "competitor leads (1.55x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 1.360000000000582, + "maxMs": 1.4799999999959255, + "ratio": 1.0882352941141853 + }, + "competitor": { + "minMs": 0.9199999999982538, + "maxMs": 1.0400000000008731, + "ratio": 1.1304347826117904 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + } + ] + }, + { + "title": "Interaction", + "rows": [ + { + "archetype": "interaction-picking", + "category": "Interaction", + "count": 10000, + "loadId": "10k", + "unit": "rects", + "primary": true, + "cells": [ + { + "competitor": "excalibur", + "referenceMs": 0.6300000000010186, + "referenceP95Ms": 1, + "referenceGpuMs": 16.68000000000029, + "referenceOverFrameBudget": false, + "competitorMs": 23.820000000006985, + "competitorP95Ms": 26.260000000009313, + "competitorGpuMs": 24.580000000001746, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.02644836272043803, + "factor": 37.80952380947376, + "label": "ExoJS leads clearly (37.81x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.5400000000008731, + "maxMs": 0.6399999999994179, + "ratio": 1.185185185182191 + }, + "competitor": { + "minMs": 22.280000000006112, + "maxMs": 25.580000000001746, + "ratio": 1.148114901256496 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "phaser", + "referenceMs": 0.6300000000010186, + "referenceP95Ms": 1, + "referenceGpuMs": 16.68000000000029, + "referenceOverFrameBudget": false, + "competitorMs": 32.219999999997526, + "competitorP95Ms": 35.099999999998545, + "competitorGpuMs": 32.779999999998836, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.01955307262573144, + "factor": 51.142857142770524, + "label": "ExoJS leads clearly (51.14x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.5400000000008731, + "maxMs": 0.6399999999994179, + "ratio": 1.185185185182191 + }, + "competitor": { + "minMs": 31.55999999999767, + "maxMs": 32.530000000000655, + "ratio": 1.0307351077314022 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 0.6300000000010186, + "referenceP95Ms": 1, + "referenceGpuMs": 16.68000000000029, + "referenceOverFrameBudget": false, + "competitorMs": 17.039999999997235, + "competitorP95Ms": 21.580000000001746, + "competitorGpuMs": 17.44000000000233, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.03697183098598127, + "factor": 27.047619047570926, + "label": "ExoJS leads clearly (27.05x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.5400000000008731, + "maxMs": 0.6399999999994179, + "ratio": 1.185185185182191 + }, + "competitor": { + "minMs": 17.0099999999984, + "maxMs": 17.139999999999418, + "ratio": 1.0076425631981794 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + } + ] + } + ] + } + ], + "excluded": [ + { + "archetype": "overdraw", + "reason": "not measured in this run" + }, + { + "archetype": "split-screen", + "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" + }, + { + "archetype": "mixed-material", + "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" + }, + { + "archetype": "instanced-batch", + "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" + }, + { + "archetype": "mixed-sprite-mesh-static", + "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" + }, + { + "archetype": "mixed-sprite-mesh-array", + "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" + }, + { + "archetype": "mixed-material-atlased", + "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" + }, + { + "archetype": "fx-blur", + "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" + } + ], + "webgl1": [] + }, + { + "backend": "webgpu", + "headlineCount": null, + "competitors": [ + "pixi" + ], + "sections": [ + { + "title": "Node scaling", + "rows": [ + { + "archetype": "static-heavy", + "category": "Node scaling", + "count": 1000, + "loadId": "1k", + "unit": "sprites", + "primary": false, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.3000000000000682, + "referenceP95Ms": 0.580000000000382, + "referenceGpuMs": 0.073354, + "referenceOverFrameBudget": false, + "competitorMs": 0.3100000000000591, + "competitorP95Ms": 0.6200000000000045, + "competitorGpuMs": 0.073521, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 0.9677419354839064, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.2999999999999545, + "maxMs": 0.3200000000001637, + "ratio": 1.066666666667374 + }, + "competitor": { + "minMs": 0.2999999999999545, + "maxMs": 0.33999999999980446, + "ratio": 1.1333333333328532 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + } + ] + }, + { + "archetype": "static-heavy", + "category": "Node scaling", + "count": 10000, + "loadId": "10k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.21000000000003638, + "referenceP95Ms": 0.4600000000000364, + "referenceGpuMs": 0.20637499999999998, + "referenceOverFrameBudget": false, + "competitorMs": 0.23999999999978172, + "competitorP95Ms": 0.4600000000000364, + "competitorGpuMs": 0.1905625, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.1999999999998181, + "maxMs": 0.23999999999978172, + "ratio": 1.2 + }, + "competitor": { + "minMs": 0.22000000000025466, + "maxMs": 0.25999999999976353, + "ratio": 1.181818181815739 + }, + "stable": false, + "rungs": [ + "level", + "exojs-leads", + "level" + ] + } + } + ] + }, + { + "archetype": "static-heavy", + "category": "Node scaling", + "count": 100000, + "loadId": "100k", + "unit": "sprites", + "primary": false, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.05999999999949068, + "referenceP95Ms": 0.24000000000069122, + "referenceGpuMs": 0.32764550000000003, + "referenceOverFrameBudget": false, + "competitorMs": 0.1000000000003638, + "competitorP95Ms": 0.5, + "competitorGpuMs": 0.2720205, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.040000000000873115, + "maxMs": 0.19000000000005457, + "ratio": 4.749999999897682 + }, + "competitor": { + "minMs": 0.07999999999992724, + "maxMs": 0.11999999999989086, + "ratio": 1.5 + }, + "stable": false, + "rungs": [ + "competitor-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + }, + { + "archetype": "dynamic-heavy", + "category": "Node scaling", + "count": 10000, + "loadId": "10k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 1.9900000000002365, + "referenceP95Ms": 2.519999999999527, + "referenceGpuMs": 0.207709, + "referenceOverFrameBudget": false, + "competitorMs": 1.2199999999998, + "competitorP95Ms": 1.8400000000001455, + "competitorGpuMs": 0.1908955, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.631147540984068, + "factor": 1.631147540984068, + "label": "competitor leads (1.63x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 1.9899999999997817, + "maxMs": 2.0100000000002183, + "ratio": 1.010050251256502 + }, + "competitor": { + "minMs": 1.2100000000000364, + "maxMs": 1.2800000000002, + "ratio": 1.057851239669555 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "dynamic-all", + "category": "Node scaling", + "count": 1000, + "loadId": "1k", + "unit": "sprites", + "primary": false, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 1.669999999999618, + "referenceP95Ms": 2.1599999999998545, + "referenceGpuMs": 0.07360449999999999, + "referenceOverFrameBudget": false, + "competitorMs": 1.0399999999999636, + "competitorP95Ms": 1.3400000000001455, + "competitorGpuMs": 0.0724795, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.6057692307689198, + "factor": 1.6057692307689198, + "label": "competitor leads (1.61x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 1.5799999999999272, + "maxMs": 1.7200000000002547, + "ratio": 1.0886075949369203 + }, + "competitor": { + "minMs": 1.0199999999999818, + "maxMs": 1.0600000000004002, + "ratio": 1.0392156862749207 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "dynamic-all", + "category": "Node scaling", + "count": 10000, + "loadId": "10k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 4.1599999999998545, + "referenceP95Ms": 9.300000000001091, + "referenceGpuMs": 0.21006249999999999, + "referenceOverFrameBudget": false, + "competitorMs": 2.6900000000005093, + "competitorP95Ms": 9.6200000000008, + "competitorGpuMs": 0.192271, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.5464684014866419, + "factor": 1.5464684014866419, + "label": "competitor leads (1.55x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 3.819999999999709, + "maxMs": 4.460000000000946, + "ratio": 1.1675392670160434 + }, + "competitor": { + "minMs": 2.619999999999891, + "maxMs": 2.7100000000000364, + "ratio": 1.034351145038225 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "dynamic-all", + "category": "Node scaling", + "count": 100000, + "loadId": "100k", + "unit": "sprites", + "primary": false, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 38.5, + "referenceP95Ms": 41.779999999998836, + "referenceGpuMs": 0.5527500000000001, + "referenceOverFrameBudget": true, + "competitorMs": 28.11999999999989, + "competitorP95Ms": 31.76000000000022, + "competitorGpuMs": 0.7896875, + "competitorOverFrameBudget": true, + "verdict": { + "side": "competitor", + "ratio": 1.369132290184927, + "factor": 1.369132290184927, + "label": "competitor leads (1.37x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 37.75999999999931, + "maxMs": 38.66999999999916, + "ratio": 1.024099576271183 + }, + "competitor": { + "minMs": 27.86999999999989, + "maxMs": 28.199999999999818, + "ratio": 1.0118406889128069 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "deep-hierarchy", + "category": "Node scaling", + "count": 10000, + "loadId": "10k", + "unit": "nodes", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.48000000000047294, + "referenceP95Ms": 0.7199999999993452, + "referenceGpuMs": 0.2288125, + "referenceOverFrameBudget": false, + "competitorMs": 0.5200000000004366, + "competitorP95Ms": 0.819999999999709, + "competitorGpuMs": 0.1311875, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 0.9230769230770576, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.48000000000047294, + "maxMs": 0.5, + "ratio": 1.0416666666656402 + }, + "competitor": { + "minMs": 0.5, + "maxMs": 0.5200000000004366, + "ratio": 1.0400000000008731 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + } + ] + }, + { + "archetype": "lifecycle-churn", + "category": "Node scaling", + "count": 5000, + "loadId": "5k", + "unit": "nodes", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 2.6900000000005093, + "referenceP95Ms": 6.8400000000001455, + "referenceGpuMs": 0.14314549999999998, + "referenceOverFrameBudget": false, + "competitorMs": 2.459999999999127, + "competitorP95Ms": 7.260000000002037, + "competitorGpuMs": 0.159375, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "competitor issues fewer buffer uploads (13 vs 3 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 2.6599999999998545, + "maxMs": 3.06999999999789, + "ratio": 1.1541353383451345 + }, + "competitor": { + "minMs": 2.4500000000007276, + "maxMs": 2.5200000000004366, + "ratio": 1.0285714285713012 + }, + "stable": false, + "rungs": [ + "competitor-leads", + "level", + "level" + ] + } + } + ] } ] }, { - "archetype": "text-static", - "category": "Text", - "count": 5000, - "cells": [ - { - "competitor": "phaser", - "referenceMs": 0.08000000001629815, - "referenceP95Ms": 0.14000000001396984, - "referenceOverFrameBudget": false, - "competitorMs": 6.260000000002037, - "competitorP95Ms": 7.380000000004657, - "competitorOverFrameBudget": false, - "verdict": { - "side": "exojs", - "ratio": 0.01277955271825433, - "factor": 78.24999998408384, - "label": "ExoJS leads clearly (78.25x)", - "structural": true - }, - "mechanism": null, - "aggregate": { - "runs": 3, - "reference": { - "minMs": 0.07999999998719431, - "maxMs": 0.1200000000098953, - "ratio": 1.5000000003637979 - }, - "competitor": { - "minMs": 6.119999999995343, - "maxMs": 6.440000000002328, - "ratio": 1.0522875817005275 - }, - "stable": true, - "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" - ] - } + "title": "Fill and GPU state", + "rows": [ + { + "archetype": "fill-layers", + "category": "Fill and GPU state", + "count": 32, + "loadId": "32", + "unit": "layers", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.15999999999985448, + "referenceP95Ms": 0.28000000000065484, + "referenceGpuMs": 0.590417, + "referenceOverFrameBudget": false, + "competitorMs": 0.2000000000007276, + "competitorP95Ms": 0.37999999999919964, + "competitorGpuMs": 0.739271, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.13999999999941792, + "maxMs": 0.18000000000029104, + "ratio": 1.28571428572171 + }, + "competitor": { + "minMs": 0.2000000000007276, + "maxMs": 0.21000000000003638, + "ratio": 1.049999999996362 + }, + "stable": false, + "rungs": [ + "level", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + }, + { + "archetype": "batch-breaking", + "category": "Fill and GPU state", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.5399999999972351, + "referenceP95Ms": 1.2399999999979627, + "referenceGpuMs": 0.219646, + "referenceOverFrameBudget": false, + "competitorMs": 0.6699999999982538, + "competitorP95Ms": 1.0199999999986176, + "competitorGpuMs": 0.12397849999999999, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "competitor issues fewer texture binds (626 vs 314 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.5, + "maxMs": 0.6000000000021828, + "ratio": 1.2000000000043656 + }, + "competitor": { + "minMs": 0.6399999999994179, + "maxMs": 0.6900000000005093, + "ratio": 1.0781250000017764 + }, + "stable": false, + "rungs": [ + "exojs-leads", + "exojs-leads", + "level" + ] + } + } + ] + }, + { + "archetype": "mixed-blend", + "category": "Fill and GPU state", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.4000000000014552, + "referenceP95Ms": 0.5200000000004366, + "referenceGpuMs": 0.118896, + "referenceOverFrameBudget": false, + "competitorMs": 0.5400000000008731, + "competitorP95Ms": 0.7200000000011642, + "competitorGpuMs": 0.119166, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "competitor issues fewer texture binds (240 vs 159 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.27999999999883585, + "maxMs": 0.4399999999986903, + "ratio": 1.5714285714304275 + }, + "competitor": { + "minMs": 0.5399999999990541, + "maxMs": 0.5599999999976717, + "ratio": 1.0370370370345419 + }, + "stable": false, + "rungs": [ + "exojs-leads", + "exojs-leads", + "level" + ] + } + } + ] } ] }, { - "archetype": "text-dynamic", - "category": "Text", - "count": 5000, - "cells": [ - { - "competitor": "phaser", - "referenceMs": 7.460000000006403, - "referenceP95Ms": 8.64000000001397, - "referenceOverFrameBudget": false, - "competitorMs": 6.809999999997672, - "competitorP95Ms": 8.07999999999447, - "competitorOverFrameBudget": false, - "verdict": { - "side": "neither", - "ratio": 1.095447870779582, - "factor": 1, - "label": "level", - "structural": false - }, - "mechanism": null, - "aggregate": { - "runs": 3, - "reference": { - "minMs": 7.44999999999709, - "maxMs": 7.540000000008149, - "ratio": 1.012080536914241 - }, - "competitor": { - "minMs": 6.5900000000001455, - "maxMs": 6.969999999997526, - "ratio": 1.0576631259480078 - }, - "stable": true, - "rungs": [ - "level", - "level", - "level" - ] - } + "title": "Material variety", + "rows": [ + { + "archetype": "batch-breaking-atlased", + "category": "Material variety", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.11999999999898137, + "referenceP95Ms": 0.18000000000029104, + "referenceGpuMs": 0.126417, + "referenceOverFrameBudget": false, + "competitorMs": 0.18000000000029104, + "competitorP95Ms": 0.3400000000001455, + "competitorGpuMs": 0.11560400000000001, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.6666666666599297, + "factor": 1.5000000000151583, + "label": "ExoJS leads (1.50x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.10000000000218279, + "maxMs": 0.13999999999941792, + "ratio": 1.3999999999636201 + }, + "competitor": { + "minMs": 0.18000000000029104, + "maxMs": 0.2000000000007276, + "ratio": 1.1111111111133567 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] } ] }, { - "archetype": "lifecycle-churn", - "category": "Node scaling", - "count": 5000, - "cells": [ - { - "competitor": "phaser", - "referenceMs": 4.009999999994761, - "referenceP95Ms": 6.119999999995343, - "referenceOverFrameBudget": false, - "competitorMs": 10.590000000000146, - "competitorP95Ms": 11.339999999996508, - "competitorOverFrameBudget": false, - "verdict": { - "side": "exojs", - "ratio": 0.37865911236966066, - "factor": 2.640897755614459, - "label": "ExoJS leads (2.64x)", - "structural": false - }, - "mechanism": null, - "aggregate": { - "runs": 3, - "reference": { - "minMs": 3.9100000000034925, - "maxMs": 4.690000000002328, - "ratio": 1.1994884910481174 - }, - "competitor": { - "minMs": 10.380000000004657, - "maxMs": 10.670000000001892, - "ratio": 1.0279383429669657 - }, - "stable": true, - "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads" - ] - } - } - ] - } - ] - }, - { - "backend": "webgpu", - "headlineCount": 5000, - "competitors": [ - "pixi" - ], - "sections": [ - { - "title": "Node scaling", + "title": "Text", "rows": [ { - "archetype": "static-heavy", - "category": "Node scaling", + "archetype": "text-static", + "category": "Text", + "count": 1000, + "loadId": "1k", + "unit": "labels", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.18000000000029104, + "referenceP95Ms": 0.31999999999970896, + "referenceGpuMs": 1.4492085000000001, + "referenceOverFrameBudget": false, + "competitorMs": 0.18000000000029104, + "competitorP95Ms": 0.3400000000001455, + "competitorGpuMs": 0.246812, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.18000000000029104, + "maxMs": 0.22000000000116415, + "ratio": 1.2222222222267136 + }, + "competitor": { + "minMs": 0.18000000000029104, + "maxMs": 0.18000000000029104, + "ratio": 1 + }, + "stable": false, + "rungs": [ + "competitor-leads", + "level", + "level" + ] + } + } + ] + }, + { + "archetype": "text-static", + "category": "Text", "count": 5000, + "loadId": "5k", + "unit": "labels", + "primary": false, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.1400000000030559, + "referenceP95Ms": 0.27999999999883585, + "referenceGpuMs": 3.0184375, + "referenceOverFrameBudget": false, + "competitorMs": 0.18000000000029104, + "competitorP95Ms": 0.3600000000005821, + "competitorGpuMs": 0.364646, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.13999999999941792, + "maxMs": 0.18000000000029104, + "ratio": 1.28571428572171 + }, + "competitor": { + "minMs": 0.18000000000029104, + "maxMs": 0.2000000000007276, + "ratio": 1.1111111111133567 + }, + "stable": false, + "rungs": [ + "level", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + }, + { + "archetype": "text-dynamic", + "category": "Text", + "count": 1000, + "loadId": "1k", + "unit": "labels", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 0.21000000000003638, - "referenceP95Ms": 0.38000000000010914, + "referenceMs": 2.6599999999998545, + "referenceP95Ms": 7.200000000000728, + "referenceGpuMs": 1.487604, "referenceOverFrameBudget": false, - "competitorMs": 0.23999999999978172, - "competitorP95Ms": 0.419999999999618, + "competitorMs": 4.250000000001819, + "competitorP95Ms": 5.599999999998545, + "competitorGpuMs": 0.2921045, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": 0.8750000000009474, - "factor": 1, - "label": "level", + "side": "exojs", + "ratio": 0.6258823529408744, + "factor": 1.5977443609030269, + "label": "ExoJS leads (1.60x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/3 vs 1/2/3, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.20000000000027285, - "maxMs": 0.2199999999997999, - "ratio": 1.099999999997499 + "minMs": 2.430000000000291, + "maxMs": 2.680000000000291, + "ratio": 1.1028806584362016 }, "competitor": { - "minMs": 0.22000000000025466, - "maxMs": 0.2400000000000091, - "ratio": 1.0909090909078696 + "minMs": 4.180000000000291, + "maxMs": 4.419999999998254, + "ratio": 1.0574162679420924 }, "stable": true, "rungs": [ - "level", - "level", - "level" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } } ] }, { - "archetype": "dynamic-heavy", - "category": "Node scaling", + "archetype": "text-dynamic", + "category": "Text", + "count": 5000, + "loadId": "5k", + "unit": "labels", + "primary": false, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 7.8700000000008, + "referenceP95Ms": 10.900000000001455, + "referenceGpuMs": 3.727396, + "referenceOverFrameBudget": false, + "competitorMs": 19.159999999999854, + "competitorP95Ms": 21.92000000000189, + "competitorGpuMs": 0.8017084999999999, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.4107515657620491, + "factor": 2.434561626429213, + "label": "ExoJS leads (2.43x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/3 vs 1/2/3, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 7.430000000000291, + "maxMs": 8.199999999998909, + "ratio": 1.1036339165543185 + }, + "competitor": { + "minMs": 18.829999999999927, + "maxMs": 19.6200000000008, + "ratio": 1.0419543281997279 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + } + ] + }, + { + "title": "Render targets", + "rows": [ + { + "archetype": "filter-chain-1", + "category": "Render targets", "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 0.9800000000000182, - "referenceP95Ms": 1.3799999999991996, + "referenceMs": 0.5200000000004366, + "referenceP95Ms": 0.9000000000014552, + "referenceGpuMs": 0.235479, "referenceOverFrameBudget": false, - "competitorMs": 0.8000000000006366, - "competitorP95Ms": 1.2000000000007276, + "competitorMs": 2.1700000000000728, + "competitorP95Ms": 3.7400000000016007, + "competitorGpuMs": 0.28045850000000005, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "exojs", + "ratio": 0.2396313364057231, + "factor": 4.17307692307356, + "label": "ExoJS leads (4.17x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/6/3 vs 2/4/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.9599999999991269, - "maxMs": 1.0700000000006185, - "ratio": 1.1145833333349913 + "minMs": 0.5199999999986176, + "maxMs": 0.6699999999982538, + "ratio": 1.2884615384616058 }, "competitor": { - "minMs": 0.7399999999997817, - "maxMs": 0.8999999999996362, - "ratio": 1.2162162162160834 + "minMs": 2.0900000000037835, + "maxMs": 2.1999999999989086, + "ratio": 1.0526315789449407 }, - "stable": false, + "stable": true, "rungs": [ - "competitor-leads", - "competitor-leads", - "level" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } } ] }, { - "archetype": "deep-hierarchy", - "category": "Node scaling", + "archetype": "filter-chain-2", + "category": "Render targets", "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 0.2899999999999636, - "referenceP95Ms": 0.48000000000138243, + "referenceMs": 0.4599999999991269, + "referenceP95Ms": 0.680000000000291, + "referenceGpuMs": 0.241541, "referenceOverFrameBudget": false, - "competitorMs": 0.37999999999919964, - "competitorP95Ms": 0.6000000000003638, + "competitorMs": 2.219999999997526, + "competitorP95Ms": 3.779999999998836, + "competitorGpuMs": 0.44899999999999995, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "exojs", + "ratio": 0.2072072072070448, + "factor": 4.826086956525522, + "label": "ExoJS leads (4.83x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 4/8/3 vs 3/6/3, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.2899999999999636, - "maxMs": 0.3000000000010914, - "ratio": 1.0344827586245828 + "minMs": 0.41999999999825377, + "maxMs": 0.4800000000032014, + "ratio": 1.142857142869517 }, "competitor": { - "minMs": 0.3600000000005821, - "maxMs": 0.42999999999938154, - "ratio": 1.1944444444407953 + "minMs": 2.0899999999965075, + "maxMs": 2.279999999998836, + "ratio": 1.090909090910357 }, - "stable": false, + "stable": true, "rungs": [ "exojs-leads", - "level", + "exojs-leads", "exojs-leads" ] } @@ -1866,85 +4615,92 @@ ] }, { - "archetype": "lifecycle-churn", - "category": "Node scaling", + "archetype": "filter-chain-4", + "category": "Render targets", "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 3.930000000000291, - "referenceP95Ms": 5.180000000007567, + "referenceMs": 0.42000000000552973, + "referenceP95Ms": 0.5800000000017462, + "referenceGpuMs": 0.320333, "referenceOverFrameBudget": false, - "competitorMs": 2.389999999992142, - "competitorP95Ms": 6.639999999999418, + "competitorMs": 2.2599999999983993, + "competitorP95Ms": 3.6599999999962165, + "competitorGpuMs": 0.763207, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 1.6443514644406747, - "factor": 1.6443514644406747, - "label": "competitor leads (1.64x)", - "structural": false + "side": "exojs", + "ratio": 0.1858407079671802, + "factor": 5.380952380877724, + "label": "ExoJS leads clearly (5.38x)", + "structural": true }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/3, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 6/12/3 vs 5/10/5, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 3.720000000001164, - "maxMs": 3.960000000006403, - "ratio": 1.0645161290336462 + "minMs": 0.42000000000189175, + "maxMs": 0.4500000000007276, + "ratio": 1.0714285714254779 }, "competitor": { - "minMs": 2.3800000000046566, - "maxMs": 2.5800000000017462, - "ratio": 1.084033613443991 + "minMs": 2.1900000000023283, + "maxMs": 2.319999999999709, + "ratio": 1.0593607305923483 }, "stable": true, "rungs": [ - "competitor-leads", - "competitor-leads", - "competitor-leads" + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" ] } } ] - } - ] - }, - { - "title": "Fill and GPU state", - "rows": [ + }, { - "archetype": "overdraw", - "category": "Fill and GPU state", + "archetype": "composite", + "category": "Render targets", "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 0.020000000000436557, - "referenceP95Ms": 0.06000000000130967, + "referenceMs": 0.46000000000276486, + "referenceP95Ms": 0.7000000000043656, + "referenceGpuMs": 0.377042, "referenceOverFrameBudget": false, - "competitorMs": 0.040000000000873115, - "competitorP95Ms": 0.09999999999854481, + "competitorMs": 0.7599999999983993, + "competitorP95Ms": 1.1600000000034925, + "competitorGpuMs": 0.45610400000000006, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5, - "factor": 2, - "label": "ExoJS leads (2.00x)", + "ratio": 0.6052631578996496, + "factor": 1.652173913030068, + "label": "ExoJS leads (1.65x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "ExoJS issues fewer draw calls (5 vs 8 per frame)", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.020000000000436557, - "maxMs": 0.020000000000436557, - "ratio": 1 + "minMs": 0.4599999999991269, + "maxMs": 0.47999999999592546, + "ratio": 1.043478260862688 }, "competitor": { - "minMs": 0.040000000000873115, - "maxMs": 0.040000000000873115, - "ratio": 1 + "minMs": 0.7200000000011642, + "maxMs": 0.7899999999972351, + "ratio": 1.0972222222166081 }, "stable": true, "rungs": [ @@ -1957,17 +4713,22 @@ ] }, { - "archetype": "batch-breaking", - "category": "Fill and GPU state", + "archetype": "mask-clip", + "category": "Render targets", "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 0.7399999999979627, - "referenceP95Ms": 1.4600000000064028, + "referenceMs": 0.3600000000005821, + "referenceP95Ms": 0.5, + "referenceGpuMs": 0.296, "referenceOverFrameBudget": false, - "competitorMs": 0.6200000000026193, - "competitorP95Ms": 0.9800000000032014, + "competitorMs": 0.2999999999956344, + "competitorP95Ms": 0.4800000000032014, + "competitorGpuMs": 0.1538545, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -1976,41 +4737,47 @@ "label": "unstable across runs", "structural": false }, - "mechanism": "competitor issues fewer texture binds (626 vs 314 per frame)", + "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.680000000000291, - "maxMs": 0.7899999999972351, - "ratio": 1.1617647058777898 + "minMs": 0.3600000000005821, + "maxMs": 0.3600000000005821, + "ratio": 1 }, "competitor": { - "minMs": 0.569999999999709, - "maxMs": 0.6500000000014552, - "ratio": 1.1403508771961177 + "minMs": 0.27999999999883585, + "maxMs": 0.31999999999970896, + "ratio": 1.1428571428608552 }, "stable": false, "rungs": [ - "level", "competitor-leads", - "competitor-leads" + "competitor-leads", + "level" ] } } ] }, { - "archetype": "mixed-blend", - "category": "Fill and GPU state", + "archetype": "mask-clip-animated", + "category": "Render targets", "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 0.4599999999991269, - "referenceP95Ms": 0.5800000000017462, + "referenceMs": 0.3600000000005821, + "referenceP95Ms": 0.5599999999976717, + "referenceGpuMs": 0.322584, "referenceOverFrameBudget": false, - "competitorMs": 0.5200000000040745, - "competitorP95Ms": 0.7000000000043656, + "competitorMs": 0.4400000000023283, + "competitorP95Ms": 0.6599999999962165, + "competitorGpuMs": 0.153729, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -2019,24 +4786,25 @@ "label": "unstable across runs", "structural": false }, - "mechanism": "competitor issues fewer texture binds (240 vs 159 per frame)", + "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.36999999999898137, - "maxMs": 0.47999999999592546, - "ratio": 1.2972972972898567 + "minMs": 0.3400000000037835, + "maxMs": 0.37999999999738066, + "ratio": 1.1176470588033884 }, "competitor": { - "minMs": 0.5, + "minMs": 0.41999999999825377, "maxMs": 0.5400000000008731, - "ratio": 1.0800000000017462 + "ratio": 1.28571428572171 }, "stable": false, "rungs": [ - "level", "exojs-leads", - "level" + "level", + "exojs-leads" ] } } @@ -2045,46 +4813,52 @@ ] }, { - "title": "Material variety", + "title": "Camera and world", "rows": [ { - "archetype": "batch-breaking-atlased", - "category": "Material variety", - "count": 5000, + "archetype": "scrolling-world", + "category": "Camera and world", + "count": 10000, + "loadId": "10k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 0.11999999999534339, - "referenceP95Ms": 0.180000000007567, + "referenceMs": 0.2400000000016007, + "referenceP95Ms": 1.5400000000008731, + "referenceGpuMs": 0.10166649999999999, "referenceOverFrameBudget": false, - "competitorMs": 0.16000000000349246, - "competitorP95Ms": 0.33999999999650754, + "competitorMs": 0.1999999999989086, + "competitorP95Ms": 0.3599999999969441, + "competitorGpuMs": 0.09775, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.7499999999545253, - "factor": 1.3333333334141773, - "label": "ExoJS leads (1.33x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", + "timer": "limited", "aggregate": { "runs": 3, "reference": { - "minMs": 0.10000000000582077, - "maxMs": 0.11999999999534339, - "ratio": 1.1999999998835846 + "minMs": 0.23999999999796273, + "maxMs": 0.2800000000024738, + "ratio": 1.1666666666868777 }, "competitor": { - "minMs": 0.16000000000349246, - "maxMs": 0.19999999999708962, - "ratio": 1.2499999999545253 + "minMs": 0.1999999999989086, + "maxMs": 0.2000000000007276, + "ratio": 1.000000000009095 }, - "stable": true, + "stable": false, "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads" + "competitor-leads", + "competitor-leads", + "level" ] } } @@ -2093,83 +4867,95 @@ ] }, { - "title": "Text", + "title": "Tilemaps", "rows": [ { - "archetype": "text-static", - "category": "Text", - "count": 5000, + "archetype": "tilemap-scroll", + "category": "Tilemaps", + "count": 10000, + "loadId": "10k", + "unit": "tiles", + "primary": false, "cells": [ { "competitor": "pixi", - "referenceMs": 0.13999999999941792, - "referenceP95Ms": 0.23999999999068677, + "referenceMs": 0.2599999999947613, + "referenceP95Ms": 1.4200000000055297, + "referenceGpuMs": 0.068354, "referenceOverFrameBudget": false, - "competitorMs": 0.16000000000349246, - "competitorP95Ms": 0.3200000000069849, + "competitorMs": 0.4000000000014552, + "competitorP95Ms": 0.7200000000011642, + "competitorGpuMs": 0.077521, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": 0.8749999999772626, - "factor": 1, - "label": "level", + "side": "exojs", + "ratio": 0.6499999999845386, + "factor": 1.5384615384981335, + "label": "ExoJS leads (1.54x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "ExoJS issues fewer buffer uploads (326 vs 652 per frame)", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.13999999999941792, - "maxMs": 0.16000000000349246, - "ratio": 1.1428571428868406 + "minMs": 0.22999999999956344, + "maxMs": 0.2599999999983993, + "ratio": 1.1304347826038816 }, "competitor": { - "minMs": 0.16000000000349246, - "maxMs": 0.18000000000029104, - "ratio": 1.1249999999772626 + "minMs": 0.3900000000030559, + "maxMs": 0.4400000000023283, + "ratio": 1.128205128202258 }, "stable": true, "rungs": [ - "level", - "level", - "level" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } } ] }, { - "archetype": "text-dynamic", - "category": "Text", - "count": 5000, + "archetype": "tilemap-scroll", + "category": "Tilemaps", + "count": 100000, + "loadId": "100k", + "unit": "tiles", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 7.650000000001455, - "referenceP95Ms": 8.819999999992433, + "referenceMs": 0.20999999999912689, + "referenceP95Ms": 0.7999999999956344, + "referenceGpuMs": 0.067208, "referenceOverFrameBudget": false, - "competitorMs": 21.5, - "competitorP95Ms": 26.679999999993015, - "competitorOverFrameBudget": true, + "competitorMs": 0.3500000000021828, + "competitorP95Ms": 0.5599999999976717, + "competitorGpuMs": 0.0774795, + "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.35581395348843975, - "factor": 2.810457516339335, - "label": "ExoJS leads (2.81x)", + "ratio": 0.5999999999937634, + "factor": 1.6666666666839904, + "label": "ExoJS leads (1.67x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/3 vs 1/2/3, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "competitor issues fewer draw calls (60 vs 2 per frame)", + "timer": "limited", "aggregate": { "runs": 3, "reference": { - "minMs": 7.299999999995634, - "maxMs": 7.769999999996799, - "ratio": 1.0643835616440336 + "minMs": 0.19999999999708962, + "maxMs": 0.22000000000116415, + "ratio": 1.100000000021828 }, "competitor": { - "minMs": 21.450000000004366, - "maxMs": 21.83999999998923, - "ratio": 1.0181818181811089 + "minMs": 0.3100000000013097, + "maxMs": 0.38000000000101863, + "ratio": 1.2258064516110103 }, "stable": true, "rungs": [ @@ -2180,222 +4966,252 @@ } } ] - } - ] - }, - { - "title": "Render targets", - "rows": [ + }, { - "archetype": "filter-chain-1", - "category": "Render targets", - "count": 5000, + "archetype": "tilemap-edit", + "category": "Tilemaps", + "count": 100000, + "loadId": "100k", + "unit": "tiles", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 0.3800000000046566, - "referenceP95Ms": 0.6000000000058208, + "referenceMs": 0.9399999999986903, + "referenceP95Ms": 1.3399999999965075, + "referenceGpuMs": 0.066771, "referenceOverFrameBudget": false, - "competitorMs": 1.9599999999991269, - "competitorP95Ms": 3.0800000000017462, + "competitorMs": 2.3299999999981083, + "competitorP95Ms": 3.139999999999418, + "competitorGpuMs": 0.06914600000000001, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "exojs", + "ratio": 0.40343347639461524, + "factor": 2.47872340425676, + "label": "ExoJS leads (2.48x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/6/3 vs 2/4/2, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/4/2 vs 2/4/3, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.3799999999901047, - "maxMs": 0.41999999999825377, - "ratio": 1.105263157918923 + "minMs": 0.7800000000024738, + "maxMs": 0.9500000000007276, + "ratio": 1.217948717945788 }, "competitor": { - "minMs": 1.929999999993015, - "maxMs": 2.0100000000020373, - "ratio": 1.0414507772068973 + "minMs": 1.4700000000011642, + "maxMs": 2.3700000000026193, + "ratio": 1.6122448979596888 }, - "stable": false, + "stable": true, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", + "exojs-leads", + "exojs-leads", "exojs-leads" ] } } ] - }, + } + ] + }, + { + "title": "Particles", + "rows": [ { - "archetype": "filter-chain-2", - "category": "Render targets", - "count": 5000, + "archetype": "particles-draw", + "category": "Particles", + "count": 1000, + "loadId": "1k", + "unit": "particles", + "primary": false, "cells": [ { "competitor": "pixi", - "referenceMs": 0.3800000000046566, - "referenceP95Ms": 0.5800000000017462, + "referenceMs": 0.27999999999883585, + "referenceP95Ms": 0.41999999999825377, + "referenceGpuMs": 0.0715, "referenceOverFrameBudget": false, - "competitorMs": 1.9500000000043656, - "competitorP95Ms": 2.9199999999982538, + "competitorMs": 0.26000000000203727, + "competitorP95Ms": 0.5, + "competitorGpuMs": 0.0687085, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.19487179487374662, - "factor": 5.131578947317026, - "label": "ExoJS leads clearly (5.13x)", - "structural": true + "side": "neither", + "ratio": 1.076923076910161, + "factor": 1, + "label": "level", + "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 4/8/3 vs 3/6/3, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.3600000000005821, - "maxMs": 0.3800000000046566, - "ratio": 1.055555555566784 + "minMs": 0.26000000000203727, + "maxMs": 0.27999999999883585, + "ratio": 1.076923076910161 }, "competitor": { - "minMs": 1.9100000000034925, - "maxMs": 2.099999999991269, - "ratio": 1.0994764397839942 + "minMs": 0.23999999999796273, + "maxMs": 0.27999999999883585, + "ratio": 1.1666666666717194 }, "stable": true, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" + "level", + "level", + "level" ] } } ] }, { - "archetype": "filter-chain-4", - "category": "Render targets", - "count": 5000, + "archetype": "particles-draw", + "category": "Particles", + "count": 10000, + "loadId": "10k", + "unit": "particles", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 0.3699999999953434, - "referenceP95Ms": 0.6199999999953434, + "referenceMs": 0.4500000000007276, + "referenceP95Ms": 0.5999999999985448, + "referenceGpuMs": 0.18533349999999998, "referenceOverFrameBudget": false, - "competitorMs": 1.999999999992724, - "competitorP95Ms": 3.5, + "competitorMs": 0.22999999999956344, + "competitorP95Ms": 0.43999999999505235, + "competitorGpuMs": 0.232604, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.1849999999983447, - "factor": 5.40540540545377, - "label": "ExoJS leads clearly (5.41x)", - "structural": true + "side": "competitor", + "ratio": 1.956521739137312, + "factor": 1.956521739137312, + "label": "competitor leads (1.96x)", + "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 6/12/3 vs 5/10/5, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.3600000000005821, - "maxMs": 0.3800000000046566, - "ratio": 1.055555555566784 + "minMs": 0.43999999999505235, + "maxMs": 0.4599999999991269, + "ratio": 1.045454545464317 }, "competitor": { - "minMs": 1.9799999999959255, - "maxMs": 2.1300000000046566, - "ratio": 1.0757575757621414 + "minMs": 0.22000000000116415, + "maxMs": 0.2999999999956344, + "ratio": 1.3636363636093043 }, "stable": true, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" + "competitor-leads", + "competitor-leads", + "competitor-leads" ] } } ] }, { - "archetype": "composite", - "category": "Render targets", - "count": 5000, + "archetype": "particles-draw", + "category": "Particles", + "count": 100000, + "loadId": "100k", + "unit": "particles", + "primary": false, "cells": [ { "competitor": "pixi", - "referenceMs": 0.4100000000107684, - "referenceP95Ms": 0.6199999999953434, + "referenceMs": 1.9199999999982538, + "referenceP95Ms": 2.9400000000023283, + "referenceGpuMs": 0.415875, "referenceOverFrameBudget": false, - "competitorMs": 0.6100000000005821, - "competitorP95Ms": 0.9400000000023283, + "competitorMs": 0.2999999999956344, + "competitorP95Ms": 0.4800000000032014, + "competitorGpuMs": 0.30574999999999997, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.6721311475579954, - "factor": 1.4878048780111237, - "label": "ExoJS leads (1.49x)", - "structural": false + "side": "competitor", + "ratio": 6.400000000087312, + "factor": 6.400000000087312, + "label": "competitor leads clearly (6.40x)", + "structural": true }, - "mechanism": "ExoJS issues fewer draw calls (5 vs 8 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.39999999999417923, - "maxMs": 0.43000000000029104, - "ratio": 1.0750000000163709 + "minMs": 1.889999999999418, + "maxMs": 2.0499999999992724, + "ratio": 1.0846560846560338 }, "competitor": { - "minMs": 0.569999999999709, - "maxMs": 0.6399999999994179, - "ratio": 1.1228070175434117 + "minMs": 0.23999999999796273, + "maxMs": 0.3100000000013097, + "ratio": 1.291666666683088 }, "stable": true, "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads" + "competitor-leads-clearly", + "competitor-leads-clearly", + "competitor-leads-clearly" ] } } ] }, { - "archetype": "mask-clip", - "category": "Render targets", - "count": 5000, + "archetype": "particles-lifecycle", + "category": "Particles", + "count": 10000, + "loadId": "10k", + "unit": "particles", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 3.2600000000093132, - "referenceP95Ms": 6.419999999983702, + "referenceMs": 0.6999999999970896, + "referenceP95Ms": 0.8600000000005821, + "referenceGpuMs": 0.22829149999999998, "referenceOverFrameBudget": false, - "competitorMs": 0.27999999999883585, - "competitorP95Ms": 0.4400000000023283, + "competitorMs": 0.9400000000023283, + "competitorP95Ms": 1.1999999999970896, + "competitorGpuMs": 0.138812, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 11.642857142938812, - "factor": 11.642857142938812, - "label": "competitor leads clearly (11.64x)", - "structural": true + "side": "exojs", + "ratio": 0.7446808510588891, + "factor": 1.342857142866052, + "label": "ExoJS leads (1.34x)", + "structural": false }, - "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", + "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 2.6600000000034925, - "maxMs": 3.3000000000029104, - "ratio": 1.2406015037588638 + "minMs": 0.6600000000034925, + "maxMs": 0.7000000000043656, + "ratio": 1.0606060606070629 }, "competitor": { - "minMs": 0.2600000000093132, - "maxMs": 0.27999999999883585, - "ratio": 1.076923076880024 + "minMs": 0.9400000000023283, + "maxMs": 1.0400000000008731, + "ratio": 1.1063829787215926 }, "stable": true, "rungs": [ - "competitor-leads-clearly", - "competitor-leads-clearly", - "competitor-leads-clearly" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } } @@ -2404,46 +5220,52 @@ ] }, { - "title": "Camera and world", + "title": "Interaction", "rows": [ { - "archetype": "scrolling-world", - "category": "Camera and world", - "count": 5000, + "archetype": "interaction-picking", + "category": "Interaction", + "count": 10000, + "loadId": "10k", + "unit": "rects", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 0.17999999999301508, - "referenceP95Ms": 1.3800000000046566, + "referenceMs": 0.5600000000013097, + "referenceP95Ms": 0.8800000000046566, + "referenceGpuMs": 0.175417, "referenceOverFrameBudget": false, - "competitorMs": 0.17999999999301508, - "competitorP95Ms": 0.3800000000046566, - "competitorOverFrameBudget": false, + "competitorMs": 16.969999999997526, + "competitorP95Ms": 21.539999999993597, + "competitorGpuMs": 0.2945, + "competitorOverFrameBudget": true, "verdict": { - "side": "neither", - "ratio": 1, - "factor": 1, - "label": "level", - "structural": false + "side": "exojs", + "ratio": 0.032999410724890474, + "factor": 30.303571428496138, + "label": "ExoJS leads clearly (30.30x)", + "structural": true }, - "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.17999999999301508, - "maxMs": 0.180000000007567, - "ratio": 1.000000000080844 + "minMs": 0.5599999999976717, + "maxMs": 0.5799999999981083, + "ratio": 1.0357142857152137 }, "competitor": { - "minMs": 0.17999999999301508, - "maxMs": 0.180000000007567, - "ratio": 1.000000000080844 + "minMs": 15.900000000001455, + "maxMs": 18.080000000001746, + "ratio": 1.1371069182389995 }, "stable": true, "rungs": [ - "level", - "level", - "level" + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" ] } } @@ -2453,6 +5275,10 @@ } ], "excluded": [ + { + "archetype": "overdraw", + "reason": "not measured in this run" + }, { "archetype": "split-screen", "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" @@ -2476,6 +5302,10 @@ { "archetype": "mixed-material-atlased", "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" + }, + { + "archetype": "fx-blur", + "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" } ], "webgl1": [] @@ -2514,14 +5344,15 @@ "Scenes are warmed to steady state before timing; the per-cell warmupSteps/timedSteps counts are recorded for honesty.", "All arms build the byte-identical scene (bodies, positions, shapes, sizes, static/dynamic split, gravity, perturbed-body set) from the shared deterministic RNG, and the perturbed-body selection is asserted equal across arms before each cell is timed.", "Each arm runs at its own engine defaults for solver iterations, contact model and sleeping - those engine differences are the measured quantity in a native-vs-adapter comparison, disclosed per arm below.", - "Arm roles: matter-js and planck are the PURE-JS PEERS exojs-physics is compared against; rapier is a Rust/WASM engine and stands as the REFERENCE CEILING for what leaving JavaScript buys, not as a peer a JS solver is expected to match.", + "Arm roles: matter-js, planck and nape-js are the JAVASCRIPT PEERS exojs-physics is compared against; rapier is a Rust/WASM engine and stands as the REFERENCE CEILING for what leaving JavaScript buys, not as a peer a JS solver is expected to match.", "exojs-physics arm (native runtime, pure JS): TGS-Soft solver, 4 sub-steps per fixed step, sleeping ON by default (resting bodies deactivate). Contact count = solid contacts in the world contact graph.", "matter-js arm (pure-JS peer): constraint solver at matter defaults (6 position / 4 velocity / 2 constraint iterations), sleeping OFF by default (a settled stack keeps paying full solve cost); matter's default per-step air drag (frictionAir) is zeroed so all arms integrate the same pure-gravity field; gravity (px/s^2) and perturbation velocity (px/s) are mapped into matter's px-per-step unit model. Contact count = active colliding pairs (engine.pairs.collisionActive), a pair-level proxy, not identical in semantics to the exojs solid-contact count.", "planck arm (pure-JS peer): Box2D port at planck defaults (8 velocity / 3 position iterations per step), sleeping ON by default; Settings.lengthUnitsPerMeter is set to 30 so planck's absolute MKS tolerances are interpreted at the scene's pixel scale, which is the knob planck gives a pixel-coordinate game - positions, gravity (px/s^2) and velocity (px/s) then carry over unconverted. Contact count = the world contact list filtered by isTouching(), a touching collider-pair count. Rays are answered from planck's dynamic tree, but World.rayCast is Box2D's non-solid ray (an origin inside a fixture is not a hit). Continuous collision runs for every body (planck's default), where exojs and rapier restrict it to bullets and matter has none.", - "rapier arm (WASM reference ceiling, not a pure-JS peer): TGS-Soft solver at rapier defaults (4 solver / 1 internal PGS iterations), auto-sleeping ON; default lengthUnit=1 is fed a px-scale world (tuned for ~1-unit objects), exactly what attaching rapier with pixel coordinates yields. Contact count = collider pairs with a solid narrow-phase manifold (numContacts > 0), deduped." + "rapier arm (WASM reference ceiling, not a pure-JS peer): TGS-Soft solver at rapier defaults (4 solver / 1 internal PGS iterations), auto-sleeping ON; default lengthUnit=1 is fed a px-scale world (tuned for ~1-unit objects), exactly what attaching rapier with pixel coordinates yields. Contact count = collider pairs with a solid narrow-phase manifold (numContacts > 0), deduped.", + "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." ], "engineVersion": "0.17.0", - "timestamp": "2026-09-06T05:27:57.985Z" + "timestamp": "2026-09-11T23:53:53.372Z" }, { "browser": "webkit", @@ -2553,14 +5384,15 @@ "Scenes are warmed to steady state before timing; the per-cell warmupSteps/timedSteps counts are recorded for honesty.", "All arms build the byte-identical scene (bodies, positions, shapes, sizes, static/dynamic split, gravity, perturbed-body set) from the shared deterministic RNG, and the perturbed-body selection is asserted equal across arms before each cell is timed.", "Each arm runs at its own engine defaults for solver iterations, contact model and sleeping - those engine differences are the measured quantity in a native-vs-adapter comparison, disclosed per arm below.", - "Arm roles: matter-js and planck are the PURE-JS PEERS exojs-physics is compared against; rapier is a Rust/WASM engine and stands as the REFERENCE CEILING for what leaving JavaScript buys, not as a peer a JS solver is expected to match.", + "Arm roles: matter-js, planck and nape-js are the JAVASCRIPT PEERS exojs-physics is compared against; rapier is a Rust/WASM engine and stands as the REFERENCE CEILING for what leaving JavaScript buys, not as a peer a JS solver is expected to match.", "exojs-physics arm (native runtime, pure JS): TGS-Soft solver, 4 sub-steps per fixed step, sleeping ON by default (resting bodies deactivate). Contact count = solid contacts in the world contact graph.", "matter-js arm (pure-JS peer): constraint solver at matter defaults (6 position / 4 velocity / 2 constraint iterations), sleeping OFF by default (a settled stack keeps paying full solve cost); matter's default per-step air drag (frictionAir) is zeroed so all arms integrate the same pure-gravity field; gravity (px/s^2) and perturbation velocity (px/s) are mapped into matter's px-per-step unit model. Contact count = active colliding pairs (engine.pairs.collisionActive), a pair-level proxy, not identical in semantics to the exojs solid-contact count.", "planck arm (pure-JS peer): Box2D port at planck defaults (8 velocity / 3 position iterations per step), sleeping ON by default; Settings.lengthUnitsPerMeter is set to 30 so planck's absolute MKS tolerances are interpreted at the scene's pixel scale, which is the knob planck gives a pixel-coordinate game - positions, gravity (px/s^2) and velocity (px/s) then carry over unconverted. Contact count = the world contact list filtered by isTouching(), a touching collider-pair count. Rays are answered from planck's dynamic tree, but World.rayCast is Box2D's non-solid ray (an origin inside a fixture is not a hit). Continuous collision runs for every body (planck's default), where exojs and rapier restrict it to bullets and matter has none.", - "rapier arm (WASM reference ceiling, not a pure-JS peer): TGS-Soft solver at rapier defaults (4 solver / 1 internal PGS iterations), auto-sleeping ON; default lengthUnit=1 is fed a px-scale world (tuned for ~1-unit objects), exactly what attaching rapier with pixel coordinates yields. Contact count = collider pairs with a solid narrow-phase manifold (numContacts > 0), deduped." + "rapier arm (WASM reference ceiling, not a pure-JS peer): TGS-Soft solver at rapier defaults (4 solver / 1 internal PGS iterations), auto-sleeping ON; default lengthUnit=1 is fed a px-scale world (tuned for ~1-unit objects), exactly what attaching rapier with pixel coordinates yields. Contact count = collider pairs with a solid narrow-phase manifold (numContacts > 0), deduped.", + "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." ], "engineVersion": "0.17.0", - "timestamp": "2026-09-06T05:33:51.441Z" + "timestamp": "2026-09-12T00:06:03.094Z" }, { "browser": "webkit", @@ -2592,14 +5424,15 @@ "Scenes are warmed to steady state before timing; the per-cell warmupSteps/timedSteps counts are recorded for honesty.", "All arms build the byte-identical scene (bodies, positions, shapes, sizes, static/dynamic split, gravity, perturbed-body set) from the shared deterministic RNG, and the perturbed-body selection is asserted equal across arms before each cell is timed.", "Each arm runs at its own engine defaults for solver iterations, contact model and sleeping - those engine differences are the measured quantity in a native-vs-adapter comparison, disclosed per arm below.", - "Arm roles: matter-js and planck are the PURE-JS PEERS exojs-physics is compared against; rapier is a Rust/WASM engine and stands as the REFERENCE CEILING for what leaving JavaScript buys, not as a peer a JS solver is expected to match.", + "Arm roles: matter-js, planck and nape-js are the JAVASCRIPT PEERS exojs-physics is compared against; rapier is a Rust/WASM engine and stands as the REFERENCE CEILING for what leaving JavaScript buys, not as a peer a JS solver is expected to match.", "exojs-physics arm (native runtime, pure JS): TGS-Soft solver, 4 sub-steps per fixed step, sleeping ON by default (resting bodies deactivate). Contact count = solid contacts in the world contact graph.", "matter-js arm (pure-JS peer): constraint solver at matter defaults (6 position / 4 velocity / 2 constraint iterations), sleeping OFF by default (a settled stack keeps paying full solve cost); matter's default per-step air drag (frictionAir) is zeroed so all arms integrate the same pure-gravity field; gravity (px/s^2) and perturbation velocity (px/s) are mapped into matter's px-per-step unit model. Contact count = active colliding pairs (engine.pairs.collisionActive), a pair-level proxy, not identical in semantics to the exojs solid-contact count.", "planck arm (pure-JS peer): Box2D port at planck defaults (8 velocity / 3 position iterations per step), sleeping ON by default; Settings.lengthUnitsPerMeter is set to 30 so planck's absolute MKS tolerances are interpreted at the scene's pixel scale, which is the knob planck gives a pixel-coordinate game - positions, gravity (px/s^2) and velocity (px/s) then carry over unconverted. Contact count = the world contact list filtered by isTouching(), a touching collider-pair count. Rays are answered from planck's dynamic tree, but World.rayCast is Box2D's non-solid ray (an origin inside a fixture is not a hit). Continuous collision runs for every body (planck's default), where exojs and rapier restrict it to bullets and matter has none.", - "rapier arm (WASM reference ceiling, not a pure-JS peer): TGS-Soft solver at rapier defaults (4 solver / 1 internal PGS iterations), auto-sleeping ON; default lengthUnit=1 is fed a px-scale world (tuned for ~1-unit objects), exactly what attaching rapier with pixel coordinates yields. Contact count = collider pairs with a solid narrow-phase manifold (numContacts > 0), deduped." + "rapier arm (WASM reference ceiling, not a pure-JS peer): TGS-Soft solver at rapier defaults (4 solver / 1 internal PGS iterations), auto-sleeping ON; default lengthUnit=1 is fed a px-scale world (tuned for ~1-unit objects), exactly what attaching rapier with pixel coordinates yields. Contact count = collider pairs with a solid narrow-phase manifold (numContacts > 0), deduped.", + "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." ], "engineVersion": "0.17.0", - "timestamp": "2026-09-06T05:39:47.730Z" + "timestamp": "2026-09-12T00:18:12.752Z" } ], "libraries": [ @@ -2618,6 +5451,10 @@ { "name": "@dimforge/rapier2d-compat", "version": "0.19.3" + }, + { + "name": "@newkrok/nape-js", + "version": "3.42.0" } ], "section": { @@ -2626,35 +5463,80 @@ { "archetype": "box-stack", "category": "Physics", - "count": 10000, + "count": 5500, + "loadId": "5500", + "unit": "bodies", + "primary": true, "cells": [ { "competitor": "matter-js", - "referenceMs": 10.219999999999345, - "referenceP95Ms": 11.579999999999927, + "referenceMs": 5.420000000000073, + "referenceP95Ms": 5.720000000000027, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 23.37999999999738, - "competitorP95Ms": 26.95999999999185, - "competitorOverFrameBudget": true, + "competitorMs": 10.359999999998763, + "competitorP95Ms": 11.979999999999563, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.5231660231660926, + "factor": 1.9114391143908902, + "label": "ExoJS leads (1.91x)", + "structural": false + }, + "mechanism": "ExoJS resolves fewer solved contacts (5501 vs 12297); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 5.099999999999909, + "maxMs": 5.470000000000027, + "ratio": 1.0725490196078675 + }, + "competitor": { + "minMs": 10.340000000000146, + "maxMs": 10.479999999999563, + "ratio": 1.0135396518374677 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "nape-js", + "referenceMs": 5.420000000000073, + "referenceP95Ms": 5.720000000000027, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 10, + "competitorP95Ms": 12.860000000000582, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.437125748503015, - "factor": 2.2876712328766025, - "label": "ExoJS leads (2.29x)", + "ratio": 0.5420000000000073, + "factor": 1.845018450184477, + "label": "ExoJS leads (1.85x)", "structural": false }, - "mechanism": "ExoJS resolves fewer solved contacts (10002 vs 22396); the counters are not semantically identical across arms (see the run's caveats)", + "mechanism": "identical scene (5501 bodies, 5501 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 10.159999999999854, - "maxMs": 10.859999999999673, - "ratio": 1.0688976377952586 + "minMs": 5.099999999999909, + "maxMs": 5.470000000000027, + "ratio": 1.0725490196078675 }, "competitor": { - "minMs": 23.369999999995343, - "maxMs": 23.69000000000233, - "ratio": 1.0136927685069341 + "minMs": 9.609999999993306, + "maxMs": 10.110000000007858, + "ratio": 1.0520291363178877 }, "stable": true, "rungs": [ @@ -2666,31 +5548,34 @@ }, { "competitor": "planck", - "referenceMs": 10.219999999999345, - "referenceP95Ms": 11.579999999999927, + "referenceMs": 5.420000000000073, + "referenceP95Ms": 5.720000000000027, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 72.7100000000064, - "competitorP95Ms": 92.06000000002678, + "competitorMs": 29.970000000001164, + "competitorP95Ms": 37.279999999998836, + "competitorGpuMs": null, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.14055838261584988, - "factor": 7.11448140900304, - "label": "ExoJS leads clearly (7.11x)", + "ratio": 0.1808475141808429, + "factor": 5.529520295203093, + "label": "ExoJS leads clearly (5.53x)", "structural": true }, - "mechanism": "ExoJS resolves fewer solved contacts (10002 vs 19865); the counters are not semantically identical across arms (see the run's caveats)", + "mechanism": "ExoJS resolves fewer solved contacts (5501 vs 10677); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 10.159999999999854, - "maxMs": 10.859999999999673, - "ratio": 1.0688976377952586 + "minMs": 5.099999999999909, + "maxMs": 5.470000000000027, + "ratio": 1.0725490196078675 }, "competitor": { - "minMs": 72.55999999999767, - "maxMs": 72.91999999999825, - "ratio": 1.0049614112458736 + "minMs": 29.69000000000233, + "maxMs": 30, + "ratio": 1.0104412260019418 }, "stable": true, "rungs": [ @@ -2702,36 +5587,39 @@ }, { "competitor": "rapier", - "referenceMs": 10.219999999999345, - "referenceP95Ms": 11.579999999999927, + "referenceMs": 5.420000000000073, + "referenceP95Ms": 5.720000000000027, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 7.569999999977881, - "competitorP95Ms": 9.679999999993015, + "competitorMs": 4.139999999999418, + "competitorP95Ms": 5.720000000001164, + "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 1.3500660502020088, - "factor": 1.3500660502020088, - "label": "competitor leads (1.35x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, - "mechanism": "ExoJS resolves fewer solved contacts (10002 vs 17880); the counters are not semantically identical across arms (see the run's caveats)", + "mechanism": "ExoJS resolves fewer solved contacts (5501 vs 10045); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 10.159999999999854, - "maxMs": 10.859999999999673, - "ratio": 1.0688976377952586 + "minMs": 5.099999999999909, + "maxMs": 5.470000000000027, + "ratio": 1.0725490196078675 }, "competitor": { - "minMs": 7.550000000017462, - "maxMs": 7.709999999991851, - "ratio": 1.0211920529766911 + "minMs": 4.069999999999709, + "maxMs": 4.319999999999709, + "ratio": 1.0614250614250658 }, - "stable": true, + "stable": false, "rungs": [ "competitor-leads", - "competitor-leads", + "level", "competitor-leads" ] } @@ -2741,35 +5629,80 @@ { "archetype": "many-dynamic", "category": "Physics", - "count": 2200, + "count": 1500, + "loadId": "1500", + "unit": "bodies", + "primary": true, "cells": [ { "competitor": "matter-js", - "referenceMs": 8.719999999999345, - "referenceP95Ms": 11.960000000000946, + "referenceMs": 5.399999999999636, + "referenceP95Ms": 6.279999999999745, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 5.619999999998981, + "competitorP95Ms": 6, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 0.9608540925267998, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "identical scene (1504 bodies, 4236 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 5.2800000000002, + "maxMs": 5.4399999999996, + "ratio": 1.0303030303029155 + }, + "competitor": { + "minMs": 5.260000000002037, + "maxMs": 5.680000000000291, + "ratio": 1.0798479087448842 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + }, + { + "competitor": "nape-js", + "referenceMs": 5.399999999999636, + "referenceP95Ms": 6.279999999999745, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 8.139999999999418, - "competitorP95Ms": 9.5, + "competitorMs": 5.860000000000582, + "competitorP95Ms": 6.680000000007567, + "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.0712530712530675, + "ratio": 0.921501706484488, "factor": 1, "label": "level", "structural": false }, - "mechanism": "identical scene (2204 bodies, 6233 contacts, 0 joints on both arms); the difference is solver cost", + "mechanism": "identical scene (1504 bodies, 4236 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 8.460000000000036, - "maxMs": 8.760000000000218, - "ratio": 1.0354609929078227 + "minMs": 5.2800000000002, + "maxMs": 5.4399999999996, + "ratio": 1.0303030303029155 }, "competitor": { - "minMs": 8.080000000001746, - "maxMs": 8.139999999999418, - "ratio": 1.0074257425739677 + "minMs": 5.80000000000291, + "maxMs": 5.860000000000582, + "ratio": 1.0103448275858002 }, "stable": true, "rungs": [ @@ -2781,31 +5714,34 @@ }, { "competitor": "planck", - "referenceMs": 8.719999999999345, - "referenceP95Ms": 11.960000000000946, + "referenceMs": 5.399999999999636, + "referenceP95Ms": 6.279999999999745, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 9.459999999991851, - "competitorP95Ms": 14.059999999997672, + "competitorMs": 5.540000000000873, + "competitorP95Ms": 6.560000000004948, + "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9217758985208094, + "ratio": 0.974729241877037, "factor": 1, "label": "level", "structural": false }, - "mechanism": "identical scene (2204 bodies, 6233 contacts, 0 joints on both arms); the difference is solver cost", + "mechanism": "identical scene (1504 bodies, 4236 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 8.460000000000036, - "maxMs": 8.760000000000218, - "ratio": 1.0354609929078227 + "minMs": 5.2800000000002, + "maxMs": 5.4399999999996, + "ratio": 1.0303030303029155 }, "competitor": { - "minMs": 9.459999999991851, - "maxMs": 9.599999999991269, - "ratio": 1.0147991543339894 + "minMs": 5.519999999996799, + "maxMs": 5.580000000001746, + "ratio": 1.010869565218294 }, "stable": true, "rungs": [ @@ -2817,31 +5753,34 @@ }, { "competitor": "rapier", - "referenceMs": 8.719999999999345, - "referenceP95Ms": 11.960000000000946, + "referenceMs": 5.399999999999636, + "referenceP95Ms": 6.279999999999745, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 1.9199999999837019, - "competitorP95Ms": 1.9600000000209548, + "competitorMs": 1.3000000000029104, + "competitorP95Ms": 1.360000000000582, + "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 4.541666666704878, - "factor": 4.541666666704878, - "label": "competitor leads (4.54x)", + "ratio": 4.153846153836574, + "factor": 4.153846153836574, + "label": "competitor leads (4.15x)", "structural": false }, - "mechanism": "identical scene (2204 bodies, 6233 contacts, 0 joints on both arms); the difference is solver cost", + "mechanism": "identical scene (1504 bodies, 4236 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 8.460000000000036, - "maxMs": 8.760000000000218, - "ratio": 1.0354609929078227 + "minMs": 5.2800000000002, + "maxMs": 5.4399999999996, + "ratio": 1.0303030303029155 }, "competitor": { - "minMs": 1.8999999999650754, - "maxMs": 1.9400000000023283, - "ratio": 1.0210526315989412 + "minMs": 1.3000000000029104, + "maxMs": 1.319999999992433, + "ratio": 1.0153846153765214 }, "stable": true, "rungs": [ @@ -2856,35 +5795,41 @@ { "archetype": "mixed-static-dynamic", "category": "Physics", - "count": 3200, + "count": 1700, + "loadId": "1700", + "unit": "bodies", + "primary": true, "cells": [ { "competitor": "matter-js", - "referenceMs": 8.780000000000655, - "referenceP95Ms": 10.380000000001019, + "referenceMs": 4.539999999999964, + "referenceP95Ms": 5, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.639999999999418, - "competitorP95Ms": 6.80000000000291, + "competitorMs": 2.3000000000010914, + "competitorP95Ms": 2.5800000000017462, + "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.556737588652759, - "factor": 1.556737588652759, - "label": "competitor leads (1.56x)", + "ratio": 1.9739130434773084, + "factor": 1.9739130434773084, + "label": "competitor leads (1.97x)", "structural": false }, - "mechanism": "identical scene (3313 bodies, 3670 contacts, 0 joints on both arms); the difference is solver cost", + "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 8.7599999999984, - "maxMs": 8.829999999999927, - "ratio": 1.0079908675800846 + "minMs": 4.5, + "maxMs": 4.619999999999891, + "ratio": 1.0266666666666424 }, "competitor": { - "minMs": 5.619999999995343, - "maxMs": 5.830000000001746, - "ratio": 1.037366548043875 + "minMs": 2.2000000000007276, + "maxMs": 2.3000000000010914, + "ratio": 1.0454545454546957 }, "stable": true, "rungs": [ @@ -2894,33 +5839,75 @@ ] } }, + { + "competitor": "nape-js", + "referenceMs": 4.539999999999964, + "referenceP95Ms": 5, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 5.029999999998836, + "competitorP95Ms": 6.240000000005239, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 0.9025844930419512, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "competitor resolves fewer solved contacts (2195 vs 1461); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 4.5, + "maxMs": 4.619999999999891, + "ratio": 1.0266666666666424 + }, + "competitor": { + "minMs": 4.779999999998836, + "maxMs": 5.080000000001746, + "ratio": 1.0627615062767748 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + }, { "competitor": "planck", - "referenceMs": 8.780000000000655, - "referenceP95Ms": 10.380000000001019, + "referenceMs": 4.539999999999964, + "referenceP95Ms": 5, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 21.130000000004657, - "competitorP95Ms": 31.960000000020955, - "competitorOverFrameBudget": true, + "competitorMs": 7.920000000001892, + "competitorP95Ms": 10.659999999996217, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.4155229531471235, - "factor": 2.406605922551604, - "label": "ExoJS leads (2.41x)", + "ratio": 0.5732323232321818, + "factor": 1.7444933920709151, + "label": "ExoJS leads (1.74x)", "structural": false }, - "mechanism": "identical scene (3313 bodies, 3670 contacts, 0 joints on both arms); the difference is solver cost", + "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 8.7599999999984, - "maxMs": 8.829999999999927, - "ratio": 1.0079908675800846 + "minMs": 4.5, + "maxMs": 4.619999999999891, + "ratio": 1.0266666666666424 }, "competitor": { - "minMs": 20.779999999998836, - "maxMs": 21.19999999999709, - "ratio": 1.0202117420595898 + "minMs": 7.879999999997381, + "maxMs": 8.020000000004075, + "ratio": 1.0177664974627842 }, "stable": true, "rungs": [ @@ -2932,31 +5919,34 @@ }, { "competitor": "rapier", - "referenceMs": 8.780000000000655, - "referenceP95Ms": 10.380000000001019, + "referenceMs": 4.539999999999964, + "referenceP95Ms": 5, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 3.7000000000116415, - "competitorP95Ms": 4.239999999990687, + "competitorMs": 1.5800000000017462, + "competitorP95Ms": 1.860000000000582, + "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 2.372972972965684, - "factor": 2.372972972965684, - "label": "competitor leads (2.37x)", + "ratio": 2.873417721515789, + "factor": 2.873417721515789, + "label": "competitor leads (2.87x)", "structural": false }, - "mechanism": "ExoJS resolves fewer solved contacts (3670 vs 6105); the counters are not semantically identical across arms (see the run's caveats)", + "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 8.7599999999984, - "maxMs": 8.829999999999927, - "ratio": 1.0079908675800846 + "minMs": 4.5, + "maxMs": 4.619999999999891, + "ratio": 1.0266666666666424 }, "competitor": { - "minMs": 3.679999999993015, - "maxMs": 3.7199999999720603, - "ratio": 1.0108695652117177 + "minMs": 1.5800000000017462, + "maxMs": 1.639999999999418, + "ratio": 1.0379746835427883 }, "stable": true, "rungs": [ @@ -2971,35 +5961,80 @@ { "archetype": "raycast", "category": "Physics", - "count": 3200, + "count": 1700, + "loadId": "1700", + "unit": "bodies", + "primary": true, "cells": [ { "competitor": "matter-js", - "referenceMs": 10.790000000000873, - "referenceP95Ms": 11.979999999995925, + "referenceMs": 5.739999999999782, + "referenceP95Ms": 6.219999999999345, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 6.429999999998472, + "competitorP95Ms": 8.020000000000437, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 0.8926905132194628, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms) with 34 vs 44 ray hits per step; the difference is query and solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 5.640000000001237, + "maxMs": 5.819999999999709, + "ratio": 1.0319148936167433 + }, + "competitor": { + "minMs": 6.419999999998254, + "maxMs": 6.449999999998909, + "ratio": 1.004672897196365 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + }, + { + "competitor": "nape-js", + "referenceMs": 5.739999999999782, + "referenceP95Ms": 6.219999999999345, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 13.720000000001164, - "competitorP95Ms": 17.339999999996508, + "competitorMs": 12.660000000003492, + "competitorP95Ms": 14.279999999998836, + "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.7864431486880435, - "factor": 1.2715477293790598, - "label": "ExoJS leads (1.27x)", + "ratio": 0.4533965244864296, + "factor": 2.2055749128926783, + "label": "ExoJS leads (2.21x)", "structural": false }, - "mechanism": "identical scene (3313 bodies, 3670 contacts, 0 joints on both arms) with 41 vs 49 ray hits per step; the difference is query and solver cost", + "mechanism": "competitor resolves fewer solved contacts (2195 vs 1461); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 10.729999999999563, - "maxMs": 10.950000000000728, - "ratio": 1.0205032618826815 + "minMs": 5.640000000001237, + "maxMs": 5.819999999999709, + "ratio": 1.0319148936167433 }, "competitor": { - "minMs": 13.669999999998254, - "maxMs": 13.75, - "ratio": 1.0058522311632594 + "minMs": 12.619999999995343, + "maxMs": 12.69999999999709, + "ratio": 1.0063391442156717 }, "stable": true, "rungs": [ @@ -3011,31 +6046,34 @@ }, { "competitor": "planck", - "referenceMs": 10.790000000000873, - "referenceP95Ms": 11.979999999995925, + "referenceMs": 5.739999999999782, + "referenceP95Ms": 6.219999999999345, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 21.14999999999418, - "competitorP95Ms": 32.60000000000582, - "competitorOverFrameBudget": true, + "competitorMs": 8.06000000000131, + "competitorP95Ms": 10.940000000002328, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5101654846337514, - "factor": 1.9601482854487922, - "label": "ExoJS leads (1.96x)", + "ratio": 0.7121588089328597, + "factor": 1.404181184669271, + "label": "ExoJS leads (1.40x)", "structural": false }, - "mechanism": "identical scene (3313 bodies, 3670 contacts, 0 joints on both arms) with 41 vs 37 ray hits per step; the difference is query and solver cost", + "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms) with 34 vs 37 ray hits per step; the difference is query and solver cost", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 10.729999999999563, - "maxMs": 10.950000000000728, - "ratio": 1.0205032618826815 + "minMs": 5.640000000001237, + "maxMs": 5.819999999999709, + "ratio": 1.0319148936167433 }, "competitor": { - "minMs": 21.05000000000291, - "maxMs": 21.19999999999709, - "ratio": 1.0071258907360645 + "minMs": 7.879999999997381, + "maxMs": 8.150000000001455, + "ratio": 1.0342639593913914 }, "stable": true, "rungs": [ @@ -3047,31 +6085,34 @@ }, { "competitor": "rapier", - "referenceMs": 10.790000000000873, - "referenceP95Ms": 11.979999999995925, + "referenceMs": 5.739999999999782, + "referenceP95Ms": 6.219999999999345, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 3.8500000000058208, - "competitorP95Ms": 4.339999999967404, + "competitorMs": 1.7099999999991269, + "competitorP95Ms": 1.9199999999982538, + "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 2.802597402593392, - "factor": 2.802597402593392, - "label": "competitor leads (2.80x)", + "ratio": 3.3567251462004166, + "factor": 3.3567251462004166, + "label": "competitor leads (3.36x)", "structural": false }, - "mechanism": "ExoJS resolves fewer solved contacts (3670 vs 6105); the counters are not semantically identical across arms (see the run's caveats)", + "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms) with 34 vs 39 ray hits per step; the difference is query and solver cost", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 10.729999999999563, - "maxMs": 10.950000000000728, - "ratio": 1.0205032618826815 + "minMs": 5.640000000001237, + "maxMs": 5.819999999999709, + "ratio": 1.0319148936167433 }, "competitor": { - "minMs": 3.7000000000116415, - "maxMs": 3.85999999998603, - "ratio": 1.0432432432361851 + "minMs": 1.639999999999418, + "maxMs": 1.7200000000011642, + "ratio": 1.0487804878059601 }, "stable": true, "rungs": [ @@ -3086,35 +6127,41 @@ { "archetype": "body-churn", "category": "Physics", - "count": 2400, + "count": 1500, + "loadId": "1500", + "unit": "bodies", + "primary": true, "cells": [ { "competitor": "matter-js", - "referenceMs": 11.639999999999418, - "referenceP95Ms": 13.239999999997963, + "referenceMs": 5.579999999999927, + "referenceP95Ms": 6.3400000000001455, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 9.860000000000582, - "competitorP95Ms": 12.180000000007567, + "competitorMs": 5.759999999994761, + "competitorP95Ms": 6.739999999997963, + "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.1805273833670111, + "ratio": 0.9687500000008684, "factor": 1, "label": "level", "structural": false }, - "mechanism": "ExoJS resolves fewer solved contacts (2282 vs 6230); the counters are not semantically identical across arms (see the run's caveats)", + "mechanism": "ExoJS resolves fewer solved contacts (1415 vs 3762); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 11.459999999999127, - "maxMs": 11.709999999999127, - "ratio": 1.0218150087260052 + "minMs": 5.5, + "maxMs": 5.680000000000291, + "ratio": 1.0327272727273256 }, "competitor": { - "minMs": 9.859999999993306, - "maxMs": 9.880000000004657, - "ratio": 1.0020283975670754 + "minMs": 5.720000000001164, + "maxMs": 5.760000000002037, + "ratio": 1.0069930069931583 }, "stable": true, "rungs": [ @@ -3124,33 +6171,75 @@ ] } }, + { + "competitor": "nape-js", + "referenceMs": 5.579999999999927, + "referenceP95Ms": 6.3400000000001455, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 3.349999999998545, + "competitorP95Ms": 3.8999999999941792, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.6656716417917465, + "factor": 1.6656716417917465, + "label": "competitor leads (1.67x)", + "structural": false + }, + "mechanism": "identical scene (1504 bodies, 1415 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 5.5, + "maxMs": 5.680000000000291, + "ratio": 1.0327272727273256 + }, + "competitor": { + "minMs": 3.110000000000582, + "maxMs": 3.4799999999959255, + "ratio": 1.118971061091728 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + }, { "competitor": "planck", - "referenceMs": 11.639999999999418, - "referenceP95Ms": 13.239999999997963, + "referenceMs": 5.579999999999927, + "referenceP95Ms": 6.3400000000001455, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 9.889999999999418, - "competitorP95Ms": 12.160000000003492, + "competitorMs": 5.539999999993597, + "competitorP95Ms": 7.139999999999418, + "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.1769464105156828, + "ratio": 1.0072202166076492, "factor": 1, "label": "level", "structural": false }, - "mechanism": "ExoJS resolves fewer solved contacts (2282 vs 5242); the counters are not semantically identical across arms (see the run's caveats)", + "mechanism": "ExoJS resolves fewer solved contacts (1415 vs 3163); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 11.459999999999127, - "maxMs": 11.709999999999127, - "ratio": 1.0218150087260052 + "minMs": 5.5, + "maxMs": 5.680000000000291, + "ratio": 1.0327272727273256 }, "competitor": { - "minMs": 9.880000000004657, - "maxMs": 9.920000000012806, - "ratio": 1.0040485829967742 + "minMs": 5.459999999999127, + "maxMs": 5.619999999995343, + "ratio": 1.029304029303341 }, "stable": true, "rungs": [ @@ -3162,31 +6251,34 @@ }, { "competitor": "rapier", - "referenceMs": 11.639999999999418, - "referenceP95Ms": 13.239999999997963, + "referenceMs": 5.579999999999927, + "referenceP95Ms": 6.3400000000001455, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 1.2000000000116415, - "competitorP95Ms": 1.2399999999906868, + "competitorMs": 0.7599999999947613, + "competitorP95Ms": 0.7800000000133878, + "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 9.699999999905412, - "factor": 9.699999999905412, - "label": "competitor leads clearly (9.70x)", + "ratio": 7.342105263208408, + "factor": 7.342105263208408, + "label": "competitor leads clearly (7.34x)", "structural": true }, - "mechanism": "identical scene (2404 bodies, 2282 contacts, 0 joints on both arms); the difference is solver cost", + "mechanism": "identical scene (1504 bodies, 1415 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 11.459999999999127, - "maxMs": 11.709999999999127, - "ratio": 1.0218150087260052 + "minMs": 5.5, + "maxMs": 5.680000000000291, + "ratio": 1.0327272727273256 }, "competitor": { - "minMs": 1.1400000000139698, - "maxMs": 1.2000000000116415, - "ratio": 1.0526315789446812 + "minMs": 0.7599999999947613, + "maxMs": 0.7799999999988358, + "ratio": 1.0263157894792267 }, "stable": true, "rungs": [ @@ -3201,35 +6293,41 @@ { "archetype": "joints", "category": "Physics", - "count": 15000, + "count": 9000, + "loadId": "9k", + "unit": "bodies", + "primary": true, "cells": [ { "competitor": "matter-js", - "referenceMs": 9.37999999999738, - "referenceP95Ms": 10.39999999999418, + "referenceMs": 5.630000000000109, + "referenceP95Ms": 6.140000000001237, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 9.489999999997963, - "competitorP95Ms": 12.160000000003492, + "competitorMs": 5.519999999996799, + "competitorP95Ms": 7.860000000000582, + "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9884088514224862, + "ratio": 1.0199275362324953, "factor": 1, "label": "level", "structural": false }, - "mechanism": "competitor resolves fewer solved contacts (13125 vs 0); the counters are not semantically identical across arms (see the run's caveats)", + "mechanism": "competitor resolves fewer solved contacts (7875 vs 0); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 9.30000000000291, - "maxMs": 9.419999999998254, - "ratio": 1.012903225805947 + "minMs": 4.920000000000073, + "maxMs": 5.720000000001164, + "ratio": 1.1626016260164795 }, "competitor": { - "minMs": 9.470000000001164, - "maxMs": 9.639999999999418, - "ratio": 1.0179514255541957 + "minMs": 5.040000000000873, + "maxMs": 6.040000000000873, + "ratio": 1.198412698412664 }, "stable": true, "rungs": [ @@ -3239,33 +6337,75 @@ ] } }, + { + "competitor": "nape-js", + "referenceMs": 5.630000000000109, + "referenceP95Ms": 6.140000000001237, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 0, + "competitorP95Ms": 0.0020000000004074536, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "not comparable", + "structural": false + }, + "mechanism": "competitor resolves fewer solved contacts (7875 vs 0); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 4.920000000000073, + "maxMs": 5.720000000001164, + "ratio": 1.1626016260164795 + }, + "competitor": { + "minMs": 0, + "maxMs": 0, + "ratio": null + }, + "stable": true, + "rungs": [ + "not-comparable", + "not-comparable", + "not-comparable" + ] + } + }, { "competitor": "planck", - "referenceMs": 9.37999999999738, - "referenceP95Ms": 10.39999999999418, + "referenceMs": 5.630000000000109, + "referenceP95Ms": 6.140000000001237, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 0.6800000000221189, - "competitorP95Ms": 0.7399999999906868, + "competitorMs": 0.3799999999901047, + "competitorP95Ms": 0.3800000000046566, + "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 13.79411764660628, - "factor": 13.79411764660628, - "label": "competitor leads clearly (13.79x)", + "ratio": 14.815789474070305, + "factor": 14.815789474070305, + "label": "competitor leads clearly (14.82x)", "structural": true }, - "mechanism": "competitor resolves fewer solved contacts (13125 vs 0); the counters are not semantically identical across arms (see the run's caveats)", + "mechanism": "competitor resolves fewer solved contacts (7875 vs 0); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 9.30000000000291, - "maxMs": 9.419999999998254, - "ratio": 1.012903225805947 + "minMs": 4.920000000000073, + "maxMs": 5.720000000001164, + "ratio": 1.1626016260164795 }, "competitor": { - "minMs": 0.6799999999930151, - "maxMs": 0.7200000000011642, - "ratio": 1.0588235294243529 + "minMs": 0.37000000000261934, + "maxMs": 0.39999999999417923, + "ratio": 1.081081081057696 }, "stable": true, "rungs": [ @@ -3277,31 +6417,34 @@ }, { "competitor": "rapier", - "referenceMs": 9.37999999999738, - "referenceP95Ms": 10.39999999999418, + "referenceMs": 5.630000000000109, + "referenceP95Ms": 6.140000000001237, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 0.30000000004656613, - "competitorP95Ms": 0.34000000002561137, + "competitorMs": 0.19999999999708962, + "competitorP95Ms": 0.22000000000116415, + "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 31.26666666180471, - "factor": 31.26666666180471, - "label": "competitor leads clearly (31.27x)", + "ratio": 28.15000000041018, + "factor": 28.15000000041018, + "label": "competitor leads clearly (28.15x)", "structural": true }, - "mechanism": "competitor resolves fewer solved contacts (13125 vs 1875); the counters are not semantically identical across arms (see the run's caveats)", + "mechanism": "competitor resolves fewer solved contacts (7875 vs 1125); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "limited", "aggregate": { "runs": 3, "reference": { - "minMs": 9.30000000000291, - "maxMs": 9.419999999998254, - "ratio": 1.012903225805947 + "minMs": 4.920000000000073, + "maxMs": 5.720000000001164, + "ratio": 1.1626016260164795 }, "competitor": { - "minMs": 0.29999999998835847, - "maxMs": 0.3600000000442378, - "ratio": 1.2000000001940256 + "minMs": 0.19999999999708962, + "maxMs": 0.20000000001164153, + "ratio": 1.0000000000727596 }, "stable": true, "rungs": [ @@ -3316,35 +6459,80 @@ { "archetype": "settling-pile", "category": "Physics", - "count": 5800, + "count": 3000, + "loadId": "3k", + "unit": "bodies", + "primary": true, "cells": [ { "competitor": "matter-js", - "referenceMs": 8.720000000001164, - "referenceP95Ms": 10, + "referenceMs": 3.7400000000016007, + "referenceP95Ms": 4.399999999997817, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 25.860000000015134, - "competitorP95Ms": 28.95999999999185, - "competitorOverFrameBudget": true, + "competitorMs": 10.660000000003492, + "competitorP95Ms": 12.19999999999709, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.3508442776735812, + "factor": 2.8502673796788582, + "label": "ExoJS leads (2.85x)", + "structural": false + }, + "mechanism": "identical scene (3004 bodies, 6199 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 3.7000000000007276, + "maxMs": 3.7600000000020373, + "ratio": 1.016216216216567 + }, + "competitor": { + "minMs": 10.62000000000262, + "maxMs": 10.680000000000291, + "ratio": 1.0056497175139036 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "nape-js", + "referenceMs": 3.7400000000016007, + "referenceP95Ms": 4.399999999997817, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 11.020000000004075, + "competitorP95Ms": 13.900000000008731, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.33720030935792966, - "factor": 2.965596330276569, - "label": "ExoJS leads (2.97x)", + "ratio": 0.3393829401089127, + "factor": 2.9465240641709514, + "label": "ExoJS leads (2.95x)", "structural": false }, - "mechanism": "identical scene (5804 bodies, 12187 contacts, 0 joints on both arms); the difference is solver cost", + "mechanism": "identical scene (3004 bodies, 6199 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 8.690000000002328, - "maxMs": 8.839999999996508, - "ratio": 1.0172612197921909 + "minMs": 3.7000000000007276, + "maxMs": 3.7600000000020373, + "ratio": 1.016216216216567 }, "competitor": { - "minMs": 25.589999999996508, - "maxMs": 26.60999999998603, - "ratio": 1.0398593200464894 + "minMs": 10.980000000010477, + "maxMs": 11.040000000000873, + "ratio": 1.005464480873437 }, "stable": true, "rungs": [ @@ -3356,67 +6544,73 @@ }, { "competitor": "planck", - "referenceMs": 8.720000000001164, - "referenceP95Ms": 10, + "referenceMs": 3.7400000000016007, + "referenceP95Ms": 4.399999999997817, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 45.6699999999837, - "competitorP95Ms": 55.61999999999534, - "competitorOverFrameBudget": true, + "competitorMs": 13.220000000001164, + "competitorP95Ms": 19.59999999999127, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.19093496825058628, - "factor": 5.237385321098349, - "label": "ExoJS leads clearly (5.24x)", - "structural": true + "ratio": 0.28290468986393885, + "factor": 3.5347593582875683, + "label": "ExoJS leads (3.53x)", + "structural": false }, - "mechanism": "identical scene (5804 bodies, 12187 contacts, 0 joints on both arms); the difference is solver cost", + "mechanism": "identical scene (3004 bodies, 6199 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 8.690000000002328, - "maxMs": 8.839999999996508, - "ratio": 1.0172612197921909 + "minMs": 3.7000000000007276, + "maxMs": 3.7600000000020373, + "ratio": 1.016216216216567 }, "competitor": { - "minMs": 45.33999999999651, - "maxMs": 48.12999999997555, - "ratio": 1.0615350683718408 + "minMs": 13.140000000006694, + "maxMs": 13.39999999999418, + "ratio": 1.0197869101969066 }, "stable": true, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } }, { "competitor": "rapier", - "referenceMs": 8.720000000001164, - "referenceP95Ms": 10, + "referenceMs": 3.7400000000016007, + "referenceP95Ms": 4.399999999997817, + "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 4.78000000002794, - "competitorP95Ms": 5.320000000006985, + "competitorMs": 2.139999999999418, + "competitorP95Ms": 2.1999999999970896, + "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.8242677824163587, - "factor": 1.8242677824163587, - "label": "competitor leads (1.82x)", + "ratio": 1.7476635514030925, + "factor": 1.7476635514030925, + "label": "competitor leads (1.75x)", "structural": false }, - "mechanism": "identical scene (5804 bodies, 12187 contacts, 0 joints on both arms); the difference is solver cost", + "mechanism": "identical scene (3004 bodies, 6199 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 8.690000000002328, - "maxMs": 8.839999999996508, - "ratio": 1.0172612197921909 + "minMs": 3.7000000000007276, + "maxMs": 3.7600000000020373, + "ratio": 1.016216216216567 }, "competitor": { - "minMs": 4.739999999990687, - "maxMs": 5.209999999991851, - "ratio": 1.0991561181439002 + "minMs": 2.0599999999976717, + "maxMs": 2.1599999999889405, + "ratio": 1.0485436893162048 }, "stable": true, "rungs": [ @@ -3433,6 +6627,6 @@ }, "signature": { "algorithm": "sha256", - "value": "b748d194ddf086bc06a92a3048778d4afe52cef141a293c1bb5dc66f97b51d5c" + "value": "4a5fb77cfdcae08ad5ab6e822d6d2756f9477a9813adb1b878d2523b290e6adf" } } From 74a1492ab18c0a8134a85575dcfc2ea47996a461 Mon Sep 17 00:00:00 2001 From: Exoridus Date: Sat, 12 Sep 2026 02:58:14 +0200 Subject: [PATCH 02/28] fix(bench): a bare vendor word names no machine either WebKit's WebGPU adapter substitution reports `apple apple apple apple`. `dropLeadingNoise` keeps a last part rather than reducing to the empty string, so that survived as the bare vendor word `apple` - which the category-word check accepted, because `apple` is a vendor rather than a category. The adapter preference then chose it over the WebGL2 stamp's `Apple GPU`, and one machine's runs published as `apple-macos-27-beta-webkit.json` instead of falling back to the CPU model and overwriting `m3-max-macos-27-beta-webkit.json`. That is how the same Mac came to hold two profiles. A part made only of vendor and product-line words now identifies no machine, for the same reason a category word does not. The superseded physics-only profile for that machine is removed with it. --- .../results/m3-max-macos-27-beta-webkit.json | 1298 ----------------- packages/exojs-bench/src/profile/slug.ts | 16 +- packages/exojs-bench/test/profile.test.ts | 28 +- 3 files changed, 42 insertions(+), 1300 deletions(-) delete mode 100644 packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json diff --git a/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json b/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json deleted file mode 100644 index 92678330b..000000000 --- a/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "schemaVersion": 6, - "profile": { - "slug": "m3-max-macos-27-beta-webkit", - "gpu": "m3-max", - "os": "macos-27-beta", - "browser": "webkit", - "platform": { - "name": "macos", - "version": 27, - "versionSource": "declared", - "prerelease": true - }, - "engineVersion": "0.17.0", - "measuredAt": "2026-09-09T19:14:13.252Z", - "runs": 4 - }, - "physics": { - "runs": [ - { - "browser": "webkit", - "browserVersion": "26.5", - "host": { - "cpu": "Apple M3 Max", - "cpuCount": 16, - "os": "darwin 27.0.0", - "platformVersion": { - "major": 27, - "source": "declared", - "evidence": "the runner declared '27-beta'; os.release() reported '27.0.0' on darwin, which is the kernel version and does not name the operating system's own major version; declare it with --platform=[-beta]" - }, - "arch": "arm64" - }, - "prerelease": { - "value": true, - "source": "declared", - "evidence": "the runner declared the platform as '27-beta'" - }, - "fixedDelta": 0.016666666666666666, - "clock": { - "resolutionMs": 0.01999999999999602, - "crossOriginIsolated": true - }, - "caveats": [ - "Step time is CPU wall-clock per step() over the timed window (median/p95), measured in one webkit page (same-run discipline). No number here was taken in Node.", - "The page's performance.now() resolves to 20.0us (cross-origin isolated: true). A cell whose step is too fast to time individually at that resolution batches steps per timing sample and records the batch as stepsPerSample; median and p95 are then per-step averages over that batch.", - "Scenes are warmed to steady state before timing; the per-cell warmupSteps/timedSteps counts are recorded for honesty.", - "All arms build the byte-identical scene (bodies, positions, shapes, sizes, static/dynamic split, gravity, perturbed-body set) from the shared deterministic RNG, and the perturbed-body selection is asserted equal across arms before each cell is timed.", - "Each arm runs at its own engine defaults for solver iterations, contact model and sleeping - those engine differences are the measured quantity in a native-vs-adapter comparison, disclosed per arm below.", - "Arm roles: matter-js, planck and nape-js are the JAVASCRIPT PEERS exojs-physics is compared against; rapier is a Rust/WASM engine and stands as the REFERENCE CEILING for what leaving JavaScript buys, not as a peer a JS solver is expected to match.", - "exojs-physics arm (native runtime, pure JS): TGS-Soft solver, 4 sub-steps per fixed step, sleeping ON by default (resting bodies deactivate). Contact count = solid contacts in the world contact graph.", - "matter-js arm (pure-JS peer): constraint solver at matter defaults (6 position / 4 velocity / 2 constraint iterations), sleeping OFF by default (a settled stack keeps paying full solve cost); matter's default per-step air drag (frictionAir) is zeroed so all arms integrate the same pure-gravity field; gravity (px/s^2) and perturbation velocity (px/s) are mapped into matter's px-per-step unit model. Contact count = active colliding pairs (engine.pairs.collisionActive), a pair-level proxy, not identical in semantics to the exojs solid-contact count.", - "planck arm (pure-JS peer): Box2D port at planck defaults (8 velocity / 3 position iterations per step), sleeping ON by default; Settings.lengthUnitsPerMeter is set to 30 so planck's absolute MKS tolerances are interpreted at the scene's pixel scale, which is the knob planck gives a pixel-coordinate game - positions, gravity (px/s^2) and velocity (px/s) then carry over unconverted. Contact count = the world contact list filtered by isTouching(), a touching collider-pair count. Rays are answered from planck's dynamic tree, but World.rayCast is Box2D's non-solid ray (an origin inside a fixture is not a hit). Continuous collision runs for every body (planck's default), where exojs and rapier restrict it to bullets and matter has none.", - "rapier arm (WASM reference ceiling, not a pure-JS peer): TGS-Soft solver at rapier defaults (4 solver / 1 internal PGS iterations), auto-sleeping ON; default lengthUnit=1 is fed a px-scale world (tuned for ~1-unit objects), exactly what attaching rapier with pixel coordinates yields. Contact count = collider pairs with a solid narrow-phase manifold (numContacts > 0), deduped.", - "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." - ], - "engineVersion": "0.17.0", - "timestamp": "2026-09-09T18:50:50.763Z" - }, - { - "browser": "webkit", - "browserVersion": "26.5", - "host": { - "cpu": "Apple M3 Max", - "cpuCount": 16, - "os": "darwin 27.0.0", - "platformVersion": { - "major": 27, - "source": "declared", - "evidence": "the runner declared '27-beta'; os.release() reported '27.0.0' on darwin, which is the kernel version and does not name the operating system's own major version; declare it with --platform=[-beta]" - }, - "arch": "arm64" - }, - "prerelease": { - "value": true, - "source": "declared", - "evidence": "the runner declared the platform as '27-beta'" - }, - "fixedDelta": 0.016666666666666666, - "clock": { - "resolutionMs": 0.01999999999999602, - "crossOriginIsolated": true - }, - "caveats": [ - "Step time is CPU wall-clock per step() over the timed window (median/p95), measured in one webkit page (same-run discipline). No number here was taken in Node.", - "The page's performance.now() resolves to 20.0us (cross-origin isolated: true). A cell whose step is too fast to time individually at that resolution batches steps per timing sample and records the batch as stepsPerSample; median and p95 are then per-step averages over that batch.", - "Scenes are warmed to steady state before timing; the per-cell warmupSteps/timedSteps counts are recorded for honesty.", - "All arms build the byte-identical scene (bodies, positions, shapes, sizes, static/dynamic split, gravity, perturbed-body set) from the shared deterministic RNG, and the perturbed-body selection is asserted equal across arms before each cell is timed.", - "Each arm runs at its own engine defaults for solver iterations, contact model and sleeping - those engine differences are the measured quantity in a native-vs-adapter comparison, disclosed per arm below.", - "Arm roles: matter-js, planck and nape-js are the JAVASCRIPT PEERS exojs-physics is compared against; rapier is a Rust/WASM engine and stands as the REFERENCE CEILING for what leaving JavaScript buys, not as a peer a JS solver is expected to match.", - "exojs-physics arm (native runtime, pure JS): TGS-Soft solver, 4 sub-steps per fixed step, sleeping ON by default (resting bodies deactivate). Contact count = solid contacts in the world contact graph.", - "matter-js arm (pure-JS peer): constraint solver at matter defaults (6 position / 4 velocity / 2 constraint iterations), sleeping OFF by default (a settled stack keeps paying full solve cost); matter's default per-step air drag (frictionAir) is zeroed so all arms integrate the same pure-gravity field; gravity (px/s^2) and perturbation velocity (px/s) are mapped into matter's px-per-step unit model. Contact count = active colliding pairs (engine.pairs.collisionActive), a pair-level proxy, not identical in semantics to the exojs solid-contact count.", - "planck arm (pure-JS peer): Box2D port at planck defaults (8 velocity / 3 position iterations per step), sleeping ON by default; Settings.lengthUnitsPerMeter is set to 30 so planck's absolute MKS tolerances are interpreted at the scene's pixel scale, which is the knob planck gives a pixel-coordinate game - positions, gravity (px/s^2) and velocity (px/s) then carry over unconverted. Contact count = the world contact list filtered by isTouching(), a touching collider-pair count. Rays are answered from planck's dynamic tree, but World.rayCast is Box2D's non-solid ray (an origin inside a fixture is not a hit). Continuous collision runs for every body (planck's default), where exojs and rapier restrict it to bullets and matter has none.", - "rapier arm (WASM reference ceiling, not a pure-JS peer): TGS-Soft solver at rapier defaults (4 solver / 1 internal PGS iterations), auto-sleeping ON; default lengthUnit=1 is fed a px-scale world (tuned for ~1-unit objects), exactly what attaching rapier with pixel coordinates yields. Contact count = collider pairs with a solid narrow-phase manifold (numContacts > 0), deduped.", - "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." - ], - "engineVersion": "0.17.0", - "timestamp": "2026-09-09T18:58:36.106Z" - }, - { - "browser": "webkit", - "browserVersion": "26.5", - "host": { - "cpu": "Apple M3 Max", - "cpuCount": 16, - "os": "darwin 27.0.0", - "platformVersion": { - "major": 27, - "source": "declared", - "evidence": "the runner declared '27-beta'; os.release() reported '27.0.0' on darwin, which is the kernel version and does not name the operating system's own major version; declare it with --platform=[-beta]" - }, - "arch": "arm64" - }, - "prerelease": { - "value": true, - "source": "declared", - "evidence": "the runner declared the platform as '27-beta'" - }, - "fixedDelta": 0.016666666666666666, - "clock": { - "resolutionMs": 0.01999999999999602, - "crossOriginIsolated": true - }, - "caveats": [ - "Step time is CPU wall-clock per step() over the timed window (median/p95), measured in one webkit page (same-run discipline). No number here was taken in Node.", - "The page's performance.now() resolves to 20.0us (cross-origin isolated: true). A cell whose step is too fast to time individually at that resolution batches steps per timing sample and records the batch as stepsPerSample; median and p95 are then per-step averages over that batch.", - "Scenes are warmed to steady state before timing; the per-cell warmupSteps/timedSteps counts are recorded for honesty.", - "All arms build the byte-identical scene (bodies, positions, shapes, sizes, static/dynamic split, gravity, perturbed-body set) from the shared deterministic RNG, and the perturbed-body selection is asserted equal across arms before each cell is timed.", - "Each arm runs at its own engine defaults for solver iterations, contact model and sleeping - those engine differences are the measured quantity in a native-vs-adapter comparison, disclosed per arm below.", - "Arm roles: matter-js, planck and nape-js are the JAVASCRIPT PEERS exojs-physics is compared against; rapier is a Rust/WASM engine and stands as the REFERENCE CEILING for what leaving JavaScript buys, not as a peer a JS solver is expected to match.", - "exojs-physics arm (native runtime, pure JS): TGS-Soft solver, 4 sub-steps per fixed step, sleeping ON by default (resting bodies deactivate). Contact count = solid contacts in the world contact graph.", - "matter-js arm (pure-JS peer): constraint solver at matter defaults (6 position / 4 velocity / 2 constraint iterations), sleeping OFF by default (a settled stack keeps paying full solve cost); matter's default per-step air drag (frictionAir) is zeroed so all arms integrate the same pure-gravity field; gravity (px/s^2) and perturbation velocity (px/s) are mapped into matter's px-per-step unit model. Contact count = active colliding pairs (engine.pairs.collisionActive), a pair-level proxy, not identical in semantics to the exojs solid-contact count.", - "planck arm (pure-JS peer): Box2D port at planck defaults (8 velocity / 3 position iterations per step), sleeping ON by default; Settings.lengthUnitsPerMeter is set to 30 so planck's absolute MKS tolerances are interpreted at the scene's pixel scale, which is the knob planck gives a pixel-coordinate game - positions, gravity (px/s^2) and velocity (px/s) then carry over unconverted. Contact count = the world contact list filtered by isTouching(), a touching collider-pair count. Rays are answered from planck's dynamic tree, but World.rayCast is Box2D's non-solid ray (an origin inside a fixture is not a hit). Continuous collision runs for every body (planck's default), where exojs and rapier restrict it to bullets and matter has none.", - "rapier arm (WASM reference ceiling, not a pure-JS peer): TGS-Soft solver at rapier defaults (4 solver / 1 internal PGS iterations), auto-sleeping ON; default lengthUnit=1 is fed a px-scale world (tuned for ~1-unit objects), exactly what attaching rapier with pixel coordinates yields. Contact count = collider pairs with a solid narrow-phase manifold (numContacts > 0), deduped.", - "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." - ], - "engineVersion": "0.17.0", - "timestamp": "2026-09-09T19:06:25.228Z" - }, - { - "browser": "webkit", - "browserVersion": "26.5", - "host": { - "cpu": "Apple M3 Max", - "cpuCount": 16, - "os": "darwin 27.0.0", - "platformVersion": { - "major": 27, - "source": "declared", - "evidence": "the runner declared '27-beta'; os.release() reported '27.0.0' on darwin, which is the kernel version and does not name the operating system's own major version; declare it with --platform=[-beta]" - }, - "arch": "arm64" - }, - "prerelease": { - "value": true, - "source": "declared", - "evidence": "the runner declared the platform as '27-beta'" - }, - "fixedDelta": 0.016666666666666666, - "clock": { - "resolutionMs": 0.01999999999999602, - "crossOriginIsolated": true - }, - "caveats": [ - "Step time is CPU wall-clock per step() over the timed window (median/p95), measured in one webkit page (same-run discipline). No number here was taken in Node.", - "The page's performance.now() resolves to 20.0us (cross-origin isolated: true). A cell whose step is too fast to time individually at that resolution batches steps per timing sample and records the batch as stepsPerSample; median and p95 are then per-step averages over that batch.", - "Scenes are warmed to steady state before timing; the per-cell warmupSteps/timedSteps counts are recorded for honesty.", - "All arms build the byte-identical scene (bodies, positions, shapes, sizes, static/dynamic split, gravity, perturbed-body set) from the shared deterministic RNG, and the perturbed-body selection is asserted equal across arms before each cell is timed.", - "Each arm runs at its own engine defaults for solver iterations, contact model and sleeping - those engine differences are the measured quantity in a native-vs-adapter comparison, disclosed per arm below.", - "Arm roles: matter-js, planck and nape-js are the JAVASCRIPT PEERS exojs-physics is compared against; rapier is a Rust/WASM engine and stands as the REFERENCE CEILING for what leaving JavaScript buys, not as a peer a JS solver is expected to match.", - "exojs-physics arm (native runtime, pure JS): TGS-Soft solver, 4 sub-steps per fixed step, sleeping ON by default (resting bodies deactivate). Contact count = solid contacts in the world contact graph.", - "matter-js arm (pure-JS peer): constraint solver at matter defaults (6 position / 4 velocity / 2 constraint iterations), sleeping OFF by default (a settled stack keeps paying full solve cost); matter's default per-step air drag (frictionAir) is zeroed so all arms integrate the same pure-gravity field; gravity (px/s^2) and perturbation velocity (px/s) are mapped into matter's px-per-step unit model. Contact count = active colliding pairs (engine.pairs.collisionActive), a pair-level proxy, not identical in semantics to the exojs solid-contact count.", - "planck arm (pure-JS peer): Box2D port at planck defaults (8 velocity / 3 position iterations per step), sleeping ON by default; Settings.lengthUnitsPerMeter is set to 30 so planck's absolute MKS tolerances are interpreted at the scene's pixel scale, which is the knob planck gives a pixel-coordinate game - positions, gravity (px/s^2) and velocity (px/s) then carry over unconverted. Contact count = the world contact list filtered by isTouching(), a touching collider-pair count. Rays are answered from planck's dynamic tree, but World.rayCast is Box2D's non-solid ray (an origin inside a fixture is not a hit). Continuous collision runs for every body (planck's default), where exojs and rapier restrict it to bullets and matter has none.", - "rapier arm (WASM reference ceiling, not a pure-JS peer): TGS-Soft solver at rapier defaults (4 solver / 1 internal PGS iterations), auto-sleeping ON; default lengthUnit=1 is fed a px-scale world (tuned for ~1-unit objects), exactly what attaching rapier with pixel coordinates yields. Contact count = collider pairs with a solid narrow-phase manifold (numContacts > 0), deduped.", - "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." - ], - "engineVersion": "0.17.0", - "timestamp": "2026-09-09T19:14:13.252Z" - } - ], - "libraries": [ - { - "name": "@codexo/exojs-physics", - "version": "0.17.0" - }, - { - "name": "matter-js", - "version": "0.20.0" - }, - { - "name": "planck", - "version": "1.5.0" - }, - { - "name": "@dimforge/rapier2d-compat", - "version": "0.19.3" - }, - { - "name": "@newkrok/nape-js", - "version": "3.42.0" - } - ], - "section": { - "title": "Physics", - "rows": [ - { - "archetype": "box-stack", - "category": "Physics", - "count": 10000, - "cells": [ - { - "competitor": "matter-js", - "referenceMs": 10.235000000000355, - "referenceP95Ms": 11.230000000000018, - "referenceOverFrameBudget": false, - "competitorMs": 22.664999999993597, - "competitorP95Ms": 26.17000000000553, - "competitorOverFrameBudget": true, - "verdict": { - "side": "exojs", - "ratio": 0.45157732186204486, - "factor": 2.214460185636816, - "label": "ExoJS leads (2.21x)", - "structural": false - }, - "mechanism": "ExoJS resolves fewer solved contacts (10002 vs 22396); the counters are not semantically identical across arms (see the run's caveats)", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 9.659999999999854, - "maxMs": 10.389999999999873, - "ratio": 1.0755693581780568 - }, - "competitor": { - "minMs": 21.779999999998836, - "maxMs": 23.069999999992433, - "ratio": 1.0592286501374502 - }, - "stable": true, - "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" - ] - } - }, - { - "competitor": "nape-js", - "referenceMs": 10.235000000000355, - "referenceP95Ms": 11.230000000000018, - "referenceOverFrameBudget": false, - "competitorMs": 26.220000000001164, - "competitorP95Ms": 31.570000000006985, - "competitorOverFrameBudget": true, - "verdict": { - "side": "exojs", - "ratio": 0.3903508771929787, - "factor": 2.5617977528090137, - "label": "ExoJS leads (2.56x)", - "structural": false - }, - "mechanism": "identical scene (10001 bodies, 10002 contacts, 0 joints on both arms); the difference is solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 9.659999999999854, - "maxMs": 10.389999999999873, - "ratio": 1.0755693581780568 - }, - "competitor": { - "minMs": 25.620000000024447, - "maxMs": 27.410000000003492, - "ratio": 1.069867291177882 - }, - "stable": true, - "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" - ] - } - }, - { - "competitor": "planck", - "referenceMs": 10.235000000000355, - "referenceP95Ms": 11.230000000000018, - "referenceOverFrameBudget": false, - "competitorMs": 72.32499999999709, - "competitorP95Ms": 90.47999999999593, - "competitorOverFrameBudget": true, - "verdict": { - "side": "exojs", - "ratio": 0.14151399930868672, - "factor": 7.0664386907664465, - "label": "ExoJS leads clearly (7.07x)", - "structural": true - }, - "mechanism": "ExoJS resolves fewer solved contacts (10002 vs 19865); the counters are not semantically identical across arms (see the run's caveats)", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 9.659999999999854, - "maxMs": 10.389999999999873, - "ratio": 1.0755693581780568 - }, - "competitor": { - "minMs": 71.86000000000058, - "maxMs": 73.41000000000349, - "ratio": 1.0215697188978972 - }, - "stable": true, - "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" - ] - } - }, - { - "competitor": "rapier", - "referenceMs": 10.235000000000355, - "referenceP95Ms": 11.230000000000018, - "referenceOverFrameBudget": false, - "competitorMs": 7.264999999999418, - "competitorP95Ms": 9.779999999969732, - "competitorOverFrameBudget": false, - "verdict": { - "side": "competitor", - "ratio": 1.4088093599451033, - "factor": 1.4088093599451033, - "label": "competitor leads (1.41x)", - "structural": false - }, - "mechanism": "ExoJS resolves fewer solved contacts (10002 vs 17880); the counters are not semantically identical across arms (see the run's caveats)", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 9.659999999999854, - "maxMs": 10.389999999999873, - "ratio": 1.0755693581780568 - }, - "competitor": { - "minMs": 7.079999999987194, - "maxMs": 7.5600000000267755, - "ratio": 1.0677966101752048 - }, - "stable": true, - "rungs": [ - "competitor-leads", - "competitor-leads", - "competitor-leads", - "competitor-leads" - ] - } - } - ] - }, - { - "archetype": "many-dynamic", - "category": "Physics", - "count": 2200, - "cells": [ - { - "competitor": "matter-js", - "referenceMs": 8.63000000000011, - "referenceP95Ms": 9.5, - "referenceOverFrameBudget": false, - "competitorMs": 8.080000000001746, - "competitorP95Ms": 8.989999999997963, - "competitorOverFrameBudget": false, - "verdict": { - "side": "neither", - "ratio": 1.0680693069304759, - "factor": 1, - "label": "level", - "structural": false - }, - "mechanism": "identical scene (2204 bodies, 6233 contacts, 0 joints on both arms); the difference is solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 8.159999999999854, - "maxMs": 8.679999999998472, - "ratio": 1.0637254901959101 - }, - "competitor": { - "minMs": 8.020000000004075, - "maxMs": 8.10000000000582, - "ratio": 1.0099750623443524 - }, - "stable": true, - "rungs": [ - "level", - "level", - "level", - "level" - ] - } - }, - { - "competitor": "nape-js", - "referenceMs": 8.63000000000011, - "referenceP95Ms": 9.5, - "referenceOverFrameBudget": false, - "competitorMs": 8.875, - "competitorP95Ms": 9.790000000008149, - "competitorOverFrameBudget": false, - "verdict": { - "side": "neither", - "ratio": 0.9723943661971954, - "factor": 1, - "label": "level", - "structural": false - }, - "mechanism": "identical scene (2204 bodies, 6233 contacts, 0 joints on both arms); the difference is solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 8.159999999999854, - "maxMs": 8.679999999998472, - "ratio": 1.0637254901959101 - }, - "competitor": { - "minMs": 8.820000000006985, - "maxMs": 8.939999999973224, - "ratio": 1.0136054421730323 - }, - "stable": true, - "rungs": [ - "level", - "level", - "level", - "level" - ] - } - }, - { - "competitor": "planck", - "referenceMs": 8.63000000000011, - "referenceP95Ms": 9.5, - "referenceOverFrameBudget": false, - "competitorMs": 9.35000000000582, - "competitorP95Ms": 10.910000000003492, - "competitorOverFrameBudget": false, - "verdict": { - "side": "neither", - "ratio": 0.9229946524058542, - "factor": 1, - "label": "level", - "structural": false - }, - "mechanism": "identical scene (2204 bodies, 6233 contacts, 0 joints on both arms); the difference is solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 8.159999999999854, - "maxMs": 8.679999999998472, - "ratio": 1.0637254901959101 - }, - "competitor": { - "minMs": 9.320000000006985, - "maxMs": 9.380000000004657, - "ratio": 1.0064377682400887 - }, - "stable": true, - "rungs": [ - "level", - "level", - "level", - "level" - ] - } - }, - { - "competitor": "rapier", - "referenceMs": 8.63000000000011, - "referenceP95Ms": 9.5, - "referenceOverFrameBudget": false, - "competitorMs": 1.9199999999837019, - "competitorP95Ms": 2.0599999999976717, - "competitorOverFrameBudget": false, - "verdict": { - "side": "competitor", - "ratio": 4.494791666704878, - "factor": 4.494791666704878, - "label": "competitor leads (4.49x)", - "structural": false - }, - "mechanism": "identical scene (2204 bodies, 6233 contacts, 0 joints on both arms); the difference is solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 8.159999999999854, - "maxMs": 8.679999999998472, - "ratio": 1.0637254901959101 - }, - "competitor": { - "minMs": 1.8999999999941792, - "maxMs": 1.9200000000419095, - "ratio": 1.0105263158146272 - }, - "stable": true, - "rungs": [ - "competitor-leads", - "competitor-leads", - "competitor-leads", - "competitor-leads" - ] - } - } - ] - }, - { - "archetype": "mixed-static-dynamic", - "category": "Physics", - "count": 3200, - "cells": [ - { - "competitor": "matter-js", - "referenceMs": 8.514999999999418, - "referenceP95Ms": 9.719999999999345, - "referenceOverFrameBudget": false, - "competitorMs": 5.444999999992433, - "competitorP95Ms": 6.310000000004948, - "competitorOverFrameBudget": false, - "verdict": { - "side": "competitor", - "ratio": 1.5638200183675393, - "factor": 1.5638200183675393, - "label": "competitor leads (1.56x)", - "structural": false - }, - "mechanism": "identical scene (3313 bodies, 3670 contacts, 0 joints on both arms); the difference is solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 8.43999999999869, - "maxMs": 8.540000000000873, - "ratio": 1.011848341232488 - }, - "competitor": { - "minMs": 5.430000000000291, - "maxMs": 5.4799999999959255, - "ratio": 1.0092081031299507 - }, - "stable": true, - "rungs": [ - "competitor-leads", - "competitor-leads", - "competitor-leads", - "competitor-leads" - ] - } - }, - { - "competitor": "nape-js", - "referenceMs": 8.514999999999418, - "referenceP95Ms": 9.719999999999345, - "referenceOverFrameBudget": false, - "competitorMs": 11.069999999992433, - "competitorP95Ms": 13.199999999982538, - "competitorOverFrameBudget": false, - "verdict": { - "side": "exojs", - "ratio": 0.7691960252940595, - "factor": 1.3000587199052482, - "label": "ExoJS leads (1.30x)", - "structural": false - }, - "mechanism": "identical scene (3313 bodies, 3670 contacts, 0 joints on both arms); the difference is solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 8.43999999999869, - "maxMs": 8.540000000000873, - "ratio": 1.011848341232488 - }, - "competitor": { - "minMs": 10.960000000020955, - "maxMs": 11.089999999996508, - "ratio": 1.01186131386636 - }, - "stable": true, - "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" - ] - } - }, - { - "competitor": "planck", - "referenceMs": 8.514999999999418, - "referenceP95Ms": 9.719999999999345, - "referenceOverFrameBudget": false, - "competitorMs": 19.3550000000032, - "competitorP95Ms": 30.039999999993597, - "competitorOverFrameBudget": true, - "verdict": { - "side": "exojs", - "ratio": 0.4399380005165595, - "factor": 2.2730475631244302, - "label": "ExoJS leads (2.27x)", - "structural": false - }, - "mechanism": "identical scene (3313 bodies, 3670 contacts, 0 joints on both arms); the difference is solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 8.43999999999869, - "maxMs": 8.540000000000873, - "ratio": 1.011848341232488 - }, - "competitor": { - "minMs": 19.29000000000815, - "maxMs": 19.610000000015134, - "ratio": 1.0165889061693545 - }, - "stable": true, - "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" - ] - } - }, - { - "competitor": "rapier", - "referenceMs": 8.514999999999418, - "referenceP95Ms": 9.719999999999345, - "referenceOverFrameBudget": false, - "competitorMs": 3.654999999984284, - "competitorP95Ms": 4.129999999975553, - "competitorOverFrameBudget": false, - "verdict": { - "side": "competitor", - "ratio": 2.329685362526958, - "factor": 2.329685362526958, - "label": "competitor leads (2.33x)", - "structural": false - }, - "mechanism": "ExoJS resolves fewer solved contacts (3670 vs 6105); the counters are not semantically identical across arms (see the run's caveats)", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 8.43999999999869, - "maxMs": 8.540000000000873, - "ratio": 1.011848341232488 - }, - "competitor": { - "minMs": 3.6499999999941792, - "maxMs": 3.6900000000023283, - "ratio": 1.0109589041118392 - }, - "stable": true, - "rungs": [ - "competitor-leads", - "competitor-leads", - "competitor-leads", - "competitor-leads" - ] - } - } - ] - }, - { - "archetype": "raycast", - "category": "Physics", - "count": 3200, - "cells": [ - { - "competitor": "matter-js", - "referenceMs": 10.55999999999949, - "referenceP95Ms": 11.710000000000946, - "referenceOverFrameBudget": false, - "competitorMs": 13.419999999998254, - "competitorP95Ms": 16.830000000009022, - "competitorOverFrameBudget": false, - "verdict": { - "side": "exojs", - "ratio": 0.7868852459017038, - "factor": 1.2708333333332291, - "label": "ExoJS leads (1.27x)", - "structural": false - }, - "mechanism": "identical scene (3313 bodies, 3670 contacts, 0 joints on both arms) with 41 vs 49 ray hits per step; the difference is query and solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 10.459999999999127, - "maxMs": 10.630000000001019, - "ratio": 1.0162523900575435 - }, - "competitor": { - "minMs": 13.369999999995343, - "maxMs": 13.570000000006985, - "ratio": 1.0149588631272783 - }, - "stable": true, - "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" - ] - } - }, - { - "competitor": "nape-js", - "referenceMs": 10.55999999999949, - "referenceP95Ms": 11.710000000000946, - "referenceOverFrameBudget": false, - "competitorMs": 28.210000000006403, - "competitorP95Ms": 33.079999999987194, - "competitorOverFrameBudget": true, - "verdict": { - "side": "exojs", - "ratio": 0.37433534207717456, - "factor": 2.67140151515225, - "label": "ExoJS leads (2.67x)", - "structural": false - }, - "mechanism": "identical scene (3313 bodies, 3670 contacts, 0 joints on both arms) with 41 vs 41 ray hits per step; the difference is query and solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 10.459999999999127, - "maxMs": 10.630000000001019, - "ratio": 1.0162523900575435 - }, - "competitor": { - "minMs": 27.300000000046566, - "maxMs": 28.55999999999767, - "ratio": 1.0461538461519764 - }, - "stable": true, - "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" - ] - } - }, - { - "competitor": "planck", - "referenceMs": 10.55999999999949, - "referenceP95Ms": 11.710000000000946, - "referenceOverFrameBudget": false, - "competitorMs": 19.89500000001135, - "competitorP95Ms": 30.85000000000582, - "competitorOverFrameBudget": true, - "verdict": { - "side": "exojs", - "ratio": 0.5307866298061557, - "factor": 1.8839962121223777, - "label": "ExoJS leads (1.88x)", - "structural": false - }, - "mechanism": "identical scene (3313 bodies, 3670 contacts, 0 joints on both arms) with 41 vs 37 ray hits per step; the difference is query and solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 10.459999999999127, - "maxMs": 10.630000000001019, - "ratio": 1.0162523900575435 - }, - "competitor": { - "minMs": 19.830000000001746, - "maxMs": 20.010000000009313, - "ratio": 1.0090771558248892 - }, - "stable": true, - "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" - ] - } - }, - { - "competitor": "rapier", - "referenceMs": 10.55999999999949, - "referenceP95Ms": 11.710000000000946, - "referenceOverFrameBudget": false, - "competitorMs": 3.749999999985448, - "competitorP95Ms": 4.139999999984866, - "competitorOverFrameBudget": false, - "verdict": { - "side": "competitor", - "ratio": 2.8160000000107916, - "factor": 2.8160000000107916, - "label": "competitor leads (2.82x)", - "structural": false - }, - "mechanism": "ExoJS resolves fewer solved contacts (3670 vs 6105); the counters are not semantically identical across arms (see the run's caveats)", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 10.459999999999127, - "maxMs": 10.630000000001019, - "ratio": 1.0162523900575435 - }, - "competitor": { - "minMs": 3.7000000000116415, - "maxMs": 3.779999999969732, - "ratio": 1.0216216216102267 - }, - "stable": true, - "rungs": [ - "competitor-leads", - "competitor-leads", - "competitor-leads", - "competitor-leads" - ] - } - } - ] - }, - { - "archetype": "body-churn", - "category": "Physics", - "count": 2400, - "cells": [ - { - "competitor": "matter-js", - "referenceMs": 11.340000000000146, - "referenceP95Ms": 12.890000000003056, - "referenceOverFrameBudget": false, - "competitorMs": 9.840000000003783, - "competitorP95Ms": 11.450000000004366, - "competitorOverFrameBudget": false, - "verdict": { - "side": "neither", - "ratio": 1.1524390243898155, - "factor": 1, - "label": "level", - "structural": false - }, - "mechanism": "ExoJS resolves fewer solved contacts (2282 vs 6230); the counters are not semantically identical across arms (see the run's caveats)", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 10.939999999995052, - "maxMs": 11.679999999996653, - "ratio": 1.0676416819014567 - }, - "competitor": { - "minMs": 9.740000000005239, - "maxMs": 9.89999999999418, - "ratio": 1.0164271047216482 - }, - "stable": true, - "rungs": [ - "level", - "level", - "level", - "level" - ] - } - }, - { - "competitor": "nape-js", - "referenceMs": 11.340000000000146, - "referenceP95Ms": 12.890000000003056, - "referenceOverFrameBudget": false, - "competitorMs": 5.789999999979045, - "competitorP95Ms": 12.499999999970896, - "competitorOverFrameBudget": false, - "verdict": { - "side": "competitor", - "ratio": 1.9585492228050407, - "factor": 1.9585492228050407, - "label": "competitor leads (1.96x)", - "structural": false - }, - "mechanism": "identical scene (2404 bodies, 2282 contacts, 0 joints on both arms); the difference is solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 10.939999999995052, - "maxMs": 11.679999999996653, - "ratio": 1.0676416819014567 - }, - "competitor": { - "minMs": 5.160000000032596, - "maxMs": 5.839999999967404, - "ratio": 1.1317829457229673 - }, - "stable": true, - "rungs": [ - "competitor-leads", - "competitor-leads", - "competitor-leads", - "competitor-leads" - ] - } - }, - { - "competitor": "planck", - "referenceMs": 11.340000000000146, - "referenceP95Ms": 12.890000000003056, - "referenceOverFrameBudget": false, - "competitorMs": 9.839999999996508, - "competitorP95Ms": 10.89999999999418, - "competitorOverFrameBudget": false, - "verdict": { - "side": "neither", - "ratio": 1.1524390243906677, - "factor": 1, - "label": "level", - "structural": false - }, - "mechanism": "ExoJS resolves fewer solved contacts (2282 vs 5242); the counters are not semantically identical across arms (see the run's caveats)", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 10.939999999995052, - "maxMs": 11.679999999996653, - "ratio": 1.0676416819014567 - }, - "competitor": { - "minMs": 9.739999999990687, - "maxMs": 9.929999999993015, - "ratio": 1.0195071868585739 - }, - "stable": true, - "rungs": [ - "level", - "level", - "level", - "level" - ] - } - }, - { - "competitor": "rapier", - "referenceMs": 11.340000000000146, - "referenceP95Ms": 12.890000000003056, - "referenceOverFrameBudget": false, - "competitorMs": 1.1600000000034925, - "competitorP95Ms": 1.209999999991851, - "competitorOverFrameBudget": false, - "verdict": { - "side": "competitor", - "ratio": 9.77586206893621, - "factor": 9.77586206893621, - "label": "competitor leads clearly (9.78x)", - "structural": true - }, - "mechanism": "identical scene (2404 bodies, 2282 contacts, 0 joints on both arms); the difference is solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 10.939999999995052, - "maxMs": 11.679999999996653, - "ratio": 1.0676416819014567 - }, - "competitor": { - "minMs": 1.1599999999743886, - "maxMs": 1.179999999993015, - "ratio": 1.0172413793267827 - }, - "stable": true, - "rungs": [ - "competitor-leads-clearly", - "competitor-leads-clearly", - "competitor-leads-clearly", - "competitor-leads-clearly" - ] - } - } - ] - }, - { - "archetype": "joints", - "category": "Physics", - "count": 15000, - "cells": [ - { - "competitor": "matter-js", - "referenceMs": 9.350000000000364, - "referenceP95Ms": 10.580000000001746, - "referenceOverFrameBudget": false, - "competitorMs": 9.464999999996508, - "competitorP95Ms": 10.69999999999709, - "competitorOverFrameBudget": false, - "verdict": { - "side": "neither", - "ratio": 0.9878499735873021, - "factor": 1, - "label": "level", - "structural": false - }, - "mechanism": "competitor resolves fewer solved contacts (13125 vs 0); the counters are not semantically identical across arms (see the run's caveats)", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 9.260000000002037, - "maxMs": 9.419999999998254, - "ratio": 1.0172786177101707 - }, - "competitor": { - "minMs": 9.080000000001746, - "maxMs": 9.539999999993597, - "ratio": 1.0506607929506346 - }, - "stable": true, - "rungs": [ - "level", - "level", - "level", - "level" - ] - } - }, - { - "competitor": "nape-js", - "referenceMs": 9.350000000000364, - "referenceP95Ms": 10.580000000001746, - "referenceOverFrameBudget": false, - "competitorMs": 0, - "competitorP95Ms": 0.0009999999980209394, - "competitorOverFrameBudget": false, - "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "not comparable", - "structural": false - }, - "mechanism": "competitor resolves fewer solved contacts (13125 vs 0); the counters are not semantically identical across arms (see the run's caveats)", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 9.260000000002037, - "maxMs": 9.419999999998254, - "ratio": 1.0172786177101707 - }, - "competitor": { - "minMs": 0, - "maxMs": 0, - "ratio": null - }, - "stable": true, - "rungs": [ - "not-comparable", - "not-comparable", - "not-comparable", - "not-comparable" - ] - } - }, - { - "competitor": "planck", - "referenceMs": 9.350000000000364, - "referenceP95Ms": 10.580000000001746, - "referenceOverFrameBudget": false, - "competitorMs": 0.6399999999994179, - "competitorP95Ms": 0.6600000000180444, - "competitorOverFrameBudget": false, - "verdict": { - "side": "competitor", - "ratio": 14.609375000013856, - "factor": 14.609375000013856, - "label": "competitor leads clearly (14.61x)", - "structural": true - }, - "mechanism": "competitor resolves fewer solved contacts (13125 vs 0); the counters are not semantically identical across arms (see the run's caveats)", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 9.260000000002037, - "maxMs": 9.419999999998254, - "ratio": 1.0172786177101707 - }, - "competitor": { - "minMs": 0.639999999984866, - "maxMs": 0.6799999999930151, - "ratio": 1.0625000000142109 - }, - "stable": true, - "rungs": [ - "competitor-leads-clearly", - "competitor-leads-clearly", - "competitor-leads-clearly", - "competitor-leads-clearly" - ] - } - }, - { - "competitor": "rapier", - "referenceMs": 9.350000000000364, - "referenceP95Ms": 10.580000000001746, - "referenceOverFrameBudget": false, - "competitorMs": 0.29999999998835847, - "competitorP95Ms": 0.34000000002561137, - "competitorOverFrameBudget": false, - "verdict": { - "side": "competitor", - "ratio": 31.166666667877305, - "factor": 31.166666667877305, - "label": "competitor leads clearly (31.17x)", - "structural": true - }, - "mechanism": "competitor resolves fewer solved contacts (13125 vs 1875); the counters are not semantically identical across arms (see the run's caveats)", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 9.260000000002037, - "maxMs": 9.419999999998254, - "ratio": 1.0172786177101707 - }, - "competitor": { - "minMs": 0.29999999998835847, - "maxMs": 0.3200000000069849, - "ratio": 1.0666666667313418 - }, - "stable": true, - "rungs": [ - "competitor-leads-clearly", - "competitor-leads-clearly", - "competitor-leads-clearly", - "competitor-leads-clearly" - ] - } - } - ] - }, - { - "archetype": "settling-pile", - "category": "Physics", - "count": 5800, - "cells": [ - { - "competitor": "matter-js", - "referenceMs": 8.82499999999709, - "referenceP95Ms": 9.979999999995925, - "referenceOverFrameBudget": false, - "competitorMs": 25.220000000001164, - "competitorP95Ms": 29.330000000001746, - "competitorOverFrameBudget": true, - "verdict": { - "side": "exojs", - "ratio": 0.34992069785871066, - "factor": 2.857790368273029, - "label": "ExoJS leads (2.86x)", - "structural": false - }, - "mechanism": "identical scene (5804 bodies, 12187 contacts, 0 joints on both arms); the difference is solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 8.559999999997672, - "maxMs": 8.839999999996508, - "ratio": 1.0327102803737047 - }, - "competitor": { - "minMs": 25.04999999998836, - "maxMs": 25.330000000001746, - "ratio": 1.0111776447111185 - }, - "stable": true, - "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" - ] - } - }, - { - "competitor": "nape-js", - "referenceMs": 8.82499999999709, - "referenceP95Ms": 9.979999999995925, - "referenceOverFrameBudget": false, - "competitorMs": 35.81500000001688, - "competitorP95Ms": 43.20000000001164, - "competitorOverFrameBudget": true, - "verdict": { - "side": "exojs", - "ratio": 0.24640513751201815, - "factor": 4.058356940513167, - "label": "ExoJS leads (4.06x)", - "structural": false - }, - "mechanism": "identical scene (5804 bodies, 12187 contacts, 0 joints on both arms); the difference is solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 8.559999999997672, - "maxMs": 8.839999999996508, - "ratio": 1.0327102803737047 - }, - "competitor": { - "minMs": 34.85999999998603, - "maxMs": 36.84000000002561, - "ratio": 1.0567986230648414 - }, - "stable": true, - "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" - ] - } - }, - { - "competitor": "planck", - "referenceMs": 8.82499999999709, - "referenceP95Ms": 9.979999999995925, - "referenceOverFrameBudget": false, - "competitorMs": 45.02499999997963, - "competitorP95Ms": 54.179999999993015, - "competitorOverFrameBudget": true, - "verdict": { - "side": "exojs", - "ratio": 0.19600222098836387, - "factor": 5.101983002832235, - "label": "ExoJS leads clearly (5.10x)", - "structural": true - }, - "mechanism": "identical scene (5804 bodies, 12187 contacts, 0 joints on both arms); the difference is solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 8.559999999997672, - "maxMs": 8.839999999996508, - "ratio": 1.0327102803737047 - }, - "competitor": { - "minMs": 44.679999999993015, - "maxMs": 45.86999999999534, - "ratio": 1.02663384064464 - }, - "stable": true, - "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" - ] - } - }, - { - "competitor": "rapier", - "referenceMs": 8.82499999999709, - "referenceP95Ms": 9.979999999995925, - "referenceOverFrameBudget": false, - "competitorMs": 4.7999999999883585, - "competitorP95Ms": 5.279999999998836, - "competitorOverFrameBudget": false, - "verdict": { - "side": "competitor", - "ratio": 1.8385416666705194, - "factor": 1.8385416666705194, - "label": "competitor leads (1.84x)", - "structural": false - }, - "mechanism": "identical scene (5804 bodies, 12187 contacts, 0 joints on both arms); the difference is solver cost", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 8.559999999997672, - "maxMs": 8.839999999996508, - "ratio": 1.0327102803737047 - }, - "competitor": { - "minMs": 4.740000000048894, - "maxMs": 4.880000000004657, - "ratio": 1.0295358649692654 - }, - "stable": true, - "rungs": [ - "competitor-leads", - "competitor-leads", - "competitor-leads", - "competitor-leads" - ] - } - } - ] - } - ] - } - }, - "signature": { - "algorithm": "sha256", - "value": "e9ab0ae10d27ef5fbe3c0417663d3738a90cd1c97f9438f967cf23d57a68016f" - } -} diff --git a/packages/exojs-bench/src/profile/slug.ts b/packages/exojs-bench/src/profile/slug.ts index 7d89b5bc3..47547d107 100644 --- a/packages/exojs-bench/src/profile/slug.ts +++ b/packages/exojs-bench/src/profile/slug.ts @@ -86,6 +86,20 @@ const LEADING_NOISE = new Set([ */ const NON_IDENTIFYING = new Set(['gpu', 'graphics', 'renderer', 'device', 'processor', 'unknown']); +/** + * True when a normalized part is nothing but vendor and product-line words. + * + * `dropLeadingNoise` keeps a last part rather than reducing to the empty + * string, so an adapter made only of those words survives as one of them: + * WebKit's WebGPU privacy substitution reports `apple apple apple apple` and + * reduced to the bare vendor word `apple`. That passes the category-word check + * above - `apple` is a vendor, not a category - and named the machine, which + * published one machine's runs under two file names depending on which backend + * the pooling read first. A vendor word identifies no machine for the same + * reason a category word does not. + */ +const isVendorWordOnly = (name: string): boolean => name.split('-').every(word => LEADING_NOISE.has(word)); + /** * Where a model name ends. Everything from the first of these onward is the * graphics API, driver or device-id tail that the adapter string appends after @@ -118,7 +132,7 @@ const dropLeadingNoise = (parts: readonly string[]): string[] => { }; /** True when a normalized part names a specific machine rather than a category of them. */ -export const isIdentifyingPart = (name: string): boolean => name.length > 0 && !NON_IDENTIFYING.has(name); +export const isIdentifyingPart = (name: string): boolean => name.length > 0 && !NON_IDENTIFYING.has(name) && !isVendorWordOnly(name); /** * Reduce an adapter string to the GPU part it names. diff --git a/packages/exojs-bench/test/profile.test.ts b/packages/exojs-bench/test/profile.test.ts index 8416a5c94..d9ee77bb6 100644 --- a/packages/exojs-bench/test/profile.test.ts +++ b/packages/exojs-bench/test/profile.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import type { PhysicsStamp, RenderingStamp } from '../src/profile/schema'; import { computeProfileSignature } from '../src/profile/signature'; -import { deriveProfileParts, normalizeCpuModel, normalizeGpuAdapter, ProfileSlugError } from '../src/profile/slug'; +import { deriveProfileParts, isIdentifyingPart, normalizeCpuModel, normalizeGpuAdapter, ProfileSlugError } from '../src/profile/slug'; import type { PlatformVersionStamp } from '../src/shared/provenance'; /** @@ -72,6 +72,11 @@ describe('normalizeGpuAdapter', () => { it('reduces a browser that reports a constant instead of the device to a word that names no machine', () => { expect(normalizeGpuAdapter('Apple GPU')).toBe('gpu'); }); + + it("reduces WebKit's WebGPU substitution to a bare vendor word, which names no machine either", () => { + expect(normalizeGpuAdapter('apple apple apple apple')).toBe('apple'); + expect(isIdentifyingPart('apple')).toBe(false); + }); }); describe('normalizeCpuModel', () => { @@ -149,6 +154,27 @@ describe('deriveProfileParts', () => { }); }); + it('falls back to the CPU model when only one of the two backends masks the GPU', () => { + // The real WebKit pair: WebGL2 reports the constant `Apple GPU`, WebGPU the + // substitution `apple apple apple apple`. The second reduces to a vendor + // word, which must not win the adapter preference and name the machine. + const parts = deriveProfileParts({ + rendering: [ + webkitBetaStamp(), + renderingStamp('apple apple apple apple', 'webgpu', { + browser: 'webkit', + browserVersion: '26.5', + os: 'darwin 25.0.0', + platformVersion: MACOS_27_BETA, + prerelease: { value: true, source: 'declared', evidence: `the runner declared the platform as '27-beta'` }, + }), + ], + physics: physicsStamp('Apple M3 Max', 'darwin 25.0.0', MACOS_27_BETA, { browser: 'webkit', browserVersion: '26.5' }), + }); + + expect(parts.slug).toBe('m3-max-macos-27-beta-webkit'); + }); + it('refuses a rendering-only profile whose adapter names no machine, naming the domain that would', () => { expect(() => deriveProfileParts({ rendering: [webkitBetaStamp()] })).toThrow(ProfileSlugError); expect(() => deriveProfileParts({ rendering: [webkitBetaStamp()] })).toThrow(/--physics/); From e73f6ab5fbe4306fa54f2a423cb9ed15b222de89 Mon Sep 17 00:00:00 2001 From: Exoridus Date: Sat, 12 Sep 2026 02:58:18 +0200 Subject: [PATCH 03/28] build(hooks): let the pre-push gate scope match what the push changes A push carrying only a published benchmark profile rebuilt `dist` and ran all four gate groups - nine typechecks, ten lints, the sync and site groups - to validate a JSON file none of them opens. The one check that does read it, `verify:bench-results`, sits inside the lint group and was reached last. `scripts/ci/push-scope.ts` classifies the pushed range instead: a range that changes no tracked file verifies nothing, one carrying published profiles alone runs only their own gate, and anything else - including a range whose scope cannot be determined - takes the full path unchanged. The hook stays a convenience rather than an authority, so narrowing here is safe: CI re-runs its own selection on every push regardless. --- .husky/pre-push | 74 ++++++++++++++++++++++++-------------- scripts/ci/push-scope.ts | 66 ++++++++++++++++++++++++++++++++++ test/ci/push-scope.test.ts | 39 ++++++++++++++++++++ 3 files changed, 153 insertions(+), 26 deletions(-) create mode 100644 scripts/ci/push-scope.ts create mode 100644 test/ci/push-scope.test.ts diff --git a/.husky/pre-push b/.husky/pre-push index 6de739015..5afbf4d5c 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,7 +1,15 @@ #!/usr/bin/env sh # Pre-push verification. # -# Branch pushes: `verify:quick` — the static CI-parity gates: typecheck (core +# Branch pushes: first `scripts/ci/push-scope.ts`, which answers how much of +# the below the pushed range needs. A range that changes no +# tracked file verifies nothing; one carrying published +# benchmark data alone runs only `verify:bench-results`, the +# single gate that opens those files. Anything else - including +# a range whose scope cannot be determined - takes the full +# path: +# +# `verify:quick` — the static CI-parity gates: typecheck (core # + guides + examples + extension packages), lint:all, # format:check, docs:api:check and the site gates. Then the test # lanes the pushed range actually requires, chosen from @@ -53,20 +61,6 @@ while read local_ref local_sha remote_ref remote_sha; do esac done -# verify:quick includes `typecheck:site` and `full-bundle:exports:check`, which -# read the PUBLISHED entry points (package.json `exports` → dist/) rather than -# src/. A dist that is missing fails them with a wall of ts(2307) "Cannot find -# module 'exojs'"; one that merely lags the sources - the state every local -# edit leaves behind - fails the freshness gate inside the site group, after -# the typecheck and lint groups have already run. Either way the push is lost, -# the build runs by hand, and everything runs again. So the build runs here -# instead, exactly when the source stamp says it is due and never otherwise: a -# clean tree pays one hash walk. -if [ "$is_branch_push" = "1" ]; then - echo "[pre-push] checking dist against the sources" - node scripts/check-dist-fresh.ts --rebuild || exit 1 -fi - if [ "$is_tag_push" = "1" ]; then echo "[pre-push] tag push detected - checking the tagged commit is releasable" git fetch --quiet origin main || exit 1 @@ -76,12 +70,9 @@ if [ "$is_tag_push" = "1" ]; then fi if [ "$is_branch_push" = "1" ]; then - echo "[pre-push] running verify:quick (static CI-parity gates)" - npm run verify:quick || exit 1 - # Range for lane selection. A branch with no remote tracking ref yet has no # base to diff against, so fall back to the merge base with origin/HEAD - # (the remote's default branch, whichever one that is) — and to running + # (the remote's default branch, whichever one that is) - and to running # every lane if even that is unavailable, because a push whose scope cannot # be determined must not be validated partially. lane_base="$push_base_sha" @@ -89,11 +80,42 @@ if [ "$is_branch_push" = "1" ]; then lane_base=$(git merge-base "$push_head_sha" origin/HEAD 2>/dev/null || true) fi - if [ -n "$lane_base" ]; then - echo "[pre-push] running the test lanes this push requires" - npm run lanes -- --run --tests-only --base "$lane_base" || exit 1 - else - echo "[pre-push] no base commit to scope lanes against — running every lane" - npm run lanes -- --run --tests-only --all || exit 1 - fi + # How much of the above the range actually needs. `full` for anything + # `push-scope.ts` does not positively recognise, including an undeterminable + # range, so a narrower answer is always a deliberate one. + scope=$(node scripts/ci/push-scope.ts "$lane_base" "$push_head_sha") + + case "$scope" in + none) + echo "[pre-push] the pushed range changes no tracked file - nothing to verify" + ;; + data) + echo "[pre-push] published benchmark data only - running its own gate" + npm run verify:bench-results || exit 1 + ;; + *) + # verify:quick includes `typecheck:site` and `full-bundle:exports:check`, which + # read the PUBLISHED entry points (package.json `exports` → dist/) rather than + # src/. A dist that is missing fails them with a wall of ts(2307) "Cannot find + # module 'exojs'"; one that merely lags the sources - the state every local + # edit leaves behind - fails the freshness gate inside the site group, after + # the typecheck and lint groups have already run. Either way the push is lost, + # the build runs by hand, and everything runs again. So the build runs here + # instead, exactly when the source stamp says it is due and never otherwise: a + # clean tree pays one hash walk. + echo "[pre-push] checking dist against the sources" + node scripts/check-dist-fresh.ts --rebuild || exit 1 + + echo "[pre-push] running verify:quick (static CI-parity gates)" + npm run verify:quick || exit 1 + + if [ -n "$lane_base" ]; then + echo "[pre-push] running the test lanes this push requires" + npm run lanes -- --run --tests-only --base "$lane_base" || exit 1 + else + echo "[pre-push] no base commit to scope lanes against - running every lane" + npm run lanes -- --run --tests-only --all || exit 1 + fi + ;; + esac fi diff --git a/scripts/ci/push-scope.ts b/scripts/ci/push-scope.ts new file mode 100644 index 000000000..cdda6def3 --- /dev/null +++ b/scripts/ci/push-scope.ts @@ -0,0 +1,66 @@ +/** + * Pre-push scope - how much local verification a pushed range actually needs. + * + * Consumed by `.husky/pre-push`, which decides between the full gate sweep and + * a narrower one, and by `test/ci/push-scope.test.ts`. + * + * The hook is a convenience that saves a failed round trip to CI, not the + * authority on correctness: CI re-runs its own selection on every push + * regardless of what this returns. That is what makes narrowing safe here, and + * why the narrowing is deliberately small - `full` is the answer for anything + * this module does not positively recognise. + */ +import { execFileSync } from 'node:child_process'; +import { pathToFileURL } from 'node:url'; + +/** + * Published reference profiles and the README beside them. These are measured + * data rather than code: nothing typechecks, lints, bundles or imports them, + * and the single check that opens one is `verify:bench-results`. Classifying + * them keeps a push that carries a reference run from rebuilding `dist` and + * running all four gate groups to validate a file none of them reads. + */ +const isBenchResultsPath = (file: string): boolean => file.startsWith('packages/exojs-bench/results/'); + +/** + * `none` - the range changes no tracked file; nothing can have broken. + * `data` - published benchmark data only; its own gate still runs. + * `full` - anything else, including a range this module cannot determine. + */ +export type PushScope = 'none' | 'data' | 'full'; + +/** Gates a `data` scope still runs, by package.json script name. */ +export const DATA_SCOPE_GATES: readonly string[] = ['verify:bench-results']; + +export const pushScope = (changedFiles: readonly string[]): PushScope => { + const files = changedFiles.map(file => String(file).replace(/\\/g, '/').trim()).filter(file => file !== ''); + + if (files.length === 0) return 'none'; + + return files.every(isBenchResultsPath) ? 'data' : 'full'; +}; + +const changedFilesBetween = (base: string, head: string): string[] => + execFileSync('git', ['diff', '--name-only', `${base}..${head}`], { encoding: 'utf8' }).split('\n'); + +const main = (): void => { + const [base, head] = process.argv.slice(2); + + // An undeterminable range must not narrow anything: the hook reads this on + // stdout and branches on it, so failing open would silently skip the gates. + if (base === undefined || head === undefined || base === '' || head === '') { + process.stdout.write('full\n'); + return; + } + + try { + process.stdout.write(`${pushScope(changedFilesBetween(base, head))}\n`); + } catch { + process.stdout.write('full\n'); + } +}; + +const invokedPath = process.argv[1]; +if (invokedPath && import.meta.url === pathToFileURL(invokedPath).href) { + main(); +} diff --git a/test/ci/push-scope.test.ts b/test/ci/push-scope.test.ts new file mode 100644 index 000000000..d79e8a760 --- /dev/null +++ b/test/ci/push-scope.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; + +import { pushScope } from '../../scripts/ci/push-scope.ts'; + +// The logic under test is the SAME module .husky/pre-push branches on, so these +// assertions exercise the real narrowing decision rather than a copy of it. + +describe('pre-push scope selection', () => { + it('verifies nothing when the range changes no tracked file', () => { + expect(pushScope([])).toBe('none'); + expect(pushScope(['', ' '])).toBe('none'); + }); + + it('narrows a published-profile change to its own gate', () => { + expect(pushScope(['packages/exojs-bench/results/rtx-5070-ti-windows-11-chromium.json'])).toBe('data'); + expect( + pushScope([ + 'packages/exojs-bench/results/apple-macos-27-beta-webkit.json', + 'packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json', + 'packages/exojs-bench/results/README.md', + ]), + ).toBe('data'); + }); + + it('reads Windows separators, which is how git diff reaches the hook there', () => { + expect(pushScope(['packages\\exojs-bench\\results\\rtx-5070-ti-windows-11-chromium.json'])).toBe('data'); + }); + + it('takes the full path as soon as one file is not published data', () => { + expect(pushScope(['packages/exojs-bench/results/x.json', 'src/rendering/Renderer.ts'])).toBe('full'); + // The harness that PRODUCES the profiles is code, and so is its baseline. + expect(pushScope(['packages/exojs-bench/src/suite/catalog.ts'])).toBe('full'); + expect(pushScope(['packages/exojs-bench/baselines/structural.json'])).toBe('full'); + }); + + it('does not narrow on a path that merely starts like the results directory', () => { + expect(pushScope(['packages/exojs-bench/results-notes.md'])).toBe('full'); + }); +}); From 12bc712f854d156549c8d12fe14e96e49326e673 Mon Sep 17 00:00:00 2001 From: Dima Giesler Date: Sat, 12 Sep 2026 03:23:53 +0200 Subject: [PATCH 04/28] bench: publish the macOS reference run under the machine's CPU-derived name --- ...27-beta-webkit.json => m3-max-macos-27-beta-webkit.json} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename packages/exojs-bench/results/{apple-macos-27-beta-webkit.json => m3-max-macos-27-beta-webkit.json} (99%) diff --git a/packages/exojs-bench/results/apple-macos-27-beta-webkit.json b/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json similarity index 99% rename from packages/exojs-bench/results/apple-macos-27-beta-webkit.json rename to packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json index 908a3ca9a..a7d8dc514 100644 --- a/packages/exojs-bench/results/apple-macos-27-beta-webkit.json +++ b/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json @@ -1,8 +1,8 @@ { "schemaVersion": 7, "profile": { - "slug": "apple-macos-27-beta-webkit", - "gpu": "apple", + "slug": "m3-max-macos-27-beta-webkit", + "gpu": "m3-max", "os": "macos-27-beta", "browser": "webkit", "platform": { @@ -6627,6 +6627,6 @@ }, "signature": { "algorithm": "sha256", - "value": "4a5fb77cfdcae08ad5ab6e822d6d2756f9477a9813adb1b878d2523b290e6adf" + "value": "640ef3c7d832b56fda2fccbfad1eacfe6f148ab469f7444145b25bd0701642dc" } } From 4e5ab4417fc001d8f5b5b1240b72531a276a83fb Mon Sep 17 00:00:00 2001 From: Dima Giesler Date: Sat, 12 Sep 2026 04:44:16 +0200 Subject: [PATCH 05/28] bench: publish the macOS reference run under the machine's CPU-derived name --- .../results/m3-max-macos-27-beta-webkit.json | 3649 +++++++++-------- 1 file changed, 1825 insertions(+), 1824 deletions(-) diff --git a/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json b/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json index a7d8dc514..62385fd87 100644 --- a/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json +++ b/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json @@ -12,7 +12,7 @@ "prerelease": true }, "engineVersion": "0.17.0", - "measuredAt": "2026-09-12T00:18:12.752Z", + "measuredAt": "2026-09-12T02:40:41.625Z", "runs": 3 }, "rendering": { @@ -43,7 +43,7 @@ "headless": true, "software": false, "engineVersion": "0.17.0", - "timestamp": "2026-09-11T23:43:51.148Z" + "timestamp": "2026-09-12T02:06:06.620Z" }, { "backend": "webgpu", @@ -70,7 +70,7 @@ "software": false, "slotTier": 16, "engineVersion": "0.17.0", - "timestamp": "2026-09-11T23:51:08.527Z" + "timestamp": "2026-09-12T02:13:25.798Z" } ] }, @@ -100,7 +100,7 @@ "headless": true, "software": false, "engineVersion": "0.17.0", - "timestamp": "2026-09-11T23:56:01.601Z" + "timestamp": "2026-09-12T02:18:15.735Z" }, { "backend": "webgpu", @@ -127,7 +127,7 @@ "software": false, "slotTier": 16, "engineVersion": "0.17.0", - "timestamp": "2026-09-12T00:03:21.362Z" + "timestamp": "2026-09-12T02:25:50.325Z" } ] }, @@ -157,7 +157,7 @@ "headless": true, "software": false, "engineVersion": "0.17.0", - "timestamp": "2026-09-12T00:08:10.870Z" + "timestamp": "2026-09-12T02:30:39.988Z" }, { "backend": "webgpu", @@ -184,7 +184,7 @@ "software": false, "slotTier": 16, "engineVersion": "0.17.0", - "timestamp": "2026-09-12T00:15:31.445Z" + "timestamp": "2026-09-12T02:38:00.061Z" } ] } @@ -226,19 +226,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.20000000000015916, - "referenceP95Ms": 0.5199999999999818, - "referenceGpuMs": 16.660000000000082, + "referenceMs": 0.21000000000003638, + "referenceP95Ms": 0.459999999999809, + "referenceGpuMs": 16.679999999999836, "referenceOverFrameBudget": false, - "competitorMs": 2.30000000000004, - "competitorP95Ms": 8.879999999999654, + "competitorMs": 2.2700000000000955, + "competitorP95Ms": 8.340000000000146, "competitorGpuMs": 16.660000000000082, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.08695652173919813, - "factor": 11.499999999991047, - "label": "ExoJS leads clearly (11.50x)", + "ratio": 0.09251101321587117, + "factor": 10.809523809522391, + "label": "ExoJS leads clearly (10.81x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -247,13 +247,13 @@ "runs": 3, "reference": { "minMs": 0.20000000000004547, - "maxMs": 0.22000000000002728, - "ratio": 1.0999999999998864 + "maxMs": 0.21000000000003638, + "ratio": 1.0499999999999432 }, "competitor": { - "minMs": 2.2200000000000273, - "maxMs": 2.4700000000000273, - "ratio": 1.1126126126126112 + "minMs": 2.259999999999877, + "maxMs": 2.480000000000018, + "ratio": 1.0973451327434305 }, "stable": true, "rungs": [ @@ -265,19 +265,19 @@ }, { "competitor": "phaser", - "referenceMs": 0.20000000000015916, - "referenceP95Ms": 0.5199999999999818, - "referenceGpuMs": 16.660000000000082, + "referenceMs": 0.21000000000003638, + "referenceP95Ms": 0.459999999999809, + "referenceGpuMs": 16.679999999999836, "referenceOverFrameBudget": false, - "competitorMs": 1.3099999999998317, - "competitorP95Ms": 1.7999999999999545, - "competitorGpuMs": 16.679999999999836, + "competitorMs": 1.2100000000000364, + "competitorP95Ms": 1.7200000000000273, + "competitorGpuMs": 16.660000000000082, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.15267175572533195, - "factor": 6.549999999993946, - "label": "ExoJS leads clearly (6.55x)", + "ratio": 0.1735537190082893, + "factor": 5.761904761903937, + "label": "ExoJS leads clearly (5.76x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -286,13 +286,13 @@ "runs": 3, "reference": { "minMs": 0.20000000000004547, - "maxMs": 0.22000000000002728, - "ratio": 1.0999999999998864 + "maxMs": 0.21000000000003638, + "ratio": 1.0499999999999432 }, "competitor": { - "minMs": 1.2999999999999545, - "maxMs": 1.3199999999999363, - "ratio": 1.015384615384602 + "minMs": 1.1700000000000728, + "maxMs": 1.2600000000001046, + "ratio": 1.0769230769230993 }, "stable": true, "rungs": [ @@ -304,19 +304,19 @@ }, { "competitor": "pixi", - "referenceMs": 0.20000000000015916, - "referenceP95Ms": 0.5199999999999818, - "referenceGpuMs": 16.660000000000082, + "referenceMs": 0.21000000000003638, + "referenceP95Ms": 0.459999999999809, + "referenceGpuMs": 16.679999999999836, "referenceOverFrameBudget": false, - "competitorMs": 0.18000000000006366, - "competitorP95Ms": 0.4599999999999227, - "competitorGpuMs": 16.680000000000007, + "competitorMs": 0.18000000000017735, + "competitorP95Ms": 0.37999999999988177, + "competitorGpuMs": 16.660000000000025, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 1.1666666666657193, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -325,18 +325,18 @@ "runs": 3, "reference": { "minMs": 0.20000000000004547, - "maxMs": 0.22000000000002728, - "ratio": 1.0999999999998864 + "maxMs": 0.21000000000003638, + "ratio": 1.0499999999999432 }, "competitor": { - "minMs": 0.17999999999989313, - "maxMs": 0.2199999999997999, - "ratio": 1.2222222222218362 + "minMs": 0.18000000000006366, + "maxMs": 0.22000000000002728, + "ratio": 1.2222222222219414 }, - "stable": false, + "stable": true, "rungs": [ "level", - "competitor-leads", + "level", "level" ] } @@ -354,18 +354,18 @@ { "competitor": "excalibur", "referenceMs": 0.13999999999987267, - "referenceP95Ms": 0.23999999999978172, + "referenceP95Ms": 0.25999999999976353, "referenceGpuMs": 16.66000000000031, "referenceOverFrameBudget": false, - "competitorMs": 19.539999999999964, - "competitorP95Ms": 23.559999999999945, - "competitorGpuMs": 20.460000000000036, + "competitorMs": 20.329999999999927, + "competitorP95Ms": 24.90000000000009, + "competitorGpuMs": 21.199999999999818, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.007164790173995544, - "factor": 139.57142857155523, - "label": "ExoJS leads clearly (139.57x)", + "ratio": 0.006886374815537293, + "factor": 145.2142857144173, + "label": "ExoJS leads clearly (145.21x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -373,14 +373,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.13999999999987267, - "maxMs": 0.15999999999985448, - "ratio": 1.1428571428571428 + "minMs": 0.12000000000034561, + "maxMs": 0.13999999999987267, + "ratio": 1.1666666666622456 }, "competitor": { - "minMs": 19.26000000000022, - "maxMs": 19.54000000000042, - "ratio": 1.0145379023883798 + "minMs": 20.02000000000021, + "maxMs": 20.86999999999989, + "ratio": 1.0424575424575262 }, "stable": true, "rungs": [ @@ -393,18 +393,18 @@ { "competitor": "phaser", "referenceMs": 0.13999999999987267, - "referenceP95Ms": 0.23999999999978172, + "referenceP95Ms": 0.25999999999976353, "referenceGpuMs": 16.66000000000031, "referenceOverFrameBudget": false, - "competitorMs": 3.2200000000000273, - "competitorP95Ms": 7.440000000000055, - "competitorGpuMs": 16.66000000000031, + "competitorMs": 3.0499999999999545, + "competitorP95Ms": 8.279999999999745, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.043478260869525304, - "factor": 23.000000000021114, - "label": "ExoJS leads clearly (23.00x)", + "ratio": 0.04590163934422123, + "factor": 21.785714285733775, + "label": "ExoJS leads clearly (21.79x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -412,14 +412,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.13999999999987267, - "maxMs": 0.15999999999985448, - "ratio": 1.1428571428571428 + "minMs": 0.12000000000034561, + "maxMs": 0.13999999999987267, + "ratio": 1.1666666666622456 }, "competitor": { - "minMs": 3.1100000000003547, - "maxMs": 3.2500000000002274, - "ratio": 1.045016077170372 + "minMs": 2.9400000000000546, + "maxMs": 3.1399999999998727, + "ratio": 1.0680272108842905 }, "stable": true, "rungs": [ @@ -432,16 +432,16 @@ { "competitor": "pixi", "referenceMs": 0.13999999999987267, - "referenceP95Ms": 0.23999999999978172, + "referenceP95Ms": 0.25999999999976353, "referenceGpuMs": 16.66000000000031, "referenceOverFrameBudget": false, - "competitorMs": 0.15000000000009095, - "competitorP95Ms": 0.2600000000002183, - "competitorGpuMs": 16.66000000000031, + "competitorMs": 0.13999999999987267, + "competitorP95Ms": 0.2800000000002001, + "competitorGpuMs": 16.679999999999836, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9333333333319186, + "ratio": 1, "factor": 1, "label": "level", "structural": false @@ -451,9 +451,9 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.13999999999987267, - "maxMs": 0.15999999999985448, - "ratio": 1.1428571428571428 + "minMs": 0.12000000000034561, + "maxMs": 0.13999999999987267, + "ratio": 1.1666666666622456 }, "competitor": { "minMs": 0.13999999999987267, @@ -480,13 +480,13 @@ "cells": [ { "competitor": "phaser", - "referenceMs": 0.06000000000040018, - "referenceP95Ms": 0.2600000000002183, + "referenceMs": 0.05999999999994543, + "referenceP95Ms": 0.22999999999956344, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 17.629999999999654, - "competitorP95Ms": 22, - "competitorGpuMs": 18.23999999999978, + "competitorMs": 17.925000000000182, + "competitorP95Ms": 23.389999999999873, + "competitorGpuMs": 18.829999999999927, "competitorOverFrameBudget": true, "verdict": { "side": "neither", @@ -498,19 +498,20 @@ "mechanism": "competitor issues fewer draw calls (25 vs 7 per frame)", "timer": "limited", "aggregate": { - "runs": 1, + "runs": 2, "reference": { - "minMs": 0.06000000000040018, + "minMs": 0.05999999999949068, "maxMs": 0.06000000000040018, - "ratio": 1 + "ratio": 1.0000000000151583 }, "competitor": { - "minMs": 17.629999999999654, - "maxMs": 17.629999999999654, - "ratio": 1 + "minMs": 17.7800000000002, + "maxMs": 18.070000000000164, + "ratio": 1.0163104611923488 }, "stable": false, "rungs": [ + "exojs-leads-clearly", "exojs-leads-clearly" ] } @@ -518,18 +519,18 @@ { "competitor": "pixi", "referenceMs": 0.06000000000040018, - "referenceP95Ms": 0.24000000000069122, + "referenceP95Ms": 0.23999999999978172, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 0.03999999999996362, - "competitorP95Ms": 0.23999999999978172, - "competitorGpuMs": 16.68000000000029, + "competitorMs": 0.06000000000040018, + "competitorP95Ms": 0.2600000000002183, + "competitorGpuMs": 16.67999999999938, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 1, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "competitor issues fewer draw calls (25 vs 1 per frame)", @@ -537,20 +538,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.06000000000040018, - "maxMs": 0.08999999999969077, - "ratio": 1.4999999999848417 + "minMs": 0.05999999999949068, + "maxMs": 0.07000000000016371, + "ratio": 1.1666666666792986 }, "competitor": { - "minMs": 0.03000000000020009, + "minMs": 0.05999999999949068, "maxMs": 0.06000000000040018, - "ratio": 2 + "ratio": 1.0000000000151583 }, - "stable": false, + "stable": true, "rungs": [ "level", - "competitor-leads", - "competitor-leads" + "level", + "level" ] } } @@ -566,19 +567,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 1.75, - "referenceP95Ms": 2.5399999999999636, + "referenceMs": 1.699999999999818, + "referenceP95Ms": 2.399999999999636, "referenceGpuMs": 16.660000000000764, "referenceOverFrameBudget": false, - "competitorMs": 25.290000000000873, - "competitorP95Ms": 31.539999999997235, - "competitorGpuMs": 26.279999999998836, + "competitorMs": 26.090000000001965, + "competitorP95Ms": 30.040000000000873, + "competitorGpuMs": 27.279999999998836, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.06919731119019136, - "factor": 14.451428571429071, - "label": "ExoJS leads clearly (14.45x)", + "ratio": 0.06515906477576427, + "factor": 15.34705882353221, + "label": "ExoJS leads clearly (15.35x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -586,14 +587,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.6100000000001273, - "maxMs": 1.9299999999998363, - "ratio": 1.1987577639749587 + "minMs": 1.6900000000000546, + "maxMs": 1.7599999999997635, + "ratio": 1.0414201183430216 }, "competitor": { - "minMs": 24.969999999997526, - "maxMs": 26.6200000000008, - "ratio": 1.0660792951543228 + "minMs": 25.539999999999054, + "maxMs": 28.3700000000008, + "ratio": 1.1108065779170655 }, "stable": true, "rungs": [ @@ -605,19 +606,19 @@ }, { "competitor": "phaser", - "referenceMs": 1.75, - "referenceP95Ms": 2.5399999999999636, + "referenceMs": 1.699999999999818, + "referenceP95Ms": 2.399999999999636, "referenceGpuMs": 16.660000000000764, "referenceOverFrameBudget": false, - "competitorMs": 3.329999999999984, - "competitorP95Ms": 7.6400000000001, - "competitorGpuMs": 16.660000000000082, + "competitorMs": 3.0299999999999727, + "competitorP95Ms": 8.180000000000291, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.525525525525528, - "factor": 1.902857142857134, - "label": "ExoJS leads (1.90x)", + "ratio": 0.5610561056105061, + "factor": 1.7823529411766452, + "label": "ExoJS leads (1.78x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -625,14 +626,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.6100000000001273, - "maxMs": 1.9299999999998363, - "ratio": 1.1987577639749587 + "minMs": 1.6900000000000546, + "maxMs": 1.7599999999997635, + "ratio": 1.0414201183430216 }, "competitor": { - "minMs": 3.1199999999989814, - "maxMs": 3.3600000000000705, - "ratio": 1.076923076923451 + "minMs": 2.9500000000007276, + "maxMs": 3.2399999999943248, + "ratio": 1.098305084743568 }, "stable": true, "rungs": [ @@ -644,19 +645,19 @@ }, { "competitor": "pixi", - "referenceMs": 1.75, - "referenceP95Ms": 2.5399999999999636, + "referenceMs": 1.699999999999818, + "referenceP95Ms": 2.399999999999636, "referenceGpuMs": 16.660000000000764, "referenceOverFrameBudget": false, - "competitorMs": 1.319999999999709, - "competitorP95Ms": 1.9400000000005093, - "competitorGpuMs": 16.659999999999854, + "competitorMs": 1.3100000000004002, + "competitorP95Ms": 2.0399999999999636, + "competitorGpuMs": 16.660000000000764, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 1.3257575757578681, - "factor": 1.3257575757578681, - "label": "competitor leads (1.33x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -664,20 +665,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.6100000000001273, - "maxMs": 1.9299999999998363, - "ratio": 1.1987577639749587 + "minMs": 1.6900000000000546, + "maxMs": 1.7599999999997635, + "ratio": 1.0414201183430216 }, "competitor": { - "minMs": 1.2199999999998, - "maxMs": 1.4900000000002365, - "ratio": 1.22131147541023 + "minMs": 1.300000000000182, + "maxMs": 1.4299999999998363, + "ratio": 1.09999999999972 }, - "stable": true, + "stable": false, "rungs": [ "competitor-leads", "competitor-leads", - "competitor-leads" + "level" ] } } @@ -693,19 +694,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 1.6199999999998909, - "referenceP95Ms": 2.219999999999345, + "referenceMs": 1.5799999999999272, + "referenceP95Ms": 2.160000000000764, "referenceGpuMs": 16.67999999999938, "referenceOverFrameBudget": false, - "competitorMs": 3.2999999999992724, - "competitorP95Ms": 8.220000000001164, + "competitorMs": 3.390000000001237, + "competitorP95Ms": 8.360000000000582, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.49090909090916607, - "factor": 2.037037037036725, - "label": "ExoJS leads (2.04x)", + "ratio": 0.4660766961650002, + "factor": 2.1455696202540464, + "label": "ExoJS leads (2.15x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -713,14 +714,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.5300000000002, - "maxMs": 1.6400000000003274, - "ratio": 1.0718954248366752 + "minMs": 1.5399999999999636, + "maxMs": 1.6399999999998727, + "ratio": 1.0649350649350073 }, "competitor": { - "minMs": 3.2000000000007276, - "maxMs": 3.4200000000000728, - "ratio": 1.0687499999997798 + "minMs": 3.319999999999709, + "maxMs": 3.430000000000291, + "ratio": 1.0331325301206602 }, "stable": true, "rungs": [ @@ -732,19 +733,19 @@ }, { "competitor": "phaser", - "referenceMs": 1.6199999999998909, - "referenceP95Ms": 2.219999999999345, + "referenceMs": 1.5799999999999272, + "referenceP95Ms": 2.160000000000764, "referenceGpuMs": 16.67999999999938, "referenceOverFrameBudget": false, - "competitorMs": 1.180000000000291, - "competitorP95Ms": 1.6200000000003456, - "competitorGpuMs": 16.660000000003492, + "competitorMs": 1.0299999999988358, + "competitorP95Ms": 1.3800000000046566, + "competitorGpuMs": 16.679999999993015, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.3728813559317723, - "factor": 1.3728813559317723, - "label": "competitor leads (1.37x)", + "ratio": 1.533980582525935, + "factor": 1.533980582525935, + "label": "competitor leads (1.53x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -752,14 +753,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.5300000000002, - "maxMs": 1.6400000000003274, - "ratio": 1.0718954248366752 + "minMs": 1.5399999999999636, + "maxMs": 1.6399999999998727, + "ratio": 1.0649350649350073 }, "competitor": { - "minMs": 0.9599999999991269, - "maxMs": 1.2100000000000364, - "ratio": 1.260416666667851 + "minMs": 0.9800000000104774, + "maxMs": 1.2199999999998, + "ratio": 1.2448979591701599 }, "stable": true, "rungs": [ @@ -771,19 +772,19 @@ }, { "competitor": "pixi", - "referenceMs": 1.6199999999998909, - "referenceP95Ms": 2.219999999999345, + "referenceMs": 1.5799999999999272, + "referenceP95Ms": 2.160000000000764, "referenceGpuMs": 16.67999999999938, "referenceOverFrameBudget": false, - "competitorMs": 1, - "competitorP95Ms": 1.3800000000001091, - "competitorGpuMs": 16.68000000000029, + "competitorMs": 0.9800000000004729, + "competitorP95Ms": 1.360000000000582, + "competitorGpuMs": 16.660000000000764, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.6199999999998909, - "factor": 1.6199999999998909, - "label": "competitor leads (1.62x)", + "ratio": 1.6122448979583315, + "factor": 1.6122448979583315, + "label": "competitor leads (1.61x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -791,14 +792,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.5300000000002, - "maxMs": 1.6400000000003274, - "ratio": 1.0718954248366752 + "minMs": 1.5399999999999636, + "maxMs": 1.6399999999998727, + "ratio": 1.0649350649350073 }, "competitor": { "minMs": 0.9600000000000364, - "maxMs": 1.1700000000000728, - "ratio": 1.2187500000000295 + "maxMs": 1, + "ratio": 1.0416666666666272 }, "stable": true, "rungs": [ @@ -820,19 +821,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 4.1299999999992, - "referenceP95Ms": 5.300000000001091, + "referenceMs": 4.020000000000437, + "referenceP95Ms": 5.199999999998909, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 31.69999999999891, - "competitorP95Ms": 35.400000000001455, - "competitorGpuMs": 32.56000000000131, + "competitorMs": 30.80000000000109, + "competitorP95Ms": 34.94000000000233, + "competitorGpuMs": 31.840000000000146, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.13028391167190354, - "factor": 7.675544794190085, - "label": "ExoJS leads clearly (7.68x)", + "ratio": 0.13051948051949006, + "factor": 7.661691542287998, + "label": "ExoJS leads clearly (7.66x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -840,14 +841,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.8799999999991996, - "maxMs": 4.300000000001091, - "ratio": 1.1082474226809222 + "minMs": 3.9899999999997817, + "maxMs": 4.0600000000004, + "ratio": 1.0175438596492787 }, "competitor": { - "minMs": 30.44000000000051, - "maxMs": 31.859999999998763, - "ratio": 1.0466491458606515 + "minMs": 29.900000000001455, + "maxMs": 34.54999999999927, + "ratio": 1.1555183946487488 }, "stable": true, "rungs": [ @@ -859,19 +860,19 @@ }, { "competitor": "phaser", - "referenceMs": 4.1299999999992, - "referenceP95Ms": 5.300000000001091, + "referenceMs": 4.020000000000437, + "referenceP95Ms": 5.199999999998909, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 3.1600000000034925, - "competitorP95Ms": 7.800000000000182, - "competitorGpuMs": 16.659999999999854, + "competitorMs": 3.139999999999418, + "competitorP95Ms": 7.8399999999965075, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.306962025314758, - "factor": 1.306962025314758, - "label": "competitor leads (1.31x)", + "ratio": 1.28025477707044, + "factor": 1.28025477707044, + "label": "competitor leads (1.28x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -879,14 +880,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.8799999999991996, - "maxMs": 4.300000000001091, - "ratio": 1.1082474226809222 + "minMs": 3.9899999999997817, + "maxMs": 4.0600000000004, + "ratio": 1.0175438596492787 }, "competitor": { - "minMs": 3.150000000000091, - "maxMs": 3.1799999999998363, - "ratio": 1.0095238095237284 + "minMs": 2.9600000000000364, + "maxMs": 3.1999999999970896, + "ratio": 1.0810810810800846 }, "stable": true, "rungs": [ @@ -898,19 +899,19 @@ }, { "competitor": "pixi", - "referenceMs": 4.1299999999992, - "referenceP95Ms": 5.300000000001091, + "referenceMs": 4.020000000000437, + "referenceP95Ms": 5.199999999998909, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 2.719999999999345, - "competitorP95Ms": 9.180000000000291, + "competitorMs": 2.5999999999994543, + "competitorP95Ms": 10.180000000000291, "competitorGpuMs": 16.660000000001673, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.5183823529412477, - "factor": 1.5183823529412477, - "label": "competitor leads (1.52x)", + "ratio": 1.5461538461543385, + "factor": 1.5461538461543385, + "label": "competitor leads (1.55x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -918,14 +919,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.8799999999991996, - "maxMs": 4.300000000001091, - "ratio": 1.1082474226809222 + "minMs": 3.9899999999997817, + "maxMs": 4.0600000000004, + "ratio": 1.0175438596492787 }, "competitor": { - "minMs": 2.6500000000005457, - "maxMs": 2.8000000000010914, - "ratio": 1.0566037735851 + "minMs": 2.5499999999992724, + "maxMs": 2.8599999999996726, + "ratio": 1.121568627451172 }, "stable": true, "rungs": [ @@ -947,9 +948,9 @@ "cells": [ { "competitor": "phaser", - "referenceMs": 37.5600000000004, - "referenceP95Ms": 41.960000000000946, - "referenceGpuMs": 38.73999999999978, + "referenceMs": 36.82000000000062, + "referenceP95Ms": 43, + "referenceGpuMs": 38.26000000000022, "referenceOverFrameBudget": true, "competitorMs": 21.439999999995052, "competitorP95Ms": 26.020000000004075, @@ -967,8 +968,8 @@ "aggregate": { "runs": 1, "reference": { - "minMs": 37.5600000000004, - "maxMs": 37.5600000000004, + "minMs": 36.82000000000062, + "maxMs": 36.82000000000062, "ratio": 1 }, "competitor": { @@ -984,19 +985,19 @@ }, { "competitor": "pixi", - "referenceMs": 38.149999999999636, - "referenceP95Ms": 43.54000000000087, - "referenceGpuMs": 39.51999999999862, + "referenceMs": 38.22999999999956, + "referenceP95Ms": 43.63999999999942, + "referenceGpuMs": 39.280000000000655, "referenceOverFrameBudget": true, - "competitorMs": 28.73999999999978, - "competitorP95Ms": 32.56000000000131, - "competitorGpuMs": 30.040000000000873, + "competitorMs": 28.43000000000029, + "competitorP95Ms": 32.20000000000073, + "competitorGpuMs": 30.219999999999345, "competitorOverFrameBudget": true, "verdict": { "side": "competitor", - "ratio": 1.3274182324286683, - "factor": 1.3274182324286683, - "label": "competitor leads (1.33x)", + "ratio": 1.3447062961659926, + "factor": 1.3447062961659926, + "label": "competitor leads (1.34x)", "structural": false }, "mechanism": "competitor issues fewer draw calls (25 vs 1 per frame)", @@ -1004,14 +1005,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 37.5600000000004, - "maxMs": 42.67999999999938, - "ratio": 1.1363152289669576 + "minMs": 36.82000000000062, + "maxMs": 40.81999999999971, + "ratio": 1.1086366105377248 }, "competitor": { - "minMs": 28.420000000000073, - "maxMs": 28.82000000000062, - "ratio": 1.0140745953554027 + "minMs": 28.18000000000029, + "maxMs": 29.05999999999949, + "ratio": 1.0312278211497228 }, "stable": true, "rungs": [ @@ -1033,19 +1034,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.5599999999994907, - "referenceP95Ms": 0.7799999999988358, - "referenceGpuMs": 16.69999999999891, + "referenceMs": 0.5700000000006185, + "referenceP95Ms": 0.7999999999992724, + "referenceGpuMs": 16.679999999998472, "referenceOverFrameBudget": false, - "competitorMs": 38.599999999998545, - "competitorP95Ms": 56.799999999995634, + "competitorMs": 37.330000000001746, + "competitorP95Ms": 56.81999999999971, "competitorGpuMs": 39.86000000000058, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.014507772020712741, - "factor": 68.92857142863151, - "label": "ExoJS leads clearly (68.93x)", + "ratio": 0.0152692204661289, + "factor": 65.49122807010744, + "label": "ExoJS leads clearly (65.49x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1053,14 +1054,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5399999999990541, - "maxMs": 0.6300000000001091, - "ratio": 1.1666666666689123 + "minMs": 0.5399999999999636, + "maxMs": 0.5799999999999272, + "ratio": 1.0740740740740118 }, "competitor": { - "minMs": 33.52000000000044, - "maxMs": 41.849999999998545, - "ratio": 1.2485083532218975 + "minMs": 26.970000000001164, + "maxMs": 38.12000000000262, + "ratio": 1.4134223210975518 }, "stable": true, "rungs": [ @@ -1072,19 +1073,19 @@ }, { "competitor": "phaser", - "referenceMs": 0.5599999999994907, - "referenceP95Ms": 0.7799999999988358, - "referenceGpuMs": 16.69999999999891, + "referenceMs": 0.5700000000006185, + "referenceP95Ms": 0.7999999999992724, + "referenceGpuMs": 16.679999999998472, "referenceOverFrameBudget": false, - "competitorMs": 3.1300000000000523, - "competitorP95Ms": 7.259999999999991, + "competitorMs": 3.0900000000001455, + "competitorP95Ms": 7.459999999999809, "competitorGpuMs": 16.660000000000082, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.17891373801900362, - "factor": 5.589285714290891, - "label": "ExoJS leads clearly (5.59x)", + "ratio": 0.1844660194176672, + "factor": 5.421052631573321, + "label": "ExoJS leads clearly (5.42x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1092,14 +1093,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5399999999990541, - "maxMs": 0.6300000000001091, - "ratio": 1.1666666666689123 + "minMs": 0.5399999999999636, + "maxMs": 0.5799999999999272, + "ratio": 1.0740740740740118 }, "competitor": { - "minMs": 3.0499999999999545, - "maxMs": 3.1900000000023283, - "ratio": 1.0459016393450413 + "minMs": 3.0699999999999363, + "maxMs": 3.1700000000000728, + "ratio": 1.0325732899023252 }, "stable": true, "rungs": [ @@ -1111,17 +1112,17 @@ }, { "competitor": "pixi", - "referenceMs": 0.5599999999994907, - "referenceP95Ms": 0.7799999999988358, - "referenceGpuMs": 16.69999999999891, + "referenceMs": 0.5700000000006185, + "referenceP95Ms": 0.7999999999992724, + "referenceGpuMs": 16.679999999998472, "referenceOverFrameBudget": false, "competitorMs": 0.5799999999999272, "competitorP95Ms": 0.7999999999992724, - "competitorGpuMs": 16.659999999999854, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9655172413785533, + "ratio": 0.9827586206908447, "factor": 1, "label": "level", "structural": false @@ -1131,14 +1132,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5399999999990541, - "maxMs": 0.6300000000001091, - "ratio": 1.1666666666689123 + "minMs": 0.5399999999999636, + "maxMs": 0.5799999999999272, + "ratio": 1.0740740740740118 }, "competitor": { - "minMs": 0.5700000000006185, - "maxMs": 0.6599999999998545, - "ratio": 1.1578947368405936 + "minMs": 0.5599999999994907, + "maxMs": 0.6000000000003638, + "ratio": 1.0714285714301954 }, "stable": true, "rungs": [ @@ -1160,19 +1161,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 2.720000000001164, - "referenceP95Ms": 5.459999999999127, + "referenceMs": 2.8299999999999272, + "referenceP95Ms": 4.479999999999563, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 25.5199999999968, - "competitorP95Ms": 29.19999999999709, - "competitorGpuMs": 26.779999999998836, + "competitorMs": 25.139999999999418, + "competitorP95Ms": 30.04000000000815, + "competitorGpuMs": 26.679999999993015, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.10658307210037246, - "factor": 9.382352941171279, - "label": "ExoJS leads clearly (9.38x)", + "ratio": 0.11256961018297505, + "factor": 8.883392226148432, + "label": "ExoJS leads clearly (8.88x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1180,14 +1181,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.399999999997817, - "maxMs": 2.860000000000582, - "ratio": 1.191666666667993 + "minMs": 2.5600000000013097, + "maxMs": 2.8400000000001455, + "ratio": 1.1093749999994893 }, "competitor": { - "minMs": 24.779999999998836, - "maxMs": 26, - "ratio": 1.0492332526231325 + "minMs": 25.06999999999971, + "maxMs": 25.52999999999156, + "ratio": 1.0183486238528863 }, "stable": true, "rungs": [ @@ -1199,19 +1200,19 @@ }, { "competitor": "phaser", - "referenceMs": 2.720000000001164, - "referenceP95Ms": 5.459999999999127, + "referenceMs": 2.8299999999999272, + "referenceP95Ms": 4.479999999999563, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 10.589999999996508, - "competitorP95Ms": 11.059999999997672, + "competitorMs": 10.56000000000131, + "competitorP95Ms": 11.200000000000728, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.2568460812088821, - "factor": 3.893382352938226, - "label": "ExoJS leads (3.89x)", + "ratio": 0.2679924242423841, + "factor": 3.731448763251442, + "label": "ExoJS leads (3.73x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1219,14 +1220,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.399999999997817, - "maxMs": 2.860000000000582, - "ratio": 1.191666666667993 + "minMs": 2.5600000000013097, + "maxMs": 2.8400000000001455, + "ratio": 1.1093749999994893 }, "competitor": { - "minMs": 10.569999999999709, - "maxMs": 10.739999999997963, - "ratio": 1.0160832544936858 + "minMs": 10.550000000001091, + "maxMs": 10.659999999999854, + "ratio": 1.0104265402842418 }, "stable": true, "rungs": [ @@ -1238,19 +1239,19 @@ }, { "competitor": "pixi", - "referenceMs": 2.720000000001164, - "referenceP95Ms": 5.459999999999127, + "referenceMs": 2.8299999999999272, + "referenceP95Ms": 4.479999999999563, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 2.569999999999709, - "competitorP95Ms": 7.6599999999998545, + "competitorMs": 2.400000000001455, + "competitorP95Ms": 8.040000000000873, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.0583657587554367, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -1258,20 +1259,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.399999999997817, - "maxMs": 2.860000000000582, - "ratio": 1.191666666667993 + "minMs": 2.5600000000013097, + "maxMs": 2.8400000000001455, + "ratio": 1.1093749999994893 }, "competitor": { - "minMs": 2.5599999999994907, - "maxMs": 2.5800000000017462, - "ratio": 1.0078125000008826 + "minMs": 2.3400000000001455, + "maxMs": 2.5600000000013097, + "ratio": 1.0940170940175857 }, - "stable": true, + "stable": false, "rungs": [ "level", "level", - "level" + "competitor-leads" ] } } @@ -1292,18 +1293,18 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.07999999999810825, + "referenceMs": 0.07999999999992724, "referenceP95Ms": 0.12000000000080036, "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, "competitorMs": 0.6399999999994179, - "competitorP95Ms": 0.9000000000014552, - "competitorGpuMs": 16.660000000003492, + "competitorP95Ms": 0.8600000000005821, + "competitorGpuMs": 16.639999999999418, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.12499999999715783, - "factor": 8.000000000181899, + "ratio": 0.125, + "factor": 8, "label": "ExoJS leads clearly (8.00x)", "structural": true }, @@ -1312,14 +1313,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.06000000000130967, - "maxMs": 0.1000000000003638, - "ratio": 1.66666666663635 + "minMs": 0.07999999999992724, + "maxMs": 0.07999999999992724, + "ratio": 1 }, "competitor": { - "minMs": 0.6399999999994179, + "minMs": 0.4500000000007276, "maxMs": 0.6599999999962165, - "ratio": 1.0312499999950262 + "ratio": 1.4666666666558874 }, "stable": true, "rungs": [ @@ -1331,18 +1332,18 @@ }, { "competitor": "phaser", - "referenceMs": 0.07999999999810825, + "referenceMs": 0.07999999999992724, "referenceP95Ms": 0.12000000000080036, "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, "competitorMs": 0.27999999999974534, "competitorP95Ms": 0.4600000000000364, - "competitorGpuMs": 16.679999999999836, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.28571428570778934, - "factor": 3.500000000079581, + "ratio": 0.2857142857142857, + "factor": 3.5, "label": "ExoJS leads (3.50x)", "structural": false }, @@ -1351,14 +1352,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.06000000000130967, - "maxMs": 0.1000000000003638, - "ratio": 1.66666666663635 + "minMs": 0.07999999999992724, + "maxMs": 0.07999999999992724, + "ratio": 1 }, "competitor": { - "minMs": 0.19999999999708962, + "minMs": 0.25999999999976353, "maxMs": 0.2800000000002001, - "ratio": 1.400000000021373 + "ratio": 1.076923076924826 }, "stable": true, "rungs": [ @@ -1370,13 +1371,13 @@ }, { "competitor": "pixi", - "referenceMs": 0.07999999999810825, + "referenceMs": 0.07999999999992724, "referenceP95Ms": 0.12000000000080036, "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 0.1000000000003638, + "competitorMs": 0.09999999999854481, "competitorP95Ms": 0.21999999999934516, - "competitorGpuMs": 16.68000000000029, + "competitorGpuMs": 16.679999999998472, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -1390,20 +1391,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.06000000000130967, - "maxMs": 0.1000000000003638, - "ratio": 1.66666666663635 + "minMs": 0.07999999999992724, + "maxMs": 0.07999999999992724, + "ratio": 1 }, "competitor": { - "minMs": 0.07999999999992724, - "maxMs": 0.1400000000012369, - "ratio": 1.750000000017053 + "minMs": 0.09999999999854481, + "maxMs": 0.1000000000003638, + "ratio": 1.00000000001819 }, "stable": false, "rungs": [ - "exojs-leads", - "exojs-leads", - "level" + "level", + "level", + "exojs-leads" ] } } @@ -1419,19 +1420,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 1.4200000000018917, - "referenceP95Ms": 1.7600000000020373, + "referenceMs": 1.4099999999998545, + "referenceP95Ms": 1.819999999999709, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 18.860000000000582, - "competitorP95Ms": 22.5, - "competitorGpuMs": 27.840000000003783, + "competitorMs": 19.31000000000131, + "competitorP95Ms": 23.5, + "competitorGpuMs": 28.459999999999127, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.07529162248154019, - "factor": 13.281690140827786, - "label": "ExoJS leads clearly (13.28x)", + "ratio": 0.07301916105643495, + "factor": 13.69503546099525, + "label": "ExoJS leads clearly (13.70x)", "structural": true }, "mechanism": "ExoJS issues fewer buffer uploads (0 vs 313 per frame)", @@ -1439,14 +1440,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.4099999999998545, - "maxMs": 1.4400000000005093, - "ratio": 1.0212765957451475 + "minMs": 1.3700000000008004, + "maxMs": 1.4200000000000728, + "ratio": 1.0364963503644111 }, "competitor": { - "minMs": 14.56000000000131, - "maxMs": 19.830000000001746, - "ratio": 1.3619505494505468 + "minMs": 19.30000000000291, + "maxMs": 20.780000000002474, + "ratio": 1.0766839378238 }, "stable": true, "rungs": [ @@ -1458,19 +1459,19 @@ }, { "competitor": "phaser", - "referenceMs": 1.4200000000018917, - "referenceP95Ms": 1.7600000000020373, + "referenceMs": 1.4099999999998545, + "referenceP95Ms": 1.819999999999709, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 3.0300000000002, - "competitorP95Ms": 4.700000000000728, + "competitorMs": 2.7600000000002183, + "competitorP95Ms": 7.199999999999818, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.468646864687062, - "factor": 2.1338028168987067, - "label": "ExoJS leads (2.13x)", + "ratio": 0.5108695652172982, + "factor": 1.957446808510995, + "label": "ExoJS leads (1.96x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 313/5000/0 vs 313/5000/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1478,14 +1479,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.4099999999998545, - "maxMs": 1.4400000000005093, - "ratio": 1.0212765957451475 + "minMs": 1.3700000000008004, + "maxMs": 1.4200000000000728, + "ratio": 1.0364963503644111 }, "competitor": { - "minMs": 3.0199999999895226, - "maxMs": 3.0599999999999454, - "ratio": 1.01324503311608 + "minMs": 2.7200000000002547, + "maxMs": 2.9200000000000728, + "ratio": 1.0735294117646321 }, "stable": true, "rungs": [ @@ -1497,19 +1498,19 @@ }, { "competitor": "pixi", - "referenceMs": 1.4200000000018917, - "referenceP95Ms": 1.7600000000020373, + "referenceMs": 1.4099999999998545, + "referenceP95Ms": 1.819999999999709, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, "competitorMs": 1.2200000000002547, - "competitorP95Ms": 2.0599999999994907, + "competitorP95Ms": 2, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 1.1557377049176722, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 313/5000/0 vs 313/5000/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -1517,20 +1518,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.4099999999998545, - "maxMs": 1.4400000000005093, - "ratio": 1.0212765957451475 + "minMs": 1.3700000000008004, + "maxMs": 1.4200000000000728, + "ratio": 1.0364963503644111 }, "competitor": { - "minMs": 1.180000000000291, - "maxMs": 1.2399999999997817, - "ratio": 1.0508474576266744 + "minMs": 1.2200000000002547, + "maxMs": 1.3499999999985448, + "ratio": 1.1065573770477566 }, - "stable": false, + "stable": true, "rungs": [ "level", "level", - "competitor-leads" + "level" ] } } @@ -1546,19 +1547,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.47999999999956344, - "referenceP95Ms": 0.7000000000007276, + "referenceMs": 0.5399999999990541, + "referenceP95Ms": 0.7200000000011642, "referenceGpuMs": 16.679999999996653, "referenceOverFrameBudget": false, - "competitorMs": 9.639999999999418, - "competitorP95Ms": 12.900000000001455, + "competitorMs": 10.080000000001746, + "competitorP95Ms": 13.480000000003201, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.04979253112028967, - "factor": 20.08333333335039, - "label": "ExoJS leads clearly (20.08x)", + "ratio": 0.053571428571325456, + "factor": 18.666666666702596, + "label": "ExoJS leads clearly (18.67x)", "structural": true }, "mechanism": "competitor issues fewer draw calls (120 vs 1 per frame)", @@ -1566,14 +1567,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.4599999999991269, - "maxMs": 0.4899999999997817, - "ratio": 1.065217391305895 + "minMs": 0.47000000000116415, + "maxMs": 0.5399999999990541, + "ratio": 1.1489361702079077 }, "competitor": { - "minMs": 9.520000000000437, - "maxMs": 10.06000000000131, - "ratio": 1.0567226890757193 + "minMs": 9.999999999996362, + "maxMs": 10.349999999998545, + "ratio": 1.035000000000231 }, "stable": true, "rungs": [ @@ -1585,58 +1586,58 @@ }, { "competitor": "phaser", - "referenceMs": 0.47999999999956344, - "referenceP95Ms": 0.7000000000007276, + "referenceMs": 0.5399999999990541, + "referenceP95Ms": 0.7200000000011642, "referenceGpuMs": 16.679999999996653, "referenceOverFrameBudget": false, - "competitorMs": 2.6899999999996, - "competitorP95Ms": 4.819999999999709, + "competitorMs": 2.4099999999998545, + "competitorP95Ms": 4.699999999999818, "competitorGpuMs": 16.660000000000764, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.17843866170990144, - "factor": 5.604166666670929, - "label": "ExoJS leads clearly (5.60x)", - "structural": true + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false }, "mechanism": "competitor issues fewer draw calls (120 vs 1 per frame)", "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.4599999999991269, - "maxMs": 0.4899999999997817, - "ratio": 1.065217391305895 + "minMs": 0.47000000000116415, + "maxMs": 0.5399999999990541, + "ratio": 1.1489361702079077 }, "competitor": { - "minMs": 2.5300000000133878, - "maxMs": 2.869999999999891, - "ratio": 1.1343873517726102 + "minMs": 2.399999999999636, + "maxMs": 2.6400000000003274, + "ratio": 1.1000000000003032 }, - "stable": true, + "stable": false, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", + "exojs-leads", + "exojs-leads", "exojs-leads-clearly" ] } }, { "competitor": "pixi", - "referenceMs": 0.47999999999956344, - "referenceP95Ms": 0.7000000000007276, + "referenceMs": 0.5399999999990541, + "referenceP95Ms": 0.7200000000011642, "referenceGpuMs": 16.679999999996653, "referenceOverFrameBudget": false, - "competitorMs": 0.3000000000029104, - "competitorP95Ms": 0.4599999999991269, - "competitorGpuMs": 16.659999999999854, + "competitorMs": 0.31999999999970896, + "competitorP95Ms": 0.42000000000189175, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.5999999999830228, - "factor": 1.5999999999830228, - "label": "competitor leads (1.60x)", + "ratio": 1.687499999998579, + "factor": 1.687499999998579, + "label": "competitor leads (1.69x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 120/0/0 vs 158/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -1644,14 +1645,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.4599999999991269, - "maxMs": 0.4899999999997817, - "ratio": 1.065217391305895 + "minMs": 0.47000000000116415, + "maxMs": 0.5399999999990541, + "ratio": 1.1489361702079077 }, "competitor": { - "minMs": 0.2999999999992724, + "minMs": 0.3000000000029104, "maxMs": 0.31999999999970896, - "ratio": 1.0666666666682836 + "ratio": 1.0666666666553486 }, "stable": true, "rungs": [ @@ -1680,17 +1681,17 @@ "competitor": "excalibur", "referenceMs": 0.08000000000174623, "referenceP95Ms": 0.13999999999941792, - "referenceGpuMs": 16.659999999999854, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 10.099999999998545, - "competitorP95Ms": 14, + "competitorMs": 10.569999999999709, + "competitorP95Ms": 14.520000000004075, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.007920792079381955, - "factor": 126.24999999722606, - "label": "ExoJS leads clearly (126.25x)", + "ratio": 0.007568590350212718, + "factor": 132.12499999711235, + "label": "ExoJS leads clearly (132.12x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1698,14 +1699,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.07999999999810825, - "maxMs": 0.09999999999854481, - "ratio": 1.2500000000113687 + "minMs": 0.08000000000174623, + "maxMs": 0.08000000000174623, + "ratio": 1 }, "competitor": { - "minMs": 9.25, - "maxMs": 10.489999999997963, - "ratio": 1.1340540540538337 + "minMs": 10.130000000001019, + "maxMs": 10.700000000000728, + "ratio": 1.0562685093780506 }, "stable": true, "rungs": [ @@ -1719,17 +1720,17 @@ "competitor": "phaser", "referenceMs": 0.08000000000174623, "referenceP95Ms": 0.13999999999941792, - "referenceGpuMs": 16.659999999999854, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 2.75, - "competitorP95Ms": 4.540000000000873, - "competitorGpuMs": 16.67999999999938, + "competitorMs": 2.730000000000018, + "competitorP95Ms": 5, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.029090909091544083, - "factor": 34.37499999924967, - "label": "ExoJS leads clearly (34.37x)", + "ratio": 0.029304029304668754, + "factor": 34.12499999925535, + "label": "ExoJS leads clearly (34.12x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1737,14 +1738,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.07999999999810825, - "maxMs": 0.09999999999854481, - "ratio": 1.2500000000113687 + "minMs": 0.08000000000174623, + "maxMs": 0.08000000000174623, + "ratio": 1 }, "competitor": { - "minMs": 2.4400000000023283, - "maxMs": 2.9100000000003092, - "ratio": 1.1926229508186608 + "minMs": 2.5200000000004366, + "maxMs": 2.9600000000000364, + "ratio": 1.1746031746029855 }, "stable": true, "rungs": [ @@ -1758,17 +1759,17 @@ "competitor": "pixi", "referenceMs": 0.08000000000174623, "referenceP95Ms": 0.13999999999941792, - "referenceGpuMs": 16.659999999999854, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 0.11999999999898137, - "competitorP95Ms": 0.2000000000007276, + "competitorMs": 0.09999999999854481, + "competitorP95Ms": 0.21999999999752617, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 0.8000000000291039, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -1776,19 +1777,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.07999999999810825, - "maxMs": 0.09999999999854481, - "ratio": 1.2500000000113687 + "minMs": 0.08000000000174623, + "maxMs": 0.08000000000174623, + "ratio": 1 }, "competitor": { - "minMs": 0.09000000000014552, - "maxMs": 0.11999999999898137, - "ratio": 1.3333333333198594 + "minMs": 0.09999999999854481, + "maxMs": 0.1000000000003638, + "ratio": 1.00000000001819 }, - "stable": false, + "stable": true, "rungs": [ "level", - "exojs-leads", + "level", "level" ] } @@ -1810,19 +1811,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.09999999999854481, - "referenceP95Ms": 0.19999999999708962, - "referenceGpuMs": 16.68000000000029, + "referenceMs": 0.10000000000218279, + "referenceP95Ms": 0.22000000000116415, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 6.470000000001164, - "competitorP95Ms": 9.059999999997672, + "competitorMs": 6.579999999998108, + "competitorP95Ms": 9.160000000003492, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.015455950540730574, - "factor": 64.70000000095315, - "label": "ExoJS leads clearly (64.70x)", + "ratio": 0.01519756838939385, + "factor": 65.7999999985448, + "label": "ExoJS leads clearly (65.80x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1831,13 +1832,13 @@ "runs": 3, "reference": { "minMs": 0.09999999999854481, - "maxMs": 0.13999999999941792, - "ratio": 1.4000000000145518 + "maxMs": 0.10000000000218279, + "ratio": 1.0000000000363798 }, "competitor": { - "minMs": 5.860000000000582, - "maxMs": 7.009999999998399, - "ratio": 1.196245733788004 + "minMs": 6.519999999996799, + "maxMs": 7.1599999999998545, + "ratio": 1.0981595092029708 }, "stable": true, "rungs": [ @@ -1849,18 +1850,18 @@ }, { "competitor": "phaser", - "referenceMs": 0.09999999999854481, - "referenceP95Ms": 0.19999999999708962, - "referenceGpuMs": 16.68000000000029, + "referenceMs": 0.10000000000218279, + "referenceP95Ms": 0.22000000000116415, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, "competitorMs": 2.9200000000000728, - "competitorP95Ms": 4.240000000001601, - "competitorGpuMs": 16.660000000003492, + "competitorP95Ms": 3.9799999999995634, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.03424657534196655, - "factor": 29.200000000425643, + "ratio": 0.03424657534321243, + "factor": 29.199999999363353, "label": "ExoJS leads clearly (29.20x)", "structural": true }, @@ -1870,13 +1871,13 @@ "runs": 3, "reference": { "minMs": 0.09999999999854481, - "maxMs": 0.13999999999941792, - "ratio": 1.4000000000145518 + "maxMs": 0.10000000000218279, + "ratio": 1.0000000000363798 }, "competitor": { - "minMs": 2.710000000006403, - "maxMs": 3.040000000000873, - "ratio": 1.121771217709849 + "minMs": 2.8900000000003274, + "maxMs": 2.9399999999986903, + "ratio": 1.0173010380617153 }, "stable": true, "rungs": [ @@ -1888,9 +1889,9 @@ }, { "competitor": "pixi", - "referenceMs": 0.09999999999854481, - "referenceP95Ms": 0.19999999999708962, - "referenceGpuMs": 16.68000000000029, + "referenceMs": 0.10000000000218279, + "referenceP95Ms": 0.22000000000116415, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, "competitorMs": 0.10000000000218279, "competitorP95Ms": 0.22000000000116415, @@ -1898,7 +1899,7 @@ "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9999999999636202, + "ratio": 1, "factor": 1, "label": "level", "structural": false @@ -1909,13 +1910,13 @@ "runs": 3, "reference": { "minMs": 0.09999999999854481, - "maxMs": 0.13999999999941792, - "ratio": 1.4000000000145518 + "maxMs": 0.10000000000218279, + "ratio": 1.0000000000363798 }, "competitor": { - "minMs": 0.10000000000218279, - "maxMs": 0.13999999999941792, - "ratio": 1.3999999999636201 + "minMs": 0.09999999999854481, + "maxMs": 0.11999999999898137, + "ratio": 1.200000000007276 }, "stable": true, "rungs": [ @@ -1938,18 +1939,18 @@ { "competitor": "excalibur", "referenceMs": 0.09999999999854481, - "referenceP95Ms": 0.18000000000029104, + "referenceP95Ms": 0.15999999999985448, "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 33.38000000000102, - "competitorP95Ms": 37.81999999999971, - "competitorGpuMs": 34.55999999999767, + "competitorMs": 32.89999999999782, + "competitorP95Ms": 38.400000000001455, + "competitorGpuMs": 34.54000000000087, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.0029958058717358227, - "factor": 333.8000000048676, - "label": "ExoJS leads clearly (333.80x)", + "ratio": 0.003039513677767521, + "factor": 329.00000000476575, + "label": "ExoJS leads clearly (329.00x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 3/4/3, none of them large enough to account for the gap); the difference is CPU-side", @@ -1957,14 +1958,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.09999999999854481, - "maxMs": 0.11999999999898137, - "ratio": 1.200000000007276 + "minMs": 0.09000000000014552, + "maxMs": 0.09999999999854481, + "ratio": 1.1111111110931458 }, "competitor": { - "minMs": 28.489999999997963, - "maxMs": 33.400000000001455, - "ratio": 1.1723411723413073 + "minMs": 32.58999999999651, + "maxMs": 47.19000000000233, + "ratio": 1.4479901810373546 }, "stable": true, "rungs": [ @@ -1977,18 +1978,18 @@ { "competitor": "phaser", "referenceMs": 0.09999999999854481, - "referenceP95Ms": 0.18000000000029104, + "referenceP95Ms": 0.15999999999985448, "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 6.64000000001397, - "competitorP95Ms": 7.93999999999869, + "competitorMs": 6.710000000000946, + "competitorP95Ms": 8.119999999998981, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.01506024096360458, - "factor": 66.40000000110595, - "label": "ExoJS leads clearly (66.40x)", + "ratio": 0.014903129657009047, + "factor": 67.1000000009859, + "label": "ExoJS leads clearly (67.10x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 4/0/4, none of them large enough to account for the gap); the difference is CPU-side", @@ -1996,14 +1997,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.09999999999854481, - "maxMs": 0.11999999999898137, - "ratio": 1.200000000007276 + "minMs": 0.09000000000014552, + "maxMs": 0.09999999999854481, + "ratio": 1.1111111110931458 }, "competitor": { - "minMs": 6.410000000000764, - "maxMs": 6.650000000000546, - "ratio": 1.0374414976598678 + "minMs": 6.6899999999996, + "maxMs": 6.760000000000218, + "ratio": 1.0104633781764758 }, "stable": true, "rungs": [ @@ -2016,16 +2017,16 @@ { "competitor": "pixi", "referenceMs": 0.09999999999854481, - "referenceP95Ms": 0.18000000000029104, + "referenceP95Ms": 0.15999999999985448, "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 0.11999999999898137, - "competitorP95Ms": 0.23999999999796273, + "competitorMs": 0.09999999999854481, + "competitorP95Ms": 0.21999999999752617, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.8333333333282806, + "ratio": 1, "factor": 1, "label": "level", "structural": false @@ -2035,14 +2036,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.09999999999854481, - "maxMs": 0.11999999999898137, - "ratio": 1.200000000007276 + "minMs": 0.09000000000014552, + "maxMs": 0.09999999999854481, + "ratio": 1.1111111110931458 }, "competitor": { - "minMs": 0.09999999999854481, - "maxMs": 0.11999999999898137, - "ratio": 1.200000000007276 + "minMs": 0.09000000000014552, + "maxMs": 0.09999999999854481, + "ratio": 1.1111111110931458 }, "stable": true, "rungs": [ @@ -2064,19 +2065,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 2.6199999999989814, - "referenceP95Ms": 7.900000000001455, + "referenceMs": 2.539999999997235, + "referenceP95Ms": 7.1599999999998545, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 6.920000000001892, - "competitorP95Ms": 9.739999999997963, + "competitorMs": 7.019999999996799, + "competitorP95Ms": 9.30000000000291, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.3786127167627551, - "factor": 2.64122137404755, - "label": "ExoJS leads (2.64x)", + "ratio": 0.36182336182313296, + "factor": 2.7637795275608035, + "label": "ExoJS leads (2.76x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -2084,14 +2085,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.460000000000946, - "maxMs": 2.8000000000010914, - "ratio": 1.138211382113827 + "minMs": 2.4899999999997817, + "maxMs": 2.5799999999981083, + "ratio": 1.036144578312584 }, "competitor": { - "minMs": 6.140000000003056, - "maxMs": 6.990000000001601, - "ratio": 1.1384364820843846 + "minMs": 6.930000000000291, + "maxMs": 7.1599999999998545, + "ratio": 1.0331890331889688 }, "stable": true, "rungs": [ @@ -2103,17 +2104,17 @@ }, { "competitor": "phaser", - "referenceMs": 2.6199999999989814, - "referenceP95Ms": 7.900000000001455, + "referenceMs": 2.539999999997235, + "referenceP95Ms": 7.1599999999998545, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 2.8900000000003274, - "competitorP95Ms": 5.860000000000582, + "competitorMs": 2.8200000000006185, + "competitorP95Ms": 5.600000000000364, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9065743944632126, + "ratio": 0.900709219856978, "factor": 1, "label": "level", "structural": false @@ -2123,14 +2124,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.460000000000946, - "maxMs": 2.8000000000010914, - "ratio": 1.138211382113827 + "minMs": 2.4899999999997817, + "maxMs": 2.5799999999981083, + "ratio": 1.036144578312584 }, "competitor": { - "minMs": 2.5899999999819556, - "maxMs": 2.8900000000003274, - "ratio": 1.1158301158380162 + "minMs": 2.7799999999997453, + "maxMs": 3.0200000000004366, + "ratio": 1.0863309352520552 }, "stable": true, "rungs": [ @@ -2142,19 +2143,19 @@ }, { "competitor": "pixi", - "referenceMs": 2.6199999999989814, - "referenceP95Ms": 7.900000000001455, + "referenceMs": 2.539999999997235, + "referenceP95Ms": 7.1599999999998545, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 4.309999999999491, - "competitorP95Ms": 5.799999999999272, + "competitorMs": 4.43999999999869, + "competitorP95Ms": 5.900000000001455, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.6078886310903228, - "factor": 1.6450381679393764, - "label": "ExoJS leads (1.65x)", + "ratio": 0.5720720720716181, + "factor": 1.7480314960643792, + "label": "ExoJS leads (1.75x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -2162,14 +2163,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.460000000000946, - "maxMs": 2.8000000000010914, - "ratio": 1.138211382113827 + "minMs": 2.4899999999997817, + "maxMs": 2.5799999999981083, + "ratio": 1.036144578312584 }, "competitor": { - "minMs": 4.209999999999127, + "minMs": 4.230000000001382, "maxMs": 4.5, - "ratio": 1.068883610451528 + "ratio": 1.0638297872336948 }, "stable": true, "rungs": [ @@ -2191,19 +2192,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 7.199999999998909, - "referenceP95Ms": 9.119999999998981, + "referenceMs": 7.130000000001019, + "referenceP95Ms": 9.139999999999418, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 35.17999999999665, - "competitorP95Ms": 42.879999999990105, - "competitorGpuMs": 36.94000000000233, + "competitorMs": 34.44000000000233, + "competitorP95Ms": 39.16000000000349, + "competitorGpuMs": 36.19999999999709, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.20466173962477527, - "factor": 4.886111111111386, - "label": "ExoJS leads (4.89x)", + "ratio": 0.2070267131242897, + "factor": 4.830294530153914, + "label": "ExoJS leads (4.83x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 3/4/3, none of them large enough to account for the gap); the difference is CPU-side", @@ -2211,14 +2212,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 7.170000000000073, - "maxMs": 7.3400000000001455, - "ratio": 1.0237099023710001 + "minMs": 7.1299999999992, + "maxMs": 7.570000000001528, + "ratio": 1.0617110799442324 }, "competitor": { - "minMs": 34.86999999999898, - "maxMs": 35.450000000004366, - "ratio": 1.016633209062386 + "minMs": 34.31999999999971, + "maxMs": 35.36000000000058, + "ratio": 1.030303030303056 }, "stable": true, "rungs": [ @@ -2230,17 +2231,17 @@ }, { "competitor": "phaser", - "referenceMs": 7.199999999998909, - "referenceP95Ms": 9.119999999998981, + "referenceMs": 7.130000000001019, + "referenceP95Ms": 9.139999999999418, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 7.039999999979045, - "competitorP95Ms": 8.3799999999992, + "competitorMs": 7, + "competitorP95Ms": 8.55999999999949, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.022727272730162, + "ratio": 1.0185714285715741, "factor": 1, "label": "level", "structural": false @@ -2250,14 +2251,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 7.170000000000073, - "maxMs": 7.3400000000001455, - "ratio": 1.0237099023710001 + "minMs": 7.1299999999992, + "maxMs": 7.570000000001528, + "ratio": 1.0617110799442324 }, "competitor": { - "minMs": 6.8200000000006185, - "maxMs": 7.039999999999054, - "ratio": 1.0322580645158967 + "minMs": 6.989999999999782, + "maxMs": 7.130000000000109, + "ratio": 1.020028612303338 }, "stable": true, "rungs": [ @@ -2269,19 +2270,19 @@ }, { "competitor": "pixi", - "referenceMs": 7.199999999998909, - "referenceP95Ms": 9.119999999998981, + "referenceMs": 7.130000000001019, + "referenceP95Ms": 9.139999999999418, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 18.8899999999976, - "competitorP95Ms": 22.340000000000146, - "competitorGpuMs": 20.700000000000728, + "competitorMs": 18.910000000001673, + "competitorP95Ms": 24.299999999999272, + "competitorGpuMs": 20.979999999999563, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.38115404976176936, - "factor": 2.6236111111111753, - "label": "ExoJS leads (2.62x)", + "ratio": 0.37704918032788937, + "factor": 2.652173913043334, + "label": "ExoJS leads (2.65x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -2289,14 +2290,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 7.170000000000073, - "maxMs": 7.3400000000001455, - "ratio": 1.0237099023710001 + "minMs": 7.1299999999992, + "maxMs": 7.570000000001528, + "ratio": 1.0617110799442324 }, "competitor": { - "minMs": 18.520000000000437, - "maxMs": 19.220000000001164, - "ratio": 1.037796976241939 + "minMs": 18.860000000000582, + "maxMs": 19.290000000000873, + "ratio": 1.02279957582186 }, "stable": true, "rungs": [ @@ -2323,20 +2324,20 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.38000000000101863, - "referenceP95Ms": 0.7600000000020373, + "referenceMs": 0.3600000000005821, + "referenceP95Ms": 0.6399999999994179, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, "competitorMs": 2.100000000000364, - "competitorP95Ms": 4.620000000002619, + "competitorP95Ms": 3.540000000000873, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", - "structural": false + "side": "exojs", + "ratio": 0.1714285714288189, + "factor": 5.833333333324912, + "label": "ExoJS leads clearly (5.83x)", + "structural": true }, "mechanism": "competitor issues fewer texture binds (9 vs 3 per frame)", "timer": "resolved", @@ -2344,19 +2345,19 @@ "runs": 3, "reference": { "minMs": 0.3400000000001455, - "maxMs": 0.42000000000189175, - "ratio": 1.2352941176520942 + "maxMs": 0.38000000000101863, + "ratio": 1.117647058826047 }, "competitor": { - "minMs": 2.069999999999709, - "maxMs": 2.429999999998472, - "ratio": 1.1739130434776879 + "minMs": 2.0099999999983993, + "maxMs": 2.3099999999994907, + "ratio": 1.1492537313439455 }, - "stable": false, + "stable": true, "rungs": [ "exojs-leads-clearly", "exojs-leads-clearly", - "exojs-leads" + "exojs-leads-clearly" ] } } @@ -2372,19 +2373,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.3600000000005821, - "referenceP95Ms": 0.5600000000049477, + "referenceMs": 0.31999999999970896, + "referenceP95Ms": 0.5, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 2.5600000000013097, - "competitorP95Ms": 5.880000000004657, - "competitorGpuMs": 16.68000000000029, + "competitorMs": 2.609999999996944, + "competitorP95Ms": 4.1599999999962165, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.14062500000015543, - "factor": 7.111111111103251, - "label": "ExoJS leads clearly (7.11x)", + "ratio": 0.12260536398470637, + "factor": 8.156249999997868, + "label": "ExoJS leads clearly (8.16x)", "structural": true }, "mechanism": "competitor issues fewer texture binds (11 vs 3 per frame)", @@ -2393,13 +2394,13 @@ "runs": 3, "reference": { "minMs": 0.3100000000013097, - "maxMs": 0.4000000000014552, - "ratio": 1.290322580644404 + "maxMs": 0.31999999999970896, + "ratio": 1.0322580645108292 }, "competitor": { - "minMs": 2.3099999999976717, - "maxMs": 2.61000000000422, - "ratio": 1.1298701298730955 + "minMs": 2.099999999998545, + "maxMs": 2.7599999999965803, + "ratio": 1.3142857142849966 }, "stable": true, "rungs": [ @@ -2421,19 +2422,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.31999999999970896, - "referenceP95Ms": 0.4800000000032014, + "referenceMs": 0.27999999999883585, + "referenceP95Ms": 0.5, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 2.5, - "competitorP95Ms": 4.980000000003201, - "competitorGpuMs": 16.660000000003492, + "competitorMs": 2.570000000003347, + "competitorP95Ms": 4.220000000001164, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.1279999999998836, - "factor": 7.8125000000071045, - "label": "ExoJS leads clearly (7.81x)", + "ratio": 0.10894941634181758, + "factor": 9.178571428621543, + "label": "ExoJS leads clearly (9.18x)", "structural": true }, "mechanism": "competitor issues fewer texture binds (15 vs 7 per frame)", @@ -2442,13 +2443,13 @@ "runs": 3, "reference": { "minMs": 0.27999999999883585, - "maxMs": 0.32999999999810825, - "ratio": 1.1785714285695725 + "maxMs": 0.2999999999956344, + "ratio": 1.0714285714174347 }, "competitor": { - "minMs": 2.349999999998545, - "maxMs": 3.0100000000020373, - "ratio": 1.2808510638314472 + "minMs": 2.3100000000013097, + "maxMs": 2.599999999998545, + "ratio": 1.1255411255398575 }, "stable": true, "rungs": [ @@ -2471,18 +2472,18 @@ { "competitor": "pixi", "referenceMs": 0.31999999999970896, - "referenceP95Ms": 0.5400000000008731, - "referenceGpuMs": 16.660000000003492, + "referenceP95Ms": 0.6000000000058208, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 0.5299999999988358, - "competitorP95Ms": 0.8000000000029104, + "competitorMs": 0.4599999999991269, + "competitorP95Ms": 0.7200000000011642, "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.6037735849064374, - "factor": 1.6562499999978686, - "label": "ExoJS leads (1.66x)", + "ratio": 0.6956521739137312, + "factor": 1.4374999999985787, + "label": "ExoJS leads (1.44x)", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (5 vs 8 per frame)", @@ -2491,13 +2492,13 @@ "runs": 3, "reference": { "minMs": 0.31999999999970896, - "maxMs": 0.33999999999650754, - "ratio": 1.0624999999900524 + "maxMs": 0.32999999999810825, + "ratio": 1.0312499999950262 }, "competitor": { - "minMs": 0.4599999999991269, - "maxMs": 0.6199999999953434, - "ratio": 1.347826086948957 + "minMs": 0.41999999999825377, + "maxMs": 0.47999999999592546, + "ratio": 1.1428571428521932 }, "stable": true, "rungs": [ @@ -2519,19 +2520,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.2400000000016007, + "referenceMs": 0.250000000003638, "referenceP95Ms": 0.4400000000023283, - "referenceGpuMs": 16.660000000003492, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 0.19999999999708962, - "competitorP95Ms": 0.3400000000037835, - "competitorGpuMs": 16.679999999993015, + "competitorMs": 0.15999999999985448, + "competitorP95Ms": 0.27999999999883585, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 1.5625000000241585, + "factor": 1.5625000000241585, + "label": "competitor leads (1.56x)", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", @@ -2539,18 +2540,18 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.23999999999796273, - "maxMs": 0.27000000000043656, - "ratio": 1.1250000000113687 + "minMs": 0.22999999999956344, + "maxMs": 0.26000000000203727, + "ratio": 1.130434782619699 }, "competitor": { - "minMs": 0.1599999999962165, - "maxMs": 0.27999999999883585, - "ratio": 1.750000000034106 + "minMs": 0.13999999999941792, + "maxMs": 0.16000000000349246, + "ratio": 1.1428571428868406 }, - "stable": false, + "stable": true, "rungs": [ - "level", + "competitor-leads", "competitor-leads", "competitor-leads" ] @@ -2568,19 +2569,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.2599999999947613, - "referenceP95Ms": 0.4800000000032014, - "referenceGpuMs": 16.679999999993015, + "referenceMs": 0.26000000000203727, + "referenceP95Ms": 0.4400000000023283, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.46000000000276486, - "competitorP95Ms": 0.6999999999970896, + "competitorMs": 0.4099999999998545, + "competitorP95Ms": 0.6399999999994179, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.565217391289562, - "factor": 1.7692307692770513, - "label": "ExoJS leads (1.77x)", + "ratio": 0.6341463414686087, + "factor": 1.576923076910161, + "label": "ExoJS leads (1.58x)", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", @@ -2588,14 +2589,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.2400000000052387, - "maxMs": 0.27999999999883585, - "ratio": 1.16666666663635 + "minMs": 0.26000000000203727, + "maxMs": 0.26000000000203727, + "ratio": 1 }, "competitor": { - "minMs": 0.4000000000014552, - "maxMs": 0.5499999999992724, - "ratio": 1.3749999999931788 + "minMs": 0.4099999999998545, + "maxMs": 0.4399999999986903, + "ratio": 1.0731707317045036 }, "stable": true, "rungs": [ @@ -2622,19 +2623,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.2000000000007276, - "referenceP95Ms": 1.6000000000021828, + "referenceMs": 0.18999999999869033, + "referenceP95Ms": 1.4400000000023283, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 0.10000000000218279, + "competitorMs": 0.11999999999898137, "competitorP95Ms": 0.22000000000116415, - "competitorGpuMs": 16.659999999999854, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.9999999999636202, - "factor": 1.9999999999636202, - "label": "competitor leads (2.00x)", + "ratio": 1.5833333333358597, + "factor": 1.5833333333358597, + "label": "competitor leads (1.58x)", "structural": false }, "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", @@ -2642,9 +2643,9 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.18000000000029104, - "maxMs": 0.2000000000007276, - "ratio": 1.1111111111133567 + "minMs": 0.17999999999847205, + "maxMs": 0.1999999999989086, + "ratio": 1.1111111111144796 }, "competitor": { "minMs": 0.10000000000218279, @@ -2676,19 +2677,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.20000000000436557, - "referenceP95Ms": 1.2600000000020373, - "referenceGpuMs": 16.660000000003492, + "referenceMs": 0.18000000000029104, + "referenceP95Ms": 1.180000000000291, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 20.910000000003492, - "competitorP95Ms": 26.019999999989523, - "competitorGpuMs": 22.59999999999127, + "competitorMs": 20.790000000000873, + "competitorP95Ms": 25.69999999999709, + "competitorGpuMs": 21.94000000000233, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.009564801530575427, - "factor": 104.54999999773536, - "label": "ExoJS leads clearly (104.55x)", + "ratio": 0.008658008658022293, + "factor": 115.4999999998181, + "label": "ExoJS leads clearly (115.50x)", "structural": true }, "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", @@ -2696,14 +2697,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.18000000000029104, - "maxMs": 0.21000000000276486, - "ratio": 1.1666666666801406 + "minMs": 0.1599999999962165, + "maxMs": 0.18000000000029104, + "ratio": 1.1250000000284217 }, "competitor": { - "minMs": 20.859999999993306, - "maxMs": 22.150000000001455, - "ratio": 1.061840843720449 + "minMs": 20.60000000000582, + "maxMs": 21.860000000007858, + "ratio": 1.061165048543771 }, "stable": true, "rungs": [ @@ -2715,19 +2716,19 @@ }, { "competitor": "phaser", - "referenceMs": 0.20000000000436557, - "referenceP95Ms": 1.2600000000020373, - "referenceGpuMs": 16.660000000003492, + "referenceMs": 0.18000000000029104, + "referenceP95Ms": 1.180000000000291, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 0.2000000000007276, - "competitorP95Ms": 0.4000000000014552, + "competitorMs": 0.20999999999912689, + "competitorP95Ms": 0.38000000000101863, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.00000000001819, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", @@ -2735,31 +2736,31 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.18000000000029104, - "maxMs": 0.21000000000276486, - "ratio": 1.1666666666801406 + "minMs": 0.1599999999962165, + "maxMs": 0.18000000000029104, + "ratio": 1.1250000000284217 }, "competitor": { - "minMs": 0.180000000007567, + "minMs": 0.2000000000007276, "maxMs": 0.21999999999752617, - "ratio": 1.222222222157098 + "ratio": 1.0999999999836292 }, - "stable": true, + "stable": false, "rungs": [ "level", - "level", + "exojs-leads", "level" ] } }, { "competitor": "pixi", - "referenceMs": 0.20000000000436557, - "referenceP95Ms": 1.2600000000020373, - "referenceGpuMs": 16.660000000003492, + "referenceMs": 0.18000000000029104, + "referenceP95Ms": 1.180000000000291, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 0.26000000000203727, - "competitorP95Ms": 0.5, + "competitorMs": 0.22000000000116415, + "competitorP95Ms": 0.39999999999417923, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { @@ -2774,18 +2775,18 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.18000000000029104, - "maxMs": 0.21000000000276486, - "ratio": 1.1666666666801406 + "minMs": 0.1599999999962165, + "maxMs": 0.18000000000029104, + "ratio": 1.1250000000284217 }, "competitor": { - "minMs": 0.26000000000203727, - "maxMs": 0.3899999999994179, - "ratio": 1.4999999999860079 + "minMs": 0.2199999999938882, + "maxMs": 0.23999999999796273, + "ratio": 1.090909090930137 }, "stable": false, "rungs": [ - "exojs-leads", + "level", "exojs-leads", "level" ] @@ -2804,18 +2805,18 @@ { "competitor": "excalibur", "referenceMs": 0.13999999999941792, - "referenceP95Ms": 0.8600000000005821, - "referenceGpuMs": 16.660000000003492, + "referenceP95Ms": 0.7399999999979627, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 184.3199999999997, - "competitorP95Ms": 192.36000000000058, - "competitorGpuMs": 192.51999999998952, + "competitorMs": 186.42000000000553, + "competitorP95Ms": 196.44000000000233, + "competitorGpuMs": 193.6999999999971, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.0007595486111079544, - "factor": 1316.5714285769004, - "label": "ExoJS leads clearly (1316.57x)", + "ratio": 0.0007509923827884013, + "factor": 1331.5714285770043, + "label": "ExoJS leads clearly (1331.57x)", "structural": true }, "mechanism": "competitor issues fewer draw calls (30 vs 5 per frame)", @@ -2823,14 +2824,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.13000000000101863, - "maxMs": 0.13999999999941792, - "ratio": 1.076923076910161 + "minMs": 0.12000000000261934, + "maxMs": 0.16000000000349246, + "ratio": 1.3333333333333333 }, "competitor": { - "minMs": 181.1800000000003, + "minMs": 182.55999999999767, "maxMs": 189.13999999999942, - "ratio": 1.0439342090738444 + "ratio": 1.0360429447852861 }, "stable": true, "rungs": [ @@ -2843,18 +2844,18 @@ { "competitor": "phaser", "referenceMs": 0.13999999999941792, - "referenceP95Ms": 0.8600000000005821, - "referenceGpuMs": 16.660000000003492, + "referenceP95Ms": 0.7399999999979627, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 0.18000000000029104, - "competitorP95Ms": 0.3600000000005821, - "competitorGpuMs": 16.659999999999854, + "competitorMs": 0.19000000000050932, + "competitorP95Ms": 0.3400000000001455, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.7777777777732865, - "factor": 1.28571428572171, - "label": "ExoJS leads (1.29x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "competitor issues fewer draw calls (30 vs 1 per frame)", @@ -2862,38 +2863,38 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.13000000000101863, - "maxMs": 0.13999999999941792, - "ratio": 1.076923076910161 + "minMs": 0.12000000000261934, + "maxMs": 0.16000000000349246, + "ratio": 1.3333333333333333 }, "competitor": { - "minMs": 0.17999999999301508, - "maxMs": 0.18999999999869033, - "ratio": 1.0555555555892406 + "minMs": 0.18000000000029104, + "maxMs": 0.2000000000007276, + "ratio": 1.1111111111133567 }, - "stable": true, + "stable": false, "rungs": [ "exojs-leads", "exojs-leads", - "exojs-leads" + "level" ] } }, { "competitor": "pixi", "referenceMs": 0.13999999999941792, - "referenceP95Ms": 0.8600000000005821, - "referenceGpuMs": 16.660000000003492, + "referenceP95Ms": 0.7399999999979627, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 0.27999999999883585, - "competitorP95Ms": 0.4400000000023283, - "competitorGpuMs": 16.68000000000029, + "competitorMs": 0.18000000000029104, + "competitorP95Ms": 0.41999999999825377, + "competitorGpuMs": 16.659999999996217, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.5, - "factor": 2, - "label": "ExoJS leads (2.00x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "competitor issues fewer draw calls (30 vs 2 per frame)", @@ -2901,20 +2902,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.13000000000101863, - "maxMs": 0.13999999999941792, - "ratio": 1.076923076910161 + "minMs": 0.12000000000261934, + "maxMs": 0.16000000000349246, + "ratio": 1.3333333333333333 }, "competitor": { - "minMs": 0.20999999999912689, - "maxMs": 0.4800000000032014, - "ratio": 2.285714285739034 + "minMs": 0.18000000000029104, + "maxMs": 0.21999999999752617, + "ratio": 1.2222222222065025 }, - "stable": true, + "stable": false, "rungs": [ "exojs-leads", "exojs-leads", - "exojs-leads" + "level" ] } } @@ -2930,19 +2931,19 @@ "cells": [ { "competitor": "phaser", - "referenceMs": 0.680000000000291, - "referenceP95Ms": 1.2399999999979627, - "referenceGpuMs": 16.660000000003492, + "referenceMs": 0.7800000000024738, + "referenceP95Ms": 1.319999999999709, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 1.680000000000291, - "competitorP95Ms": 7.059999999997672, + "competitorMs": 1.7199999999993452, + "competitorP95Ms": 5.680000000000291, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.4047619047620079, - "factor": 2.470588235293488, - "label": "ExoJS leads (2.47x)", + "ratio": 0.45348837209463416, + "factor": 2.205128205120372, + "label": "ExoJS leads (2.21x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/3/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -2950,14 +2951,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5800000000017462, - "maxMs": 0.819999999999709, - "ratio": 1.4137931034435176 + "minMs": 0.75, + "maxMs": 0.7900000000008731, + "ratio": 1.0533333333344974 }, "competitor": { - "minMs": 1.5599999999976717, - "maxMs": 2.4799999999995634, - "ratio": 1.5897435897456826 + "minMs": 1.6200000000008004, + "maxMs": 2.4499999999989086, + "ratio": 1.5123456790109249 }, "stable": true, "rungs": [ @@ -2969,19 +2970,19 @@ }, { "competitor": "pixi", - "referenceMs": 0.680000000000291, - "referenceP95Ms": 1.2399999999979627, - "referenceGpuMs": 16.660000000003492, + "referenceMs": 0.7800000000024738, + "referenceP95Ms": 1.319999999999709, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 1.209999999995489, - "competitorP95Ms": 1.819999999999709, - "competitorGpuMs": 16.68000000000029, + "competitorMs": 0.9799999999995634, + "competitorP95Ms": 1.9399999999950523, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.5619834710767159, - "factor": 1.7794117646984868, - "label": "ExoJS leads (1.78x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 2/0/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -2989,20 +2990,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5800000000017462, - "maxMs": 0.819999999999709, - "ratio": 1.4137931034435176 + "minMs": 0.75, + "maxMs": 0.7900000000008731, + "ratio": 1.0533333333344974 }, "competitor": { - "minMs": 1.069999999999709, - "maxMs": 1.319999999999709, - "ratio": 1.2336448598131478 + "minMs": 0.7300000000032014, + "maxMs": 1, + "ratio": 1.3698630136926226 }, - "stable": true, + "stable": false, "rungs": [ "exojs-leads", "exojs-leads", - "exojs-leads" + "level" ] } } @@ -3023,19 +3024,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.26000000000203727, - "referenceP95Ms": 0.41999999999825377, - "referenceGpuMs": 16.68000000000029, + "referenceMs": 0.27999999999883585, + "referenceP95Ms": 0.3800000000046566, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 2.4499999999970896, - "competitorP95Ms": 8.740000000005239, + "competitorMs": 2.4099999999962165, + "competitorP95Ms": 6.580000000001746, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.10612244898054944, - "factor": 9.423076922991893, - "label": "ExoJS leads clearly (9.42x)", + "ratio": 0.11618257261380723, + "factor": 8.607142857165131, + "label": "ExoJS leads clearly (8.61x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3044,13 +3045,13 @@ "runs": 3, "reference": { "minMs": 0.2599999999947613, - "maxMs": 0.3000000000029104, - "ratio": 1.1538461538805962 + "maxMs": 0.27999999999883585, + "ratio": 1.0769230769402982 }, "competitor": { - "minMs": 2.4400000000096043, - "maxMs": 2.639999999999418, - "ratio": 1.0819672131102567 + "minMs": 2.290000000000873, + "maxMs": 2.4400000000023283, + "ratio": 1.065502183406724 }, "stable": true, "rungs": [ @@ -3062,19 +3063,19 @@ }, { "competitor": "phaser", - "referenceMs": 0.26000000000203727, - "referenceP95Ms": 0.41999999999825377, - "referenceGpuMs": 16.68000000000029, + "referenceMs": 0.27999999999883585, + "referenceP95Ms": 0.3800000000046566, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.8400000000001455, + "competitorMs": 0.8800000000010186, "competitorP95Ms": 1.0600000000013097, - "competitorGpuMs": 16.679999999996653, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.30952380952618125, - "factor": 3.230769230744475, - "label": "ExoJS leads (3.23x)", + "ratio": 0.318181818180127, + "factor": 3.1428571428738477, + "label": "ExoJS leads (3.14x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3083,13 +3084,13 @@ "runs": 3, "reference": { "minMs": 0.2599999999947613, - "maxMs": 0.3000000000029104, - "ratio": 1.1538461538805962 + "maxMs": 0.27999999999883585, + "ratio": 1.0769230769402982 }, "competitor": { - "minMs": 0.7799999999988358, - "maxMs": 0.8600000000005821, - "ratio": 1.1025641025664945 + "minMs": 0.7999999999992724, + "maxMs": 0.9199999999982538, + "ratio": 1.149999999998863 }, "stable": true, "rungs": [ @@ -3101,19 +3102,19 @@ }, { "competitor": "pixi", - "referenceMs": 0.26000000000203727, - "referenceP95Ms": 0.41999999999825377, - "referenceGpuMs": 16.68000000000029, + "referenceMs": 0.27999999999883585, + "referenceP95Ms": 0.3800000000046566, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.18000000000029104, - "competitorP95Ms": 0.33999999999650754, - "competitorGpuMs": 16.660000000003492, + "competitorMs": 0.13999999999941792, + "competitorP95Ms": 0.2599999999947613, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 2, + "factor": 2, + "label": "competitor leads (2.00x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -3122,17 +3123,17 @@ "runs": 3, "reference": { "minMs": 0.2599999999947613, - "maxMs": 0.3000000000029104, - "ratio": 1.1538461538805962 + "maxMs": 0.27999999999883585, + "ratio": 1.0769230769402982 }, "competitor": { - "minMs": 0.16000000000349246, - "maxMs": 0.26000000000203727, - "ratio": 1.6249999999772626 + "minMs": 0.13999999999941792, + "maxMs": 0.13999999999941792, + "ratio": 1 }, - "stable": false, + "stable": true, "rungs": [ - "level", + "competitor-leads", "competitor-leads", "competitor-leads" ] @@ -3150,19 +3151,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.47000000000116415, - "referenceP95Ms": 0.6199999999953434, + "referenceMs": 0.4599999999991269, + "referenceP95Ms": 0.5999999999985448, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 25.010000000002037, - "competitorP95Ms": 28.339999999996508, - "competitorGpuMs": 25.79999999998836, + "competitorMs": 25.42000000000553, + "competitorP95Ms": 28, + "competitorGpuMs": 26.24000000000524, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.018792483006842296, - "factor": 53.21276595731934, - "label": "ExoJS leads clearly (53.21x)", + "ratio": 0.018095987411448734, + "factor": 55.2608695653343, + "label": "ExoJS leads clearly (55.26x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3170,14 +3171,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.4599999999991269, + "minMs": 0.4500000000007276, "maxMs": 0.4800000000032014, - "ratio": 1.0434782608785054 + "ratio": 1.0666666666720563 }, "competitor": { - "minMs": 23.720000000001164, - "maxMs": 26.5, - "ratio": 1.1172006745362015 + "minMs": 24.700000000004366, + "maxMs": 25.499999999992724, + "ratio": 1.0323886639671342 }, "stable": true, "rungs": [ @@ -3189,19 +3190,19 @@ }, { "competitor": "phaser", - "referenceMs": 0.47000000000116415, - "referenceP95Ms": 0.6199999999953434, + "referenceMs": 0.4599999999991269, + "referenceP95Ms": 0.5999999999985448, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 3.069999999999709, - "competitorP95Ms": 3.820000000003347, - "competitorGpuMs": 16.660000000003492, + "competitorMs": 2.8099999999994907, + "competitorP95Ms": 3.8800000000010186, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.15309446254111034, - "factor": 6.531914893600223, - "label": "ExoJS leads clearly (6.53x)", + "ratio": 0.1637010676153773, + "factor": 6.108695652184401, + "label": "ExoJS leads clearly (6.11x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3209,14 +3210,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.4599999999991269, + "minMs": 0.4500000000007276, "maxMs": 0.4800000000032014, - "ratio": 1.0434782608785054 + "ratio": 1.0666666666720563 }, "competitor": { - "minMs": 2.8300000000017462, - "maxMs": 3.1700000000000728, - "ratio": 1.1201413427555182 + "minMs": 2.7600000000002183, + "maxMs": 3.100000000000364, + "ratio": 1.1231884057971444 }, "stable": true, "rungs": [ @@ -3228,19 +3229,19 @@ }, { "competitor": "pixi", - "referenceMs": 0.47000000000116415, - "referenceP95Ms": 0.6199999999953434, + "referenceMs": 0.4599999999991269, + "referenceP95Ms": 0.5999999999985448, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.1599999999962165, - "competitorP95Ms": 0.3000000000029104, - "competitorGpuMs": 16.68000000000029, + "competitorMs": 0.11999999999534339, + "competitorP95Ms": 0.2400000000052387, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 2.9375000000767386, - "factor": 2.9375000000767386, - "label": "competitor leads (2.94x)", + "ratio": 3.83333333347481, + "factor": 3.83333333347481, + "label": "competitor leads (3.83x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -3248,14 +3249,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.4599999999991269, + "minMs": 0.4500000000007276, "maxMs": 0.4800000000032014, - "ratio": 1.0434782608785054 + "ratio": 1.0666666666720563 }, "competitor": { - "minMs": 0.13999999999941792, - "maxMs": 0.19999999999708962, - "ratio": 1.4285714285565796 + "minMs": 0.11999999999534339, + "maxMs": 0.12000000000261934, + "ratio": 1.000000000060633 }, "stable": true, "rungs": [ @@ -3277,18 +3278,18 @@ "cells": [ { "competitor": "phaser", - "referenceMs": 1.639999999999418, - "referenceP95Ms": 2.4199999999982538, + "referenceMs": 1.6299999999973807, + "referenceP95Ms": 2.599999999998545, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 9.530000000013388, - "competitorP95Ms": 10.679999999996653, + "competitorMs": 9.469999999999345, + "competitorP95Ms": 9.80000000000291, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.17208814270693745, - "factor": 5.810975609766324, + "ratio": 0.17212249207998875, + "factor": 5.809815950929179, "label": "ExoJS leads clearly (5.81x)", "structural": true }, @@ -3297,14 +3298,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.3799999999973807, - "maxMs": 1.7999999999992724, - "ratio": 1.304347826088905 + "minMs": 1.6199999999989814, + "maxMs": 1.8099999999976717, + "ratio": 1.1172839506165493 }, "competitor": { - "minMs": 9.43000000000211, - "maxMs": 9.569999999999709, - "ratio": 1.014846235418618 + "minMs": 9.380000000001019, + "maxMs": 9.5, + "ratio": 1.0127931769721714 }, "stable": true, "rungs": [ @@ -3316,19 +3317,19 @@ }, { "competitor": "pixi", - "referenceMs": 1.639999999999418, - "referenceP95Ms": 2.4199999999982538, + "referenceMs": 1.6299999999973807, + "referenceP95Ms": 2.599999999998545, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.1500000000014552, - "competitorP95Ms": 0.37999999999738066, + "competitorMs": 0.1499999999978172, + "competitorP95Ms": 0.27999999999883585, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 10.933333333223386, - "factor": 10.933333333223386, - "label": "competitor leads clearly (10.93x)", + "ratio": 10.866666666807335, + "factor": 10.866666666807335, + "label": "competitor leads clearly (10.87x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -3336,14 +3337,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.3799999999973807, - "maxMs": 1.7999999999992724, - "ratio": 1.304347826088905 + "minMs": 1.6199999999989814, + "maxMs": 1.8099999999976717, + "ratio": 1.1172839506165493 }, "competitor": { - "minMs": 0.1499999999978172, - "maxMs": 0.22000000000116415, - "ratio": 1.4666666666957704 + "minMs": 0.13000000000101863, + "maxMs": 0.15999999999985448, + "ratio": 1.2307692307584674 }, "stable": true, "rungs": [ @@ -3365,19 +3366,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 1.430000000000291, - "referenceP95Ms": 2.279999999998836, + "referenceMs": 1.4899999999979627, + "referenceP95Ms": 2.280000000006112, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 25.67000000000553, - "competitorP95Ms": 27.75999999999476, - "competitorGpuMs": 26.919999999998254, + "competitorMs": 25.089999999996508, + "competitorP95Ms": 27.29999999998836, + "competitorGpuMs": 26.479999999995925, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.055707051032332804, - "factor": 17.951048951049163, - "label": "ExoJS leads clearly (17.95x)", + "ratio": 0.05938620964520407, + "factor": 16.838926174517322, + "label": "ExoJS leads clearly (16.84x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3385,14 +3386,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.360000000000582, - "maxMs": 1.4799999999959255, - "ratio": 1.0882352941141853 + "minMs": 1.4599999999991269, + "maxMs": 1.4900000000016007, + "ratio": 1.0205479452071862 }, "competitor": { - "minMs": 23.220000000001164, - "maxMs": 26.159999999996217, - "ratio": 1.1266149870798838 + "minMs": 24.70000000001164, + "maxMs": 26.580000000001746, + "ratio": 1.07611336032345 }, "stable": true, "rungs": [ @@ -3404,19 +3405,19 @@ }, { "competitor": "phaser", - "referenceMs": 1.430000000000291, - "referenceP95Ms": 2.279999999998836, + "referenceMs": 1.4899999999979627, + "referenceP95Ms": 2.280000000006112, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 3.2600000000002183, - "competitorP95Ms": 6.380000000001019, + "competitorMs": 3.1599999999998545, + "competitorP95Ms": 6.5, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.4386503067485262, - "factor": 2.2797202797199683, - "label": "ExoJS leads (2.28x)", + "ratio": 0.47151898734114917, + "factor": 2.120805369130319, + "label": "ExoJS leads (2.12x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3424,14 +3425,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.360000000000582, - "maxMs": 1.4799999999959255, - "ratio": 1.0882352941141853 + "minMs": 1.4599999999991269, + "maxMs": 1.4900000000016007, + "ratio": 1.0205479452071862 }, "competitor": { - "minMs": 3.1000000000058208, - "maxMs": 3.470000000001164, - "ratio": 1.1193548387079513 + "minMs": 3.139999999999418, + "maxMs": 3.2299999999995634, + "ratio": 1.0286624203822172 }, "stable": true, "rungs": [ @@ -3443,19 +3444,19 @@ }, { "competitor": "pixi", - "referenceMs": 1.430000000000291, - "referenceP95Ms": 2.279999999998836, + "referenceMs": 1.4899999999979627, + "referenceP95Ms": 2.280000000006112, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.9200000000018917, + "competitorMs": 0.9000000000014552, "competitorP95Ms": 1.139999999999418, - "competitorGpuMs": 16.679999999993015, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.5543478260840768, - "factor": 1.5543478260840768, - "label": "competitor leads (1.55x)", + "ratio": 1.6555555555506152, + "factor": 1.6555555555506152, + "label": "competitor leads (1.66x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3463,14 +3464,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.360000000000582, - "maxMs": 1.4799999999959255, - "ratio": 1.0882352941141853 + "minMs": 1.4599999999991269, + "maxMs": 1.4900000000016007, + "ratio": 1.0205479452071862 }, "competitor": { - "minMs": 0.9199999999982538, - "maxMs": 1.0400000000008731, - "ratio": 1.1304347826117904 + "minMs": 0.8799999999973807, + "maxMs": 0.9700000000048021, + "ratio": 1.1022727272814652 }, "stable": true, "rungs": [ @@ -3497,19 +3498,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.6300000000010186, - "referenceP95Ms": 1, - "referenceGpuMs": 16.68000000000029, + "referenceMs": 0.5400000000008731, + "referenceP95Ms": 0.9000000000014552, + "referenceGpuMs": 16.679999999993015, "referenceOverFrameBudget": false, - "competitorMs": 23.820000000006985, - "competitorP95Ms": 26.260000000009313, - "competitorGpuMs": 24.580000000001746, + "competitorMs": 24.04000000000815, + "competitorP95Ms": 26.139999999999418, + "competitorGpuMs": 25.160000000003492, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.02644836272043803, - "factor": 37.80952380947376, - "label": "ExoJS leads clearly (37.81x)", + "ratio": 0.02246256239603536, + "factor": 44.51851851846163, + "label": "ExoJS leads clearly (44.52x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3518,13 +3519,13 @@ "runs": 3, "reference": { "minMs": 0.5400000000008731, - "maxMs": 0.6399999999994179, - "ratio": 1.185185185182191 + "maxMs": 0.569999999996071, + "ratio": 1.055555555546573 }, "competitor": { - "minMs": 22.280000000006112, - "maxMs": 25.580000000001746, - "ratio": 1.148114901256496 + "minMs": 22.64999999999418, + "maxMs": 26.610000000015134, + "ratio": 1.1748344370870627 }, "stable": true, "rungs": [ @@ -3536,19 +3537,19 @@ }, { "competitor": "phaser", - "referenceMs": 0.6300000000010186, - "referenceP95Ms": 1, - "referenceGpuMs": 16.68000000000029, + "referenceMs": 0.5400000000008731, + "referenceP95Ms": 0.9000000000014552, + "referenceGpuMs": 16.679999999993015, "referenceOverFrameBudget": false, - "competitorMs": 32.219999999997526, - "competitorP95Ms": 35.099999999998545, - "competitorGpuMs": 32.779999999998836, + "competitorMs": 32.42000000000189, + "competitorP95Ms": 35.31999999999971, + "competitorGpuMs": 33.340000000000146, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.01955307262573144, - "factor": 51.142857142770524, - "label": "ExoJS leads clearly (51.14x)", + "ratio": 0.01665638494758919, + "factor": 60.037037036943474, + "label": "ExoJS leads clearly (60.04x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3557,13 +3558,13 @@ "runs": 3, "reference": { "minMs": 0.5400000000008731, - "maxMs": 0.6399999999994179, - "ratio": 1.185185185182191 + "maxMs": 0.569999999996071, + "ratio": 1.055555555546573 }, "competitor": { - "minMs": 31.55999999999767, - "maxMs": 32.530000000000655, - "ratio": 1.0307351077314022 + "minMs": 32.400000000001455, + "maxMs": 33.219999999999345, + "ratio": 1.0253086419752424 }, "stable": true, "rungs": [ @@ -3575,19 +3576,19 @@ }, { "competitor": "pixi", - "referenceMs": 0.6300000000010186, - "referenceP95Ms": 1, - "referenceGpuMs": 16.68000000000029, + "referenceMs": 0.5400000000008731, + "referenceP95Ms": 0.9000000000014552, + "referenceGpuMs": 16.679999999993015, "referenceOverFrameBudget": false, - "competitorMs": 17.039999999997235, - "competitorP95Ms": 21.580000000001746, - "competitorGpuMs": 17.44000000000233, + "competitorMs": 18.089999999996508, + "competitorP95Ms": 21.760000000002037, + "competitorGpuMs": 18.860000000000582, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.03697183098598127, - "factor": 27.047619047570926, - "label": "ExoJS leads clearly (27.05x)", + "ratio": 0.029850746268710746, + "factor": 33.49999999993936, + "label": "ExoJS leads clearly (33.50x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -3596,13 +3597,13 @@ "runs": 3, "reference": { "minMs": 0.5400000000008731, - "maxMs": 0.6399999999994179, - "ratio": 1.185185185182191 + "maxMs": 0.569999999996071, + "ratio": 1.055555555546573 }, "competitor": { - "minMs": 17.0099999999984, - "maxMs": 17.139999999999418, - "ratio": 1.0076425631981794 + "minMs": 16.589999999996508, + "maxMs": 18.36000000000422, + "ratio": 1.106690777577341 }, "stable": true, "rungs": [ @@ -3673,17 +3674,17 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.3000000000000682, - "referenceP95Ms": 0.580000000000382, - "referenceGpuMs": 0.073354, + "referenceMs": 0.2799999999999727, + "referenceP95Ms": 0.6000000000000227, + "referenceGpuMs": 0.072354, "referenceOverFrameBudget": false, - "competitorMs": 0.3100000000000591, - "competitorP95Ms": 0.6200000000000045, - "competitorGpuMs": 0.073521, + "competitorMs": 0.3000000000000682, + "competitorP95Ms": 0.5799999999999272, + "competitorGpuMs": 0.072292, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9677419354839064, + "ratio": 0.9333333333330301, "factor": 1, "label": "level", "structural": false @@ -3693,14 +3694,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.2999999999999545, - "maxMs": 0.3200000000001637, - "ratio": 1.066666666667374 + "minMs": 0.2600000000002183, + "maxMs": 0.3100000000000591, + "ratio": 1.1923076923069187 }, "competitor": { "minMs": 0.2999999999999545, - "maxMs": 0.33999999999980446, - "ratio": 1.1333333333328532 + "maxMs": 0.3000000000001819, + "ratio": 1.0000000000007578 }, "stable": true, "rungs": [ @@ -3722,13 +3723,13 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.21000000000003638, - "referenceP95Ms": 0.4600000000000364, - "referenceGpuMs": 0.20637499999999998, + "referenceMs": 0.1999999999998181, + "referenceP95Ms": 0.36000000000012733, + "referenceGpuMs": 0.206208, "referenceOverFrameBudget": false, - "competitorMs": 0.23999999999978172, - "competitorP95Ms": 0.4600000000000364, - "competitorGpuMs": 0.1905625, + "competitorMs": 0.25999999999976353, + "competitorP95Ms": 0.4399999999995998, + "competitorGpuMs": 0.190458, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -3738,24 +3739,24 @@ "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", - "timer": "resolved", + "timer": "limited", "aggregate": { "runs": 3, "reference": { - "minMs": 0.1999999999998181, - "maxMs": 0.23999999999978172, - "ratio": 1.2 + "minMs": 0.18000000000029104, + "maxMs": 0.20000000000027285, + "ratio": 1.1111111111108305 }, "competitor": { - "minMs": 0.22000000000025466, - "maxMs": 0.25999999999976353, - "ratio": 1.181818181815739 + "minMs": 0.23999999999978172, + "maxMs": 0.2600000000002183, + "ratio": 1.0833333333352282 }, "stable": false, "rungs": [ "level", "exojs-leads", - "level" + "exojs-leads" ] } } @@ -3771,19 +3772,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.05999999999949068, - "referenceP95Ms": 0.24000000000069122, - "referenceGpuMs": 0.32764550000000003, + "referenceMs": 0.13999999999941792, + "referenceP95Ms": 0.22000000000025466, + "referenceGpuMs": 0.4222295, "referenceOverFrameBudget": false, - "competitorMs": 0.1000000000003638, - "competitorP95Ms": 0.5, - "competitorGpuMs": 0.2720205, + "competitorMs": 0.0999999999994543, + "competitorP95Ms": 0.6000000000003638, + "competitorGpuMs": 0.2353335, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 1.400000000001819, + "factor": 1.400000000001819, + "label": "competitor leads (1.40x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3791,20 +3792,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.040000000000873115, - "maxMs": 0.19000000000005457, - "ratio": 4.749999999897682 + "minMs": 0.1299999999996544, + "maxMs": 0.14000000000032742, + "ratio": 1.0769230769284586 }, "competitor": { - "minMs": 0.07999999999992724, - "maxMs": 0.11999999999989086, - "ratio": 1.5 + "minMs": 0.09000000000014552, + "maxMs": 0.10000000000081855, + "ratio": 1.1111111111184095 }, - "stable": false, + "stable": true, "rungs": [ "competitor-leads", - "exojs-leads", - "exojs-leads" + "competitor-leads", + "competitor-leads" ] } } @@ -3820,19 +3821,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 1.9900000000002365, - "referenceP95Ms": 2.519999999999527, - "referenceGpuMs": 0.207709, + "referenceMs": 1.8400000000001455, + "referenceP95Ms": 2.399999999999636, + "referenceGpuMs": 0.20729150000000002, "referenceOverFrameBudget": false, - "competitorMs": 1.2199999999998, - "competitorP95Ms": 1.8400000000001455, - "competitorGpuMs": 0.1908955, + "competitorMs": 1.3800000000001091, + "competitorP95Ms": 2.0600000000004, + "competitorGpuMs": 0.19085449999999998, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 1.631147540984068, - "factor": 1.631147540984068, - "label": "competitor leads (1.63x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -3840,20 +3841,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.9899999999997817, - "maxMs": 2.0100000000002183, - "ratio": 1.010050251256502 + "minMs": 1.4200000000000728, + "maxMs": 1.9299999999998363, + "ratio": 1.3591549295772798 }, "competitor": { - "minMs": 1.2100000000000364, - "maxMs": 1.2800000000002, - "ratio": 1.057851239669555 + "minMs": 1.2400000000006912, + "maxMs": 1.5700000000001637, + "ratio": 1.2661290322574907 }, - "stable": true, + "stable": false, "rungs": [ "competitor-leads", "competitor-leads", - "competitor-leads" + "level" ] } } @@ -3869,19 +3870,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 1.669999999999618, - "referenceP95Ms": 2.1599999999998545, - "referenceGpuMs": 0.07360449999999999, + "referenceMs": 1.6900000000000546, + "referenceP95Ms": 2.2600000000002183, + "referenceGpuMs": 0.0739165, "referenceOverFrameBudget": false, - "competitorMs": 1.0399999999999636, - "competitorP95Ms": 1.3400000000001455, - "competitorGpuMs": 0.0724795, + "competitorMs": 1.0799999999999272, + "competitorP95Ms": 1.360000000000582, + "competitorGpuMs": 0.07185449999999999, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 1.6057692307689198, - "factor": 1.6057692307689198, - "label": "competitor leads (1.61x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -3889,20 +3890,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.5799999999999272, - "maxMs": 1.7200000000002547, - "ratio": 1.0886075949369203 + "minMs": 0.8899999999998727, + "maxMs": 1.7399999999997817, + "ratio": 1.9550561797753154 }, "competitor": { - "minMs": 1.0199999999999818, - "maxMs": 1.0600000000004002, - "ratio": 1.0392156862749207 + "minMs": 0.9699999999997999, + "maxMs": 1.0899999999996908, + "ratio": 1.1237113402060985 }, - "stable": true, + "stable": false, "rungs": [ "competitor-leads", "competitor-leads", - "competitor-leads" + "level" ] } } @@ -3918,19 +3919,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 4.1599999999998545, - "referenceP95Ms": 9.300000000001091, - "referenceGpuMs": 0.21006249999999999, + "referenceMs": 4.1200000000008, + "referenceP95Ms": 5.139999999999418, + "referenceGpuMs": 0.21029150000000002, "referenceOverFrameBudget": false, - "competitorMs": 2.6900000000005093, - "competitorP95Ms": 9.6200000000008, - "competitorGpuMs": 0.192271, + "competitorMs": 2.7200000000002547, + "competitorP95Ms": 10.979999999999563, + "competitorGpuMs": 0.193062, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.5464684014866419, - "factor": 1.5464684014866419, - "label": "competitor leads (1.55x)", + "ratio": 1.5147058823530937, + "factor": 1.5147058823530937, + "label": "competitor leads (1.51x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -3938,14 +3939,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.819999999999709, - "maxMs": 4.460000000000946, - "ratio": 1.1675392670160434 + "minMs": 3.780000000000655, + "maxMs": 4.199999999999818, + "ratio": 1.1111111111108705 }, "competitor": { - "minMs": 2.619999999999891, - "maxMs": 2.7100000000000364, - "ratio": 1.034351145038225 + "minMs": 2.599999999998545, + "maxMs": 2.730000000000473, + "ratio": 1.0500000000007697 }, "stable": true, "rungs": [ @@ -3967,19 +3968,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 38.5, - "referenceP95Ms": 41.779999999998836, - "referenceGpuMs": 0.5527500000000001, + "referenceMs": 37.26000000000022, + "referenceP95Ms": 41.220000000001164, + "referenceGpuMs": 0.9720415, "referenceOverFrameBudget": true, - "competitorMs": 28.11999999999989, - "competitorP95Ms": 31.76000000000022, - "competitorGpuMs": 0.7896875, + "competitorMs": 28.49000000000069, + "competitorP95Ms": 32.039999999999054, + "competitorGpuMs": 0.550104, "competitorOverFrameBudget": true, "verdict": { "side": "competitor", - "ratio": 1.369132290184927, - "factor": 1.369132290184927, - "label": "competitor leads (1.37x)", + "ratio": 1.3078273078272837, + "factor": 1.3078273078272837, + "label": "competitor leads (1.31x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -3987,14 +3988,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 37.75999999999931, - "maxMs": 38.66999999999916, - "ratio": 1.024099576271183 + "minMs": 37.100000000000364, + "maxMs": 37.44999999999982, + "ratio": 1.0094339622641362 }, "competitor": { - "minMs": 27.86999999999989, - "maxMs": 28.199999999999818, - "ratio": 1.0118406889128069 + "minMs": 28.18000000000029, + "maxMs": 28.51000000000022, + "ratio": 1.0117104329311541 }, "stable": true, "rungs": [ @@ -4016,17 +4017,17 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.48000000000047294, - "referenceP95Ms": 0.7199999999993452, - "referenceGpuMs": 0.2288125, + "referenceMs": 0.5200000000004366, + "referenceP95Ms": 0.7200000000011642, + "referenceGpuMs": 0.2291455, "referenceOverFrameBudget": false, - "competitorMs": 0.5200000000004366, - "competitorP95Ms": 0.819999999999709, - "competitorGpuMs": 0.1311875, + "competitorMs": 0.5, + "competitorP95Ms": 0.8000000000010914, + "competitorGpuMs": 0.21816649999999999, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9230769230770576, + "ratio": 1.0400000000008731, "factor": 1, "label": "level", "structural": false @@ -4036,14 +4037,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.48000000000047294, - "maxMs": 0.5, - "ratio": 1.0416666666656402 + "minMs": 0.5, + "maxMs": 0.5600000000004002, + "ratio": 1.1200000000008004 }, "competitor": { "minMs": 0.5, - "maxMs": 0.5200000000004366, - "ratio": 1.0400000000008731 + "maxMs": 0.5399999999999636, + "ratio": 1.0799999999999272 }, "stable": true, "rungs": [ @@ -4065,13 +4066,13 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 2.6900000000005093, - "referenceP95Ms": 6.8400000000001455, - "referenceGpuMs": 0.14314549999999998, + "referenceMs": 2.819999999999709, + "referenceP95Ms": 6.779999999998836, + "referenceGpuMs": 0.1425, "referenceOverFrameBudget": false, "competitorMs": 2.459999999999127, - "competitorP95Ms": 7.260000000002037, - "competitorGpuMs": 0.159375, + "competitorP95Ms": 8.720000000001164, + "competitorGpuMs": 0.157458, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -4085,19 +4086,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.6599999999998545, - "maxMs": 3.06999999999789, - "ratio": 1.1541353383451345 + "minMs": 2.680000000000291, + "maxMs": 3.0600000000013097, + "ratio": 1.141791044776484 }, "competitor": { - "minMs": 2.4500000000007276, - "maxMs": 2.5200000000004366, - "ratio": 1.0285714285713012 + "minMs": 2.430000000000291, + "maxMs": 2.5199999999986176, + "ratio": 1.037037037036344 }, "stable": false, "rungs": [ - "competitor-leads", "level", + "competitor-leads", "level" ] } @@ -4120,12 +4121,12 @@ { "competitor": "pixi", "referenceMs": 0.15999999999985448, - "referenceP95Ms": 0.28000000000065484, - "referenceGpuMs": 0.590417, + "referenceP95Ms": 0.27999999999883585, + "referenceGpuMs": 0.5884795, "referenceOverFrameBudget": false, "competitorMs": 0.2000000000007276, "competitorP95Ms": 0.37999999999919964, - "competitorGpuMs": 0.739271, + "competitorGpuMs": 0.7117709999999999, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -4139,20 +4140,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.13999999999941792, - "maxMs": 0.18000000000029104, - "ratio": 1.28571428572171 + "minMs": 0.1400000000012369, + "maxMs": 0.18999999999959982, + "ratio": 1.3571428571280082 }, "competitor": { "minMs": 0.2000000000007276, - "maxMs": 0.21000000000003638, - "ratio": 1.049999999996362 + "maxMs": 0.21999999999934516, + "ratio": 1.0999999999927241 }, "stable": false, "rungs": [ - "level", "exojs-leads", - "exojs-leads" + "exojs-leads", + "level" ] } } @@ -4168,13 +4169,13 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.5399999999972351, - "referenceP95Ms": 1.2399999999979627, - "referenceGpuMs": 0.219646, + "referenceMs": 0.819999999999709, + "referenceP95Ms": 1.5800000000017462, + "referenceGpuMs": 0.220604, "referenceOverFrameBudget": false, - "competitorMs": 0.6699999999982538, - "competitorP95Ms": 1.0199999999986176, - "competitorGpuMs": 0.12397849999999999, + "competitorMs": 0.6599999999998545, + "competitorP95Ms": 1.0400000000008731, + "competitorGpuMs": 0.076938, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -4188,20 +4189,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5, - "maxMs": 0.6000000000021828, - "ratio": 1.2000000000043656 + "minMs": 0.5300000000006548, + "maxMs": 0.9799999999995634, + "ratio": 1.8490566037704765 }, "competitor": { - "minMs": 0.6399999999994179, - "maxMs": 0.6900000000005093, - "ratio": 1.0781250000017764 + "minMs": 0.6599999999998545, + "maxMs": 0.7000000000007276, + "ratio": 1.0606060606073968 }, "stable": false, "rungs": [ - "exojs-leads", - "exojs-leads", - "level" + "level", + "level", + "competitor-leads" ] } } @@ -4217,19 +4218,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.4000000000014552, - "referenceP95Ms": 0.5200000000004366, - "referenceGpuMs": 0.118896, + "referenceMs": 0.42000000000189175, + "referenceP95Ms": 0.5400000000008731, + "referenceGpuMs": 0.1192915, "referenceOverFrameBudget": false, - "competitorMs": 0.5400000000008731, - "competitorP95Ms": 0.7200000000011642, - "competitorGpuMs": 0.119166, + "competitorMs": 0.5600000000013097, + "competitorP95Ms": 0.7799999999988358, + "competitorGpuMs": 0.1186245, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "exojs", + "ratio": 0.7500000000016241, + "factor": 1.333333333330446, + "label": "ExoJS leads (1.33x)", "structural": false }, "mechanism": "competitor issues fewer texture binds (240 vs 159 per frame)", @@ -4237,20 +4238,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.27999999999883585, - "maxMs": 0.4399999999986903, - "ratio": 1.5714285714304275 + "minMs": 0.38999999999759893, + "maxMs": 0.43000000000029104, + "ratio": 1.1025641025716368 }, "competitor": { - "minMs": 0.5399999999990541, - "maxMs": 0.5599999999976717, - "ratio": 1.0370370370345419 + "minMs": 0.5400000000008731, + "maxMs": 0.569999999999709, + "ratio": 1.0555555555533098 }, - "stable": false, + "stable": true, "rungs": [ "exojs-leads", "exojs-leads", - "level" + "exojs-leads" ] } } @@ -4271,18 +4272,18 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.11999999999898137, - "referenceP95Ms": 0.18000000000029104, - "referenceGpuMs": 0.126417, + "referenceMs": 0.12000000000080036, + "referenceP95Ms": 0.22000000000116415, + "referenceGpuMs": 0.1255835, "referenceOverFrameBudget": false, "competitorMs": 0.18000000000029104, - "competitorP95Ms": 0.3400000000001455, - "competitorGpuMs": 0.11560400000000001, + "competitorP95Ms": 0.31999999999970896, + "competitorGpuMs": 0.11510400000000001, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.6666666666599297, - "factor": 1.5000000000151583, + "ratio": 0.6666666666700352, + "factor": 1.499999999992421, "label": "ExoJS leads (1.50x)", "structural": false }, @@ -4291,14 +4292,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.10000000000218279, - "maxMs": 0.13999999999941792, - "ratio": 1.3999999999636201 + "minMs": 0.11999999999898137, + "maxMs": 0.12000000000261934, + "ratio": 1.0000000000303164 }, "competitor": { - "minMs": 0.18000000000029104, + "minMs": 0.15999999999985448, "maxMs": 0.2000000000007276, - "ratio": 1.1111111111133567 + "ratio": 1.2500000000056843 }, "stable": true, "rungs": [ @@ -4326,18 +4327,18 @@ { "competitor": "pixi", "referenceMs": 0.18000000000029104, - "referenceP95Ms": 0.31999999999970896, - "referenceGpuMs": 1.4492085000000001, + "referenceP95Ms": 0.3600000000005821, + "referenceGpuMs": 0.650812, "referenceOverFrameBudget": false, - "competitorMs": 0.18000000000029104, - "competitorP95Ms": 0.3400000000001455, - "competitorGpuMs": 0.246812, + "competitorMs": 0.19000000000050932, + "competitorP95Ms": 0.31999999999970896, + "competitorGpuMs": 0.246, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 0.9473684210516238, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -4346,17 +4347,17 @@ "runs": 3, "reference": { "minMs": 0.18000000000029104, - "maxMs": 0.22000000000116415, - "ratio": 1.2222222222267136 + "maxMs": 0.18000000000029104, + "ratio": 1 }, "competitor": { "minMs": 0.18000000000029104, - "maxMs": 0.18000000000029104, - "ratio": 1 + "maxMs": 0.2000000000007276, + "ratio": 1.1111111111133567 }, - "stable": false, + "stable": true, "rungs": [ - "competitor-leads", + "level", "level", "level" ] @@ -4374,13 +4375,13 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.1400000000030559, - "referenceP95Ms": 0.27999999999883585, - "referenceGpuMs": 3.0184375, + "referenceMs": 0.15999999999985448, + "referenceP95Ms": 0.2599999999983993, + "referenceGpuMs": 3.5047085, "referenceOverFrameBudget": false, "competitorMs": 0.18000000000029104, - "competitorP95Ms": 0.3600000000005821, - "competitorGpuMs": 0.364646, + "competitorP95Ms": 0.3400000000001455, + "competitorGpuMs": 0.371521, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -4400,14 +4401,14 @@ }, "competitor": { "minMs": 0.18000000000029104, - "maxMs": 0.2000000000007276, - "ratio": 1.1111111111133567 + "maxMs": 0.18000000000029104, + "ratio": 1 }, "stable": false, "rungs": [ "level", "exojs-leads", - "exojs-leads" + "level" ] } } @@ -4423,19 +4424,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 2.6599999999998545, - "referenceP95Ms": 7.200000000000728, - "referenceGpuMs": 1.487604, + "referenceMs": 2.6700000000000728, + "referenceP95Ms": 7.540000000000873, + "referenceGpuMs": 1.4847085, "referenceOverFrameBudget": false, - "competitorMs": 4.250000000001819, - "competitorP95Ms": 5.599999999998545, - "competitorGpuMs": 0.2921045, + "competitorMs": 4.5, + "competitorP95Ms": 5.779999999998836, + "competitorGpuMs": 0.291625, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.6258823529408744, - "factor": 1.5977443609030269, - "label": "ExoJS leads (1.60x)", + "ratio": 0.5933333333333495, + "factor": 1.6853932584269204, + "label": "ExoJS leads (1.69x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/3 vs 1/2/3, none of them large enough to account for the gap); the difference is CPU-side", @@ -4443,14 +4444,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.430000000000291, - "maxMs": 2.680000000000291, - "ratio": 1.1028806584362016 + "minMs": 2.600000000000364, + "maxMs": 3.399999999997817, + "ratio": 1.3076923076912852 }, "competitor": { - "minMs": 4.180000000000291, - "maxMs": 4.419999999998254, - "ratio": 1.0574162679420924 + "minMs": 4.430000000000291, + "maxMs": 4.699999999998909, + "ratio": 1.0609480812637924 }, "stable": true, "rungs": [ @@ -4472,18 +4473,18 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 7.8700000000008, - "referenceP95Ms": 10.900000000001455, - "referenceGpuMs": 3.727396, + "referenceMs": 7.899999999999636, + "referenceP95Ms": 9.639999999999418, + "referenceGpuMs": 3.821021, "referenceOverFrameBudget": false, - "competitorMs": 19.159999999999854, - "competitorP95Ms": 21.92000000000189, - "competitorGpuMs": 0.8017084999999999, + "competitorMs": 19.209999999997308, + "competitorP95Ms": 22.739999999997963, + "competitorGpuMs": 0.8035625, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.4107515657620491, - "factor": 2.434561626429213, + "ratio": 0.4112441436752079, + "factor": 2.431645569620024, "label": "ExoJS leads (2.43x)", "structural": false }, @@ -4492,14 +4493,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 7.430000000000291, - "maxMs": 8.199999999998909, - "ratio": 1.1036339165543185 + "minMs": 7.249999999998181, + "maxMs": 7.970000000001164, + "ratio": 1.0993103448280226 }, "competitor": { - "minMs": 18.829999999999927, - "maxMs": 19.6200000000008, - "ratio": 1.0419543281997279 + "minMs": 19.07999999999811, + "maxMs": 19.670000000000073, + "ratio": 1.030922431865934 }, "stable": true, "rungs": [ @@ -4526,19 +4527,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.5200000000004366, - "referenceP95Ms": 0.9000000000014552, - "referenceGpuMs": 0.235479, + "referenceMs": 0.5600000000013097, + "referenceP95Ms": 1.0600000000013097, + "referenceGpuMs": 0.2069795, "referenceOverFrameBudget": false, - "competitorMs": 2.1700000000000728, - "competitorP95Ms": 3.7400000000016007, - "competitorGpuMs": 0.28045850000000005, + "competitorMs": 2.1599999999998545, + "competitorP95Ms": 3.720000000001164, + "competitorGpuMs": 0.28029200000000004, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.2396313364057231, - "factor": 4.17307692307356, - "label": "ExoJS leads (4.17x)", + "ratio": 0.2592592592598831, + "factor": 3.857142857133576, + "label": "ExoJS leads (3.86x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/6/3 vs 2/4/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -4546,14 +4547,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5199999999986176, - "maxMs": 0.6699999999982538, - "ratio": 1.2884615384616058 + "minMs": 0.5500000000010914, + "maxMs": 0.5800000000017462, + "ratio": 1.054545454546537 }, "competitor": { - "minMs": 2.0900000000037835, - "maxMs": 2.1999999999989086, - "ratio": 1.0526315789449407 + "minMs": 2.030000000000655, + "maxMs": 2.4099999999980355, + "ratio": 1.1871921182252503 }, "stable": true, "rungs": [ @@ -4575,19 +4576,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.4599999999991269, - "referenceP95Ms": 0.680000000000291, - "referenceGpuMs": 0.241541, + "referenceMs": 0.4400000000023283, + "referenceP95Ms": 0.7000000000043656, + "referenceGpuMs": 0.2582715, "referenceOverFrameBudget": false, - "competitorMs": 2.219999999997526, - "competitorP95Ms": 3.779999999998836, - "competitorGpuMs": 0.44899999999999995, + "competitorMs": 2.290000000000873, + "competitorP95Ms": 3.3399999999965075, + "competitorGpuMs": 0.4509175, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.2072072072070448, - "factor": 4.826086956525522, - "label": "ExoJS leads (4.83x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 4/8/3 vs 3/6/3, none of them large enough to account for the gap); the difference is CPU-side", @@ -4595,20 +4596,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.41999999999825377, - "maxMs": 0.4800000000032014, - "ratio": 1.142857142869517 + "minMs": 0.4099999999998545, + "maxMs": 0.4599999999991269, + "ratio": 1.1219512195104637 }, "competitor": { - "minMs": 2.0899999999965075, - "maxMs": 2.279999999998836, - "ratio": 1.090909090910357 + "minMs": 1.9799999999995634, + "maxMs": 2.2999999999992724, + "ratio": 1.1616161616160503 }, - "stable": true, + "stable": false, "rungs": [ "exojs-leads", "exojs-leads", - "exojs-leads" + "exojs-leads-clearly" ] } } @@ -4624,40 +4625,40 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.42000000000552973, - "referenceP95Ms": 0.5800000000017462, - "referenceGpuMs": 0.320333, + "referenceMs": 0.4400000000023283, + "referenceP95Ms": 0.680000000000291, + "referenceGpuMs": 0.3397715, "referenceOverFrameBudget": false, - "competitorMs": 2.2599999999983993, - "competitorP95Ms": 3.6599999999962165, - "competitorGpuMs": 0.763207, + "competitorMs": 2.1600000000034925, + "competitorP95Ms": 3.860000000000582, + "competitorGpuMs": 0.7453745, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.1858407079671802, - "factor": 5.380952380877724, - "label": "ExoJS leads clearly (5.38x)", - "structural": true + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 6/12/3 vs 5/10/5, none of them large enough to account for the gap); the difference is CPU-side", "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.42000000000189175, - "maxMs": 0.4500000000007276, - "ratio": 1.0714285714254779 + "minMs": 0.43999999999505235, + "maxMs": 0.47999999999592546, + "ratio": 1.0909090909120975 }, "competitor": { - "minMs": 2.1900000000023283, + "minMs": 2.150000000005093, "maxMs": 2.319999999999709, - "ratio": 1.0593607305923483 + "ratio": 1.079069767439169 }, - "stable": true, + "stable": false, "rungs": [ + "exojs-leads", "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" + "exojs-leads" ] } } @@ -4673,19 +4674,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.46000000000276486, - "referenceP95Ms": 0.7000000000043656, - "referenceGpuMs": 0.377042, + "referenceMs": 0.4800000000032014, + "referenceP95Ms": 0.7200000000011642, + "referenceGpuMs": 0.316583, "referenceOverFrameBudget": false, - "competitorMs": 0.7599999999983993, - "competitorP95Ms": 1.1600000000034925, - "competitorGpuMs": 0.45610400000000006, + "competitorMs": 0.7600000000020373, + "competitorP95Ms": 1.0800000000017462, + "competitorGpuMs": 0.4396245, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.6052631578996496, - "factor": 1.652173913030068, - "label": "ExoJS leads (1.65x)", + "ratio": 0.6315789473709404, + "factor": 1.5833333333270174, + "label": "ExoJS leads (1.58x)", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (5 vs 8 per frame)", @@ -4693,14 +4694,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.4599999999991269, - "maxMs": 0.47999999999592546, - "ratio": 1.043478260862688 + "minMs": 0.46000000000276486, + "maxMs": 0.5099999999983993, + "ratio": 1.1086956521637694 }, "competitor": { - "minMs": 0.7200000000011642, - "maxMs": 0.7899999999972351, - "ratio": 1.0972222222166081 + "minMs": 0.6599999999962165, + "maxMs": 0.7999999999992724, + "ratio": 1.2121212121270584 }, "stable": true, "rungs": [ @@ -4722,19 +4723,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.3600000000005821, - "referenceP95Ms": 0.5, - "referenceGpuMs": 0.296, + "referenceMs": 0.3400000000001455, + "referenceP95Ms": 0.5400000000008731, + "referenceGpuMs": 0.3265625, "referenceOverFrameBudget": false, - "competitorMs": 0.2999999999956344, + "competitorMs": 0.3000000000029104, "competitorP95Ms": 0.4800000000032014, - "competitorGpuMs": 0.1538545, + "competitorGpuMs": 0.153188, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 1.1333333333228237, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", @@ -4742,19 +4743,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.3600000000005821, + "minMs": 0.3100000000013097, "maxMs": 0.3600000000005821, - "ratio": 1 + "ratio": 1.1612903225776168 }, "competitor": { - "minMs": 0.27999999999883585, + "minMs": 0.2900000000008731, "maxMs": 0.31999999999970896, - "ratio": 1.1428571428608552 + "ratio": 1.103448275857743 }, - "stable": false, + "stable": true, "rungs": [ - "competitor-leads", - "competitor-leads", + "level", + "level", "level" ] } @@ -4772,18 +4773,18 @@ { "competitor": "pixi", "referenceMs": 0.3600000000005821, - "referenceP95Ms": 0.5599999999976717, - "referenceGpuMs": 0.322584, + "referenceP95Ms": 0.5, + "referenceGpuMs": 0.32695799999999997, "referenceOverFrameBudget": false, - "competitorMs": 0.4400000000023283, - "competitorP95Ms": 0.6599999999962165, - "competitorGpuMs": 0.153729, + "competitorMs": 0.41999999999825377, + "competitorP95Ms": 0.6399999999994179, + "competitorGpuMs": 0.15181250000000002, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 0.8571428571478068, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", @@ -4791,20 +4792,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.3400000000037835, + "minMs": 0.3600000000005821, "maxMs": 0.37999999999738066, - "ratio": 1.1176470588033884 + "ratio": 1.055555555546573 }, "competitor": { - "minMs": 0.41999999999825377, - "maxMs": 0.5400000000008731, - "ratio": 1.28571428572171 + "minMs": 0.37999999999738066, + "maxMs": 0.4500000000007276, + "ratio": 1.1842105263258669 }, - "stable": false, + "stable": true, "rungs": [ - "exojs-leads", "level", - "exojs-leads" + "level", + "level" ] } } @@ -4826,12 +4827,12 @@ { "competitor": "pixi", "referenceMs": 0.2400000000016007, - "referenceP95Ms": 1.5400000000008731, - "referenceGpuMs": 0.10166649999999999, + "referenceP95Ms": 1.4600000000027649, + "referenceGpuMs": 0.10002050000000001, "referenceOverFrameBudget": false, - "competitorMs": 0.1999999999989086, - "competitorP95Ms": 0.3599999999969441, - "competitorGpuMs": 0.09775, + "competitorMs": 0.2000000000007276, + "competitorP95Ms": 0.3600000000005821, + "competitorGpuMs": 0.0978125, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -4841,24 +4842,24 @@ "structural": false }, "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", - "timer": "limited", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { "minMs": 0.23999999999796273, - "maxMs": 0.2800000000024738, - "ratio": 1.1666666666868777 + "maxMs": 0.25, + "ratio": 1.041666666675509 }, "competitor": { - "minMs": 0.1999999999989086, + "minMs": 0.2000000000007276, "maxMs": 0.2000000000007276, - "ratio": 1.000000000009095 + "ratio": 1 }, "stable": false, "rungs": [ + "level", "competitor-leads", - "competitor-leads", - "level" + "competitor-leads" ] } } @@ -4880,18 +4881,18 @@ { "competitor": "pixi", "referenceMs": 0.2599999999947613, - "referenceP95Ms": 1.4200000000055297, - "referenceGpuMs": 0.068354, + "referenceP95Ms": 1.319999999999709, + "referenceGpuMs": 0.06943750000000001, "referenceOverFrameBudget": false, - "competitorMs": 0.4000000000014552, - "competitorP95Ms": 0.7200000000011642, - "competitorGpuMs": 0.077521, + "competitorMs": 0.41999999999825377, + "competitorP95Ms": 0.7799999999988358, + "competitorGpuMs": 0.077208, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.6499999999845386, - "factor": 1.5384615384981335, - "label": "ExoJS leads (1.54x)", + "ratio": 0.6190476190377198, + "factor": 1.6153846154104472, + "label": "ExoJS leads (1.62x)", "structural": false }, "mechanism": "ExoJS issues fewer buffer uploads (326 vs 652 per frame)", @@ -4899,14 +4900,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.22999999999956344, - "maxMs": 0.2599999999983993, - "ratio": 1.1304347826038816 + "minMs": 0.2400000000052387, + "maxMs": 0.26000000000203727, + "ratio": 1.0833333333181752 }, "competitor": { - "minMs": 0.3900000000030559, - "maxMs": 0.4400000000023283, - "ratio": 1.128205128202258 + "minMs": 0.4099999999998545, + "maxMs": 0.43000000000029104, + "ratio": 1.0487804878059601 }, "stable": true, "rungs": [ @@ -4928,19 +4929,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.20999999999912689, - "referenceP95Ms": 0.7999999999956344, - "referenceGpuMs": 0.067208, + "referenceMs": 0.22000000000116415, + "referenceP95Ms": 0.8799999999973807, + "referenceGpuMs": 0.06697900000000001, "referenceOverFrameBudget": false, - "competitorMs": 0.3500000000021828, - "competitorP95Ms": 0.5599999999976717, - "competitorGpuMs": 0.0774795, + "competitorMs": 0.38000000000101863, + "competitorP95Ms": 0.6200000000026193, + "competitorGpuMs": 0.07572899999999999, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5999999999937634, - "factor": 1.6666666666839904, - "label": "ExoJS leads (1.67x)", + "ratio": 0.5789473684225642, + "factor": 1.7272727272682176, + "label": "ExoJS leads (1.73x)", "structural": false }, "mechanism": "competitor issues fewer draw calls (60 vs 2 per frame)", @@ -4948,14 +4949,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.19999999999708962, + "minMs": 0.18999999999869033, "maxMs": 0.22000000000116415, - "ratio": 1.100000000021828 + "ratio": 1.1578947368562138 }, "competitor": { - "minMs": 0.3100000000013097, - "maxMs": 0.38000000000101863, - "ratio": 1.2258064516110103 + "minMs": 0.3600000000005821, + "maxMs": 0.3999999999978172, + "ratio": 1.1111111111032512 }, "stable": true, "rungs": [ @@ -4977,19 +4978,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.9399999999986903, - "referenceP95Ms": 1.3399999999965075, - "referenceGpuMs": 0.066771, + "referenceMs": 0.7999999999992724, + "referenceP95Ms": 1.2600000000020373, + "referenceGpuMs": 0.067771, "referenceOverFrameBudget": false, - "competitorMs": 2.3299999999981083, - "competitorP95Ms": 3.139999999999418, - "competitorGpuMs": 0.06914600000000001, + "competitorMs": 1.7799999999988358, + "competitorP95Ms": 3.180000000000291, + "competitorGpuMs": 0.06868750000000001, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.40343347639461524, - "factor": 2.47872340425676, - "label": "ExoJS leads (2.48x)", + "ratio": 0.4494382022470762, + "factor": 2.2250000000005685, + "label": "ExoJS leads (2.23x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/4/2 vs 2/4/3, none of them large enough to account for the gap); the difference is CPU-side", @@ -4997,14 +4998,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.7800000000024738, - "maxMs": 0.9500000000007276, - "ratio": 1.217948717945788 + "minMs": 0.5999999999985448, + "maxMs": 0.9400000000023283, + "ratio": 1.566666666674347 }, "competitor": { - "minMs": 1.4700000000011642, - "maxMs": 2.3700000000026193, - "ratio": 1.6122448979596888 + "minMs": 1.5900000000001455, + "maxMs": 1.8699999999953434, + "ratio": 1.1761006289277813 }, "stable": true, "rungs": [ @@ -5032,12 +5033,12 @@ { "competitor": "pixi", "referenceMs": 0.27999999999883585, - "referenceP95Ms": 0.41999999999825377, - "referenceGpuMs": 0.0715, + "referenceP95Ms": 0.4599999999991269, + "referenceGpuMs": 0.070687, "referenceOverFrameBudget": false, "competitorMs": 0.26000000000203727, - "competitorP95Ms": 0.5, - "competitorGpuMs": 0.0687085, + "competitorP95Ms": 0.43999999999505235, + "competitorGpuMs": 0.069, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -5051,14 +5052,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.26000000000203727, - "maxMs": 0.27999999999883585, - "ratio": 1.076923076910161 + "minMs": 0.27999999999883585, + "maxMs": 0.2999999999956344, + "ratio": 1.0714285714174347 }, "competitor": { - "minMs": 0.23999999999796273, - "maxMs": 0.27999999999883585, - "ratio": 1.1666666666717194 + "minMs": 0.2599999999983993, + "maxMs": 0.26000000000203727, + "ratio": 1.0000000000139921 }, "stable": true, "rungs": [ @@ -5080,19 +5081,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.4500000000007276, - "referenceP95Ms": 0.5999999999985448, - "referenceGpuMs": 0.18533349999999998, + "referenceMs": 0.46000000000640284, + "referenceP95Ms": 0.6000000000058208, + "referenceGpuMs": 0.1444375, "referenceOverFrameBudget": false, - "competitorMs": 0.22999999999956344, + "competitorMs": 0.22000000000116415, "competitorP95Ms": 0.43999999999505235, - "competitorGpuMs": 0.232604, + "competitorGpuMs": 0.232792, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.956521739137312, - "factor": 1.956521739137312, - "label": "competitor leads (1.96x)", + "ratio": 2.0909090909271306, + "factor": 2.0909090909271306, + "label": "competitor leads (2.09x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -5100,14 +5101,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.43999999999505235, - "maxMs": 0.4599999999991269, - "ratio": 1.045454545464317 + "minMs": 0.4599999999991269, + "maxMs": 0.4900000000016007, + "ratio": 1.0652173913098495 }, "competitor": { "minMs": 0.22000000000116415, - "maxMs": 0.2999999999956344, - "ratio": 1.3636363636093043 + "maxMs": 0.23999999999796273, + "ratio": 1.090909090894058 }, "stable": true, "rungs": [ @@ -5129,19 +5130,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 1.9199999999982538, - "referenceP95Ms": 2.9400000000023283, - "referenceGpuMs": 0.415875, + "referenceMs": 2.210000000002765, + "referenceP95Ms": 2.680000000000291, + "referenceGpuMs": 0.3617295, "referenceOverFrameBudget": false, - "competitorMs": 0.2999999999956344, - "competitorP95Ms": 0.4800000000032014, - "competitorGpuMs": 0.30574999999999997, + "competitorMs": 0.27000000000043656, + "competitorP95Ms": 0.41999999999825377, + "competitorGpuMs": 0.25004149999999997, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 6.400000000087312, - "factor": 6.400000000087312, - "label": "competitor leads clearly (6.40x)", + "ratio": 8.18518518518219, + "factor": 8.18518518518219, + "label": "competitor leads clearly (8.19x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -5149,14 +5150,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.889999999999418, - "maxMs": 2.0499999999992724, - "ratio": 1.0846560846560338 + "minMs": 1.7800000000024738, + "maxMs": 2.2299999999995634, + "ratio": 1.2528089887620586 }, "competitor": { - "minMs": 0.23999999999796273, - "maxMs": 0.3100000000013097, - "ratio": 1.291666666683088 + "minMs": 0.250000000003638, + "maxMs": 0.2999999999956344, + "ratio": 1.1999999999650754 }, "stable": true, "rungs": [ @@ -5178,19 +5179,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.6999999999970896, - "referenceP95Ms": 0.8600000000005821, - "referenceGpuMs": 0.22829149999999998, + "referenceMs": 0.6899999999986903, + "referenceP95Ms": 0.8999999999941792, + "referenceGpuMs": 0.2272915, "referenceOverFrameBudget": false, - "competitorMs": 0.9400000000023283, - "competitorP95Ms": 1.1999999999970896, - "competitorGpuMs": 0.138812, + "competitorMs": 0.9200000000055297, + "competitorP95Ms": 1.180000000000291, + "competitorGpuMs": 0.22462500000000002, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.7446808510588891, - "factor": 1.342857142866052, - "label": "ExoJS leads (1.34x)", + "ratio": 0.7499999999940685, + "factor": 1.3333333333438782, + "label": "ExoJS leads (1.33x)", "structural": false }, "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", @@ -5198,14 +5199,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.6600000000034925, - "maxMs": 0.7000000000043656, - "ratio": 1.0606060606070629 + "minMs": 0.680000000000291, + "maxMs": 0.6999999999970896, + "ratio": 1.0294117647011618 }, "competitor": { - "minMs": 0.9400000000023283, - "maxMs": 1.0400000000008731, - "ratio": 1.1063829787215926 + "minMs": 0.9200000000055297, + "maxMs": 0.9800000000032014, + "ratio": 1.0652173913014251 }, "stable": true, "rungs": [ @@ -5232,19 +5233,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.5600000000013097, - "referenceP95Ms": 0.8800000000046566, - "referenceGpuMs": 0.175417, + "referenceMs": 0.6399999999994179, + "referenceP95Ms": 0.9799999999959255, + "referenceGpuMs": 0.3017085, "referenceOverFrameBudget": false, - "competitorMs": 16.969999999997526, - "competitorP95Ms": 21.539999999993597, - "competitorGpuMs": 0.2945, - "competitorOverFrameBudget": true, + "competitorMs": 16.350000000002183, + "competitorP95Ms": 20.099999999998545, + "competitorGpuMs": 0.29231300000000005, + "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.032999410724890474, - "factor": 30.303571428496138, - "label": "ExoJS leads clearly (30.30x)", + "ratio": 0.03914373088680933, + "factor": 25.546875000026642, + "label": "ExoJS leads clearly (25.55x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -5252,14 +5253,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5599999999976717, - "maxMs": 0.5799999999981083, - "ratio": 1.0357142857152137 + "minMs": 0.5999999999985448, + "maxMs": 0.6599999999962165, + "ratio": 1.099999999996362 }, "competitor": { - "minMs": 15.900000000001455, - "maxMs": 18.080000000001746, - "ratio": 1.1371069182389995 + "minMs": 16.029999999998836, + "maxMs": 16.429999999996653, + "ratio": 1.0249532127260041 }, "stable": true, "rungs": [ @@ -5352,7 +5353,7 @@ "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." ], "engineVersion": "0.17.0", - "timestamp": "2026-09-11T23:53:53.372Z" + "timestamp": "2026-09-12T02:16:07.435Z" }, { "browser": "webkit", @@ -5392,7 +5393,7 @@ "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." ], "engineVersion": "0.17.0", - "timestamp": "2026-09-12T00:06:03.094Z" + "timestamp": "2026-09-12T02:28:32.450Z" }, { "browser": "webkit", @@ -5432,7 +5433,7 @@ "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." ], "engineVersion": "0.17.0", - "timestamp": "2026-09-12T00:18:12.752Z" + "timestamp": "2026-09-12T02:40:41.625Z" } ], "libraries": [ @@ -5470,19 +5471,19 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 5.420000000000073, - "referenceP95Ms": 5.720000000000027, + "referenceMs": 5.480000000000018, + "referenceP95Ms": 5.919999999999845, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 10.359999999998763, - "competitorP95Ms": 11.979999999999563, + "competitorMs": 10.339999999998327, + "competitorP95Ms": 12.059999999997672, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5231660231660926, - "factor": 1.9114391143908902, - "label": "ExoJS leads (1.91x)", + "ratio": 0.5299806576403197, + "factor": 1.8868613138683015, + "label": "ExoJS leads (1.89x)", "structural": false }, "mechanism": "ExoJS resolves fewer solved contacts (5501 vs 12297); the counters are not semantically identical across arms (see the run's caveats)", @@ -5490,14 +5491,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.099999999999909, - "maxMs": 5.470000000000027, - "ratio": 1.0725490196078675 + "minMs": 5.379999999999654, + "maxMs": 5.5, + "ratio": 1.0223048327138202 }, "competitor": { - "minMs": 10.340000000000146, - "maxMs": 10.479999999999563, - "ratio": 1.0135396518374677 + "minMs": 10.30000000000291, + "maxMs": 10.399999999997817, + "ratio": 1.0097087378635805 }, "stable": true, "rungs": [ @@ -5509,19 +5510,19 @@ }, { "competitor": "nape-js", - "referenceMs": 5.420000000000073, - "referenceP95Ms": 5.720000000000027, + "referenceMs": 5.480000000000018, + "referenceP95Ms": 5.919999999999845, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 10, - "competitorP95Ms": 12.860000000000582, + "competitorMs": 10.020000000004075, + "competitorP95Ms": 13.440000000002328, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5420000000000073, - "factor": 1.845018450184477, - "label": "ExoJS leads (1.85x)", + "ratio": 0.5469061876245299, + "factor": 1.828467153285409, + "label": "ExoJS leads (1.83x)", "structural": false }, "mechanism": "identical scene (5501 bodies, 5501 contacts, 0 joints on both arms); the difference is solver cost", @@ -5529,14 +5530,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.099999999999909, - "maxMs": 5.470000000000027, - "ratio": 1.0725490196078675 + "minMs": 5.379999999999654, + "maxMs": 5.5, + "ratio": 1.0223048327138202 }, "competitor": { - "minMs": 9.609999999993306, - "maxMs": 10.110000000007858, - "ratio": 1.0520291363178877 + "minMs": 9.610000000000582, + "maxMs": 10.029999999998836, + "ratio": 1.0437044745055388 }, "stable": true, "rungs": [ @@ -5548,19 +5549,19 @@ }, { "competitor": "planck", - "referenceMs": 5.420000000000073, - "referenceP95Ms": 5.720000000000027, + "referenceMs": 5.480000000000018, + "referenceP95Ms": 5.919999999999845, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 29.970000000001164, - "competitorP95Ms": 37.279999999998836, + "competitorMs": 29.979999999999563, + "competitorP95Ms": 36.36000000000058, "competitorGpuMs": null, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.1808475141808429, - "factor": 5.529520295203093, - "label": "ExoJS leads clearly (5.53x)", + "ratio": 0.18278852568379247, + "factor": 5.470802919707931, + "label": "ExoJS leads clearly (5.47x)", "structural": true }, "mechanism": "ExoJS resolves fewer solved contacts (5501 vs 10677); the counters are not semantically identical across arms (see the run's caveats)", @@ -5568,14 +5569,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.099999999999909, - "maxMs": 5.470000000000027, - "ratio": 1.0725490196078675 + "minMs": 5.379999999999654, + "maxMs": 5.5, + "ratio": 1.0223048327138202 }, "competitor": { - "minMs": 29.69000000000233, - "maxMs": 30, - "ratio": 1.0104412260019418 + "minMs": 29.43000000000029, + "maxMs": 30.4800000000032, + "ratio": 1.0356778797146755 }, "stable": true, "rungs": [ @@ -5587,19 +5588,19 @@ }, { "competitor": "rapier", - "referenceMs": 5.420000000000073, - "referenceP95Ms": 5.720000000000027, + "referenceMs": 5.480000000000018, + "referenceP95Ms": 5.919999999999845, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 4.139999999999418, - "competitorP95Ms": 5.720000000001164, + "competitorMs": 4.060000000004948, + "competitorP95Ms": 5.740000000005239, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 1.3497536945796405, + "factor": 1.3497536945796405, + "label": "competitor leads (1.35x)", "structural": false }, "mechanism": "ExoJS resolves fewer solved contacts (5501 vs 10045); the counters are not semantically identical across arms (see the run's caveats)", @@ -5607,19 +5608,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.099999999999909, - "maxMs": 5.470000000000027, - "ratio": 1.0725490196078675 + "minMs": 5.379999999999654, + "maxMs": 5.5, + "ratio": 1.0223048327138202 }, "competitor": { - "minMs": 4.069999999999709, - "maxMs": 4.319999999999709, - "ratio": 1.0614250614250658 + "minMs": 4.060000000004948, + "maxMs": 4.260000000002037, + "ratio": 1.0492610837430656 }, - "stable": false, + "stable": true, "rungs": [ "competitor-leads", - "level", + "competitor-leads", "competitor-leads" ] } @@ -5636,17 +5637,17 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 5.399999999999636, - "referenceP95Ms": 6.279999999999745, + "referenceMs": 5.420000000000073, + "referenceP95Ms": 6.480000000000018, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.619999999998981, - "competitorP95Ms": 6, + "competitorMs": 5.3400000000001455, + "competitorP95Ms": 6.080000000001746, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9608540925267998, + "ratio": 1.0149812734082257, "factor": 1, "label": "level", "structural": false @@ -5656,14 +5657,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.2800000000002, - "maxMs": 5.4399999999996, - "ratio": 1.0303030303029155 + "minMs": 5.3400000000001455, + "maxMs": 5.5600000000004, + "ratio": 1.0411985018727057 }, "competitor": { - "minMs": 5.260000000002037, - "maxMs": 5.680000000000291, - "ratio": 1.0798479087448842 + "minMs": 5.279999999998836, + "maxMs": 5.559999999999491, + "ratio": 1.0530303030304387 }, "stable": true, "rungs": [ @@ -5675,17 +5676,17 @@ }, { "competitor": "nape-js", - "referenceMs": 5.399999999999636, - "referenceP95Ms": 6.279999999999745, + "referenceMs": 5.420000000000073, + "referenceP95Ms": 6.480000000000018, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.860000000000582, - "competitorP95Ms": 6.680000000007567, + "competitorMs": 5.80000000000291, + "competitorP95Ms": 6.80000000000291, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.921501706484488, + "ratio": 0.9344827586202333, "factor": 1, "label": "level", "structural": false @@ -5695,14 +5696,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.2800000000002, - "maxMs": 5.4399999999996, - "ratio": 1.0303030303029155 + "minMs": 5.3400000000001455, + "maxMs": 5.5600000000004, + "ratio": 1.0411985018727057 }, "competitor": { - "minMs": 5.80000000000291, - "maxMs": 5.860000000000582, - "ratio": 1.0103448275858002 + "minMs": 5.779999999998836, + "maxMs": 5.820000000006985, + "ratio": 1.0069204152263247 }, "stable": true, "rungs": [ @@ -5714,17 +5715,17 @@ }, { "competitor": "planck", - "referenceMs": 5.399999999999636, - "referenceP95Ms": 6.279999999999745, + "referenceMs": 5.420000000000073, + "referenceP95Ms": 6.480000000000018, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.540000000000873, - "competitorP95Ms": 6.560000000004948, + "competitorMs": 5.740000000005239, + "competitorP95Ms": 6.459999999999127, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.974729241877037, + "ratio": 0.9442508710792903, "factor": 1, "label": "level", "structural": false @@ -5734,14 +5735,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.2800000000002, - "maxMs": 5.4399999999996, - "ratio": 1.0303030303029155 + "minMs": 5.3400000000001455, + "maxMs": 5.5600000000004, + "ratio": 1.0411985018727057 }, "competitor": { "minMs": 5.519999999996799, - "maxMs": 5.580000000001746, - "ratio": 1.010869565218294 + "maxMs": 6, + "ratio": 1.0869565217397608 }, "stable": true, "rungs": [ @@ -5753,8 +5754,8 @@ }, { "competitor": "rapier", - "referenceMs": 5.399999999999636, - "referenceP95Ms": 6.279999999999745, + "referenceMs": 5.420000000000073, + "referenceP95Ms": 6.480000000000018, "referenceGpuMs": null, "referenceOverFrameBudget": false, "competitorMs": 1.3000000000029104, @@ -5763,9 +5764,9 @@ "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 4.153846153836574, - "factor": 4.153846153836574, - "label": "competitor leads (4.15x)", + "ratio": 4.169230769221492, + "factor": 4.169230769221492, + "label": "competitor leads (4.17x)", "structural": false }, "mechanism": "identical scene (1504 bodies, 4236 contacts, 0 joints on both arms); the difference is solver cost", @@ -5773,14 +5774,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.2800000000002, - "maxMs": 5.4399999999996, - "ratio": 1.0303030303029155 + "minMs": 5.3400000000001455, + "maxMs": 5.5600000000004, + "ratio": 1.0411985018727057 }, "competitor": { - "minMs": 1.3000000000029104, - "maxMs": 1.319999999992433, - "ratio": 1.0153846153765214 + "minMs": 1.2800000000061118, + "maxMs": 1.3000000000029104, + "ratio": 1.0156249999974243 }, "stable": true, "rungs": [ @@ -5802,19 +5803,19 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 4.539999999999964, - "referenceP95Ms": 5, + "referenceMs": 4.579999999999927, + "referenceP95Ms": 5.059999999999491, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 2.3000000000010914, - "competitorP95Ms": 2.5800000000017462, + "competitorMs": 2.2399999999979627, + "competitorP95Ms": 2.599999999998545, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.9739130434773084, - "factor": 1.9739130434773084, - "label": "competitor leads (1.97x)", + "ratio": 2.044642857144684, + "factor": 2.044642857144684, + "label": "competitor leads (2.04x)", "structural": false }, "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms); the difference is solver cost", @@ -5822,14 +5823,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.5, - "maxMs": 4.619999999999891, - "ratio": 1.0266666666666424 + "minMs": 4.539999999999964, + "maxMs": 4.600000000000364, + "ratio": 1.0132158590309253 }, "competitor": { - "minMs": 2.2000000000007276, - "maxMs": 2.3000000000010914, - "ratio": 1.0454545454546957 + "minMs": 2.1700000000000728, + "maxMs": 2.2999999999992724, + "ratio": 1.0599078341010117 }, "stable": true, "rungs": [ @@ -5841,17 +5842,17 @@ }, { "competitor": "nape-js", - "referenceMs": 4.539999999999964, - "referenceP95Ms": 5, + "referenceMs": 4.579999999999927, + "referenceP95Ms": 5.059999999999491, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.029999999998836, - "competitorP95Ms": 6.240000000005239, + "competitorMs": 5.019999999989523, + "competitorP95Ms": 6.059999999997672, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9025844930419512, + "ratio": 0.9123505976114514, "factor": 1, "label": "level", "structural": false @@ -5861,14 +5862,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.5, - "maxMs": 4.619999999999891, - "ratio": 1.0266666666666424 + "minMs": 4.539999999999964, + "maxMs": 4.600000000000364, + "ratio": 1.0132158590309253 }, "competitor": { - "minMs": 4.779999999998836, - "maxMs": 5.080000000001746, - "ratio": 1.0627615062767748 + "minMs": 4.959999999999127, + "maxMs": 5.150000000001455, + "ratio": 1.0383064516133793 }, "stable": true, "rungs": [ @@ -5880,19 +5881,19 @@ }, { "competitor": "planck", - "referenceMs": 4.539999999999964, - "referenceP95Ms": 5, + "referenceMs": 4.579999999999927, + "referenceP95Ms": 5.059999999999491, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 7.920000000001892, - "competitorP95Ms": 10.659999999996217, + "competitorMs": 7.860000000000582, + "competitorP95Ms": 10.69999999999709, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5732323232321818, - "factor": 1.7444933920709151, - "label": "ExoJS leads (1.74x)", + "ratio": 0.5826972010177593, + "factor": 1.7161572052403289, + "label": "ExoJS leads (1.72x)", "structural": false }, "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms); the difference is solver cost", @@ -5900,14 +5901,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.5, - "maxMs": 4.619999999999891, - "ratio": 1.0266666666666424 + "minMs": 4.539999999999964, + "maxMs": 4.600000000000364, + "ratio": 1.0132158590309253 }, "competitor": { - "minMs": 7.879999999997381, - "maxMs": 8.020000000004075, - "ratio": 1.0177664974627842 + "minMs": 7.769999999996799, + "maxMs": 7.979999999999563, + "ratio": 1.027027027027394 }, "stable": true, "rungs": [ @@ -5919,19 +5920,19 @@ }, { "competitor": "rapier", - "referenceMs": 4.539999999999964, - "referenceP95Ms": 5, + "referenceMs": 4.579999999999927, + "referenceP95Ms": 5.059999999999491, "referenceGpuMs": null, "referenceOverFrameBudget": false, "competitorMs": 1.5800000000017462, - "competitorP95Ms": 1.860000000000582, + "competitorP95Ms": 1.8399999999965075, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 2.873417721515789, - "factor": 2.873417721515789, - "label": "competitor leads (2.87x)", + "ratio": 2.89873417721194, + "factor": 2.89873417721194, + "label": "competitor leads (2.90x)", "structural": false }, "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms); the difference is solver cost", @@ -5939,14 +5940,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.5, - "maxMs": 4.619999999999891, - "ratio": 1.0266666666666424 + "minMs": 4.539999999999964, + "maxMs": 4.600000000000364, + "ratio": 1.0132158590309253 }, "competitor": { - "minMs": 1.5800000000017462, + "minMs": 1.5799999999871943, "maxMs": 1.639999999999418, - "ratio": 1.0379746835427883 + "ratio": 1.037974683552348 }, "stable": true, "rungs": [ @@ -5968,17 +5969,17 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 5.739999999999782, - "referenceP95Ms": 6.219999999999345, + "referenceMs": 5.720000000000255, + "referenceP95Ms": 6.319999999999709, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 6.429999999998472, + "competitorMs": 6.520000000000437, "competitorP95Ms": 8.020000000000437, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.8926905132194628, + "ratio": 0.8773006134969128, "factor": 1, "label": "level", "structural": false @@ -5989,13 +5990,13 @@ "runs": 3, "reference": { "minMs": 5.640000000001237, - "maxMs": 5.819999999999709, - "ratio": 1.0319148936167433 + "maxMs": 5.739999999999782, + "ratio": 1.0177304964536389 }, "competitor": { - "minMs": 6.419999999998254, - "maxMs": 6.449999999998909, - "ratio": 1.004672897196365 + "minMs": 6.440000000000509, + "maxMs": 6.550000000001091, + "ratio": 1.017080745341704 }, "stable": true, "rungs": [ @@ -6007,18 +6008,18 @@ }, { "competitor": "nape-js", - "referenceMs": 5.739999999999782, - "referenceP95Ms": 6.219999999999345, + "referenceMs": 5.720000000000255, + "referenceP95Ms": 6.319999999999709, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 12.660000000003492, - "competitorP95Ms": 14.279999999998836, + "competitorMs": 12.619999999995343, + "competitorP95Ms": 14.059999999997672, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.4533965244864296, - "factor": 2.2055749128926783, + "ratio": 0.453248811410647, + "factor": 2.206293706292794, "label": "ExoJS leads (2.21x)", "structural": false }, @@ -6028,13 +6029,13 @@ "runs": 3, "reference": { "minMs": 5.640000000001237, - "maxMs": 5.819999999999709, - "ratio": 1.0319148936167433 + "maxMs": 5.739999999999782, + "ratio": 1.0177304964536389 }, "competitor": { - "minMs": 12.619999999995343, + "minMs": 12.539999999993597, "maxMs": 12.69999999999709, - "ratio": 1.0063391442156717 + "ratio": 1.0127591706541925 }, "stable": true, "rungs": [ @@ -6046,19 +6047,19 @@ }, { "competitor": "planck", - "referenceMs": 5.739999999999782, - "referenceP95Ms": 6.219999999999345, + "referenceMs": 5.720000000000255, + "referenceP95Ms": 6.319999999999709, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 8.06000000000131, - "competitorP95Ms": 10.940000000002328, + "competitorMs": 8.059999999997672, + "competitorP95Ms": 10.339999999996508, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.7121588089328597, - "factor": 1.404181184669271, - "label": "ExoJS leads (1.40x)", + "ratio": 0.7096774193550753, + "factor": 1.4090909090904393, + "label": "ExoJS leads (1.41x)", "structural": false }, "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms) with 34 vs 37 ray hits per step; the difference is query and solver cost", @@ -6067,13 +6068,13 @@ "runs": 3, "reference": { "minMs": 5.640000000001237, - "maxMs": 5.819999999999709, - "ratio": 1.0319148936167433 + "maxMs": 5.739999999999782, + "ratio": 1.0177304964536389 }, "competitor": { - "minMs": 7.879999999997381, - "maxMs": 8.150000000001455, - "ratio": 1.0342639593913914 + "minMs": 7.990000000001601, + "maxMs": 8.100000000002183, + "ratio": 1.013767209011334 }, "stable": true, "rungs": [ @@ -6085,18 +6086,18 @@ }, { "competitor": "rapier", - "referenceMs": 5.739999999999782, - "referenceP95Ms": 6.219999999999345, + "referenceMs": 5.720000000000255, + "referenceP95Ms": 6.319999999999709, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 1.7099999999991269, - "competitorP95Ms": 1.9199999999982538, + "competitorMs": 1.7000000000116415, + "competitorP95Ms": 1.860000000000582, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 3.3567251462004166, - "factor": 3.3567251462004166, + "ratio": 3.3647058823300497, + "factor": 3.3647058823300497, "label": "competitor leads (3.36x)", "structural": false }, @@ -6106,13 +6107,13 @@ "runs": 3, "reference": { "minMs": 5.640000000001237, - "maxMs": 5.819999999999709, - "ratio": 1.0319148936167433 + "maxMs": 5.739999999999782, + "ratio": 1.0177304964536389 }, "competitor": { - "minMs": 1.639999999999418, + "minMs": 1.6499999999941792, "maxMs": 1.7200000000011642, - "ratio": 1.0487804878059601 + "ratio": 1.0424242424286254 }, "stable": true, "rungs": [ @@ -6134,17 +6135,17 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 5.579999999999927, - "referenceP95Ms": 6.3400000000001455, + "referenceMs": 5.599999999999454, + "referenceP95Ms": 6.399999999999636, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.759999999994761, - "competitorP95Ms": 6.739999999997963, + "competitorMs": 5.709999999999127, + "competitorP95Ms": 6.700000000000728, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9687500000008684, + "ratio": 0.9807355516638022, "factor": 1, "label": "level", "structural": false @@ -6154,14 +6155,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.5, - "maxMs": 5.680000000000291, - "ratio": 1.0327272727273256 + "minMs": 5.520000000000437, + "maxMs": 5.6899999999996, + "ratio": 1.0307971014491213 }, "competitor": { - "minMs": 5.720000000001164, - "maxMs": 5.760000000002037, - "ratio": 1.0069930069931583 + "minMs": 5.639999999999418, + "maxMs": 5.739999999997963, + "ratio": 1.0177304964536444 }, "stable": true, "rungs": [ @@ -6173,19 +6174,19 @@ }, { "competitor": "nape-js", - "referenceMs": 5.579999999999927, - "referenceP95Ms": 6.3400000000001455, + "referenceMs": 5.599999999999454, + "referenceP95Ms": 6.399999999999636, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 3.349999999998545, - "competitorP95Ms": 3.8999999999941792, + "competitorMs": 3.039999999993597, + "competitorP95Ms": 3.5599999999976717, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.6656716417917465, - "factor": 1.6656716417917465, - "label": "competitor leads (1.67x)", + "ratio": 1.842105263161595, + "factor": 1.842105263161595, + "label": "competitor leads (1.84x)", "structural": false }, "mechanism": "identical scene (1504 bodies, 1415 contacts, 0 joints on both arms); the difference is solver cost", @@ -6193,14 +6194,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.5, - "maxMs": 5.680000000000291, - "ratio": 1.0327272727273256 + "minMs": 5.520000000000437, + "maxMs": 5.6899999999996, + "ratio": 1.0307971014491213 }, "competitor": { - "minMs": 3.110000000000582, - "maxMs": 3.4799999999959255, - "ratio": 1.118971061091728 + "minMs": 2.820000000006985, + "maxMs": 3.1000000000058208, + "ratio": 1.0992907801411853 }, "stable": true, "rungs": [ @@ -6212,17 +6213,17 @@ }, { "competitor": "planck", - "referenceMs": 5.579999999999927, - "referenceP95Ms": 6.3400000000001455, + "referenceMs": 5.599999999999454, + "referenceP95Ms": 6.399999999999636, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.539999999993597, - "competitorP95Ms": 7.139999999999418, + "competitorMs": 5.430000000000291, + "competitorP95Ms": 7.180000000007567, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.0072202166076492, + "ratio": 1.0313075506444114, "factor": 1, "label": "level", "structural": false @@ -6232,14 +6233,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.5, - "maxMs": 5.680000000000291, - "ratio": 1.0327272727273256 + "minMs": 5.520000000000437, + "maxMs": 5.6899999999996, + "ratio": 1.0307971014491213 }, "competitor": { - "minMs": 5.459999999999127, + "minMs": 5.280000000006112, "maxMs": 5.619999999995343, - "ratio": 1.029304029303341 + "ratio": 1.0643939393918254 }, "stable": true, "rungs": [ @@ -6251,19 +6252,19 @@ }, { "competitor": "rapier", - "referenceMs": 5.579999999999927, - "referenceP95Ms": 6.3400000000001455, + "referenceMs": 5.599999999999454, + "referenceP95Ms": 6.399999999999636, "referenceGpuMs": null, "referenceOverFrameBudget": false, "competitorMs": 0.7599999999947613, - "competitorP95Ms": 0.7800000000133878, + "competitorP95Ms": 0.7799999999988358, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 7.342105263208408, - "factor": 7.342105263208408, - "label": "competitor leads clearly (7.34x)", + "ratio": 7.368421052681652, + "factor": 7.368421052681652, + "label": "competitor leads clearly (7.37x)", "structural": true }, "mechanism": "identical scene (1504 bodies, 1415 contacts, 0 joints on both arms); the difference is solver cost", @@ -6271,14 +6272,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.5, - "maxMs": 5.680000000000291, - "ratio": 1.0327272727273256 + "minMs": 5.520000000000437, + "maxMs": 5.6899999999996, + "ratio": 1.0307971014491213 }, "competitor": { "minMs": 0.7599999999947613, - "maxMs": 0.7799999999988358, - "ratio": 1.0263157894792267 + "maxMs": 0.8000000000029104, + "ratio": 1.0526315789584537 }, "stable": true, "rungs": [ @@ -6300,17 +6301,17 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 5.630000000000109, - "referenceP95Ms": 6.140000000001237, + "referenceMs": 5.240000000000691, + "referenceP95Ms": 5.459999999999127, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.519999999996799, - "competitorP95Ms": 7.860000000000582, + "competitorMs": 4.819999999999709, + "competitorP95Ms": 6.580000000001746, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.0199275362324953, + "ratio": 1.08713692946079, "factor": 1, "label": "level", "structural": false @@ -6320,14 +6321,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.920000000000073, - "maxMs": 5.720000000001164, - "ratio": 1.1626016260164795 + "minMs": 4.980000000000473, + "maxMs": 5.699999999998909, + "ratio": 1.144578313252684 }, "competitor": { - "minMs": 5.040000000000873, - "maxMs": 6.040000000000873, - "ratio": 1.198412698412664 + "minMs": 4.740000000001601, + "maxMs": 5.879999999997381, + "ratio": 1.2405063291129526 }, "stable": true, "rungs": [ @@ -6339,12 +6340,12 @@ }, { "competitor": "nape-js", - "referenceMs": 5.630000000000109, - "referenceP95Ms": 6.140000000001237, + "referenceMs": 5.240000000000691, + "referenceP95Ms": 5.459999999999127, "referenceGpuMs": null, "referenceOverFrameBudget": false, "competitorMs": 0, - "competitorP95Ms": 0.0020000000004074536, + "competitorP95Ms": 0, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { @@ -6359,9 +6360,9 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.920000000000073, - "maxMs": 5.720000000001164, - "ratio": 1.1626016260164795 + "minMs": 4.980000000000473, + "maxMs": 5.699999999998909, + "ratio": 1.144578313252684 }, "competitor": { "minMs": 0, @@ -6378,19 +6379,19 @@ }, { "competitor": "planck", - "referenceMs": 5.630000000000109, - "referenceP95Ms": 6.140000000001237, + "referenceMs": 5.240000000000691, + "referenceP95Ms": 5.459999999999127, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 0.3799999999901047, - "competitorP95Ms": 0.3800000000046566, + "competitorMs": 0.39999999999417923, + "competitorP95Ms": 0.40000000000873115, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 14.815789474070305, - "factor": 14.815789474070305, - "label": "competitor leads clearly (14.82x)", + "ratio": 13.100000000192358, + "factor": 13.100000000192358, + "label": "competitor leads clearly (13.10x)", "structural": true }, "mechanism": "competitor resolves fewer solved contacts (7875 vs 0); the counters are not semantically identical across arms (see the run's caveats)", @@ -6398,14 +6399,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.920000000000073, - "maxMs": 5.720000000001164, - "ratio": 1.1626016260164795 + "minMs": 4.980000000000473, + "maxMs": 5.699999999998909, + "ratio": 1.144578313252684 }, "competitor": { - "minMs": 0.37000000000261934, - "maxMs": 0.39999999999417923, - "ratio": 1.081081081057696 + "minMs": 0.36499999999796273, + "maxMs": 0.41000000000349246, + "ratio": 1.1232876712487148 }, "stable": true, "rungs": [ @@ -6417,8 +6418,8 @@ }, { "competitor": "rapier", - "referenceMs": 5.630000000000109, - "referenceP95Ms": 6.140000000001237, + "referenceMs": 5.240000000000691, + "referenceP95Ms": 5.459999999999127, "referenceGpuMs": null, "referenceOverFrameBudget": false, "competitorMs": 0.19999999999708962, @@ -6427,9 +6428,9 @@ "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 28.15000000041018, - "factor": 28.15000000041018, - "label": "competitor leads clearly (28.15x)", + "ratio": 26.200000000384716, + "factor": 26.200000000384716, + "label": "competitor leads clearly (26.20x)", "structural": true }, "mechanism": "competitor resolves fewer solved contacts (7875 vs 1125); the counters are not semantically identical across arms (see the run's caveats)", @@ -6437,9 +6438,9 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.920000000000073, - "maxMs": 5.720000000001164, - "ratio": 1.1626016260164795 + "minMs": 4.980000000000473, + "maxMs": 5.699999999998909, + "ratio": 1.144578313252684 }, "competitor": { "minMs": 0.19999999999708962, @@ -6467,18 +6468,18 @@ { "competitor": "matter-js", "referenceMs": 3.7400000000016007, - "referenceP95Ms": 4.399999999997817, + "referenceP95Ms": 4.440000000002328, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 10.660000000003492, - "competitorP95Ms": 12.19999999999709, + "competitorMs": 10.640000000003056, + "competitorP95Ms": 12.339999999996508, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.3508442776735812, - "factor": 2.8502673796788582, - "label": "ExoJS leads (2.85x)", + "ratio": 0.3515037593985457, + "factor": 2.8449197860958564, + "label": "ExoJS leads (2.84x)", "structural": false }, "mechanism": "identical scene (3004 bodies, 6199 contacts, 0 joints on both arms); the difference is solver cost", @@ -6486,14 +6487,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.7000000000007276, - "maxMs": 3.7600000000020373, - "ratio": 1.016216216216567 + "minMs": 3.719999999997526, + "maxMs": 3.7599999999983993, + "ratio": 1.0107526881722848 }, "competitor": { - "minMs": 10.62000000000262, + "minMs": 10.639999999999418, "maxMs": 10.680000000000291, - "ratio": 1.0056497175139036 + "ratio": 1.003759398496323 }, "stable": true, "rungs": [ @@ -6506,18 +6507,18 @@ { "competitor": "nape-js", "referenceMs": 3.7400000000016007, - "referenceP95Ms": 4.399999999997817, + "referenceP95Ms": 4.440000000002328, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 11.020000000004075, - "competitorP95Ms": 13.900000000008731, + "competitorMs": 11.059999999997672, + "competitorP95Ms": 14.099999999991269, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.3393829401089127, - "factor": 2.9465240641709514, - "label": "ExoJS leads (2.95x)", + "ratio": 0.33815551537092114, + "factor": 2.9572192513350104, + "label": "ExoJS leads (2.96x)", "structural": false }, "mechanism": "identical scene (3004 bodies, 6199 contacts, 0 joints on both arms); the difference is solver cost", @@ -6525,14 +6526,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.7000000000007276, - "maxMs": 3.7600000000020373, - "ratio": 1.016216216216567 + "minMs": 3.719999999997526, + "maxMs": 3.7599999999983993, + "ratio": 1.0107526881722848 }, "competitor": { - "minMs": 10.980000000010477, - "maxMs": 11.040000000000873, - "ratio": 1.005464480873437 + "minMs": 11, + "maxMs": 11.080000000001746, + "ratio": 1.007272727272886 }, "stable": true, "rungs": [ @@ -6545,18 +6546,18 @@ { "competitor": "planck", "referenceMs": 3.7400000000016007, - "referenceP95Ms": 4.399999999997817, + "referenceP95Ms": 4.440000000002328, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 13.220000000001164, - "competitorP95Ms": 19.59999999999127, + "competitorMs": 13.299999999988358, + "competitorP95Ms": 19.679999999993015, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.28290468986393885, - "factor": 3.5347593582875683, - "label": "ExoJS leads (3.53x)", + "ratio": 0.2812030075191635, + "factor": 3.556149732615686, + "label": "ExoJS leads (3.56x)", "structural": false }, "mechanism": "identical scene (3004 bodies, 6199 contacts, 0 joints on both arms); the difference is solver cost", @@ -6564,14 +6565,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.7000000000007276, - "maxMs": 3.7600000000020373, - "ratio": 1.016216216216567 + "minMs": 3.719999999997526, + "maxMs": 3.7599999999983993, + "ratio": 1.0107526881722848 }, "competitor": { - "minMs": 13.140000000006694, - "maxMs": 13.39999999999418, - "ratio": 1.0197869101969066 + "minMs": 13.240000000005239, + "maxMs": 13.330000000001746, + "ratio": 1.0067975830813045 }, "stable": true, "rungs": [ @@ -6584,18 +6585,18 @@ { "competitor": "rapier", "referenceMs": 3.7400000000016007, - "referenceP95Ms": 4.399999999997817, + "referenceP95Ms": 4.440000000002328, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 2.139999999999418, - "competitorP95Ms": 2.1999999999970896, + "competitorMs": 2.099999999991269, + "competitorP95Ms": 2.2599999999947613, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.7476635514030925, - "factor": 1.7476635514030925, - "label": "competitor leads (1.75x)", + "ratio": 1.7809523809605479, + "factor": 1.7809523809605479, + "label": "competitor leads (1.78x)", "structural": false }, "mechanism": "identical scene (3004 bodies, 6199 contacts, 0 joints on both arms); the difference is solver cost", @@ -6603,14 +6604,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.7000000000007276, - "maxMs": 3.7600000000020373, - "ratio": 1.016216216216567 + "minMs": 3.719999999997526, + "maxMs": 3.7599999999983993, + "ratio": 1.0107526881722848 }, "competitor": { - "minMs": 2.0599999999976717, - "maxMs": 2.1599999999889405, - "ratio": 1.0485436893162048 + "minMs": 2.0800000000017462, + "maxMs": 2.139999999999418, + "ratio": 1.0288461538450102 }, "stable": true, "rungs": [ @@ -6627,6 +6628,6 @@ }, "signature": { "algorithm": "sha256", - "value": "640ef3c7d832b56fda2fccbfad1eacfe6f148ab469f7444145b25bd0701642dc" + "value": "d251be16370d0abc9c51f386e8a873c328de964248aaaf83e80546a0ea754888" } } From 218ad725ede716c34115c462c9292975c9757c0e Mon Sep 17 00:00:00 2001 From: Exoridus Date: Sat, 12 Sep 2026 03:41:25 +0200 Subject: [PATCH 06/28] bench: add the ui-layout-update arms and stop the catalog dropping a scenario The archetype had a catalog entry, a ladder and no arm, so every reference run resolved 23 of 24 scenarios and nothing said so. The suite-plan test skipped any catalogued scenario without an archetype ladder, which is exactly the shape of that gap; it now asserts the ids instead. ExoJS lays the tree out through `Stack`, Pixi through `@pixi/layout` over Yoga. Phaser 4 and Excalibur 0.32 ship no layout engine at all and sit the archetype out, so this is a published two-arm comparison. The harness checks the resolved rectangles against a closed-form model of the shared definition rather than against the other arm, and the structural probe's "a non-empty scene must draw" self-check is exempted here: this is the one scene that submits nothing by design. Found on the way: a nested `Stack` was pinned to an explicit box the first time its parent laid it out, because the parent wrote back a size the child already had. It then stopped sizing itself to its content for the rest of its life. --- .../exojs-bench/baselines/structural.json | 22 +- packages/exojs-bench/competitors/package.json | 4 +- .../exojs-bench/competitors/pnpm-lock.yaml | 105 ++++++++ .../src/rendering/EngineAdapter.ts | 32 +++ .../src/rendering/adapters/excalibur.ts | 7 +- .../src/rendering/adapters/exojs.ts | 176 ++++++++++++- .../src/rendering/adapters/phaser.ts | 10 +- .../src/rendering/adapters/pixi.ts | 241 ++++++++++++++++- .../exojs-bench/src/rendering/archetypes.ts | 37 +++ packages/exojs-bench/src/rendering/driver.ts | 11 +- .../exojs-bench/src/rendering/page/harness.ts | 38 ++- .../exojs-bench/src/rendering/uiLayout.ts | 244 ++++++++++++++++++ packages/exojs-bench/test/suite-plan.test.ts | 15 +- src/ui/Stack.ts | 11 +- test/ui/layout.test.ts | 16 ++ 15 files changed, 952 insertions(+), 17 deletions(-) create mode 100644 packages/exojs-bench/src/rendering/uiLayout.ts diff --git a/packages/exojs-bench/baselines/structural.json b/packages/exojs-bench/baselines/structural.json index aab6203a2..2838777d3 100644 --- a/packages/exojs-bench/baselines/structural.json +++ b/packages/exojs-bench/baselines/structural.json @@ -1,6 +1,6 @@ { "recorded": { - "at": "2026-09-11T10:10:10.636Z", + "at": "2026-09-12T01:40:26.907Z", "engineVersion": "0.17.0", "adapter": "ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)" }, @@ -275,6 +275,16 @@ "textureBinds": 0, "bufferUploads": 0 }, + { + "engine": "exojs", + "config": "current", + "backend": "webgl2", + "archetype": "ui-layout-update", + "nodeCount": 200, + "drawCalls": 0, + "textureBinds": 0, + "bufferUploads": 0 + }, { "engine": "exojs", "config": "retained", @@ -544,6 +554,16 @@ "drawCalls": 1, "textureBinds": 0, "bufferUploads": 0 + }, + { + "engine": "exojs", + "config": "retained", + "backend": "webgl2", + "archetype": "ui-layout-update", + "nodeCount": 200, + "drawCalls": 0, + "textureBinds": 0, + "bufferUploads": 0 } ] } diff --git a/packages/exojs-bench/competitors/package.json b/packages/exojs-bench/competitors/package.json index 1d88bb78a..3ceab2800 100644 --- a/packages/exojs-bench/competitors/package.json +++ b/packages/exojs-bench/competitors/package.json @@ -6,12 +6,14 @@ "dependencies": { "pixi.js": "8.19.0", "@pixi/tilemap": "5.0.2", + "@pixi/layout": "3.2.1", "phaser": "4.2.1", "excalibur": "0.32.0", "matter-js": "0.20.0", "planck": "1.5.0", "@newkrok/nape-js": "3.42.0", "@types/matter-js": "0.20.2", - "@dimforge/rapier2d-compat": "0.19.3" + "@dimforge/rapier2d-compat": "0.19.3", + "yoga-layout": "3.2.1" } } diff --git a/packages/exojs-bench/competitors/pnpm-lock.yaml b/packages/exojs-bench/competitors/pnpm-lock.yaml index 39a5e96da..bf200087a 100644 --- a/packages/exojs-bench/competitors/pnpm-lock.yaml +++ b/packages/exojs-bench/competitors/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@newkrok/nape-js': specifier: 3.42.0 version: 3.42.0 + '@pixi/layout': + specifier: 3.2.1 + version: 3.2.1(@types/react@19.2.18)(pixi.js@8.19.0)(react@19.2.8)(yoga-layout@3.2.1) '@pixi/tilemap': specifier: 5.0.2 version: 5.0.2(pixi.js@8.19.0) @@ -35,6 +38,9 @@ importers: planck: specifier: 1.5.0 version: 1.5.0(stage-js@1.0.2) + yoga-layout: + specifier: 3.2.1 + version: 3.2.1 packages: @@ -48,6 +54,18 @@ packages: '@pixi/colord@2.9.6': resolution: {integrity: sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==} + '@pixi/layout@3.2.1': + resolution: {integrity: sha512-ANg9pk7qo4psnafupTY47ZYoL2Jp1f8djiEUGJQAPEr+m2QIPaCR+dWjzSl1kqZTHlYmxxVNnztCRfrPu+q62w==} + peerDependencies: + pixi.js: ^8 + yoga-layout: ^3 + + '@pixi/react@8.0.5': + resolution: {integrity: sha512-Z1VRdnv9Gh+lTLzNKjpS7GaTNDjUxRVJNtIdtK2i0sCpigvjx5mvJ72EPLhBFgepB6j3ZCkL0YBb6BqgTGbhGA==} + peerDependencies: + pixi.js: ^8.2.6 + react: '>=19.0.0' + '@pixi/tilemap@5.0.2': resolution: {integrity: sha512-J+K1eU7uB58WPVD33d7usXrhKA03IGq3IfPvXBFr1027Bb9ScxrGhH0NomR9VpVVUPiHkK7FcTGuOtJMVGAv6A==} peerDependencies: @@ -59,6 +77,14 @@ packages: '@types/matter-js@0.20.2': resolution: {integrity: sha512-3PPKy3QxvZ89h9+wdBV2488I1JLVs7DEpIkPvgO8JC1mUdiVSO37ZIvVctOTD7hIq8OAL2gJ3ugGSuUip6DhCw==} + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + '@webgpu/types@0.1.71': resolution: {integrity: sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==} @@ -67,6 +93,9 @@ packages: engines: {node: '>=10.0.0'} deprecated: this version has critical issues, please update to the latest version + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + earcut@3.2.3: resolution: {integrity: sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==} @@ -82,6 +111,11 @@ packages: ismobilejs@1.1.1: resolution: {integrity: sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==} + its-fine@2.0.0: + resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==} + peerDependencies: + react: ^19.0.0 + js-binary-schema-parser@2.0.3: resolution: {integrity: sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==} @@ -103,6 +137,19 @@ packages: peerDependencies: stage-js: ^1.0.0-alpha.12 + react-reconciler@0.31.0: + resolution: {integrity: sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^19.0.0 + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + + scheduler@0.25.0: + resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + stage-js@1.0.2: resolution: {integrity: sha512-EWTRBYlg7Qv9wGUao99/PfRe3KaiQqWmgSvTOXvaWnu1Jk/q/vV8yJVu6bi/3EqDZeMVnCPAjheba6OFc5k1GQ==} engines: {node: '>=24.0'} @@ -111,6 +158,9 @@ packages: resolution: {integrity: sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==} engines: {node: '>=12'} + yoga-layout@3.2.1: + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + snapshots: '@dimforge/rapier2d-compat@0.19.3': {} @@ -119,6 +169,26 @@ snapshots: '@pixi/colord@2.9.6': {} + '@pixi/layout@3.2.1(@types/react@19.2.18)(pixi.js@8.19.0)(react@19.2.8)(yoga-layout@3.2.1)': + dependencies: + pixi.js: 8.19.0 + yoga-layout: 3.2.1 + optionalDependencies: + '@pixi/react': 8.0.5(@types/react@19.2.18)(pixi.js@8.19.0)(react@19.2.8) + transitivePeerDependencies: + - '@types/react' + - react + + '@pixi/react@8.0.5(@types/react@19.2.18)(pixi.js@8.19.0)(react@19.2.8)': + dependencies: + its-fine: 2.0.0(@types/react@19.2.18)(react@19.2.8) + pixi.js: 8.19.0 + react: 19.2.8 + react-reconciler: 0.31.0(react@19.2.8) + transitivePeerDependencies: + - '@types/react' + optional: true + '@pixi/tilemap@5.0.2(pixi.js@8.19.0)': dependencies: pixi.js: 8.19.0 @@ -127,10 +197,23 @@ snapshots: '@types/matter-js@0.20.2': {} + '@types/react-reconciler@0.28.9(@types/react@19.2.18)': + dependencies: + '@types/react': 19.2.18 + optional: true + + '@types/react@19.2.18': + dependencies: + csstype: 3.2.3 + optional: true + '@webgpu/types@0.1.71': {} '@xmldom/xmldom@0.8.13': {} + csstype@3.2.3: + optional: true + earcut@3.2.3: {} eventemitter3@5.0.4: {} @@ -143,6 +226,14 @@ snapshots: ismobilejs@1.1.1: {} + its-fine@2.0.0(@types/react@19.2.18)(react@19.2.8): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@19.2.18) + react: 19.2.8 + transitivePeerDependencies: + - '@types/react' + optional: true + js-binary-schema-parser@2.0.3: {} matter-js@0.20.0: {} @@ -170,6 +261,20 @@ snapshots: dependencies: stage-js: 1.0.2 + react-reconciler@0.31.0(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.25.0 + optional: true + + react@19.2.8: + optional: true + + scheduler@0.25.0: + optional: true + stage-js@1.0.2: {} tiny-lru@11.4.7: {} + + yoga-layout@3.2.1: {} diff --git a/packages/exojs-bench/src/rendering/EngineAdapter.ts b/packages/exojs-bench/src/rendering/EngineAdapter.ts index d8f06693f..1cb516535 100644 --- a/packages/exojs-bench/src/rendering/EngineAdapter.ts +++ b/packages/exojs-bench/src/rendering/EngineAdapter.ts @@ -18,6 +18,7 @@ export type ArchetypeId = | 'particles-lifecycle' | 'fx-blur' | 'interaction-picking' + | 'ui-layout-update' | 'batch-breaking' | 'batch-breaking-atlased' | 'split-screen' @@ -317,6 +318,21 @@ export interface ArchetypeSpec { * many interactive rectangles the query has to search. */ readonly pointerQueriesPerFrame?: number; + /** + * Box-tree layout passes resolved per frame, or `undefined` for the scenes + * that resolve none. + * + * Setting it makes the scene a tree of nested horizontal and vertical boxes + * and the frame's work a block of that many layout passes, each changing a + * tenth of the leaf widths and alternating the viewport the root resolves + * against. The node count is how many leaf widgets the tree holds. + * + * The block is the published unit, not one pass: a single resolve lands under + * the clock's resolution on both arms. Nothing is drawn while the block runs - + * this archetype measures a layout engine, and a draw would put an arm's + * renderer into a number that claims to be about layout. + */ + readonly layoutPassesPerFrame?: number; /** * Number of chained post-process filters applied to the scene root, or * `undefined` for the unfiltered scene every other archetype builds. @@ -451,6 +467,14 @@ export interface CellResult extends BaseCellResult { } /** Neutral contract an engine arm implements so the harness can drive it identically across arms. */ +/** What an arm reports about the geometry its last layout pass produced. */ +export interface LayoutDigestReport { + /** Index of the last pass the arm resolved, counted from the first pass of the cell (warmup included). */ + readonly pass: number; + /** Digest of that pass's resolved rectangles, per `uiLayout.ts::layoutDigest`. */ + readonly digest: number; +} + export interface EngineAdapter { /** Engine label, e.g. `'exojs'` or `'reference'`. */ readonly engine: string; @@ -499,6 +523,14 @@ export interface EngineAdapter { * it. */ pickHits?(): number; + /** + * The UI-layout archetype's last resolved pass and a digest of the rectangles + * it produced (see `uiLayout.ts::layoutDigest`). The harness checks the digest + * against the geometry the shared definition prescribes for that pass, so an + * arm that laid out a different scene fails loudly instead of reporting a + * faster time for less work. Optional - only the layout arms report it. + */ + layoutDigest?(): LayoutDigestReport; /** * The live WebGPU device when this adapter was initialised on the `'webgpu'` * backend, so the harness can attach a structural probe to it - unlike a diff --git a/packages/exojs-bench/src/rendering/adapters/excalibur.ts b/packages/exojs-bench/src/rendering/adapters/excalibur.ts index fb0e08f0f..ea0949c2c 100644 --- a/packages/exojs-bench/src/rendering/adapters/excalibur.ts +++ b/packages/exojs-bench/src/rendering/adapters/excalibur.ts @@ -5,6 +5,7 @@ import type { ArchetypeSpec, Backend, EngineAdapter } from '../EngineAdapter'; import { createDigitAtlasCanvas, createDistinctTextureCanvas, DIGIT_ALPHABET, DIGIT_CELL_HEIGHT, DIGIT_CELL_WIDTH, TEXT_FONT_SIZE } from '../sceneAssets'; import { isTilemap } from '../tilemap'; import { hasFullViewportLeaves, isChurning, isTextArchetype, isTextUpdating, leafAlpha, textForLeaf, usesRenderTargets } from '../traits'; +import { isUiLayoutScene } from '../uiLayout'; import { GRID_MARGIN, gridLayout, gridPosition, isScrolling, SPRITE_SIZE, VIEWPORT_HEIGHT, VIEWPORT_WIDTH } from '../world'; /** @@ -136,7 +137,11 @@ export const createExcaliburAdapter = (): EngineAdapter => { // are being compared on, so its cell would answer a different question // than the row it sat in. Building one here would mean writing the arm's // missing feature rather than adapting to it. - return !isScrolling(spec) && !usesRenderTargets(spec) && !isTilemap(spec); + // + // The UI-layout archetype is sat out for the same capability reason: + // Excalibur 0.32 has no layout container and no flexbox, so the box tree + // the archetype re-solves has no public equivalent to build it on. + return !isScrolling(spec) && !usesRenderTargets(spec) && !isTilemap(spec) && !isUiLayoutScene(spec); }, async init(canvas: HTMLCanvasElement, target: Backend): Promise { diff --git a/packages/exojs-bench/src/rendering/adapters/exojs.ts b/packages/exojs-bench/src/rendering/adapters/exojs.ts index e8f18fd05..ac8376b44 100644 --- a/packages/exojs-bench/src/rendering/adapters/exojs.ts +++ b/packages/exojs-bench/src/rendering/adapters/exojs.ts @@ -30,10 +30,12 @@ import { TextureRegion } from '#rendering/texture/TextureRegion'; import { BlendModes } from '#rendering/types'; import { View } from '#rendering/View'; import type { WebGpuBackend } from '#rendering/webgpu/WebGpuBackend'; +import { Stack } from '#ui/Stack'; +import { Widget } from '#ui/Widget'; import { mutationSignature, selectMutationIndices, wobbleOffsetAt } from '../../shared/mutation'; import { BLUR_TAPS_PER_SIDE } from '../archetypes'; -import type { ArchetypeSpec, Backend, EngineAdapter } from '../EngineAdapter'; +import type { ArchetypeSpec, Backend, EngineAdapter, LayoutDigestReport } from '../EngineAdapter'; import { isParticleLifecycle, isParticles, @@ -73,6 +75,22 @@ import { pointerQueriesPerFrame, textForLeaf, } from '../traits'; +import type { LayoutRect } from '../uiLayout'; +import { + BOX_GAP, + BOX_PADDING, + forEachMutatedWidget, + isUiLayoutScene, + LAYOUT_PASSES_PER_FRAME, + layoutDigest, + layoutTreeShape, + layoutViewportAt, + ROWS_PER_COLUMN, + WIDGET_HEIGHT, + WIDGET_WIDE, + WIDGET_WIDTH, + widgetsInRow, +} from '../uiLayout'; import { BLOOM_DOWNSCALE, cameraCenterAt, @@ -216,6 +234,18 @@ interface MutableLeaf { * the glyph raster - and therefore the atlas pressure a text scene puts on the * engine - is a property of the archetype instead of a property of the cell. */ +/** + * A leaf of the UI-layout tree: a widget with a layout box and nothing painted + * in it. + * + * Painting is left out deliberately. A widget that redrew a background on every + * resize would put its own repaint into a number about box-tree layout, and the + * Pixi arm's leaf - a plain layout container - draws nothing either. What the + * archetype compares is the container that resolves the boxes, so the leaves on + * both sides are the same thing: a size the layout engine reads and writes. + */ +class LayoutLeaf extends Widget {} + const createTextLeaf = (index: number, glyphs: number): Text => new Text(textForLeaf(index, glyphs), { fontSize: TEXT_FONT_SIZE }); /** @@ -770,6 +800,110 @@ export const createExoJsAdapter = (backendFilter?: readonly Backend[], config: E app!.interaction.attachRoot(scene); }; + /** Box-tree layout state, or nulls for every archetype that resolves no layout passes. */ + let layoutSpec: ArchetypeSpec | null = null; + let layoutStack: Stack | null = null; + let layoutLeaves: LayoutLeaf[] = []; + let layoutWidgets = 0; + /** + * Passes resolved since the scene was built, warmup included. + * + * Counted here rather than derived from `mutate`'s frame index: the harness + * restarts that index at zero between warmup and the timed window, and a pass + * only puts back what the PREVIOUS pass widened - so a restarted index would + * leave the last warmup pass's leaves wide for the rest of the cell and the + * tree would stop matching the shared definition. + */ + let layoutPass = 0; + + /** + * Build the layout scene: a root row of column boxes, each holding rows of + * fixed-size leaf widgets, sized to the first viewport before the measurement + * window opens. + * + * `Stack` re-flows on its own whenever a child widget resizes, so a pass here + * is a leaf `setSize` and a root `setSize` - there is no separate "solve now" + * call to make. That is a different algorithm from the Pixi arm's dirty flag + * plus one Yoga solve, and deliberately so: the shared scope is the tree and + * the mutation, not how an engine chooses to propagate them. + */ + const buildLayoutScene = (spec: ArchetypeSpec, nodeCount: number): void => { + const shape = layoutTreeShape(nodeCount); + const stack = new Stack({ direction: 'row', spacing: BOX_GAP, padding: BOX_PADDING, align: 'start' }); + const columns: Stack[] = []; + const leaves: LayoutLeaf[] = []; + + for (let column = 0; column < shape.columns; column += 1) { + const box = new Stack({ direction: 'column', spacing: BOX_GAP, padding: BOX_PADDING, align: 'start' }); + + columns.push(box); + stack.addItem(box); + } + + for (let row = 0; row < shape.rows; row += 1) { + const box = new Stack({ direction: 'row', spacing: BOX_GAP, padding: BOX_PADDING, align: 'start' }); + + columns[Math.floor(row / ROWS_PER_COLUMN)]!.addItem(box); + + for (let slot = 0; slot < widgetsInRow(shape, row); slot += 1) { + const leaf = new LayoutLeaf(); + + leaf.setSize(WIDGET_WIDTH, WIDGET_HEIGHT); + leaves.push(leaf); + box.addItem(leaf); + } + } + + const viewport = layoutViewportAt(0); + + stack.setSize(viewport.width, viewport.height); + + root = stack; + layoutStack = stack; + layoutLeaves = leaves; + layoutWidgets = shape.widgets; + layoutPass = 0; + layoutSpec = spec; + }; + + /** Drop the layout scene so a rebuild (or teardown) leaks nothing. */ + const releaseLayout = (): void => { + layoutSpec = null; + layoutStack = null; + layoutLeaves = []; + layoutWidgets = 0; + layoutPass = 0; + }; + + /** + * The resolved leaf rectangles, in leaf-index order and in the root box's + * coordinates. + * + * Read back from the widgets rather than recorded while the pass ran: the + * point of the check is that the tree ENDED where the shared definition says, + * and a value captured on the way through would be the arm reporting its own + * intermediate state. Called outside the timed bracket only. + */ + const layoutRects = (): readonly LayoutRect[] => { + const rects: LayoutRect[] = []; + + if (layoutStack === null) { + return rects; + } + + for (const column of layoutStack.children) { + for (const row of (column as Container).children) { + for (const leaf of (row as Container).children) { + const widget = leaf as LayoutLeaf; + + rects.push({ x: column.x + row.x + widget.x, y: column.y + row.y + widget.y, width: widget.uiWidth, height: widget.uiHeight }); + } + } + } + + return rects; + }; + /** Drop the picking scene so a rebuild (or teardown) leaks nothing. */ const releasePicking = (): void => { if (pickingSpec !== null && root !== null) { @@ -840,6 +974,7 @@ export const createExoJsAdapter = (backendFilter?: readonly Backend[], config: E releaseParticles(); releaseBlur(); releasePicking(); + releaseLayout(); sharedMeshGeometry?.destroy(); sharedMeshGeometry = null; @@ -871,6 +1006,12 @@ export const createExoJsAdapter = (backendFilter?: readonly Backend[], config: E return; } + if (isUiLayoutScene(spec)) { + buildLayoutScene(spec, nodeCount); + + return; + } + // `instanced-batch` leaves the scene graph behind entirely: nodeCount // instances are laid out on the same grid every other archetype uses, but // submitted as ceil(nodeCount / batchSize) explicit drawBatch calls over one @@ -1135,11 +1276,43 @@ export const createExoJsAdapter = (backendFilter?: readonly Backend[], config: E return pickHits; }, + layoutDigest(): LayoutDigestReport { + return { pass: layoutPass - 1, digest: layoutDigest(layoutRects()) }; + }, + mutationSignature(): string { return mutationSignature(mutableIndices); }, mutate(frame: number): void { + // Layout scene: one block of layout passes, inside the bracket because + // resolving the tree IS the frame's work here. Each pass puts the previous + // pass's widened leaves back and widens this pass's, then re-sizes the root + // to the viewport this pass resolves against. `Stack` re-flows from the + // resize itself, so the widths and the root box ARE the pass. + if (layoutSpec !== null && layoutStack !== null) { + for (let step = 0; step < LAYOUT_PASSES_PER_FRAME; step += 1) { + const pass = layoutPass; + + if (pass > 0) { + forEachMutatedWidget(pass - 1, layoutWidgets, index => { + layoutLeaves[index]?.setSize(WIDGET_WIDTH, WIDGET_HEIGHT); + }); + } + + forEachMutatedWidget(pass, layoutWidgets, index => { + layoutLeaves[index]?.setSize(WIDGET_WIDE, WIDGET_HEIGHT); + }); + + const viewport = layoutViewportAt(pass); + + layoutStack.setSize(viewport.width, viewport.height); + layoutPass = pass + 1; + } + + return; + } + // Picking scene: one block of point queries through the engine's own // public query, inside the bracket because resolving them IS the frame's // work here. The hit count is kept so a smoke run can check that every arm @@ -1328,6 +1501,7 @@ export const createExoJsAdapter = (backendFilter?: readonly Backend[], config: E releaseComposite(); releaseTilemap(); releaseParticles(); + releaseLayout(); if (root !== null) { root.destroy(); diff --git a/packages/exojs-bench/src/rendering/adapters/phaser.ts b/packages/exojs-bench/src/rendering/adapters/phaser.ts index b38a0c928..35e9dd81c 100644 --- a/packages/exojs-bench/src/rendering/adapters/phaser.ts +++ b/packages/exojs-bench/src/rendering/adapters/phaser.ts @@ -26,6 +26,7 @@ import { textForLeaf, usesRenderTargets, } from '../traits'; +import { isUiLayoutScene } from '../uiLayout'; import { GRID_MARGIN, gridLayout, gridPosition, isScrolling, VIEWPORT_HEIGHT, VIEWPORT_WIDTH } from '../world'; /** @@ -356,7 +357,14 @@ export const createPhaserAdapter = (): EngineAdapter => { // // Render-target archetypes remain out until their Phaser semantics are // validated against the shared filter/mask contract. - return !isScrolling(spec) && !usesRenderTargets(spec); + // + // The UI-layout archetype is sat out for a capability reason rather than a + // policy one. Phaser 4 ships no layout engine: `Actions.GridAlign` places + // objects once on a fixed raster and `GameObjects.Grid` draws one, and + // neither re-solves a box when a child resizes. A cell here could only + // measure a flexbox implementation written inside the harness, which is + // the arm's missing feature rather than an adaptation to it. + return !isScrolling(spec) && !usesRenderTargets(spec) && !isUiLayoutScene(spec); }, async init(canvas: HTMLCanvasElement, target: Backend): Promise { diff --git a/packages/exojs-bench/src/rendering/adapters/pixi.ts b/packages/exojs-bench/src/rendering/adapters/pixi.ts index fb98c5099..93691e9a9 100644 --- a/packages/exojs-bench/src/rendering/adapters/pixi.ts +++ b/packages/exojs-bench/src/rendering/adapters/pixi.ts @@ -1,3 +1,10 @@ +// Side-effect import: registers the Yoga-backed `LayoutSystem` on the renderer +// and mixes `Container.layout` in. Harmless for every other archetype - the +// system's per-frame hook is disabled in `init` (`autoUpdate: false`), and the +// mixin only changes a container once a layout is actually assigned to it. +import '@pixi/layout'; + +import type { LayoutStyles, LayoutSystemOptions } from '@pixi/layout'; import { settings as tilemapSettings, Tilemap } from '@pixi/tilemap'; import { Application, @@ -21,7 +28,7 @@ import { import { mutationSignature, selectMutationIndices, wobbleOffsetAt } from '../../shared/mutation'; import { BLUR_TAPS_PER_SIDE } from '../archetypes'; -import type { ArchetypeSpec, Backend, EngineAdapter } from '../EngineAdapter'; +import type { ArchetypeSpec, Backend, EngineAdapter, LayoutDigestReport } from '../EngineAdapter'; import { isParticleLifecycle, isParticles, PARTICLE_ALPHA, PARTICLE_LIFETIME, PARTICLE_PREROLL_STEPS, PARTICLE_STEP, particleSeedAt } from '../particles'; import { isPickingScene, PICK_RECT_SIZE, pickPointAt, pickRectAt } from '../picking'; import { createBlurSourceCanvas, createDistinctTextureCanvas, createParticleCanvas, createTileAtlasCanvas, TEXT_FONT_SIZE } from '../sceneAssets'; @@ -52,6 +59,22 @@ import { pointerQueriesPerFrame, textForLeaf, } from '../traits'; +import type { LayoutRect } from '../uiLayout'; +import { + BOX_GAP, + BOX_PADDING, + forEachMutatedWidget, + isUiLayoutScene, + LAYOUT_PASSES_PER_FRAME, + layoutDigest, + layoutTreeShape, + layoutViewportAt, + ROWS_PER_COLUMN, + WIDGET_HEIGHT, + WIDGET_WIDE, + WIDGET_WIDTH, + widgetsInRow, +} from '../uiLayout'; import { BLOOM_DOWNSCALE, cameraCenterAt, @@ -599,6 +622,169 @@ export const createPixiAdapter = (config: PixiAdapterConfig = 'default'): Engine pickingSpec = spec; }; + /** Box-tree layout state, or nulls for every archetype that resolves no layout passes. */ + let layoutSpec: ArchetypeSpec | null = null; + let layoutRoot: Container | null = null; + let layoutNodes: Container[] = []; + let layoutLeaves: Container[] = []; + let layoutWidgets = 0; + /** + * Passes resolved since the scene was built, warmup included. + * + * Counted here rather than derived from `mutate`'s frame index: the harness + * restarts that index at zero between warmup and the timed window, and a pass + * only puts back what the PREVIOUS pass widened - so a restarted index would + * leave the last warmup pass's leaves wide for the rest of the cell and the + * tree would stop matching the shared definition. + */ + let layoutPass = 0; + + /** + * Flex properties every box in the layout tree shares. + * + * `flexShrink: 0` overrides `@pixi/layout`'s default of `1`: the narrow + * viewport is deliberately too small for the tree, and a shrinking arm would + * answer the overflow by resizing the leaves - which is a different scene from + * the one the ExoJS arm lays out, where a stack never resizes what it did not + * grow. `alignItems: 'flex-start'` pins the cross axis for the same reason; + * Yoga's own default stretches, and stretching is exactly the "differing flex + * distribution" the shared scope excludes. + */ + const LAYOUT_BOX_STYLE = { + gap: BOX_GAP, + padding: BOX_PADDING, + alignItems: 'flex-start', + flexShrink: 0, + boxSizing: 'border-box', + } as const satisfies LayoutStyles; + + /** Style of a leaf widget at `width`. Leaves carry no children, so only their box matters. */ + const layoutLeafStyle = (width: number): LayoutStyles => ({ width, height: WIDGET_HEIGHT, flexShrink: 0, boxSizing: 'border-box' }); + + /** Style of the root box against viewport `pass`. */ + const layoutRootStyle = (pass: number): LayoutStyles => { + const viewport = layoutViewportAt(pass); + + return { ...LAYOUT_BOX_STYLE, flexDirection: 'row', width: viewport.width, height: viewport.height }; + }; + + /** + * Build the layout scene: a root row of column boxes, each holding rows of + * fixed-size leaf containers, resolved once before the measurement window + * opens. + * + * The tree is the ExoJS arm's tree, node for node. What differs is how a + * change reaches the solver: assigning a style marks the root dirty, and the + * whole tree is re-solved by one `layout.update` call - against the ExoJS + * arm's eager per-resize reflow. Both are the arm's own public layout path, + * which is what the shared scope permits. + */ + const buildLayoutScene = (spec: ArchetypeSpec, nodeCount: number): void => { + const shape = layoutTreeShape(nodeCount); + const scene = new Container(); + const nodes: Container[] = []; + const columns: Container[] = []; + const leaves: Container[] = []; + + scene.layout = layoutRootStyle(0); + + for (let column = 0; column < shape.columns; column += 1) { + const box = new Container(); + + box.layout = { ...LAYOUT_BOX_STYLE, flexDirection: 'column' }; + columns.push(box); + nodes.push(box); + scene.addChild(box); + } + + for (let row = 0; row < shape.rows; row += 1) { + const box = new Container(); + + box.layout = { ...LAYOUT_BOX_STYLE, flexDirection: 'row' }; + nodes.push(box); + columns[Math.floor(row / ROWS_PER_COLUMN)]!.addChild(box); + + for (let slot = 0; slot < widgetsInRow(shape, row); slot += 1) { + const leaf = new Container(); + + leaf.layout = layoutLeafStyle(WIDGET_WIDTH); + leaves.push(leaf); + nodes.push(leaf); + box.addChild(leaf); + } + } + + app!.renderer.layout.update(scene); + + root = scene; + layoutRoot = scene; + layoutNodes = nodes; + layoutLeaves = leaves; + layoutWidgets = shape.widgets; + layoutPass = 0; + layoutSpec = spec; + }; + + /** + * Drop the layout scene. + * + * Clearing every `layout` explicitly rather than relying on the destroy of the + * tree: assigning the first layout replaces `Container.prototype`'s `visible` + * accessor page-wide, and only assigning `null` puts the original back. A page + * runs every cell of an arm in sequence, so a layout cell that skipped this + * would leave that accessor in place for every Pixi cell measured after it. + */ + const releaseLayout = (): void => { + for (const node of layoutNodes) { + node.layout = null; + } + + if (root === layoutRoot) { + root = null; + } + + layoutRoot?.destroy({ children: true }); + layoutRoot = null; + layoutNodes = []; + layoutLeaves = []; + layoutWidgets = 0; + layoutPass = 0; + layoutSpec = null; + }; + + /** + * The resolved leaf rectangles, in leaf-index order and in the root box's + * coordinates. + * + * Read from the Yoga boxes rather than from the containers' transforms: a + * transform is only written when the scene is next rendered, so a digest taken + * off it would report the previous pass on the arm that batches. Called + * outside the timed bracket only. + */ + const layoutRects = (): readonly LayoutRect[] => { + const rects: LayoutRect[] = []; + + if (layoutRoot === null) { + return rects; + } + + for (const column of layoutRoot.children) { + const columnBox = column.layout!.computedLayout; + + for (const row of column.children) { + const rowBox = row.layout!.computedLayout; + + for (const leaf of row.children) { + const box = leaf.layout!.computedLayout; + + rects.push({ x: columnBox.left + rowBox.left + box.left, y: columnBox.top + rowBox.top + box.top, width: box.width, height: box.height }); + } + } + } + + return rects; + }; + /** Drop the picking scene so a rebuild (or teardown) leaks nothing. */ const releasePicking = (): void => { pickTexture?.destroy(true); @@ -648,6 +834,16 @@ export const createPixiAdapter = (config: PixiAdapterConfig = 'default'): Engine autoStart: false, sharedTicker: false, hello: false, + // The layout pass belongs to the measured block, never to a prerender + // hook that would put it outside the bracket. Everything else about the + // system is left at its defaults - the intrinsic-size walk's throttle + // included, because turning that off would make the arm pay a whole-tree + // walk per pass that a Pixi application never pays. + // + // The cast is the package's own declaration being one level off: it types + // the renderer option as the system's whole options object, while the + // system reads the inner record out of the renderer options it is handed. + layout: { autoUpdate: false } as unknown as LayoutSystemOptions, }); if (instance.renderer.type !== EXPECTED_RENDERER_TYPE[target]) { @@ -675,6 +871,7 @@ export const createPixiAdapter = (config: PixiAdapterConfig = 'default'): Engine releaseParticles(); releaseBlur(); releasePicking(); + releaseLayout(); textures = []; @@ -710,6 +907,12 @@ export const createPixiAdapter = (config: PixiAdapterConfig = 'default'): Engine return; } + if (isUiLayoutScene(spec)) { + buildLayoutScene(spec, nodeCount); + + return; + } + // Nested-container spine of depth `nestingDepth`, exactly as the ExoJS arm // builds it. Pixi has no separate retained/immediate tier here, so this one // arm is the whole Pixi comparison; the spine still exercises deep transform @@ -915,11 +1118,46 @@ export const createPixiAdapter = (config: PixiAdapterConfig = 'default'): Engine return pickHits; }, + layoutDigest(): LayoutDigestReport { + return { pass: layoutPass - 1, digest: layoutDigest(layoutRects()) }; + }, + mutationSignature(): string { return mutationSignature(mutableIndices); }, mutate(frame: number): void { + // Layout scene: one block of layout passes, inside the bracket because + // resolving the tree IS the frame's work here. Each pass puts the previous + // pass's widened leaves back, widens this pass's, re-styles the root to the + // viewport it resolves against, and asks the layout system to solve - the + // solve is one call here because assigning a style only marks the root + // dirty. + if (layoutSpec !== null && layoutRoot !== null && app !== null) { + const narrow = layoutLeafStyle(WIDGET_WIDTH); + const wide = layoutLeafStyle(WIDGET_WIDE); + + for (let step = 0; step < LAYOUT_PASSES_PER_FRAME; step += 1) { + const pass = layoutPass; + + if (pass > 0) { + forEachMutatedWidget(pass - 1, layoutWidgets, index => { + layoutLeaves[index]?.layout?.setStyle(narrow); + }); + } + + forEachMutatedWidget(pass, layoutWidgets, index => { + layoutLeaves[index]?.layout?.setStyle(wide); + }); + + layoutRoot.layout!.setStyle(layoutRootStyle(pass)); + app.renderer.layout.update(layoutRoot); + layoutPass = pass + 1; + } + + return; + } + // Picking scene: one block of point queries through Pixi's own event // boundary, the structure under comparison. The boundary reports the root // itself where nothing was hit, which is this arm's way of saying "no @@ -1089,6 +1327,7 @@ export const createPixiAdapter = (config: PixiAdapterConfig = 'default'): Engine releaseParticles(); releaseBlur(); releasePicking(); + releaseLayout(); if (root !== null) { root.destroy({ children: true }); diff --git a/packages/exojs-bench/src/rendering/archetypes.ts b/packages/exojs-bench/src/rendering/archetypes.ts index ddc4ab491..34b1e0c8b 100644 --- a/packages/exojs-bench/src/rendering/archetypes.ts +++ b/packages/exojs-bench/src/rendering/archetypes.ts @@ -1,5 +1,6 @@ import { createRng } from '../shared/rng'; import type { ArchetypeSpec, Backend, CellSpec, EngineAdapter } from './EngineAdapter'; +import { LAYOUT_PASSES_PER_FRAME } from './uiLayout'; // Re-exported from `shared/` so existing importers (e.g. the archetype tests and // `shared/mutation.ts`'s canonical selection) keep a single RNG implementation @@ -110,6 +111,13 @@ const PICKING_COUNTS = [1_000, 10_000, 100_000] as const; */ export const POINTER_QUERIES_PER_FRAME = 100; +/** + * Leaf widget counts for the UI-layout scene. A hundred is a settings panel, a + * thousand an inventory or a property grid, five thousand the point where a + * layout engine's per-node cost is the whole frame. + */ +const UI_WIDGET_COUNTS = [100, 1_000, 5_000] as const; + /** * Characters per text leaf across both text archetypes. Twelve is the length of * an ordinary label (a score, a name, a damage number) - long enough that layout @@ -735,6 +743,35 @@ export const ARCHETYPES: readonly ArchetypeSpec[] = [ cullingEnabled: false, pointerQueriesPerFrame: POINTER_QUERIES_PER_FRAME, }, + // BOX-TREE LAYOUT - the cost an interface pays when a widget resizes and the + // boxes around it have to be resolved again. The scene draws nothing worth + // measuring: the frame's work is a block of layout passes over a tree of + // nested horizontal and vertical boxes, each pass changing a tenth of the leaf + // widths and alternating the viewport the root resolves against. + // + // A TWO-ARM comparison, and that is a capability finding rather than an + // omission. ExoJS resolves the tree through its own `Stack`, Pixi through + // `@pixi/layout` over Yoga. Phaser 4 ships no layout engine at all - its + // `Actions.GridAlign` places objects on a fixed raster and re-solves nothing - + // and Excalibur ships none either, so neither arm has a public path that + // answers this question. Building one inside the harness would measure the + // harness. + // + // The two algorithms are allowed to differ; the shared scope is what is + // fixed, and `layoutDigest` compares the resolved rectangles outside the + // timed bracket so a divergence surfaces as a failed check rather than as a + // faster arm. + { + id: 'ui-layout-update', + category: 'interaction', + crossArm: true, + nodeCounts: UI_WIDGET_COUNTS, + nestingDepth: 3, + textureCount: 1, + mutationFraction: 0, + cullingEnabled: false, + layoutPassesPerFrame: LAYOUT_PASSES_PER_FRAME, + }, { id: 'mask-clip-animated', category: 'render-targets', diff --git a/packages/exojs-bench/src/rendering/driver.ts b/packages/exojs-bench/src/rendering/driver.ts index f7348f5c6..231a8fcae 100644 --- a/packages/exojs-bench/src/rendering/driver.ts +++ b/packages/exojs-bench/src/rendering/driver.ts @@ -22,6 +22,7 @@ import type { ArchetypeSpec, Backend, CellResult, CellSpec, EngineAdapter } from import type { MatrixSelection } from './selection'; import { applyPlan, applySelection } from './selection'; import { usesRenderTargets } from './traits'; +import { isUiLayoutScene } from './uiLayout'; import { isScrolling, VIEWPORT_HEIGHT, VIEWPORT_WIDTH } from './world'; // Re-exported so the rendering barrel and the CLI keep importing the selection @@ -244,8 +245,14 @@ const ADAPTER_CAPABILITIES: readonly EngineAdapter[] = [ // `mask-clip`): Phaser's adapter and Excalibur 0.32 have no validated // per-node equivalent for the shared filter/mask scenes, so approximating // the cell would violate the fairness rule. - capabilityDescriptor('phaser', 'webgl2', ['webgl2'], spec => !isScrolling(spec) && !usesRenderTargets(spec)), - capabilityDescriptor('excalibur', 'default', ['webgl2'], spec => !isScrolling(spec) && !usesRenderTargets(spec)), + // + // Both sit out the UI-layout archetype as well, and that one is a capability + // finding rather than a policy: neither library ships a layout engine at all. + // Phaser 4's `Actions.GridAlign` places objects once on a fixed raster and + // `GameObjects.Grid` draws one; Excalibur 0.32 has no layout container. A cell + // on either arm could only measure a flexbox written inside the harness. + capabilityDescriptor('phaser', 'webgl2', ['webgl2'], spec => !isScrolling(spec) && !usesRenderTargets(spec) && !isUiLayoutScene(spec)), + capabilityDescriptor('excalibur', 'default', ['webgl2'], spec => !isScrolling(spec) && !usesRenderTargets(spec) && !isUiLayoutScene(spec)), ]; /** diff --git a/packages/exojs-bench/src/rendering/page/harness.ts b/packages/exojs-bench/src/rendering/page/harness.ts index ff2ef7fbe..ae85cc732 100644 --- a/packages/exojs-bench/src/rendering/page/harness.ts +++ b/packages/exojs-bench/src/rendering/page/harness.ts @@ -11,6 +11,7 @@ import { createExoJsAdapter } from '../adapters/exojs'; import { ARCHETYPES } from '../archetypes'; import type { CellResult, CellSpec, EngineAdapter, StructuralCounters } from '../EngineAdapter'; import { attachWebGl2Probe, attachWebGpuProbe, type StructuralProbe } from '../structural'; +import { expectedLayoutRects, isUiLayoutScene, layoutDigest } from '../uiLayout'; import type { GpuFrameTimer } from './gpuFrameTimer'; import { createWebGl2GpuTimer, @@ -403,18 +404,47 @@ export const runCell = async (adapter: EngineAdapter, spec: CellSpec, canvas: HT // context AFTER engine init (the device/context does not exist earlier), so an // engine that cached its draw/bind method references at init would bypass the // wrappers and silently report zero - an undercount masquerading as truth. - // Every archetype places drawable, on-screen sprites, so a non-empty cell MUST - // issue at least one draw; a zero here means the probe was bypassed, not that - // the scene drew nothing. Fail loudly rather than report the undercount. + // Every archetype that draws places drawable, on-screen sprites, so a + // non-empty cell MUST issue at least one draw; a zero there means the probe + // was bypassed, not that the scene drew nothing. Fail loudly rather than + // report the undercount. The UI-layout archetype is the one scene that + // genuinely submits nothing - it measures a box-tree solve, and its leaves + // are layout boxes with no painted surface - so it has no draw for the + // check to find and is exempt rather than given a quad to satisfy it. // (Pre-wrapping the WebGL2 context BEFORE init was rejected: creating the // context early would freeze the attributes the engine sets on its first // getContext - e.g. antialias - changing what is measured.) - if (structuralNote === null && spec.nodeCount > 0 && probe.counters.drawCalls === 0) { + if (structuralNote === null && spec.nodeCount > 0 && !isUiLayoutScene(archetype) && probe.counters.drawCalls === 0) { throw new Error( `Structural probe recorded 0 draw calls for a non-empty ${spec.backend} scene (engine='${spec.engine}' config='${spec.config}' archetype='${spec.archetype}' n=${spec.nodeCount}); the probe wrappers were bypassed — counts are untrustworthy.`, ); } + // Geometry check for the UI-layout archetype, outside the timed bracket. The + // arms are allowed to reach the boxes through different public layout + // algorithms, so what has to match is the RESULT: the rectangles the shared + // definition prescribes for the pass the arm last resolved. An arm that + // resolved a smaller tree, skipped a reflow or distributed the leftover + // space differently is faster for a reason that is not a faster layout + // engine, and is stopped here rather than published. + if (isUiLayoutScene(archetype)) { + const reported = adapter.layoutDigest?.(); + + if (reported === undefined) { + console.warn( + `[baseline] arm engine='${spec.engine}' config='${spec.config}' reports no layout digest; the UI-layout geometry is UNVERIFIED for this arm (see EngineAdapter.layoutDigest).`, + ); + } else { + const expected = layoutDigest(expectedLayoutRects(spec.nodeCount, reported.pass)); + + if (reported.digest !== expected) { + throw new Error( + `UI-layout geometry mismatch: engine='${spec.engine}' config='${spec.config}' resolved pass ${String(reported.pass)} of archetype='${spec.archetype}' n=${String(spec.nodeCount)} to digest ${String(reported.digest)}, but the shared definition prescribes ${String(expected)}. The arm laid out a different scene, so its timing is not comparable.`, + ); + } + } + } + const { structural, note: unevenNote } = perFrameStructural(probe.counters, measuredFrames); const gpuSamples = await gpuTimer.collect(); diff --git a/packages/exojs-bench/src/rendering/uiLayout.ts b/packages/exojs-bench/src/rendering/uiLayout.ts new file mode 100644 index 000000000..6a51b89a3 --- /dev/null +++ b/packages/exojs-bench/src/rendering/uiLayout.ts @@ -0,0 +1,244 @@ +import type { ArchetypeSpec } from './EngineAdapter'; +import { VIEWPORT_HEIGHT, VIEWPORT_WIDTH } from './world'; + +/** + * The UI-layout scene's shared definition: the widget tree every arm builds, + * the widths each pass changes, and the two viewports it alternates between. + * + * This archetype is the only one whose frame does no drawing work worth + * measuring. What it measures is a layout engine resolving a box tree, so the + * tree, the mutation and the viewport sequence have to be identical across arms + * or the two engines are answering different questions. Only the shared scope is + * fixed here - nested horizontal and vertical boxes, fixed leaf sizes, padding + * and gap. No text shaping, no grid, no wrapping and no differing flex + * distribution: those are where the arms' public layout algorithms legitimately + * diverge, and a comparison may not depend on them. + */ + +/** Leaf widget height in logical pixels. Fixed, so only widths drive a reflow. */ +export const WIDGET_HEIGHT = 24; + +/** Base leaf widget width, before a pass swaps it for {@link WIDGET_WIDE}. */ +export const WIDGET_WIDTH = 96; + +/** The alternate width a mutated leaf takes. Different enough that the box above it has to re-resolve. */ +export const WIDGET_WIDE = 144; + +/** Padding inside every box, in logical pixels. */ +export const BOX_PADDING = 8; + +/** Gap between siblings inside a box, in logical pixels. */ +export const BOX_GAP = 4; + +/** Leaf widgets per innermost row. The tree grows in rows, so this fixes the tree's shape as the count scales. */ +export const WIDGETS_PER_ROW = 8; + +/** Innermost rows per column box, one nesting level up. */ +export const ROWS_PER_COLUMN = 8; + +/** + * Layout passes per frame. + * + * The published unit is CPU-ms per block of this many passes rather than per + * pass: a single box-tree resolve lands under the clock's resolution on both + * arms, and a sub-microsecond figure derived by dividing one through would read + * as a measurement rather than as arithmetic. Twenty already clears the coarsest + * clock the harness reports by more than two orders of magnitude, which is the + * only thing the block exists for. + * + * Not larger, because the block is a frame and the harness abandons a cell whose + * trailing frames pass its 200ms budget. At the reference rung the slower arm + * costs roughly 5ms per pass, so a block of a hundred would abort it after three + * frames - and the row would then compare a three-sample number against a + * full-length one, which is a worse answer than a shorter block. + */ +export const LAYOUT_PASSES_PER_FRAME = 20; + +/** Share of leaf widths a pass changes. The rest keep theirs, so a pass is a reflow rather than a rebuild. */ +export const LAYOUT_MUTATION_FRACTION = 0.1; + +/** The two viewports a run alternates between, so the root box is re-solved against a changing constraint. */ +export const LAYOUT_VIEWPORTS = [ + { width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT }, + { width: Math.round(VIEWPORT_WIDTH * 0.75), height: VIEWPORT_HEIGHT }, +] as const; + +/** A resolved rectangle, in the root box's coordinates. */ +export interface LayoutRect { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; +} + +/** Shape of the widget tree for a given leaf count: how many rows, and how many columns hold them. */ +export interface LayoutTreeShape { + /** Leaf widgets in total. Equal to the requested count; the last row is short when it does not divide evenly. */ + readonly widgets: number; + /** Innermost horizontal boxes. */ + readonly rows: number; + /** Vertical boxes holding the rows. */ + readonly columns: number; +} + +/** + * Tree shape for a leaf count. + * + * Depth is fixed at three boxes - root column, column boxes, rows - and width + * grows instead, because a tree that deepened with the count would turn the + * ladder into a depth study rather than a width one. Every arm therefore + * resolves the same number of levels at 100 widgets and at 5000. + */ +export const layoutTreeShape = (widgets: number): LayoutTreeShape => { + const total = Math.max(1, Math.trunc(widgets)); + const rows = Math.ceil(total / WIDGETS_PER_ROW); + + return { widgets: total, rows, columns: Math.ceil(rows / ROWS_PER_COLUMN) }; +}; + +/** Leaf widgets in row `row` of a tree of `shape`; the last row is short when the count does not divide evenly. */ +export const widgetsInRow = (shape: LayoutTreeShape, row: number): number => Math.max(0, Math.min(WIDGETS_PER_ROW, shape.widgets - row * WIDGETS_PER_ROW)); + +/** + * Distance between two changed leaves. + * + * A stride walk rather than a random draw or a contiguous block: a block would + * leave whole rows untouched, and an arm that caches a clean subtree would then + * be measured on a scene its cache happens to fit. Walking by a stride spreads + * the changed leaves across every row, and offsetting the walk by the pass index + * moves them from pass to pass, so no row is ever clean twice in a row. + */ +export const widgetMutationStride = (widgets: number): number => { + const total = Math.max(1, Math.trunc(widgets)); + const mutated = Math.max(1, Math.round(total * LAYOUT_MUTATION_FRACTION)); + + return Math.max(1, Math.floor(total / mutated)); +}; + +/** Whether leaf `index` has its width changed on pass `pass`. */ +export const isWidgetMutated = (index: number, pass: number, widgets: number): boolean => (index + pass) % widgetMutationStride(widgets) === 0; + +/** + * Visit the leaves pass `pass` changes, in ascending index order. + * + * The arms walk the changed set directly rather than testing every leaf: the + * arithmetic that decides WHICH leaves change is harness work, and running it + * over the whole tree each pass would put a per-leaf modulo into a measurement + * of a per-changed-leaf reflow, growing with the rung it is supposed to be + * independent of. + * + * A pass changes the leaves this yields to {@link WIDGET_WIDE} and puts pass + * `pass - 1`'s leaves back to {@link WIDGET_WIDTH}. The two sets are disjoint + * for every tree the ladder holds, so the resulting widths are exactly what + * {@link widgetWidthAt} reports - which is what lets the geometry check compare + * a digest against the shared definition rather than against an arm. + */ +export const forEachMutatedWidget = (pass: number, widgets: number, visit: (index: number) => void): void => { + const total = Math.max(1, Math.trunc(widgets)); + const stride = widgetMutationStride(total); + + for (let index = (stride - (((pass % stride) + stride) % stride)) % stride; index < total; index += stride) { + visit(index); + } +}; + +/** Width leaf `index` carries on pass `pass`. */ +export const widgetWidthAt = (index: number, pass: number, widgets: number): number => (isWidgetMutated(index, pass, widgets) ? WIDGET_WIDE : WIDGET_WIDTH); + +/** Viewport pass `pass` resolves against. */ +export const layoutViewportAt = (pass: number): (typeof LAYOUT_VIEWPORTS)[number] => LAYOUT_VIEWPORTS[pass % LAYOUT_VIEWPORTS.length]!; + +/** + * The rectangles a correctly resolved pass produces, in leaf-index order and in + * the root box's coordinates. + * + * Derived from the shared definition rather than from either arm, which is what + * makes the geometry check an assertion instead of a cross-arm agreement: two + * arms that both got the tree wrong the same way would still agree. The + * arithmetic is closed-form because the shared scope has no case that needs a + * solver - fixed leaf sizes, start alignment on both axes, no grow and no wrap, + * so a box is its content plus its padding and a child sits at the running + * offset. An arm whose public layout algorithm produces anything else has laid + * out a different scene. + */ +export const expectedLayoutRects = (widgets: number, pass: number): readonly LayoutRect[] => { + const shape = layoutTreeShape(widgets); + const rowHeight = WIDGET_HEIGHT + BOX_PADDING * 2; + const rowWidths: number[] = []; + + for (let row = 0; row < shape.rows; row += 1) { + const count = widgetsInRow(shape, row); + let width = BOX_PADDING * 2 + Math.max(0, count - 1) * BOX_GAP; + + for (let slot = 0; slot < count; slot += 1) { + width += widgetWidthAt(row * WIDGETS_PER_ROW + slot, pass, widgets); + } + + rowWidths.push(width); + } + + const rects: LayoutRect[] = []; + let columnX = BOX_PADDING; + + for (let column = 0; column < shape.columns; column += 1) { + const first = column * ROWS_PER_COLUMN; + const last = Math.min(shape.rows, first + ROWS_PER_COLUMN); + let columnWidth = 0; + + for (let row = first; row < last; row += 1) { + columnWidth = Math.max(columnWidth, rowWidths[row]!); + } + + columnWidth += BOX_PADDING * 2; + + for (let row = first; row < last; row += 1) { + const rowX = columnX + BOX_PADDING; + const rowY = BOX_PADDING + BOX_PADDING + (row - first) * (rowHeight + BOX_GAP); + const count = widgetsInRow(shape, row); + let slotX = rowX + BOX_PADDING; + + for (let slot = 0; slot < count; slot += 1) { + const width = widgetWidthAt(row * WIDGETS_PER_ROW + slot, pass, widgets); + + rects.push({ x: slotX, y: rowY + BOX_PADDING, width, height: WIDGET_HEIGHT }); + slotX += width + BOX_GAP; + } + } + + columnX += columnWidth + BOX_GAP; + } + + return rects; +}; + +/** + * Digest of a pass's resolved geometry, compared between arms OUTSIDE the timed + * bracket. + * + * Rounded to whole pixels before hashing: the arms reach the same boxes through + * different arithmetic - one accumulates in JavaScript, the other in Yoga's + * WASM heap - and a sub-pixel difference in the last place is not a difference + * in what was laid out. A digest rather than a full rect list because the check + * only has to answer whether both arms resolved the same scene; where they did + * not, the scene is rebuilt outside the harness and diffed rect by rect. + */ +export const layoutDigest = (rects: readonly LayoutRect[]): number => { + let digest = 0; + + for (const rect of rects) { + for (const value of [rect.x, rect.y, rect.width, rect.height]) { + // FNV-1a over the rounded coordinate, kept in 32 bits by the shift-add + // chain so the digest is stable across engines rather than drifting into + // double precision at large trees. + digest = Math.imul(digest ^ Math.round(value), 0x01000193) >>> 0; + } + } + + return digest; +}; + +/** Layout passes the archetype resolves per frame; `0` when it resolves none. */ +export const layoutPassesPerFrame = (spec: ArchetypeSpec): number => Math.max(0, Math.trunc(spec.layoutPassesPerFrame ?? 0)); + +/** Whether the archetype measures box-tree layout rather than drawing. */ +export const isUiLayoutScene = (spec: ArchetypeSpec): boolean => layoutPassesPerFrame(spec) > 0; diff --git a/packages/exojs-bench/test/suite-plan.test.ts b/packages/exojs-bench/test/suite-plan.test.ts index f7688dc4f..5e56cde79 100644 --- a/packages/exojs-bench/test/suite-plan.test.ts +++ b/packages/exojs-bench/test/suite-plan.test.ts @@ -56,6 +56,17 @@ describe('published workload catalog', () => { expect(offenders.map(scenario => scenario.scenarioId)).toStrictEqual([]); }); + it(`${domain}: every catalogued scenario is an archetype the domain implements`, () => { + // A catalogued scenario whose id matches no archetype ladder resolves to + // no workload at all, so it drops out of every plan silently - the + // published page then shows one scenario fewer than the catalog promises + // and nothing in the suite says so. Asserting the ids here is what turns + // that into a failure at the catalog, where the typo is. + const ladders = domain === 'rendering' ? RENDERING_LADDERS : PHYSICS_LADDERS; + + expect(scenarios.map(scenario => scenario.scenarioId).filter(id => !ladders.has(id))).toStrictEqual([]); + }); + it(`${domain}: no extreme load is part of the reference plan`, () => { for (const scenario of scenarios) { for (const load of scenario.loads.filter(entry => entry.extreme === true)) { @@ -86,10 +97,6 @@ describe('suite resolution', () => { } for (const scenario of RENDERING_SCENARIOS) { - if (!RENDERING_LADDERS.has(scenario.scenarioId)) { - continue; - } - const headline = primaryLoadOf(scenario); expect(plan.workloads.some(workload => workload.scenarioId === scenario.scenarioId && workload.loadId === headline.loadId && workload.primary)).toBe( diff --git a/src/ui/Stack.ts b/src/ui/Stack.ts index 2eaa66a65..df746b86a 100644 --- a/src/ui/Stack.ts +++ b/src/ui/Stack.ts @@ -290,7 +290,16 @@ export class Stack extends Widget { this._natural.delete(child); } - child.setSize(isRow ? mainSize : crossSize, isRow ? crossSize : mainSize); + const width = isRow ? mainSize : crossSize; + const height = isRow ? crossSize : mainSize; + + // Only write a size the child does not already have. Re-setting the same + // extents looks harmless and is not: a nested Stack reads any setSize as its + // caller pinning an explicit box, so a stack of stacks would stop sizing + // itself to its content the first time its parent laid it out. + if (child.uiWidth !== width || child.uiHeight !== height) { + child.setSize(width, height); + } } /** A child's layout extent: a widget's explicit size, or a drawn node's bounds. */ diff --git a/test/ui/layout.test.ts b/test/ui/layout.test.ts index 158823fd5..211a6b0f0 100644 --- a/test/ui/layout.test.ts +++ b/test/ui/layout.test.ts @@ -119,6 +119,22 @@ describe('Stack reactivity', () => { expect(two.uiWidth).toBe(95); }); + test('keeps a nested stack sizing to its content after its parent has laid it out', () => { + const outer = new Stack({ direction: 'column', spacing: 0 }); + const inner = new Stack({ direction: 'row', spacing: 0 }); + const child = new Panel({ width: 20, height: 10 }); + + outer.addChild(inner); + inner.addChild(child); + + expect(inner.uiWidth).toBe(20); + + child.setSize(50, 10); + + expect(inner.uiWidth).toBe(50); + expect(outer.uiWidth).toBe(50); + }); + test('drops a grow factor when it is cleared or its child leaves', () => { const stack = new Stack({ direction: 'row', spacing: 0 }); const child = new Panel({ width: 20, height: 10 }); From f784dc454a8bf9f2aebf4e6257ae1a93531146e4 Mon Sep 17 00:00:00 2001 From: Exoridus Date: Sat, 12 Sep 2026 03:45:06 +0200 Subject: [PATCH 07/28] bench: publish fx-blur as a cross-arm row and fix what kept it out The exclusion rested on a 45 % pixel difference against Pixi and on ExoJS issuing twenty draw calls to Pixi's three. The draw count was fixed upstream; the picture difference turned out to be two mistakes in this arm, not in either filter. `BlurFilter.strength` is the spacing between taps, not a standard deviation - the weights are a fixed table that already is a sigma-2 kernel - so handing it the archetype's sigma blurred the Pixi arm about twice as far as the contract asks. And the two filters derive their reach from different multiples of the blur they were given, which left a band a few pixels deep at the filtered region's border. With both corrected the arms agree to within 18 of 255 on the worst channel of the worst pixel at every rung, over 98 % of pixels inside 8. Evidence, including the numbers before each correction, is in the private workspace. Phaser stays out: its blur is an iterative step filter on a camera, with no kernel to configure to the shared contract. --- .../src/rendering/adapters/pixi.ts | 32 ++++++++++++-- .../exojs-bench/src/rendering/archetypes.ts | 42 +++++++++++++------ packages/exojs-bench/src/suite/catalog.ts | 9 ++++ 3 files changed, 67 insertions(+), 16 deletions(-) diff --git a/packages/exojs-bench/src/rendering/adapters/pixi.ts b/packages/exojs-bench/src/rendering/adapters/pixi.ts index 93691e9a9..f5205ef13 100644 --- a/packages/exojs-bench/src/rendering/adapters/pixi.ts +++ b/packages/exojs-bench/src/rendering/adapters/pixi.ts @@ -27,7 +27,7 @@ import { } from 'pixi.js'; import { mutationSignature, selectMutationIndices, wobbleOffsetAt } from '../../shared/mutation'; -import { BLUR_TAPS_PER_SIDE } from '../archetypes'; +import { BLUR_KERNEL_SIGMAS, BLUR_TAPS_PER_SIDE } from '../archetypes'; import type { ArchetypeSpec, Backend, EngineAdapter, LayoutDigestReport } from '../EngineAdapter'; import { isParticleLifecycle, isParticles, PARTICLE_ALPHA, PARTICLE_LIFETIME, PARTICLE_PREROLL_STEPS, PARTICLE_STEP, particleSeedAt } from '../particles'; import { isPickingScene, PICK_RECT_SIZE, pickPointAt, pickRectAt } from '../picking'; @@ -560,6 +560,23 @@ export const createPixiAdapter = (config: PixiAdapterConfig = 'default'): Engine * one: raising it would run the filter several times over and publish the * extra passes as a slower blur rather than as the different effect they are. */ + /** + * Standard deviation, in texels, of Pixi's nine-tap weight table at unit tap + * spacing. + * + * `BlurFilter.strength` is NOT a sigma, which is the trap this constant exists + * to close. The weights come from a fixed per-kernel-size table and never + * change; `strength` scales how far apart the taps are sampled, so it + * multiplies whatever standard deviation the table already has. Passing the + * archetype's sigma straight through therefore blurred this arm roughly twice + * as far as the shared contract asks. + * + * The table's own value: its ratios are a clean Gaussian - w1/w0 and w2/w0 both + * solve exp(-x^2 / 2*sigma^2) at sigma 2.021 - so dividing the target sigma by + * it gives the tap spacing that realizes the shared kernel. + */ + const PIXI_KERNEL_SIGMA = 2.021; + const buildBlurScene = (spec: ArchetypeSpec, nodeCount: number): void => { const texture = Texture.from(createBlurSourceCanvas()); const sprite = new Sprite(texture); @@ -572,10 +589,17 @@ export const createPixiAdapter = (config: PixiAdapterConfig = 'default'): Engine const scene = new Container(); + const blur = new BlurFilter({ strength: blurStrength(spec) / PIXI_KERNEL_SIGMA, quality: 1, kernelSize: BLUR_TAPS_PER_SIDE * 2 + 1 }); + + // Edge handling is part of the shared contract, and the two filters derive + // their reach from different multiples of the blur they were given - Pixi + // twice its tap spacing, ExoJS three sigmas. Left alone, this arm would hold + // back the effect a few pixels sooner at the filtered region's border and + // the difference would read as a different kernel. Stated as the contract's + // own reach instead. + blur.padding = blurStrength(spec) * BLUR_KERNEL_SIGMAS; scene.addChild(sprite); - // `strength` is Pixi's sigma, while the archetype states a reach; halving - // converts one into the other, so both arms blur the same distance. - scene.filters = [new BlurFilter({ strength: blurStrength(spec), quality: 1, kernelSize: BLUR_TAPS_PER_SIDE * 2 + 1 })]; + scene.filters = [blur]; root = scene; blurTexture = texture; diff --git a/packages/exojs-bench/src/rendering/archetypes.ts b/packages/exojs-bench/src/rendering/archetypes.ts index 34b1e0c8b..9350b55ed 100644 --- a/packages/exojs-bench/src/rendering/archetypes.ts +++ b/packages/exojs-bench/src/rendering/archetypes.ts @@ -84,6 +84,17 @@ const BLUR_HEIGHTS = [360, 720, 1_080] as const; */ export const BLUR_SIGMA = 2; +/** + * Standard deviations the shared blur reaches outside its input on every edge. + * + * Part of the contract rather than an implementation detail: a filter that held + * the effect back sooner would darken the border of the filtered region, and the + * arms derive their own padding from different multiples, so the reach is stated + * here and each arm is configured to it. Three sigmas is where a Gaussian has + * less than half a percent of its mass left. + */ +export const BLUR_KERNEL_SIGMAS = 3; + /** * Taps per side of the shared blur kernel, so both arms sweep 4 + 1 + 4 = nine. * @@ -700,22 +711,29 @@ export const ARCHETYPES: readonly ArchetypeSpec[] = [ // is a single textured quad, so nothing about scene traversal enters the // measurement: what scales is the filter's own target passes. // - // NOT a cross-arm comparison, and the reason is the finding. Both arms are - // configured to the same contract - a separable two-pass Gaussian, nine taps, - // sigma 2 - but they do not produce the same picture: a capture of the two at - // 1280x720 differs on 45 % of its pixels. Two filters whose output differs - // that much are not doing the same work, so a wall-clock row would attribute a - // difference in effect to a difference in speed. The structural counters say - // as much on their own: ExoJS issues about twenty draw calls where Pixi issues - // three, which is a tap-per-draw implementation against a nine-tap shader. + // A TWO-ARM comparison. ExoJS and Pixi both realize the shared contract - a + // separable two-pass Gaussian, nine taps, sigma 2, three sigmas of reach - and + // a capture of the two agrees to within 18 of 255 on the worst channel of the + // worst pixel at every rung, with more than 98 % of pixels inside 8. That is + // the stated tolerance: two GPU filters are never bit-identical, and what has + // to match is the kernel rather than the rounding. // - // It stays in the matrix as an ExoJS-internal probe because that structure is - // worth tracking. Making it a published comparison needs the two outputs - // brought inside a stated tolerance first. + // Getting there took two corrections on the Pixi side, both in the arm rather + // than in either engine, and both worth knowing before the row is read. + // `BlurFilter.strength` is a tap SPACING, not a sigma - the weights are a + // fixed table - so passing the archetype's sigma through blurred that arm + // about twice as far; and the two filters derive their reach from different + // multiples of the blur, which showed up as a several-pixel band at the + // filtered region's border. See the Pixi adapter's blur scene for both. + // + // Phaser sits the archetype out. `Filters.Blur` is an iterative step blur on a + // camera, parameterized by an offset and a step count rather than by a kernel, + // so it cannot be configured to the shared nine taps at sigma 2 - and matching + // it would mean writing a kernel of our own rather than measuring Phaser's. { id: 'fx-blur', category: 'render-targets', - crossArm: false, + crossArm: true, nodeCounts: BLUR_HEIGHTS, nestingDepth: 1, textureCount: 1, diff --git a/packages/exojs-bench/src/suite/catalog.ts b/packages/exojs-bench/src/suite/catalog.ts index dd8b16a71..bbde2b17f 100644 --- a/packages/exojs-bench/src/suite/catalog.ts +++ b/packages/exojs-bench/src/suite/catalog.ts @@ -315,6 +315,15 @@ export const RENDERING_SCENARIOS: readonly ScenarioLoads[] = [ [100_000, ''], ]), }, + { + scenarioId: 'fx-blur', + unit: 'viewport', + loads: loads([ + [360, '', '640 x 360'], + [720, 'r*', '1280 x 720'], + [1_080, '', '1920 x 1080'], + ]), + }, { scenarioId: 'ui-layout-update', unit: 'widgets', From b5123066b906e5d0de44e68916941aeeae5333e7 Mon Sep 17 00:00:00 2001 From: Exoridus Date: Sat, 12 Sep 2026 03:49:55 +0200 Subject: [PATCH 08/28] bench: print the run summary as a table, and make the README a guide The per-cell summary was `key=value` runs padded by hand, so two arms of one scenario could not be compared without parsing a line first - and they were printed arm-major, which put them nowhere near each other. Now one table, grouped by scenario and load, in both domains. The README was 748 lines of contract with the commands at the top. The contract moves to docs/harness.md unchanged; the README becomes what someone needs to run a benchmark, read what it printed, and contribute their machine's numbers - and says how to do that last part, which nothing pointed at before. `results/README.md` still documented a three-run recipe per domain; the harness has measured both domains in one `bench:reference` invocation for a while. --- packages/exojs-bench/README.md | 861 ++++------------------- packages/exojs-bench/docs/harness.md | 730 +++++++++++++++++++ packages/exojs-bench/results/README.md | 39 +- packages/exojs-bench/src/run.ts | 88 ++- packages/exojs-bench/src/shared/table.ts | 51 ++ 5 files changed, 1013 insertions(+), 756 deletions(-) create mode 100644 packages/exojs-bench/docs/harness.md create mode 100644 packages/exojs-bench/src/shared/table.ts diff --git a/packages/exojs-bench/README.md b/packages/exojs-bench/README.md index afe59b1c8..a1b7a0d3c 100644 --- a/packages/exojs-bench/README.md +++ b/packages/exojs-bench/README.md @@ -1,748 +1,161 @@ # @codexo/exojs-bench -Private, reproducible cross-library rendering/physics benchmark harness for ExoJS. -Not published. Compares ExoJS against competitor libraries (Pixi, Phaser, -Excalibur, matter-js, planck, rapier2d-compat) kept in an isolated `competitors/` -manifest — see `competitors/package.json` and `competitors/link.ts` for why -they're excluded from the workspace install. +Runs ExoJS and the other 2D libraries through the same scenes, in a real browser +against a real GPU, and prints what each one costs per frame. + +Private to this repository, never published to npm. The competitor libraries +(Pixi, Phaser, Excalibur, matter-js, planck, rapier2d-compat) live in their own +`competitors/` manifest, so a normal `pnpm install` downloads none of them. ## Setup +Once per checkout, ~235 MB: + ```sh -pnpm --filter @codexo/exojs-bench bench:setup # installs + links the competitor libs (one-time, ~235MB) -pnpm --filter @codexo/exojs-bench bench # runs the benchmark +pnpm --filter @codexo/exojs-bench bench:setup ``` -Everything else the package offers: +## Run one ```sh -pnpm --filter @codexo/exojs-bench bench --domain=physics # the CPU-only physics matrix -pnpm --filter @codexo/exojs-bench bench:compare \ - --rendering run-1/results.json \ - --rendering run-2/results.json \ - --rendering run-3/results.json # generate the published comparison -pnpm gate:bench:structural # the structural counter gate (also a CI lane) -pnpm --filter @codexo/exojs-bench gate:timing # the manual timing gate +pnpm --filter @codexo/exojs-bench bench ``` -## What it measures - -Two domains, selected with `--domain` (default `rendering`): - -- **`rendering`** drives a real headless Chromium against the real GPU. For each - matrix cell — one `(engine, config, backend, archetype, nodeCount)` combination - — it builds a scene, warms it up, then renders a fixed number of timed frames - from `requestAnimationFrame`, sampling per-frame CPU time, full-frame time and - draw-call structure. -- **`physics`** drives a real headless browser too, but no GPU: a straight loop - over `world.step` inside the harness page, sampling per-step CPU time plus - body, contact, joint and ray-hit counts. - - It is measured in a browser rather than in the driver's Node process because - nobody runs ExoJS physics in Node, and because the runtime is part of what a - step time measures: heap limits, garbage collection and WASM compilation are - the browser's, and the JavaScript engine is whichever the selected browser - ships. Measured on the same machine, moving the matrix from Node into Chromium - moved per-step medians by −80% to +29% depending on the arm, which is enough to - reorder the arms against each other; measuring it in WebKit instead moves them - again. A physics number is therefore only comparable against another taken in - the same browser, and `--browser` applies to this domain exactly as it does to - rendering. - -Everything below describes the rendering domain, except the physics sections that -name themselves. - -A cell's scene comes from a fixed **archetype** (`src/rendering/archetypes.ts`): -`static-heavy`, `dynamic-heavy`, `deep-hierarchy`, `lifecycle-churn`, -`overdraw`, `batch-breaking`, `batch-breaking-atlased`, `split-screen`, -`mixed-blend`, `mixed-material`, `mixed-material-atlased`, `instanced-batch`, -`mixed-sprite-mesh-static`, `mixed-sprite-mesh-array`, `scrolling-world`, -`text-static`, `text-dynamic`, `filter-chain-1`, `filter-chain-2`, -`filter-chain-4`, `mask-clip`, `mask-clip-animated`, `composite`. Each pins nesting depth, texture count, per-frame -mutation fraction and whatever dimension it exists to isolate, and sweeps a -ladder of node counts. - -Most of them are readable as a DELTA against another row, which is where their -value is: `lifecycle-churn` differs from `dynamic-heavy` only in destroying the -leaves it would otherwise have moved, so the difference between the two rows is -what structural invalidation costs; `text-dynamic` differs from `text-static` -only in re-setting strings; `mask-clip-animated` differs from `mask-clip` only in -moving its mask rects every frame, so the delta is what an effect change alone -costs the retained products around it; each `filter-chain-*` step adds one render-target -pass; `composite` is `filter-chain-1`'s scene rendered as a bloom-shaped -multipass (off-screen capture, downscaled blur, direct draw, additive overlay), -so the two rows separate one node-attached filter from an explicit multipass -over the same content. Every archetype also declares whether a cross-arm comparison of it is -meaningful at all (`crossArm`) - the ExoJS-internal probes say no, and the -published comparison excludes them by construction. - -Text is compared on each library's GLYPH-ATLAS path, never its canvas-raster -one: ExoJS SDF text, Pixi `BitmapText` (not `Text`, which rasterizes one canvas -texture per node), Phaser `BitmapText` over a `RetroFont` grid, Excalibur -`SpriteFont`. The last two have no dynamic font generation, so they parse a -generated digit sheet the harness supplies; `src/rendering/digitAtlas.ts` states -what that buys them. - -The physics domain's archetypes work the same way: `box-stack`, `many-dynamic`, -`mixed-static-dynamic`, plus `raycast` (the mixed scene with 64 ray queries per -step), `body-churn` (the many-dynamic scene with 5 % of its bodies destroyed and -rebuilt per step) and `joints` (chains of 8 bodies on revolute constraints). The -per-cell seed is keyed on the SCENE rather than on the archetype, so a query or -churn row and its base row simulate the byte-identical world and their delta -carries one cause. Every arm builds the identical scene from the -identical seed; the harness asserts that by comparing each arm's mutation-index -signature against a canonical selection and failing the cell loudly on any -divergence. - -### Physics body-count ladders - -Each physics archetype sweeps **its own body-count ladder**, placed so its rungs -straddle a 60 fps frame: two inside the frame and one past it. They are not -interchangeable — at a fixed body count the archetypes differ by nearly an order -of magnitude, so `many-dynamic` reaches a whole frame at 2 200 bodies while -`joints` does not until 15 000. One shared ladder therefore spends most of its -rungs on scenes nobody could ship for the expensive archetypes and never reaches -the interesting region for the cheap ones. - -| archetype | ladder | native median per step | -| ---------------------- | ---------------------- | ----------------------- | -| `box-stack` | 3 000 / 5 500 / 10 000 | 4.05 / 9.02 / 18.88 ms | -| `many-dynamic` | 800 / 1 500 / 2 200 | 4.57 / 10.59 / 17.31 ms | -| `mixed-static-dynamic` | 900 / 1 700 / 3 200 | 4.08 / 8.51 / 17.17 ms | -| `raycast` | 900 / 1 700 / 3 200 | 6.48 / 12.40 / 19.80 ms | -| `body-churn` | 800 / 1 500 / 2 400 | 2.67 / 9.14 / 18.99 ms | -| `joints` | 4 500 / 9 000 / 15 000 | 6.28 / 10.52 / 17.44 ms | -| `settling-pile` | 1 500 / 3 000 / 5 800 | 3.04 / 8.43 / 17.23 ms | - -The medians are a placement sweep of the native arm alone on one machine -(Ryzen 7 3700X, Chromium) over a thin timed window. They locate each frame -crossing and are not a published measurement. - -An archetype that is read as a **delta** against another shares at least one rung -with it, which is what the delta needs: `seedFor` folds the body count in, so two -archetypes that name the same scene build the identical world only at a count -both ladders contain. `raycast` and `mixed-static-dynamic` share their whole -ladder, `body-churn` shares two rungs with `many-dynamic`, and `settling-pile` -shares one. - -**Moving a rung changes the scene, not just its size.** Because the seed folds in -the body count, numbers taken at an earlier ladder describe different worlds and -are not comparable with these; there is no conversion between them, and nothing -published is carried across. `--bodies` filters the matrix rather than replacing -a ladder (unlike the rendering domain's `--nodes`), so an off-ladder probe needs -a source edit. - -The physics arms are not one flat field of competitors. **matter-js** and -**planck** are the pure-JS **peers** — the libraries an ExoJS app would -realistically attach instead of the native runtime, and what `exojs-physics` is -compared against. **rapier** is a Rust engine compiled to WASM and is read as the -**reference ceiling**: it measures what leaving JavaScript buys, not a bar a JS -solver is expected to reach. Each arm's solver iterations, sleeping default and -contact-count semantics are disclosed per arm in every report's caveats block. - -What is **not** measured: game-loop update, input, physics (in the rendering -domain), asset loading, GC headroom — everything outside `mutate` + -`renderFrame`. A benchmark millisecond is not a game frame. - -## Metrics - -| column | what it is | -| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| `cpuMsMedian` / `cpuMsP95` | wall clock bracketing `mutate` + `renderFrame`, per frame. The primary metric: the CPU cost of the render path. | -| `frameMsMedian` / `frameMsP95` | GPU time for the frame, from a HARDWARE clock (see below). | -| `queueMsMedian` / `queueMsP95` | WebGPU only: queue occupancy attributed to the frame that caused it. A different measurement from `frameMs*`, not a second opinion on it (see below). | -| `drawCalls` / `textureBinds` / `bufferUploads` | per-frame counts from a probe wrapped around the live graphics context. | -| `status` | `ok`, `exceeded` (aborted on a sustained slowdown), or `unavailable` (never measured). | -| `note` | per-cell disclosure: which frame-time source was used, why a cell aborted, which counters were skipped. | - -Frame time is never fabricated, and its source differs per backend — which -matters when reading a WebGL2 row against a WebGPU one: - -- **WebGL2** uses the hardware `EXT_disjoint_timer_query_webgl2` query when the - browser exposes it (browsers gate it behind privacy policy), discarding any - sample the driver flags as disjoint. The query brackets `mutate` + - `renderFrame`, so it covers the frame's WHOLE GL command stream — uploads - (`texSubImage2D`) included. -- **WebGPU** uses hardware `timestamp-query` writes around each of the frame's - render passes, summed per frame. The feature is obtained by requesting it on - every device the page creates (additively, identically for every arm) and the - writes are injected into the render-pass descriptors the arm hands to - `beginRenderPass`; query slots are resolved once, after the timed window, so - the instrument adds no wait to the measured path. This covers render-pass - EXECUTION only: `queue.writeBuffer` is a queue operation outside every command - buffer, so its device copy cannot be bracketed by any timestamp pair. On an - upload-heavy frame that copy is the dominant GPU cost — `queueMs*` is the - column that sees it. -- **Fallback**: the delta between consecutive `requestAnimationFrame` callbacks, - which is display-present cadence rather than GPU work. Cells that fall back to - it say so in their `note`. - -### `queueMs*`, and why it is not the WebGPU frame time - -`queueMs*` is the `queue.onSubmittedWorkDone` wall clock, with each frame charged -only the interval past the PREVIOUS frame's observed completion -(`doneAt − max(submitAt, previous doneAt)`). Completion is cumulative, so without -that attribution one stall is reported by every frame submitted behind it — -measured on a 1M-node cell, a single 27.7ms upload event was reported by three -consecutive frames as `27.74 / 27.13 / 27.43`; attributed, it reads -`27.74 / 0.03 / 1.09`. Warmup work is drained at the warmup/timing boundary for -the same reason. - -It is nonetheless NOT a per-frame GPU time, and was demoted out of `frameMs*` -because of what the instrument's floor is. `onSubmittedWorkDone` reports when the -browser OBSERVES completion, not when the GPU finished: a rAF-paced submit that -clears the canvas swapchain — 2µs of GPU work by the hardware clock — reports -0.50ms, and the identical clear into an offscreen texture reports 3.18ms, because -a present flushes the device and nothing else does. Real frames land in both -regimes, which is why the old WebGPU `frameMsMedian` was bimodal between ~0.6 and -~3.0ms run to run while the GPU work behind it never moved. Read `queueMs*` for -large events (uploads, stalls); never as a small-frame GPU time. - -### Diagnostic drivers - -Two frame-series probes live beside the matrix harness. Neither runs during a -matrix cell; both share the harness's page, server and launch flags so their -numbers are produced under the same conditions. +That is the full rendering matrix and takes a while. For a first look, ask for +one scenario: ```sh -pnpm perf webgpu:alloc # per-frame allocation, wall-clock or work-unit counters, one browser per cell -pnpm perf webgpu:timer # per-frame timer methodology: raw vs attributed queue - # latency vs hardware timestamps, plus serialized / - # canvas-clear / offscreen-clear control arms and the - # clock + scheduler controls behind the floor above +pnpm --filter @codexo/exojs-bench bench --archetype=fx-blur --backend=webgl2 +``` + +``` +=== Results === + scenario load backend arm cpu ms cpu p95 draws status + -------- ---- ------- -------------- ------ ------- ----- ------ + fx-blur 720 webgl2 exojs current 0.245 0.340 4 ok + fx-blur 720 webgl2 exojs retained 0.220 0.285 4 ok + fx-blur 720 webgl2 pixi default 0.120 0.140 3 ok ``` -**Do not compare a WebGL2 frame-time column against a WebGPU one as if the two -came from the same instrument.** Both are hardware clocks, but they bracket -different things — WebGL2 the whole command stream, WebGPU the render passes — -and the gap is not small: the same `retained scrolling-world 100k` cell measures -0.397ms on WebGL2 and 0.065ms on WebGPU. `cpuMs*` is measured identically -everywhere and is the column to compare across backends and arms. - -The structural counters are the durable half of the report: exact, deterministic -and reproducible where a timing is not. A non-empty scene that reports zero draw -calls fails the cell rather than reporting the undercount. - -## Median vs p95 - -`median` is the amortised cost of a typical frame. `p95` is the frame the player -feels. - -Both are needed, because an optimisation that converts per-frame work into -_periodic_ work improves the median exactly as much as one that removed the work -— while the worst frame is unchanged. The report marks such a row `hitching` when -`cpuMsP95` is at least 4x `cpuMsMedian` **and** at least 8 ms. A `hitching` row -means the two columns answer different questions and the median must not be -quoted on its own. - -Percentiles use the nearest-rank method on the sorted samples -(`index = ceil(p / 100 * n) - 1`). The timed-frame count shrinks as node count -grows (see below), so a 100k+ cell has 30 samples and its p95 is the second-worst -of those 30 — a real worst-case indicator, but a coarse one. Read it next to -`timedFrames`, which every report row carries for exactly this reason. - -### Why `scrolling-world` needs p95 - -`scrolling-world` is the only archetype with content outside the view, and the -only one with a moving camera: its leaves are laid out over four times the -viewport's area, so roughly 25% are visible at any moment, and the camera travels -the world diagonal at a fixed speed, reflecting off the world edges on a path -that is a closed form in the frame index. - -That makes its cost distribution bimodal by construction. A large static world -can keep its renderable state persistent while the camera moves, so moving the -camera does not require rebuilding the visible scene from scratch — most frames -touch only what entered or left the view, and the frames that revalidate a larger -part of the world are periodic rather than per-frame. A median over that -distribution reports the cheap frames and hides the expensive ones entirely. - -So on `scrolling-world`: quote `cpuMsP95`, or both columns, never the median -alone. This is the archetype the `hitching` marker exists for. - -## Warmup and timed frames - -Both counts are derived from the cell's node count and recorded per row in the -report, so a median over 30 frames is never presented as equal in confidence to -one over 120. - -| node count | warmup frames | timed frames | -| --------------- | ------------: | -----------: | -| < 5 000 | 10 | 120 | -| 5 000 – 24 999 | 10 | 90 | -| 25 000 – 99 999 | 25 | 60 | -| >= 100 000 | 40 | 30 | - -Warmup settles shader compilation, texture upload and JIT; its frames are -discarded. It scales _up_ with node count precisely because the timed window -scales _down_ — a warmup shortfall would eat a far larger share of a 30-frame -window than of a 120-frame one. It is additionally capped at 10 s of wall clock, -so a pathological cell stops warming instead of grinding. - -Timed frames are driven from `requestAnimationFrame`, one measured frame per -callback. A cell aborts as `exceeded` when the trailing 3-frame median exceeds -200 ms (a sustained slowdown, not a single GC or scheduler spike), or when one -frame alone exceeds 2 000 ms. Outside the page, the driver abandons any cell that -returns no result within 60 s as `unavailable` and relaunches the browser — a -mid-frame GPU-driver stall cannot be interrupted from inside the page. - -## Browser and environment - -- **`--browser=chromium` (default) or `--browser=webkit`**, both headless via - Playwright. The choice belongs to the run, not to the harness: it is stamped - into every provenance block and forms part of the published profile's file - name, so a WebKit number can never be read as a Chromium one and the two never - pool into one profile. -- Chromium is launched on the `chromium` channel with - `--force-device-scale-factor=1` so `devicePixelRatio` is 1 and the canvas - backing size is deterministic. WebGPU adds `--enable-unsafe-webgpu`. -- WebKit is launched with **no arguments at all**: every flag here is a Chromium - one, and its stamp records an empty flag set rather than claiming a launch - that never happened. Nothing is lost by it - the harness page sizes its own - 1280x720 backing store and the Pixi arm pins `resolution: 1`, so no - measurement depends on a pinned device scale factor. -- **A backend the selected browser does not expose produces no number.** WebKit - reaches WebGPU on macOS alone; elsewhere `navigator.gpu` is undefined and every - WebGPU cell is emitted `unavailable` with that reason attached, exactly as a - refused software adapter is. -- `--profile` (the V8 CPU sampler) is Chromium-only, because the sampler is - driven over a CDP session. It refuses in any other browser rather than falling - back and attributing one engine's frame cost to another engine's name. -- No software rasterizer is ever forced. `--use-angle=swiftshader` and - `--enable-features=Vulkan` are deliberately absent; either would land the run - on SwiftShader and make every timing worthless. -- The page is served **cross-origin isolated** (COOP + COEP), which lifts the - browser's Spectre clamp on `performance.now()` from ~100 µs back to ~5 µs. - Without it the cheap cells quantise to the timer floor. -- Fixed 1280x720 canvas, a **fresh canvas per cell**, with the previous cell's GL - context force-lost so contexts cannot pile up past the browser's live-context - cap. -- **One browser session per arm**, holding all of that arm's cells, with each cell - fully initialising and tearing down its engine. -- Engine source is compiled with `__DEV__ = false` and served through the real - shader loader. A dev build carries per-frame diagnostics no shipped game runs, - and would be measured against competitors' production dist bundles. - -## Provenance stamped into every report - -`results.json` / `results.md` carry, per backend: - -- **GPU adapter identity.** On WebGL2 the `WEBGL_debug_renderer_info` renderer - string, read from the stage canvas's own context after the first measured cell - — not from a throwaway canvas, which can report a different adapter on a - multi-GPU machine. On WebGPU the adapter's vendor / architecture / device / - description. How much this identifies is the browser's choice: Chromium - unmasks the device model, while WebKit substitutes a constant vendor-level - string (`Apple GPU`) for it on every platform — including a Windows machine - with an NVIDIA card — so a WebKit profile is named after the CPU model - instead, which the physics domain records. -- **Browser and browser version**, as the browser reported it, plus the - operating system of the host that drove it. -- **`platformVersion`** — the operating system's major version and what - established it: `detected` where the host reports one (Windows, whose - `10.0.` release string names Windows 11 from build 22000 up), or - `declared` from `--platform`. It is separate from the `os` field because that - field carries the kernel release, which on macOS no longer tracks the product - version. -- **`prerelease`** — whether the platform is a pre-release build, and a `source` - saying what that rests on: `detected` from a browser version string naming a - non-shipping build, `declared` from `--platform=-beta` (needed for a - beta OS, which is not readable at runtime), or `assumed-stable`, which records - that nothing established it and is weaker than a stable platform. -- **`software`** — the honesty bit. A WebGL2 run on a software rasterizer marks - every timing column `UNTRUSTED` in the Markdown report; a software WebGPU - adapter is refused outright and its cells are emitted `unavailable`. -- Launch flags, headless flag, engine version, ISO timestamp. -- The WebGPU sprite-batch texture-slot tier (8 / 16 / 32) negotiated for the - adapter, so a slot-sensitive archetype's measured code path stays auditable - across machines. -- Each competitor library's exact version and the path it resolved from. - -The physics domain records the browser and browser version, the CPU model, -logical CPU count, OS and architecture of the host that drove it, and what the -measuring page's `performance.now()` could resolve. It does **not** record the -driver process's Node version: no step was taken there, so a field naming it -would describe nothing about where the numbers came from. - -### Physics timing resolution - -The fastest cells of the physics matrix step in single-digit microseconds, and -`performance.now()` is clamped as a Spectre mitigation. The harness page is -served cross-origin isolated (COOP/COEP), which lifts the clamp to 5 µs in -Chromium and 20 µs in WebKit — measured per run, not assumed, and stamped into -the provenance. - -A step that cannot clear that grid on its own is not timed on its own. Each cell -estimates its per-step cost from the last 60 warmup steps and then times steps in -batches large enough for one sample to span 20 clock ticks, dividing the sample -by its batch. The batch is recorded per row as `stepsPerSample`; `1` means every -step was timed individually and the row is byte-for-byte the old contract. The -**timed-step budget is unchanged** — the batch only decides how finely the fixed -window is sampled — and the batch stops growing once a cell would be left with -fewer than 12 samples, because a median and a p95 need a distribution behind -them. A cell that hits that cap before clearing the grid says so in its `note`, -naming the quantisation it still carries, rather than reporting a coarse number -that looks precise. - -**Gap worth knowing:** the rendering domain records the GPU, the browser and the -operating system, but not the rest of the host. CPU model, RAM and GPU driver -version are _not_ captured automatically — record them by hand alongside any run -you intend to quote. - -## Reproducing a single scenario - -Every selection flag accepts a comma-separated list, and `--nodes` _replaces_ the -archetype's own ladder rather than filtering it, so an off-ladder probe needs no -source edit: +The same rows land in `results.json`, `results.csv` and `results.md` in the +output directory the run names at the end. + +### Reading a row + +- **load** is what the scenario scales: sprites for most of them, but tiles, + particles, widgets or a render height for others. The unit is in `results.md`; + the number alone does not carry it. +- **cpu ms** is the median time one frame spent in JavaScript - the scene's + per-frame work plus submitting it. This is the comparable number. +- **cpu p95** is the same window's 95th percentile. Far above the median means + the cost arrives in periodic spikes, which a player feels as a hitch and a + median alone hides; the harness marks those `hitching`. +- **draws** is draw calls per frame, counted by wrapping the graphics context. + It is what says _why_ one arm is faster, and a row whose timing moved without + its draw count moving usually moved for a reason outside the engine. +- **status** is `ok`, `exceeded` (the cell went past the harness's frame budget + and was stopped early), or `unavailable` with a reason recorded in the report. + +Physics runs print the same shape with `step ms`, the bodies actually simulated +and the contacts resolved. + +### Narrowing a run + +Every selection flag takes a comma-separated list. + +| Flag | What it selects | +| ----------------------------- | -------------------------------------------------------------------------------- | +| `--domain=rendering\|physics` | Which matrix to run. Default `rendering`. | +| `--archetype=` | Scenarios, by id. | +| `--engine=` / `--config=` | Arms - `exojs`, `pixi`, `phaser`, `excalibur`, and each one's configs. | +| `--backend=webgl2\|webgpu` | Graphics backend. | +| `--browser=chromium\|webkit` | Browser the run is measured in. Default `chromium`. | +| `--nodes=` / `--bodies=` | **Replaces** the scenario's ladder, so an off-ladder probe needs no source edit. | +| `--frames=` | Timed frames per cell. Thin sampling; for looking, not for publishing. | +| `--out=` | Output directory. | +| `--capture=` | Write a PNG of each cell's last frame, to see what was measured. | +| `--profile` | V8 CPU profile of one cell, by file and by function. Chromium only. | + +A run that uses any of these prints `SUBSET RUN - not a reportable comparison` +and means it: the published comparison is a whole matrix measured in one go. + +## Producing numbers worth publishing + +One run does not support a claim - the same code on the same idle machine moves +a cell's median far enough to flip which arm leads. So a published measurement is +**three separate runs**, pooled: ```sh -pnpm --filter @codexo/exojs-bench bench \ - --domain=rendering \ - --engine=exojs --config=current \ - --archetype=scrolling-world \ - --nodes=1000000 \ - --backend=webgl2,webgpu \ - --out=.workspace/output/my-run +pnpm --filter @codexo/exojs-bench bench:reference --out run-1 +pnpm --filter @codexo/exojs-bench bench:reference --out run-2 +pnpm --filter @codexo/exojs-bench bench:reference --out run-3 + +pnpm --filter @codexo/exojs-bench bench:compare --profile \ + --rendering run-1/rendering/results.json \ + --rendering run-2/rendering/results.json \ + --rendering run-3/rendering/results.json \ + --physics run-1/physics/results.json \ + --physics run-2/physics/results.json \ + --physics run-3/physics/results.json ``` -On a POSIX shell no `--` separator is needed with `pnpm --filter …`; pnpm forwards -these straight to the script. Running the same script from inside -`packages/exojs-bench` (`pnpm bench -- --out=…`) works too, and `--out` is then -relative to the package directory either way. The root `pnpm bench` forwards here -as well; the engine's own `vitest bench` micro-benchmarks are `pnpm bench:micro`, -and there is no root `bench:compare`. - -> **PowerShell: quote every comma list.** PowerShell reads an unquoted `a,b` -> argument as an array literal and rejoins it with spaces on the way through -> pnpm's `.ps1` shim, so `--backend=webgl2,webgpu` arrives as -> `--backend=webgl2 webgpu`. Write `"--backend=webgl2,webgpu"` instead. A `--` -> separator does **not** help — the rewrite happens in the argument binder, -> before pnpm sees anything — and a POSIX shell is unaffected. The CLI refuses a -> selection value containing whitespace rather than acting on it, because -> `--nodes` is the one flag where the damage is silent: `1000 5000` survives -> `parseInt` as `1000`, and the run would publish one node count under the -> provenance of two. - -The run writes `results.json`, `results.csv` and `results.md` into `--out` -(default `.workspace/output/baseline/`, gitignored), plus a `checkpoint.jsonl` -appended per cell as it lands, so a crash never discards finished work. - -A `results.json` already in `--out` is merged into, not replaced: a cell the -run measured with status `ok` replaces the cell with the same spec, a cell it -did not run stays as it was, and an `exceeded` or `unavailable` cell never -overwrites a measurement (it is added only when the cell had no entry). The CSV -and Markdown are re-rendered from the merged set. That is what lets a matrix be -built up one archetype or one backend at a time - but it also means a kept cell -keeps its earlier provenance, and the table does not mark which cells a rerun -did not touch. Delete or move `results.json` before a run that must stand on -its own, and give pooled runs separate `--out` directories as before. The -checkpoint files are per run and are truncated at start. - -Other flags: - -- `--browser=chromium` (default) / `--browser=webkit` — the engine to measure in. - Not a subset marker: a run in either browser is a full measurement of that - browser, published under its own profile. -- `--platform=` / `--platform=-beta` — declare the operating - system's major version, and with the suffix that the build is a pre-release - one. Required on macOS and Linux, whose kernel version names no product - version; optional on Windows, which reports its own and refuses a declaration - contradicting it. A full run without it fails before measuring anything, a - narrowed run only warns. Pass it on **every** pooled run: both the version and - the beta marker are part of the profile's file name, and runs disagreeing on - either are refused rather than pooled. -- `--backend=webgl2` / `--backend=webgpu` — omit for both. -- `--engine`, `--config`, `--archetype`, `--nodes` — comma-separated selections. -- `--frames=N` — override every cell's timed-frame count for a fast spot check. - **Never for a quoted run:** it flattens the per-node-count budgets the - `timedFrames` column exists to make honest. -- `--profile` — run the selected cells under the V8 CPU sampler and print self - time by source file and by function instead of measuring wall clock. It answers - "which code made the frame expensive", never "how expensive is the frame", and - its output is deliberately printed rather than written into `results.*`. - -Any run that narrows the matrix prints `SUBSET RUN — not a reportable comparison`. -That is expected for a targeted question: it warns about scope, not correctness. -A subset run's numbers are valid for the cells in it and are not a matrix result. - -## How to read a result — and how not to - -- **A number belongs to a machine.** GPU, driver, Chromium build, thermal state - and engine commit all move it. Reproduce locally before comparing against - anything recorded here. -- **Compare within one invocation.** Cross-arm and cross-backend claims rest on - the arms having run back to back in the same process on the same machine. Two - invocations are two browser sessions, and not a comparison. -- **For a before/after of an engine change, measure a single cell per - invocation.** Within one invocation an arm's cells share a browser session, and - accumulated driver/adapter state has been observed to move — in one case invert - — the verdict of a multi-archetype run. It moves the number too, and in one - direction: on one commit and one machine, `lifecycle-churn` at 5 000 nodes - measured 3.84 ms alone, 4.26 ms with the other 21 archetypes in the session, - and 4.82 ms in the full published matrix. Same engine, same structural - counters, a 26% spread from session shape alone — which is why a narrowed - run's absolute number must never be read against a recorded profile, and why - a before/after pair has to be two runs of the SAME shape. -- **`nodeCount` is the world total, not the drawn count.** On `scrolling-world` - only about a quarter of it is on screen; the rest is the off-screen content - under study. -- **Rows at 100 000+ nodes are marked `beyond-frame-budget`.** They are stress - probes past any interactive budget, not target configurations. -- **Never turn a benchmark millisecond into an FPS claim.** `cpuMs*` covers the - render path only — no update, input, physics, asset work or GC headroom, and no - vsync. -- **Never quote a median from a `hitching` row on its own**, and never quote a - timing from an `exceeded` or `unavailable` cell: those statuses exist to record - that the cell produced no trustworthy distribution. -- **Structural counters travel better than timings.** `drawCalls` and - `bufferUploads` are deterministic; a claim meant to survive a hardware change - should be made about those. - -## Reference result: `scrolling-world` at 1M sprites - -A dated reference point, measured on **one** machine. It is not a hardware -minimum, not a maximum capability, and not an FPS guarantee — it is a -reproduction target: the same command on the same class of machine should land in -the same neighbourhood, and a large deviation is worth investigating. - -- Date: 2026-08-15 · ExoJS 0.15.2 -- GPU: NVIDIA GeForce RTX 5070 Ti · headless Chromium via Playwright -- Cell: `engine=exojs config=current archetype=scrolling-world nodes=1000000` — - 1 000 000 static sprites laid out over 4x the viewport's area with a moving - camera, 40 warmup frames, 30 timed frames - -| backend | CPU median | CPU p95 | frame median | frame p95 | draw calls / frame | -| ------- | ---------: | -------: | -----------: | --------: | -----------------: | -| WebGL2 | 0.402 ms | 10.08 ms | 0.330 ms | 1.17 ms | 1 | -| WebGPU | 0.380 ms | 10.45 ms | 2.98 ms | 4.66 ms | 1 | - -Reading notes, in the order they matter: - -- **CPU p95 is the headline, and it is a CPU number.** 10.08 ms is the - 95th-percentile time the engine spent in the render path on the CPU, i.e. the - second-worst of 30 timed frames. It is neither GPU frame time nor a whole game - frame. -- **The two frame-time columns come from different instruments** — a hardware - timer query on WebGL2, a queue-completion wall clock on WebGPU (see - [Metrics](#metrics)) — and are not comparable 1:1 across the two rows. -- **One draw call on both backends** is the structural fact behind the timings: - the visible world is drawn as a single batched submission even while the camera - moves. The table's per-frame `1` is derived. The report itself prints the - window's raw total in that column — `drawCalls = 30` over 30 timed frames — - with the note `structural counters did not divide evenly over 30 frame(s); raw -totals reported`, which trips on this cell because a sibling counter - (`bufferUploads`) has no whole-frame quotient. Divide the column by - `timedFrames` when a cell carries that note. -- **This is one workload.** `scrolling-world` is a deliberately extreme static - world. Nothing here generalises to a million _animated_ sprites, to other - archetypes, or to other hardware. - -For context on where those numbers came from: before the engine kept its -renderable state persistent across camera movement, the same cell measured a CPU -p95 of 203.14 ms on WebGL2 and 234.19 ms on WebGPU, and the WebGPU cell aborted at -the harness watchdog after 26 frames instead of completing its timed window. The -medians were already low then — only p95 showed the per-frame rebuild. That is the -concrete reason this archetype is quoted on p95. - -Numbers produced by the engine's internal Node-side CPU stubs — isolated -CPU-path measurements with no browser and no GPU, used while iterating on a change -— are a different measurement entirely. They are not comparable with the table -above and are never published as ExoJS performance figures. - -## The two regression gates - -They guard different defect classes and are deliberately unlike each other. - -**Structural** (`pnpm gate:bench:structural`) compares exact integer -draw/bind/upload counters against `baselines/structural.json`. The values are -decided CPU-side, so they do not drift and the baseline carries no tolerance -band: any deviation fails, as does a guarded cell that disappears or stops -measuring. It catches the fault that renders the identical picture and is merely -expensive - a batching collapse from 782 to 25 000 draw calls passes every -correctness test. - -It runs on the software rasterizer, which is what puts it in CI. That its -counters are genuinely backend-independent was measured, not assumed: every -archetype was run on a real GPU and on SwiftShader, and all three counters came -out byte-identical everywhere except `batch-breaking` (does not complete on a -software rasterizer) and `text-dynamic` (aborts there as too slow). Those two are -unguarded, named in the source with the reason; no counter needed a tolerance. -Re-record with `--update` in the same commit as an intended change. - -**Timing** (`pnpm --filter @codexo/exojs-bench gate:timing`) compares wall-clock -medians against `baselines/timing.json` and is a RELEASE PRECONDITION invoked by -hand on a machine you have confirmed idle. It is not a hook and not a CI job: -the matrix needs an idle machine, a push is by definition not one, and a gate -that goes falsely red gets bypassed. - -A cell fails when its median exceeds the baseline by more than 25 % **and** by at -least 0.5 ms. The absolute floor is not a softening - without it the gate is -unusable on sub-millisecond cells: two consecutive runs of the same code on the -same machine put 15 of 42 cells over 25 %, the smallest moving 0.060 ms to -0.095 ms. p95 is reported on every row and gates nothing, being the noisier -statistic. The committed baseline is stamped `confirmedIdle: false` and the gate -repeats that caveat on every run; re-record it with `--update --idle` on a quiet -machine before relying on it for a release. - -Correctness assertions stay in the test suite, which is fast, CI-resident and -needs no GPU. Nothing moved out of it: the gates add the other defect class - -faults that look correct and are merely expensive. - -## The published comparison - -`bench:compare` reads the `results.json` files of several runs and generates the -comparison document. It is generated, never hand-maintained, because a -hand-written comparison drifts from the harness and once it drifts the honesty -is gone without anyone noticing. - -### A reference measurement is three runs - -A published claim is a ratio between two arms, and one run does not support one: -the same code measured twice on this machine moved a physics cell's median by -2.5x with byte-identical contact counts behind both runs, which was enough to -reverse seven verdicts. That spread was observed while physics still ran in the -driver's Node process; it has not been re-derived since the matrix moved into the -browser, so read it as the reason for pooling rather than as this harness's -current noise figure. So `--rendering` and `--physics` are **repeatable, once -per run**, and the runs are pooled: +`bench:reference` measures both domains at each scenario's headline load. Three +separate invocations, on an otherwise idle machine, back to back - repeating the +matrix inside one process shares JIT and heap state and measures the same warm +state three times. Pass **none** of the narrowing flags above, `--capture` +included: each one marks the run a subset, and a subset does not publish. + +`bench:compare` publishes the median of the per-run medians, the spread those +runs showed, and a verdict only where all three agreed on one. It refuses to pool +runs that are not repetitions of the same measurement - a different machine, +browser, platform or engine version among them. + +`--profile` writes the pooled comparison to `results/` as a signed machine +profile. On macOS and Linux add `--platform=` (with `-beta` if the OS is a +pre-release build), because those systems do not report their own product +version. + +## The gates ```sh -pnpm --filter @codexo/exojs-bench bench --out run-1 -pnpm --filter @codexo/exojs-bench bench --out run-2 -pnpm --filter @codexo/exojs-bench bench --out run-3 -pnpm --filter @codexo/exojs-bench bench:compare \ - --rendering run-1/results.json --rendering run-2/results.json --rendering run-3/results.json +pnpm gate:bench:structural # draw/bind/upload counters against a committed baseline +pnpm --filter @codexo/exojs-bench gate:timing # the manual timing gate ``` -A single path still behaves exactly as it always did. The runs must come from -**separate invocations**, each with its own `--out` directory: repeating a matrix -inside one process shares JIT and heap state across the repetitions and measures -the same warm state several times, which is the effect the repetition exists to -expose. - -Per cell, the pooled comparison publishes the median of the per-run medians, the -median of the per-run p95s, the range those runs observed (printed in brackets -beside the value), the frame-budget mark where the pooled median is past 16.7 ms, -and a stability flag: the verdict is computed from each run separately, and the -cell is stable only when every run reached the same one. **An unstable cell -publishes no verdict** - it keeps its row, its value and its range, and states -what each run said instead. - -`bench:compare` refuses to pool runs that are not repetitions of one measurement: -a differing engine version, a differing set of arms or versions, a differing set -of measured cells, a row that landed on a different count in one run than in -another, or a differing machine. - -The machine check is what a second reference machine makes necessary — three -runs from two machines would otherwise pool into a median belonging to neither, -with a spread reporting the gap between two computers as the noise of one. A -rendering run's identity is the normalized GPU the adapter string names, the -operating system with its major version and pre-release bit, and the browser, -using the same normalizations the profile slug uses, so runs may pool exactly -when they would be written to one file. A physics run's identity is the CPU -model, the same platform identity, the architecture, and the browser. Tolerated within one -machine: timestamps, an adapter string's driver / device-id / shader-model tail, -the OS patch level, and the browser's patch version, which a checkout pins and -which every stamp records in full anyway. Assembling one profile from a -rendering measurement on one machine and a physics measurement on another is -refused for the same reason. - -One limit is inherent: where the browser reports a constant instead of the GPU, -a rendering run identifies its machine no more precisely than that constant -does, because the CPU model that names it in the file is recorded by the physics -domain. Two such machines on the same operating system and browser are -indistinguishable to the check. - -Rules the generator enforces rather than merely intends: - -- Rows are archetypes; categories are section headings. Nothing aggregates - across archetypes, because any mean over a category hides its worst cell. -- Verdicts are computed from the two medians. A ratio inside 0.8-1.2 is `level` - (the matrix's own noise band); outside it the faster arm `leads`, and at 5x or - more `leads clearly` - a gap too large for machine mood to explain. -- A row's count is chosen from the archetype ladders BEFORE any timing is read - - the largest rung at which every arm produced a valid cell - so it can never be - picked to suit an outcome. A rendering table goes further and uses one node - count for every row, because its archetypes share their ladders. -- Physics rows each state their OWN body count, because each physics archetype - has its own ladder and their intersection is empty. Rows are therefore not - comparable with one another; only the arms within a row are, which is what the - table is for. Two rows were always two different scenes — the shared count made - that look otherwise. -- Every value is a median AND the p95 of the same timed window. Verdicts are - computed from the medians alone; the p95 is the step or frame a player feels, - so a pair far apart hitches where the median reads as comfortable. There is no - p99: the largest cells time 120 steps, so a p99 there is the second-worst - sample rather than a percentile. -- A median past **16.7 ms** — a whole 60 fps frame — is marked. The line is the - entire frame deliberately: how much of a frame this work may take is the - reader's decision, but a step or frame costing more than the frame it has to - fit in is unplayable regardless of that decision. Nothing is derived from the - mark; a "bodies at N ms" capacity figure would interpolate between rungs rather - than report a measurement. -- Every row names the mechanism behind its difference, drawn from the structural - counters. A row whose mechanism cannot be evidenced is not published - it is - listed under Omissions with the reason, so a dropped row stays auditable. -- One column per competitor, no "best competitor" composite. Phaser occupies its - own WebGL1 block, CPU time only, explicitly carrying no mechanism. -- Cells where ExoJS loses are published exactly like the cells where it wins. - -### Machine profiles - -`bench:compare --profile` additionally writes the comparison as JSON into -`results/`, one file per machine, named -`--[-beta]-.json` after the provenance — the GPU or, -where the browser reports a constant instead of one, the CPU model; the -operating system with its major version and pre-release marker; and the browser. -The name is derived from the stamps, so -re-measuring a machine overwrites its file and a different machine can only -arrive as a new one. Each file carries every pooled run's provenance and a -signature over its own contents, and `verify:bench-results` (in the `lint` gate -group) rejects a file that pools fewer than three runs, or whose signature does -not recompute - which is what keeps a typed number and a one-run claim out. See -[`results/README.md`](./results/README.md). - -## Cross-library numbers - -The harness runs competitor arms, and no cross-library figure is published in the -ExoJS documentation. Publishing one requires all of: a frozen version of every -library involved, an identical scenario across arms, identical browser and -hardware conditions, the harness for each arm published alongside the numbers, the -caveats stated with the result, and a defined process for re-measuring when any of -those move. Until that exists, a cross-library number measured here is an -engineering signal for the maintainers, not a claim. - -## What runs in CI, and what does not - -Three different things live in this package, and CI treats them differently. - -**The harness's own tests** (`test/`) exercise the profile contract, the slug, -the signature, the run pooling and the archetype definitions. They need none -of the competitor libraries - the adapters that import those are loaded by -`import()` inside the benchmark page, never by a test - so they run in the -ordinary `pnpm test` project list on every push, like any other package. - -**The typecheck** does need the competitors: the adapters are typed against -their APIs, which is exactly what catches an upstream change on a version -bump. `pnpm typecheck:bench` installs them first. It runs in the path-gated -`bench` CI lane, alongside the structural gate, whenever a change touches this -package, the rendering source or the baselines. - -**Measurements** never run in CI. A shared runner is neither idle nor a known -machine, and a number it produced would carry provenance nobody can reproduce. -Reference profiles are measured by hand on an idle machine and committed as -signed files - see `results/README.md`. - -## The competitor install and the supply-chain gate - -`bench:setup` runs `pnpm install --dir competitors --frozen-lockfile`. The -`competitors/` directory is its own workspace root (it carries a -`pnpm-workspace.yaml`), which does two things: a plain root `pnpm install` -never resolves or downloads anything in it, so a contributor who never -benchmarks pays nothing for ~235MB of libraries whose only purpose is being -compared against; and the install applies the same `minimumReleaseAge` -quarantine the repository workspace enforces, so a version bump here is held -back exactly as long as any other dependency. The lockfile is frozen in both -CI and local use: a new version enters through a reviewed lockfile change, not -through an install. - -`pnpm doctor` reports whether the competitors are linked. +The structural gate runs on a software rasterizer and compares integer counters, +so it gives the same answer on any machine and runs in CI. The timing gate reads +wall clocks and does not. + +## Contributing your machine's numbers + +The published comparison pages are generated from one JSON file per machine in +[`results/`](./results/) - and every machine that is not in there yet is a gap. +Different GPUs, different operating systems, and WebKit against Chromium all +reorder these results, and no single developer owns enough hardware to find that +out. + +If you have a machine that is not represented: + +1. Follow [Producing numbers worth publishing](#producing-numbers-worth-publishing) + above, with `--profile` on the `bench:compare` call. +2. Open a pull request containing **only** the one new file in `results/`. + Nothing else needs to change; the published pages pick it up from there. + +The file is named after your machine and browser, derived from what the harness +stamped rather than typed by hand, so re-measuring a machine updates its own file +and can never overwrite someone else's. A validation gate checks on every CI run +that a profile pools at least three runs, that its provenance is complete, and +that its signature still recomputes - which only `bench:compare` can write. If it +rejects your file, re-run the harness rather than editing it. + +[`results/README.md`](./results/README.md) has the details: what a profile +contains, how the browser and platform flags change it, and what the pooling +rules refuse. + +## Going deeper + +[`docs/harness.md`](./docs/harness.md) is the reference: every scenario and what +it is for, what each metric does and does not mean, how warmup and timed frames +are chosen, what the harness pins about the browser, what provenance is stamped +into a report, how to read a result and how not to, and what the published +comparison is allowed to claim. diff --git a/packages/exojs-bench/docs/harness.md b/packages/exojs-bench/docs/harness.md new file mode 100644 index 000000000..f92341932 --- /dev/null +++ b/packages/exojs-bench/docs/harness.md @@ -0,0 +1,730 @@ +# The benchmark harness + +Reference documentation for `@codexo/exojs-bench`: what every scenario measures, +what each metric means, what the harness pins about the browser and the machine, +and what the published comparison is allowed to claim. + +Start at the [package README](../README.md) if you only want to run a benchmark. +Everything below is the contract behind the numbers it prints. + +## What it measures + +Two domains, selected with `--domain` (default `rendering`): + +- **`rendering`** drives a real headless Chromium against the real GPU. For each + matrix cell — one `(engine, config, backend, archetype, nodeCount)` combination + — it builds a scene, warms it up, then renders a fixed number of timed frames + from `requestAnimationFrame`, sampling per-frame CPU time, full-frame time and + draw-call structure. +- **`physics`** drives a real headless browser too, but no GPU: a straight loop + over `world.step` inside the harness page, sampling per-step CPU time plus + body, contact, joint and ray-hit counts. + + It is measured in a browser rather than in the driver's Node process because + nobody runs ExoJS physics in Node, and because the runtime is part of what a + step time measures: heap limits, garbage collection and WASM compilation are + the browser's, and the JavaScript engine is whichever the selected browser + ships. Measured on the same machine, moving the matrix from Node into Chromium + moved per-step medians by −80% to +29% depending on the arm, which is enough to + reorder the arms against each other; measuring it in WebKit instead moves them + again. A physics number is therefore only comparable against another taken in + the same browser, and `--browser` applies to this domain exactly as it does to + rendering. + +Everything below describes the rendering domain, except the physics sections that +name themselves. + +A cell's scene comes from a fixed **archetype** (`src/rendering/archetypes.ts`): +`static-heavy`, `dynamic-heavy`, `deep-hierarchy`, `lifecycle-churn`, +`overdraw`, `batch-breaking`, `batch-breaking-atlased`, `split-screen`, +`mixed-blend`, `mixed-material`, `mixed-material-atlased`, `instanced-batch`, +`mixed-sprite-mesh-static`, `mixed-sprite-mesh-array`, `scrolling-world`, +`text-static`, `text-dynamic`, `filter-chain-1`, `filter-chain-2`, +`filter-chain-4`, `mask-clip`, `mask-clip-animated`, `composite`. Each pins nesting depth, texture count, per-frame +mutation fraction and whatever dimension it exists to isolate, and sweeps a +ladder of node counts. + +Most of them are readable as a DELTA against another row, which is where their +value is: `lifecycle-churn` differs from `dynamic-heavy` only in destroying the +leaves it would otherwise have moved, so the difference between the two rows is +what structural invalidation costs; `text-dynamic` differs from `text-static` +only in re-setting strings; `mask-clip-animated` differs from `mask-clip` only in +moving its mask rects every frame, so the delta is what an effect change alone +costs the retained products around it; each `filter-chain-*` step adds one render-target +pass; `composite` is `filter-chain-1`'s scene rendered as a bloom-shaped +multipass (off-screen capture, downscaled blur, direct draw, additive overlay), +so the two rows separate one node-attached filter from an explicit multipass +over the same content. Every archetype also declares whether a cross-arm comparison of it is +meaningful at all (`crossArm`) - the ExoJS-internal probes say no, and the +published comparison excludes them by construction. + +Text is compared on each library's GLYPH-ATLAS path, never its canvas-raster +one: ExoJS SDF text, Pixi `BitmapText` (not `Text`, which rasterizes one canvas +texture per node), Phaser `BitmapText` over a `RetroFont` grid, Excalibur +`SpriteFont`. The last two have no dynamic font generation, so they parse a +generated digit sheet the harness supplies; `src/rendering/digitAtlas.ts` states +what that buys them. + +The physics domain's archetypes work the same way: `box-stack`, `many-dynamic`, +`mixed-static-dynamic`, plus `raycast` (the mixed scene with 64 ray queries per +step), `body-churn` (the many-dynamic scene with 5 % of its bodies destroyed and +rebuilt per step) and `joints` (chains of 8 bodies on revolute constraints). The +per-cell seed is keyed on the SCENE rather than on the archetype, so a query or +churn row and its base row simulate the byte-identical world and their delta +carries one cause. Every arm builds the identical scene from the +identical seed; the harness asserts that by comparing each arm's mutation-index +signature against a canonical selection and failing the cell loudly on any +divergence. + +### Physics body-count ladders + +Each physics archetype sweeps **its own body-count ladder**, placed so its rungs +straddle a 60 fps frame: two inside the frame and one past it. They are not +interchangeable — at a fixed body count the archetypes differ by nearly an order +of magnitude, so `many-dynamic` reaches a whole frame at 2 200 bodies while +`joints` does not until 15 000. One shared ladder therefore spends most of its +rungs on scenes nobody could ship for the expensive archetypes and never reaches +the interesting region for the cheap ones. + +| archetype | ladder | native median per step | +| ---------------------- | ---------------------- | ----------------------- | +| `box-stack` | 3 000 / 5 500 / 10 000 | 4.05 / 9.02 / 18.88 ms | +| `many-dynamic` | 800 / 1 500 / 2 200 | 4.57 / 10.59 / 17.31 ms | +| `mixed-static-dynamic` | 900 / 1 700 / 3 200 | 4.08 / 8.51 / 17.17 ms | +| `raycast` | 900 / 1 700 / 3 200 | 6.48 / 12.40 / 19.80 ms | +| `body-churn` | 800 / 1 500 / 2 400 | 2.67 / 9.14 / 18.99 ms | +| `joints` | 4 500 / 9 000 / 15 000 | 6.28 / 10.52 / 17.44 ms | +| `settling-pile` | 1 500 / 3 000 / 5 800 | 3.04 / 8.43 / 17.23 ms | + +The medians are a placement sweep of the native arm alone on one machine +(Ryzen 7 3700X, Chromium) over a thin timed window. They locate each frame +crossing and are not a published measurement. + +An archetype that is read as a **delta** against another shares at least one rung +with it, which is what the delta needs: `seedFor` folds the body count in, so two +archetypes that name the same scene build the identical world only at a count +both ladders contain. `raycast` and `mixed-static-dynamic` share their whole +ladder, `body-churn` shares two rungs with `many-dynamic`, and `settling-pile` +shares one. + +**Moving a rung changes the scene, not just its size.** Because the seed folds in +the body count, numbers taken at an earlier ladder describe different worlds and +are not comparable with these; there is no conversion between them, and nothing +published is carried across. `--bodies` filters the matrix rather than replacing +a ladder (unlike the rendering domain's `--nodes`), so an off-ladder probe needs +a source edit. + +The physics arms are not one flat field of competitors. **matter-js** and +**planck** are the pure-JS **peers** — the libraries an ExoJS app would +realistically attach instead of the native runtime, and what `exojs-physics` is +compared against. **rapier** is a Rust engine compiled to WASM and is read as the +**reference ceiling**: it measures what leaving JavaScript buys, not a bar a JS +solver is expected to reach. Each arm's solver iterations, sleeping default and +contact-count semantics are disclosed per arm in every report's caveats block. + +What is **not** measured: game-loop update, input, physics (in the rendering +domain), asset loading, GC headroom — everything outside `mutate` + +`renderFrame`. A benchmark millisecond is not a game frame. + +## Metrics + +| column | what it is | +| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `cpuMsMedian` / `cpuMsP95` | wall clock bracketing `mutate` + `renderFrame`, per frame. The primary metric: the CPU cost of the render path. | +| `frameMsMedian` / `frameMsP95` | GPU time for the frame, from a HARDWARE clock (see below). | +| `queueMsMedian` / `queueMsP95` | WebGPU only: queue occupancy attributed to the frame that caused it. A different measurement from `frameMs*`, not a second opinion on it (see below). | +| `drawCalls` / `textureBinds` / `bufferUploads` | per-frame counts from a probe wrapped around the live graphics context. | +| `status` | `ok`, `exceeded` (aborted on a sustained slowdown), or `unavailable` (never measured). | +| `note` | per-cell disclosure: which frame-time source was used, why a cell aborted, which counters were skipped. | + +Frame time is never fabricated, and its source differs per backend — which +matters when reading a WebGL2 row against a WebGPU one: + +- **WebGL2** uses the hardware `EXT_disjoint_timer_query_webgl2` query when the + browser exposes it (browsers gate it behind privacy policy), discarding any + sample the driver flags as disjoint. The query brackets `mutate` + + `renderFrame`, so it covers the frame's WHOLE GL command stream — uploads + (`texSubImage2D`) included. +- **WebGPU** uses hardware `timestamp-query` writes around each of the frame's + render passes, summed per frame. The feature is obtained by requesting it on + every device the page creates (additively, identically for every arm) and the + writes are injected into the render-pass descriptors the arm hands to + `beginRenderPass`; query slots are resolved once, after the timed window, so + the instrument adds no wait to the measured path. This covers render-pass + EXECUTION only: `queue.writeBuffer` is a queue operation outside every command + buffer, so its device copy cannot be bracketed by any timestamp pair. On an + upload-heavy frame that copy is the dominant GPU cost — `queueMs*` is the + column that sees it. +- **Fallback**: the delta between consecutive `requestAnimationFrame` callbacks, + which is display-present cadence rather than GPU work. Cells that fall back to + it say so in their `note`. + +### `queueMs*`, and why it is not the WebGPU frame time + +`queueMs*` is the `queue.onSubmittedWorkDone` wall clock, with each frame charged +only the interval past the PREVIOUS frame's observed completion +(`doneAt − max(submitAt, previous doneAt)`). Completion is cumulative, so without +that attribution one stall is reported by every frame submitted behind it — +measured on a 1M-node cell, a single 27.7ms upload event was reported by three +consecutive frames as `27.74 / 27.13 / 27.43`; attributed, it reads +`27.74 / 0.03 / 1.09`. Warmup work is drained at the warmup/timing boundary for +the same reason. + +It is nonetheless NOT a per-frame GPU time, and was demoted out of `frameMs*` +because of what the instrument's floor is. `onSubmittedWorkDone` reports when the +browser OBSERVES completion, not when the GPU finished: a rAF-paced submit that +clears the canvas swapchain — 2µs of GPU work by the hardware clock — reports +0.50ms, and the identical clear into an offscreen texture reports 3.18ms, because +a present flushes the device and nothing else does. Real frames land in both +regimes, which is why the old WebGPU `frameMsMedian` was bimodal between ~0.6 and +~3.0ms run to run while the GPU work behind it never moved. Read `queueMs*` for +large events (uploads, stalls); never as a small-frame GPU time. + +### Diagnostic drivers + +Two frame-series probes live beside the matrix harness. Neither runs during a +matrix cell; both share the harness's page, server and launch flags so their +numbers are produced under the same conditions. + +```sh +pnpm perf webgpu:alloc # per-frame allocation, wall-clock or work-unit counters, one browser per cell +pnpm perf webgpu:timer # per-frame timer methodology: raw vs attributed queue + # latency vs hardware timestamps, plus serialized / + # canvas-clear / offscreen-clear control arms and the + # clock + scheduler controls behind the floor above +``` + +**Do not compare a WebGL2 frame-time column against a WebGPU one as if the two +came from the same instrument.** Both are hardware clocks, but they bracket +different things — WebGL2 the whole command stream, WebGPU the render passes — +and the gap is not small: the same `retained scrolling-world 100k` cell measures +0.397ms on WebGL2 and 0.065ms on WebGPU. `cpuMs*` is measured identically +everywhere and is the column to compare across backends and arms. + +The structural counters are the durable half of the report: exact, deterministic +and reproducible where a timing is not. A non-empty scene that reports zero draw +calls fails the cell rather than reporting the undercount. + +## Median vs p95 + +`median` is the amortised cost of a typical frame. `p95` is the frame the player +feels. + +Both are needed, because an optimisation that converts per-frame work into +_periodic_ work improves the median exactly as much as one that removed the work +— while the worst frame is unchanged. The report marks such a row `hitching` when +`cpuMsP95` is at least 4x `cpuMsMedian` **and** at least 8 ms. A `hitching` row +means the two columns answer different questions and the median must not be +quoted on its own. + +Percentiles use the nearest-rank method on the sorted samples +(`index = ceil(p / 100 * n) - 1`). The timed-frame count shrinks as node count +grows (see below), so a 100k+ cell has 30 samples and its p95 is the second-worst +of those 30 — a real worst-case indicator, but a coarse one. Read it next to +`timedFrames`, which every report row carries for exactly this reason. + +### Why `scrolling-world` needs p95 + +`scrolling-world` is the only archetype with content outside the view, and the +only one with a moving camera: its leaves are laid out over four times the +viewport's area, so roughly 25% are visible at any moment, and the camera travels +the world diagonal at a fixed speed, reflecting off the world edges on a path +that is a closed form in the frame index. + +That makes its cost distribution bimodal by construction. A large static world +can keep its renderable state persistent while the camera moves, so moving the +camera does not require rebuilding the visible scene from scratch — most frames +touch only what entered or left the view, and the frames that revalidate a larger +part of the world are periodic rather than per-frame. A median over that +distribution reports the cheap frames and hides the expensive ones entirely. + +So on `scrolling-world`: quote `cpuMsP95`, or both columns, never the median +alone. This is the archetype the `hitching` marker exists for. + +## Warmup and timed frames + +Both counts are derived from the cell's node count and recorded per row in the +report, so a median over 30 frames is never presented as equal in confidence to +one over 120. + +| node count | warmup frames | timed frames | +| --------------- | ------------: | -----------: | +| < 5 000 | 10 | 120 | +| 5 000 – 24 999 | 10 | 90 | +| 25 000 – 99 999 | 25 | 60 | +| >= 100 000 | 40 | 30 | + +Warmup settles shader compilation, texture upload and JIT; its frames are +discarded. It scales _up_ with node count precisely because the timed window +scales _down_ — a warmup shortfall would eat a far larger share of a 30-frame +window than of a 120-frame one. It is additionally capped at 10 s of wall clock, +so a pathological cell stops warming instead of grinding. + +Timed frames are driven from `requestAnimationFrame`, one measured frame per +callback. A cell aborts as `exceeded` when the trailing 3-frame median exceeds +200 ms (a sustained slowdown, not a single GC or scheduler spike), or when one +frame alone exceeds 2 000 ms. Outside the page, the driver abandons any cell that +returns no result within 60 s as `unavailable` and relaunches the browser — a +mid-frame GPU-driver stall cannot be interrupted from inside the page. + +## Browser and environment + +- **`--browser=chromium` (default) or `--browser=webkit`**, both headless via + Playwright. The choice belongs to the run, not to the harness: it is stamped + into every provenance block and forms part of the published profile's file + name, so a WebKit number can never be read as a Chromium one and the two never + pool into one profile. +- Chromium is launched on the `chromium` channel with + `--force-device-scale-factor=1` so `devicePixelRatio` is 1 and the canvas + backing size is deterministic. WebGPU adds `--enable-unsafe-webgpu`. +- WebKit is launched with **no arguments at all**: every flag here is a Chromium + one, and its stamp records an empty flag set rather than claiming a launch + that never happened. Nothing is lost by it - the harness page sizes its own + 1280x720 backing store and the Pixi arm pins `resolution: 1`, so no + measurement depends on a pinned device scale factor. +- **A backend the selected browser does not expose produces no number.** WebKit + reaches WebGPU on macOS alone; elsewhere `navigator.gpu` is undefined and every + WebGPU cell is emitted `unavailable` with that reason attached, exactly as a + refused software adapter is. +- `--profile` (the V8 CPU sampler) is Chromium-only, because the sampler is + driven over a CDP session. It refuses in any other browser rather than falling + back and attributing one engine's frame cost to another engine's name. +- No software rasterizer is ever forced. `--use-angle=swiftshader` and + `--enable-features=Vulkan` are deliberately absent; either would land the run + on SwiftShader and make every timing worthless. +- The page is served **cross-origin isolated** (COOP + COEP), which lifts the + browser's Spectre clamp on `performance.now()` from ~100 µs back to ~5 µs. + Without it the cheap cells quantise to the timer floor. +- Fixed 1280x720 canvas, a **fresh canvas per cell**, with the previous cell's GL + context force-lost so contexts cannot pile up past the browser's live-context + cap. +- **One browser session per arm**, holding all of that arm's cells, with each cell + fully initialising and tearing down its engine. +- Engine source is compiled with `__DEV__ = false` and served through the real + shader loader. A dev build carries per-frame diagnostics no shipped game runs, + and would be measured against competitors' production dist bundles. + +## Provenance stamped into every report + +`results.json` / `results.md` carry, per backend: + +- **GPU adapter identity.** On WebGL2 the `WEBGL_debug_renderer_info` renderer + string, read from the stage canvas's own context after the first measured cell + — not from a throwaway canvas, which can report a different adapter on a + multi-GPU machine. On WebGPU the adapter's vendor / architecture / device / + description. How much this identifies is the browser's choice: Chromium + unmasks the device model, while WebKit substitutes a constant vendor-level + string (`Apple GPU`) for it on every platform — including a Windows machine + with an NVIDIA card — so a WebKit profile is named after the CPU model + instead, which the physics domain records. +- **Browser and browser version**, as the browser reported it, plus the + operating system of the host that drove it. +- **`platformVersion`** — the operating system's major version and what + established it: `detected` where the host reports one (Windows, whose + `10.0.` release string names Windows 11 from build 22000 up), or + `declared` from `--platform`. It is separate from the `os` field because that + field carries the kernel release, which on macOS no longer tracks the product + version. +- **`prerelease`** — whether the platform is a pre-release build, and a `source` + saying what that rests on: `detected` from a browser version string naming a + non-shipping build, `declared` from `--platform=-beta` (needed for a + beta OS, which is not readable at runtime), or `assumed-stable`, which records + that nothing established it and is weaker than a stable platform. +- **`software`** — the honesty bit. A WebGL2 run on a software rasterizer marks + every timing column `UNTRUSTED` in the Markdown report; a software WebGPU + adapter is refused outright and its cells are emitted `unavailable`. +- Launch flags, headless flag, engine version, ISO timestamp. +- The WebGPU sprite-batch texture-slot tier (8 / 16 / 32) negotiated for the + adapter, so a slot-sensitive archetype's measured code path stays auditable + across machines. +- Each competitor library's exact version and the path it resolved from. + +The physics domain records the browser and browser version, the CPU model, +logical CPU count, OS and architecture of the host that drove it, and what the +measuring page's `performance.now()` could resolve. It does **not** record the +driver process's Node version: no step was taken there, so a field naming it +would describe nothing about where the numbers came from. + +### Physics timing resolution + +The fastest cells of the physics matrix step in single-digit microseconds, and +`performance.now()` is clamped as a Spectre mitigation. The harness page is +served cross-origin isolated (COOP/COEP), which lifts the clamp to 5 µs in +Chromium and 20 µs in WebKit — measured per run, not assumed, and stamped into +the provenance. + +A step that cannot clear that grid on its own is not timed on its own. Each cell +estimates its per-step cost from the last 60 warmup steps and then times steps in +batches large enough for one sample to span 20 clock ticks, dividing the sample +by its batch. The batch is recorded per row as `stepsPerSample`; `1` means every +step was timed individually and the row is byte-for-byte the old contract. The +**timed-step budget is unchanged** — the batch only decides how finely the fixed +window is sampled — and the batch stops growing once a cell would be left with +fewer than 12 samples, because a median and a p95 need a distribution behind +them. A cell that hits that cap before clearing the grid says so in its `note`, +naming the quantisation it still carries, rather than reporting a coarse number +that looks precise. + +**Gap worth knowing:** the rendering domain records the GPU, the browser and the +operating system, but not the rest of the host. CPU model, RAM and GPU driver +version are _not_ captured automatically — record them by hand alongside any run +you intend to quote. + +## Reproducing a single scenario + +Every selection flag accepts a comma-separated list, and `--nodes` _replaces_ the +archetype's own ladder rather than filtering it, so an off-ladder probe needs no +source edit: + +```sh +pnpm --filter @codexo/exojs-bench bench \ + --domain=rendering \ + --engine=exojs --config=current \ + --archetype=scrolling-world \ + --nodes=1000000 \ + --backend=webgl2,webgpu \ + --out=.workspace/output/my-run +``` + +On a POSIX shell no `--` separator is needed with `pnpm --filter …`; pnpm forwards +these straight to the script. Running the same script from inside +`packages/exojs-bench` (`pnpm bench -- --out=…`) works too, and `--out` is then +relative to the package directory either way. The root `pnpm bench` forwards here +as well; the engine's own `vitest bench` micro-benchmarks are `pnpm bench:micro`, +and there is no root `bench:compare`. + +> **PowerShell: quote every comma list.** PowerShell reads an unquoted `a,b` +> argument as an array literal and rejoins it with spaces on the way through +> pnpm's `.ps1` shim, so `--backend=webgl2,webgpu` arrives as +> `--backend=webgl2 webgpu`. Write `"--backend=webgl2,webgpu"` instead. A `--` +> separator does **not** help — the rewrite happens in the argument binder, +> before pnpm sees anything — and a POSIX shell is unaffected. The CLI refuses a +> selection value containing whitespace rather than acting on it, because +> `--nodes` is the one flag where the damage is silent: `1000 5000` survives +> `parseInt` as `1000`, and the run would publish one node count under the +> provenance of two. + +The run writes `results.json`, `results.csv` and `results.md` into `--out` +(default `.workspace/output/baseline/`, gitignored), plus a `checkpoint.jsonl` +appended per cell as it lands, so a crash never discards finished work. + +A `results.json` already in `--out` is merged into, not replaced: a cell the +run measured with status `ok` replaces the cell with the same spec, a cell it +did not run stays as it was, and an `exceeded` or `unavailable` cell never +overwrites a measurement (it is added only when the cell had no entry). The CSV +and Markdown are re-rendered from the merged set. That is what lets a matrix be +built up one archetype or one backend at a time - but it also means a kept cell +keeps its earlier provenance, and the table does not mark which cells a rerun +did not touch. Delete or move `results.json` before a run that must stand on +its own, and give pooled runs separate `--out` directories as before. The +checkpoint files are per run and are truncated at start. + +Other flags: + +- `--browser=chromium` (default) / `--browser=webkit` — the engine to measure in. + Not a subset marker: a run in either browser is a full measurement of that + browser, published under its own profile. +- `--platform=` / `--platform=-beta` — declare the operating + system's major version, and with the suffix that the build is a pre-release + one. Required on macOS and Linux, whose kernel version names no product + version; optional on Windows, which reports its own and refuses a declaration + contradicting it. A full run without it fails before measuring anything, a + narrowed run only warns. Pass it on **every** pooled run: both the version and + the beta marker are part of the profile's file name, and runs disagreeing on + either are refused rather than pooled. +- `--backend=webgl2` / `--backend=webgpu` — omit for both. +- `--engine`, `--config`, `--archetype`, `--nodes` — comma-separated selections. +- `--frames=N` — override every cell's timed-frame count for a fast spot check. + **Never for a quoted run:** it flattens the per-node-count budgets the + `timedFrames` column exists to make honest. +- `--profile` — run the selected cells under the V8 CPU sampler and print self + time by source file and by function instead of measuring wall clock. It answers + "which code made the frame expensive", never "how expensive is the frame", and + its output is deliberately printed rather than written into `results.*`. + +Any run that narrows the matrix prints `SUBSET RUN — not a reportable comparison`. +That is expected for a targeted question: it warns about scope, not correctness. +A subset run's numbers are valid for the cells in it and are not a matrix result. + +## How to read a result — and how not to + +- **A number belongs to a machine.** GPU, driver, Chromium build, thermal state + and engine commit all move it. Reproduce locally before comparing against + anything recorded here. +- **Compare within one invocation.** Cross-arm and cross-backend claims rest on + the arms having run back to back in the same process on the same machine. Two + invocations are two browser sessions, and not a comparison. +- **For a before/after of an engine change, measure a single cell per + invocation.** Within one invocation an arm's cells share a browser session, and + accumulated driver/adapter state has been observed to move — in one case invert + — the verdict of a multi-archetype run. It moves the number too, and in one + direction: on one commit and one machine, `lifecycle-churn` at 5 000 nodes + measured 3.84 ms alone, 4.26 ms with the other 21 archetypes in the session, + and 4.82 ms in the full published matrix. Same engine, same structural + counters, a 26% spread from session shape alone — which is why a narrowed + run's absolute number must never be read against a recorded profile, and why + a before/after pair has to be two runs of the SAME shape. +- **`nodeCount` is the world total, not the drawn count.** On `scrolling-world` + only about a quarter of it is on screen; the rest is the off-screen content + under study. +- **Rows at 100 000+ nodes are marked `beyond-frame-budget`.** They are stress + probes past any interactive budget, not target configurations. +- **Never turn a benchmark millisecond into an FPS claim.** `cpuMs*` covers the + render path only — no update, input, physics, asset work or GC headroom, and no + vsync. +- **Never quote a median from a `hitching` row on its own**, and never quote a + timing from an `exceeded` or `unavailable` cell: those statuses exist to record + that the cell produced no trustworthy distribution. +- **Structural counters travel better than timings.** `drawCalls` and + `bufferUploads` are deterministic; a claim meant to survive a hardware change + should be made about those. + +## Reference result: `scrolling-world` at 1M sprites + +A dated reference point, measured on **one** machine. It is not a hardware +minimum, not a maximum capability, and not an FPS guarantee — it is a +reproduction target: the same command on the same class of machine should land in +the same neighbourhood, and a large deviation is worth investigating. + +- Date: 2026-08-15 · ExoJS 0.15.2 +- GPU: NVIDIA GeForce RTX 5070 Ti · headless Chromium via Playwright +- Cell: `engine=exojs config=current archetype=scrolling-world nodes=1000000` — + 1 000 000 static sprites laid out over 4x the viewport's area with a moving + camera, 40 warmup frames, 30 timed frames + +| backend | CPU median | CPU p95 | frame median | frame p95 | draw calls / frame | +| ------- | ---------: | -------: | -----------: | --------: | -----------------: | +| WebGL2 | 0.402 ms | 10.08 ms | 0.330 ms | 1.17 ms | 1 | +| WebGPU | 0.380 ms | 10.45 ms | 2.98 ms | 4.66 ms | 1 | + +Reading notes, in the order they matter: + +- **CPU p95 is the headline, and it is a CPU number.** 10.08 ms is the + 95th-percentile time the engine spent in the render path on the CPU, i.e. the + second-worst of 30 timed frames. It is neither GPU frame time nor a whole game + frame. +- **The two frame-time columns come from different instruments** — a hardware + timer query on WebGL2, a queue-completion wall clock on WebGPU (see + [Metrics](#metrics)) — and are not comparable 1:1 across the two rows. +- **One draw call on both backends** is the structural fact behind the timings: + the visible world is drawn as a single batched submission even while the camera + moves. The table's per-frame `1` is derived. The report itself prints the + window's raw total in that column — `drawCalls = 30` over 30 timed frames — + with the note `structural counters did not divide evenly over 30 frame(s); raw +totals reported`, which trips on this cell because a sibling counter + (`bufferUploads`) has no whole-frame quotient. Divide the column by + `timedFrames` when a cell carries that note. +- **This is one workload.** `scrolling-world` is a deliberately extreme static + world. Nothing here generalises to a million _animated_ sprites, to other + archetypes, or to other hardware. + +For context on where those numbers came from: before the engine kept its +renderable state persistent across camera movement, the same cell measured a CPU +p95 of 203.14 ms on WebGL2 and 234.19 ms on WebGPU, and the WebGPU cell aborted at +the harness watchdog after 26 frames instead of completing its timed window. The +medians were already low then — only p95 showed the per-frame rebuild. That is the +concrete reason this archetype is quoted on p95. + +Numbers produced by the engine's internal Node-side CPU stubs — isolated +CPU-path measurements with no browser and no GPU, used while iterating on a change +— are a different measurement entirely. They are not comparable with the table +above and are never published as ExoJS performance figures. + +## The two regression gates + +They guard different defect classes and are deliberately unlike each other. + +**Structural** (`pnpm gate:bench:structural`) compares exact integer +draw/bind/upload counters against `baselines/structural.json`. The values are +decided CPU-side, so they do not drift and the baseline carries no tolerance +band: any deviation fails, as does a guarded cell that disappears or stops +measuring. It catches the fault that renders the identical picture and is merely +expensive - a batching collapse from 782 to 25 000 draw calls passes every +correctness test. + +It runs on the software rasterizer, which is what puts it in CI. That its +counters are genuinely backend-independent was measured, not assumed: every +archetype was run on a real GPU and on SwiftShader, and all three counters came +out byte-identical everywhere except `batch-breaking` (does not complete on a +software rasterizer) and `text-dynamic` (aborts there as too slow). Those two are +unguarded, named in the source with the reason; no counter needed a tolerance. +Re-record with `--update` in the same commit as an intended change. + +**Timing** (`pnpm --filter @codexo/exojs-bench gate:timing`) compares wall-clock +medians against `baselines/timing.json` and is a RELEASE PRECONDITION invoked by +hand on a machine you have confirmed idle. It is not a hook and not a CI job: +the matrix needs an idle machine, a push is by definition not one, and a gate +that goes falsely red gets bypassed. + +A cell fails when its median exceeds the baseline by more than 25 % **and** by at +least 0.5 ms. The absolute floor is not a softening - without it the gate is +unusable on sub-millisecond cells: two consecutive runs of the same code on the +same machine put 15 of 42 cells over 25 %, the smallest moving 0.060 ms to +0.095 ms. p95 is reported on every row and gates nothing, being the noisier +statistic. The committed baseline is stamped `confirmedIdle: false` and the gate +repeats that caveat on every run; re-record it with `--update --idle` on a quiet +machine before relying on it for a release. + +Correctness assertions stay in the test suite, which is fast, CI-resident and +needs no GPU. Nothing moved out of it: the gates add the other defect class - +faults that look correct and are merely expensive. + +## The published comparison + +`bench:compare` reads the `results.json` files of several runs and generates the +comparison document. It is generated, never hand-maintained, because a +hand-written comparison drifts from the harness and once it drifts the honesty +is gone without anyone noticing. + +### A reference measurement is three runs + +A published claim is a ratio between two arms, and one run does not support one: +the same code measured twice on this machine moved a physics cell's median by +2.5x with byte-identical contact counts behind both runs, which was enough to +reverse seven verdicts. That spread was observed while physics still ran in the +driver's Node process; it has not been re-derived since the matrix moved into the +browser, so read it as the reason for pooling rather than as this harness's +current noise figure. So `--rendering` and `--physics` are **repeatable, once +per run**, and the runs are pooled: + +```sh +pnpm --filter @codexo/exojs-bench bench --out run-1 +pnpm --filter @codexo/exojs-bench bench --out run-2 +pnpm --filter @codexo/exojs-bench bench --out run-3 +pnpm --filter @codexo/exojs-bench bench:compare \ + --rendering run-1/results.json --rendering run-2/results.json --rendering run-3/results.json +``` + +A single path still behaves exactly as it always did. The runs must come from +**separate invocations**, each with its own `--out` directory: repeating a matrix +inside one process shares JIT and heap state across the repetitions and measures +the same warm state several times, which is the effect the repetition exists to +expose. + +Per cell, the pooled comparison publishes the median of the per-run medians, the +median of the per-run p95s, the range those runs observed (printed in brackets +beside the value), the frame-budget mark where the pooled median is past 16.7 ms, +and a stability flag: the verdict is computed from each run separately, and the +cell is stable only when every run reached the same one. **An unstable cell +publishes no verdict** - it keeps its row, its value and its range, and states +what each run said instead. + +`bench:compare` refuses to pool runs that are not repetitions of one measurement: +a differing engine version, a differing set of arms or versions, a differing set +of measured cells, a row that landed on a different count in one run than in +another, or a differing machine. + +The machine check is what a second reference machine makes necessary — three +runs from two machines would otherwise pool into a median belonging to neither, +with a spread reporting the gap between two computers as the noise of one. A +rendering run's identity is the normalized GPU the adapter string names, the +operating system with its major version and pre-release bit, and the browser, +using the same normalizations the profile slug uses, so runs may pool exactly +when they would be written to one file. A physics run's identity is the CPU +model, the same platform identity, the architecture, and the browser. Tolerated within one +machine: timestamps, an adapter string's driver / device-id / shader-model tail, +the OS patch level, and the browser's patch version, which a checkout pins and +which every stamp records in full anyway. Assembling one profile from a +rendering measurement on one machine and a physics measurement on another is +refused for the same reason. + +One limit is inherent: where the browser reports a constant instead of the GPU, +a rendering run identifies its machine no more precisely than that constant +does, because the CPU model that names it in the file is recorded by the physics +domain. Two such machines on the same operating system and browser are +indistinguishable to the check. + +Rules the generator enforces rather than merely intends: + +- Rows are archetypes; categories are section headings. Nothing aggregates + across archetypes, because any mean over a category hides its worst cell. +- Verdicts are computed from the two medians. A ratio inside 0.8-1.2 is `level` + (the matrix's own noise band); outside it the faster arm `leads`, and at 5x or + more `leads clearly` - a gap too large for machine mood to explain. +- A row's count is chosen from the archetype ladders BEFORE any timing is read - + the largest rung at which every arm produced a valid cell - so it can never be + picked to suit an outcome. A rendering table goes further and uses one node + count for every row, because its archetypes share their ladders. +- Physics rows each state their OWN body count, because each physics archetype + has its own ladder and their intersection is empty. Rows are therefore not + comparable with one another; only the arms within a row are, which is what the + table is for. Two rows were always two different scenes — the shared count made + that look otherwise. +- Every value is a median AND the p95 of the same timed window. Verdicts are + computed from the medians alone; the p95 is the step or frame a player feels, + so a pair far apart hitches where the median reads as comfortable. There is no + p99: the largest cells time 120 steps, so a p99 there is the second-worst + sample rather than a percentile. +- A median past **16.7 ms** — a whole 60 fps frame — is marked. The line is the + entire frame deliberately: how much of a frame this work may take is the + reader's decision, but a step or frame costing more than the frame it has to + fit in is unplayable regardless of that decision. Nothing is derived from the + mark; a "bodies at N ms" capacity figure would interpolate between rungs rather + than report a measurement. +- Every row names the mechanism behind its difference, drawn from the structural + counters. A row whose mechanism cannot be evidenced is not published - it is + listed under Omissions with the reason, so a dropped row stays auditable. +- One column per competitor, no "best competitor" composite. Phaser occupies its + own WebGL1 block, CPU time only, explicitly carrying no mechanism. +- Cells where ExoJS loses are published exactly like the cells where it wins. + +### Machine profiles + +`bench:compare --profile` additionally writes the comparison as JSON into +`results/`, one file per machine, named +`--[-beta]-.json` after the provenance — the GPU or, +where the browser reports a constant instead of one, the CPU model; the +operating system with its major version and pre-release marker; and the browser. +The name is derived from the stamps, so +re-measuring a machine overwrites its file and a different machine can only +arrive as a new one. Each file carries every pooled run's provenance and a +signature over its own contents, and `verify:bench-results` (in the `lint` gate +group) rejects a file that pools fewer than three runs, or whose signature does +not recompute - which is what keeps a typed number and a one-run claim out. See +[`results/README.md`](./results/README.md). + +## Cross-library numbers + +The harness runs competitor arms, and no cross-library figure is published in the +ExoJS documentation. Publishing one requires all of: a frozen version of every +library involved, an identical scenario across arms, identical browser and +hardware conditions, the harness for each arm published alongside the numbers, the +caveats stated with the result, and a defined process for re-measuring when any of +those move. Until that exists, a cross-library number measured here is an +engineering signal for the maintainers, not a claim. + +## What runs in CI, and what does not + +Three different things live in this package, and CI treats them differently. + +**The harness's own tests** (`test/`) exercise the profile contract, the slug, +the signature, the run pooling and the archetype definitions. They need none +of the competitor libraries - the adapters that import those are loaded by +`import()` inside the benchmark page, never by a test - so they run in the +ordinary `pnpm test` project list on every push, like any other package. + +**The typecheck** does need the competitors: the adapters are typed against +their APIs, which is exactly what catches an upstream change on a version +bump. `pnpm typecheck:bench` installs them first. It runs in the path-gated +`bench` CI lane, alongside the structural gate, whenever a change touches this +package, the rendering source or the baselines. + +**Measurements** never run in CI. A shared runner is neither idle nor a known +machine, and a number it produced would carry provenance nobody can reproduce. +Reference profiles are measured by hand on an idle machine and committed as +signed files - see `results/README.md`. + +## The competitor install and the supply-chain gate + +`bench:setup` runs `pnpm install --dir competitors --frozen-lockfile`. The +`competitors/` directory is its own workspace root (it carries a +`pnpm-workspace.yaml`), which does two things: a plain root `pnpm install` +never resolves or downloads anything in it, so a contributor who never +benchmarks pays nothing for ~235MB of libraries whose only purpose is being +compared against; and the install applies the same `minimumReleaseAge` +quarantine the repository workspace enforces, so a version bump here is held +back exactly as long as any other dependency. The lockfile is frozen in both +CI and local use: a new version enters through a reviewed lockfile change, not +through an install. + +`pnpm doctor` reports whether the competitors are linked. diff --git a/packages/exojs-bench/results/README.md b/packages/exojs-bench/results/README.md index 94f6628cf..72ad46ca2 100644 --- a/packages/exojs-bench/results/README.md +++ b/packages/exojs-bench/results/README.md @@ -128,28 +128,29 @@ machine". ## Producing one ```sh -pnpm bootstrap # installs the competitor libraries too -cd packages/exojs-bench - -pnpm bench -- --out=run-1 -pnpm bench -- --out=run-2 -pnpm bench -- --out=run-3 - -pnpm bench:compare -- \ - --rendering=run-1/results.json \ - --rendering=run-2/results.json \ - --rendering=run-3/results.json \ - --profile +pnpm bootstrap:dev # installs and links the competitor libraries too + +pnpm --filter @codexo/exojs-bench bench:reference --out run-1 +pnpm --filter @codexo/exojs-bench bench:reference --out run-2 +pnpm --filter @codexo/exojs-bench bench:reference --out run-3 + +pnpm --filter @codexo/exojs-bench bench:compare --profile \ + --rendering run-1/rendering/results.json \ + --rendering run-2/rendering/results.json \ + --rendering run-3/rendering/results.json \ + --physics run-1/physics/results.json \ + --physics run-2/physics/results.json \ + --physics run-3/physics/results.json ``` -The harness scripts live in `packages/exojs-bench`; the root `pnpm bench` forwards -to the same harness, while `bench:compare` is run from the package. The `--` -separates the harness's flags from pnpm's own, and output directories are -relative to the package either way. +`bench:reference` measures both domains at each scenario's headline load and +writes `run-N/rendering/results.json` and `run-N/physics/results.json` under +`packages/exojs-bench/`. `--rendering` and `--physics` are repeatable, once per +run, in run order; both domains of a profile pool the same number of runs. -`--rendering` and `--physics` are repeatable, once per run, in run order. Repeat -the same three-run pattern for `--domain=physics` and pass both sets to one -`bench:compare`; both domains of a profile pool the same number of runs. +Pass no narrowing flag on a reference run - not `--capture`, `--frames`, +`--backend`, `--archetype`, `--nodes`, `--engine` or `--config`. Each marks the +run a subset, and a subset is not reportable. Measure on an otherwise idle machine, and run the repetitions back to back rather than days apart: these are wall-clock comparisons, and background load diff --git a/packages/exojs-bench/src/run.ts b/packages/exojs-bench/src/run.ts index 03ed52f39..48fb386a8 100644 --- a/packages/exojs-bench/src/run.ts +++ b/packages/exojs-bench/src/run.ts @@ -13,6 +13,7 @@ import { parseArgs } from './shared/args'; import { createCheckpointWriter } from './shared/checkpoint'; import type { PlatformDeclaration } from './shared/provenance'; import { parsePlatformDeclaration, PLATFORM_DECLARATION_SYNTAX, readPlatformVersion } from './shared/provenance'; +import { printTextTable } from './shared/table'; import { PHYSICS_LIBRARY_ARMS } from './shared/viteServer'; import type { RunPlan } from './suite/plan'; import { parseSuite, resolveSuitePlan } from './suite/plan'; @@ -443,14 +444,42 @@ const runRenderingDomain = async (args: Map, selector: DomainSel writeReport(data, outDir); - // Structural sanity summary: per-frame draw calls per arm/archetype/node count. - console.log('\n=== Per-frame draw calls (structural sanity) ==='); - - for (const result of data.results) { - console.log( - ` ${result.spec.engine.padEnd(6)} ${result.spec.config.padEnd(9)} ${result.spec.backend.padEnd(6)} ${result.spec.archetype.padEnd(15)} n=${String(result.spec.nodeCount).padStart(7)} drawCalls=${String(result.structural.drawCalls).padStart(8)} cpuMsMedian=${result.cpuMsMedian.toFixed(3)} cpuMsP95=${result.cpuMsP95.toFixed(3).padStart(8)} status=${result.status}${isHitching(result) ? ' HITCHING' : ''}`, - ); - } + // Per-cell summary. Grouped by scenario and load so the arms of one row sit + // under one another: the comparison a reader is after is between arms, and the + // matrix is emitted arm-major. + console.log('\n=== Results ==='); + + printTextTable( + [ + { header: 'scenario' }, + { header: 'load', align: 'right' }, + { header: 'backend' }, + { header: 'arm' }, + { header: 'cpu ms', align: 'right' }, + { header: 'cpu p95', align: 'right' }, + { header: 'draws', align: 'right' }, + { header: 'status' }, + ], + [...data.results] + .sort( + (left, right) => + left.spec.archetype.localeCompare(right.spec.archetype) || + left.spec.nodeCount - right.spec.nodeCount || + left.spec.backend.localeCompare(right.spec.backend) || + left.spec.engine.localeCompare(right.spec.engine) || + left.spec.config.localeCompare(right.spec.config), + ) + .map(result => [ + result.spec.archetype, + String(result.spec.nodeCount), + result.spec.backend, + `${result.spec.engine} ${result.spec.config}`, + result.cpuMsMedian.toFixed(3), + `${result.cpuMsP95.toFixed(3)}${isHitching(result) ? ' hitching' : ''}`, + String(result.structural.drawCalls), + result.status, + ]), + ); console.log(`\nReport written to ${outDir} (results.json, results.csv, results.md)`); }; @@ -576,12 +605,45 @@ const runPhysicsDomain = async (args: Map, selector: DomainSelec writePhysicsReport(data, outDir); - console.log('\n=== Per-step time (median) + structural ==='); + console.log('\n=== Results ==='); + + printTextTable( + [ + { header: 'scenario' }, + { header: 'bodies', align: 'right' }, + { header: 'arm' }, + { header: 'step ms', align: 'right' }, + { header: 'step p95', align: 'right' }, + { header: 'simulated', align: 'right' }, + { header: 'contacts', align: 'right' }, + { header: 'steps/sample', align: 'right' }, + { header: 'status' }, + ], + [...data.results] + .sort( + (left, right) => + left.spec.archetype.localeCompare(right.spec.archetype) || + left.spec.bodyCount - right.spec.bodyCount || + left.spec.engine.localeCompare(right.spec.engine) || + left.spec.config.localeCompare(right.spec.config), + ) + .map(result => [ + result.spec.archetype, + String(result.spec.bodyCount), + `${result.spec.engine} ${result.spec.config}`, + result.stepMsMedian.toFixed(4), + result.stepMsP95.toFixed(4), + String(result.structural.bodyCount), + String(result.structural.contactCount), + String(result.stepsPerSample), + result.status, + ]), + ); - for (const result of data.results) { - console.log( - ` ${result.spec.engine.padEnd(14)} ${result.spec.config.padEnd(7)} ${result.spec.archetype.padEnd(20)} n=${String(result.spec.bodyCount).padStart(6)} bodies=${String(result.structural.bodyCount).padStart(6)} contacts=${String(result.structural.contactCount).padStart(6)} stepMsMedian=${result.stepMsMedian.toFixed(4)} stepMsP95=${result.stepMsP95.toFixed(4)} steps/sample=${String(result.stepsPerSample).padStart(3)} status=${result.status}${result.note === undefined ? '' : ` (${result.note})`}`, - ); + // Notes are per cell and long enough that a column would set the table's width + // from the worst one, so they follow it instead. + for (const result of data.results.filter(entry => entry.note !== undefined)) { + console.log(` note ${result.spec.engine} ${result.spec.config} ${result.spec.archetype} n=${String(result.spec.bodyCount)}: ${result.note ?? ''}`); } console.log(`\nReport written to ${outDir} (results.json, results.csv, results.md)`); diff --git a/packages/exojs-bench/src/shared/table.ts b/packages/exojs-bench/src/shared/table.ts new file mode 100644 index 000000000..b536d6c63 --- /dev/null +++ b/packages/exojs-bench/src/shared/table.ts @@ -0,0 +1,51 @@ +/** + * Fixed-width table rendering for the run summaries the CLI prints. + * + * The summaries used to be `key=value` runs padded by hand, which meant every + * reader parsed a line before comparing two of them. A column that is a column + * is compared by eye, and the same rows land in the report artifacts anyway - so + * this exists to make the terminal the first place a result is readable, not the + * last. + */ + +/** Horizontal placement of a column's cells and of its header. */ +export type ColumnAlign = 'left' | 'right'; + +/** One column of a printed table. */ +export interface TableColumn { + /** Header text. Also the column's minimum width. */ + readonly header: string; + /** Cell placement; numbers read as a column only when they are right-aligned. Default `'left'`. */ + readonly align?: ColumnAlign; +} + +/** Two spaces between columns: enough to separate them, narrow enough to keep a wide matrix on one line. */ +const GUTTER = ' '; + +const pad = (text: string, width: number, align: ColumnAlign): string => (align === 'right' ? text.padStart(width) : text.padEnd(width)); + +/** + * Render `rows` under `columns` as aligned text, header and rule included. + * + * Rows shorter than the column list are padded with empty cells, so a caller may + * leave a trailing column off a row it has nothing to say in. Returns the lines + * rather than printing them, which is what lets a caller indent a table inside a + * larger block. + */ +export const formatTextTable = (columns: readonly TableColumn[], rows: ReadonlyArray): readonly string[] => { + const widths = columns.map((column, index) => rows.reduce((width, row) => Math.max(width, (row[index] ?? '').length), column.header.length)); + const line = (cells: readonly string[]): string => + columns + .map((column, index) => pad(cells[index] ?? '', widths[index]!, column.align ?? 'left')) + .join(GUTTER) + .trimEnd(); + + return [line(columns.map(column => column.header)), widths.map(width => '-'.repeat(width)).join(GUTTER), ...rows.map(row => line(row))]; +}; + +/** Print a table produced by {@link formatTextTable}, each line indented by `indent`. */ +export const printTextTable = (columns: readonly TableColumn[], rows: ReadonlyArray, indent = ' '): void => { + for (const text of formatTextTable(columns, rows)) { + console.log(`${indent}${text}`); + } +}; From 958a850bb2e0425f5e9c9f8fb2708ef4c710f826 Mon Sep 17 00:00:00 2001 From: Exoridus Date: Sat, 12 Sep 2026 03:57:09 +0200 Subject: [PATCH 09/28] bench: one definition of which archetypes an arm covers, and a bordered table `coversArchetype` existed twice - once in each adapter, once in the driver's capability list - and only the driver's copy decides which cells are built. A clause present in an adapter alone therefore changed nothing: the cell was created anyway and `buildScene` fell through to the ordinary sprite scene. Two rows were being published that way. The Excalibur arm implements neither hit testing nor a particle system, and was measured on both: `interaction-picking` and the particle scenarios were sprite counts under a name that promised something else. Its tilemap clause had drifted the same way, as had the culled Pixi arm's. The predicates move to `coverage.ts` and both sides import them. The summary table is bordered now, with a rule between scenarios so the arms of one row read as a group. --- packages/exojs-bench/README.md | 12 +-- .../src/rendering/adapters/excalibur.ts | 30 +------- .../src/rendering/adapters/phaser.ts | 21 +----- .../src/rendering/adapters/pixi.ts | 10 +-- .../exojs-bench/src/rendering/coverage.ts | 73 ++++++++++++++++++ packages/exojs-bench/src/rendering/driver.ts | 40 +++------- packages/exojs-bench/src/run.ts | 2 + packages/exojs-bench/src/shared/table.ts | 75 ++++++++++++++++--- 8 files changed, 166 insertions(+), 97 deletions(-) create mode 100644 packages/exojs-bench/src/rendering/coverage.ts diff --git a/packages/exojs-bench/README.md b/packages/exojs-bench/README.md index a1b7a0d3c..c1fe89d04 100644 --- a/packages/exojs-bench/README.md +++ b/packages/exojs-bench/README.md @@ -30,11 +30,13 @@ pnpm --filter @codexo/exojs-bench bench --archetype=fx-blur --backend=webgl2 ``` === Results === - scenario load backend arm cpu ms cpu p95 draws status - -------- ---- ------- -------------- ------ ------- ----- ------ - fx-blur 720 webgl2 exojs current 0.245 0.340 4 ok - fx-blur 720 webgl2 exojs retained 0.220 0.285 4 ok - fx-blur 720 webgl2 pixi default 0.120 0.140 3 ok + ┌──────────┬──────┬─────────┬────────────────┬────────┬─────────┬───────┬────────┐ + │ scenario │ load │ backend │ arm │ cpu ms │ cpu p95 │ draws │ status │ + ├──────────┼──────┼─────────┼────────────────┼────────┼─────────┼───────┼────────┤ + │ fx-blur │ 720 │ webgl2 │ exojs current │ 0.230 │ 0.310 │ 4 │ ok │ + │ fx-blur │ 720 │ webgl2 │ exojs retained │ 0.220 │ 0.255 │ 4 │ ok │ + │ fx-blur │ 720 │ webgl2 │ pixi default │ 0.120 │ 0.205 │ 3 │ ok │ + └──────────┴──────┴─────────┴────────────────┴────────┴─────────┴───────┴────────┘ ``` The same rows land in `results.json`, `results.csv` and `results.md` in the diff --git a/packages/exojs-bench/src/rendering/adapters/excalibur.ts b/packages/exojs-bench/src/rendering/adapters/excalibur.ts index ea0949c2c..93e831faf 100644 --- a/packages/exojs-bench/src/rendering/adapters/excalibur.ts +++ b/packages/exojs-bench/src/rendering/adapters/excalibur.ts @@ -1,12 +1,11 @@ import * as ex from 'excalibur'; import { mutationSignature, selectMutationIndices, wobbleOffsetAt } from '../../shared/mutation'; +import { excaliburCovers } from '../coverage'; import type { ArchetypeSpec, Backend, EngineAdapter } from '../EngineAdapter'; import { createDigitAtlasCanvas, createDistinctTextureCanvas, DIGIT_ALPHABET, DIGIT_CELL_HEIGHT, DIGIT_CELL_WIDTH, TEXT_FONT_SIZE } from '../sceneAssets'; -import { isTilemap } from '../tilemap'; -import { hasFullViewportLeaves, isChurning, isTextArchetype, isTextUpdating, leafAlpha, textForLeaf, usesRenderTargets } from '../traits'; -import { isUiLayoutScene } from '../uiLayout'; -import { GRID_MARGIN, gridLayout, gridPosition, isScrolling, SPRITE_SIZE, VIEWPORT_HEIGHT, VIEWPORT_WIDTH } from '../world'; +import { hasFullViewportLeaves, isChurning, isTextArchetype, isTextUpdating, leafAlpha, textForLeaf } from '../traits'; +import { GRID_MARGIN, gridLayout, gridPosition, SPRITE_SIZE, VIEWPORT_HEIGHT, VIEWPORT_WIDTH } from '../world'; /** * Excalibur 0.32 arm of the rendering benchmark. @@ -120,28 +119,7 @@ export const createExcaliburAdapter = (): EngineAdapter => { }, coversArchetype(spec: ArchetypeSpec): boolean { - // This arm builds a fixed, viewport-sized scene with a static camera. A - // scrolling archetype would silently render as an ordinary fully-visible - // one here, i.e. a row that looks comparable and is not - so the arm sits - // the archetype out instead. - // - // The render-target archetypes are sat out because Excalibur 0.32 has no - // equivalent API at all: its `PostProcessor` chain is a full-SCREEN pass - // rather than a filtered subtree, and it ships no mask source, so those - // cells could only be approximated - which the fairness rule forbids. - // - // The tilemap archetypes are sat out for a capability reason rather than a - // policy one. Excalibur 0.32's `TileMap` is a grid of `Tile`s each holding - // its own graphics list, drawn through the ordinary graphics path - there - // is no dedicated tile submission path of the kind the other three arms - // are being compared on, so its cell would answer a different question - // than the row it sat in. Building one here would mean writing the arm's - // missing feature rather than adapting to it. - // - // The UI-layout archetype is sat out for the same capability reason: - // Excalibur 0.32 has no layout container and no flexbox, so the box tree - // the archetype re-solves has no public equivalent to build it on. - return !isScrolling(spec) && !usesRenderTargets(spec) && !isTilemap(spec) && !isUiLayoutScene(spec); + return excaliburCovers(spec); }, async init(canvas: HTMLCanvasElement, target: Backend): Promise { diff --git a/packages/exojs-bench/src/rendering/adapters/phaser.ts b/packages/exojs-bench/src/rendering/adapters/phaser.ts index 35e9dd81c..d4dc45712 100644 --- a/packages/exojs-bench/src/rendering/adapters/phaser.ts +++ b/packages/exojs-bench/src/rendering/adapters/phaser.ts @@ -1,6 +1,7 @@ import * as Phaser from 'phaser'; import { mutationSignature, selectMutationIndices, wobbleOffsetAt } from '../../shared/mutation'; +import { phaserCovers } from '../coverage'; import type { ArchetypeSpec, Backend, EngineAdapter } from '../EngineAdapter'; import { isParticleLifecycle, isParticles, PARTICLE_ALPHA, PARTICLE_LIFETIME, PARTICLE_STEP, particleSeedAt } from '../particles'; import { isPickingScene, PICK_RECT_SIZE, pickPointAt, pickRectAt } from '../picking'; @@ -24,10 +25,8 @@ import { leafAlpha, pointerQueriesPerFrame, textForLeaf, - usesRenderTargets, } from '../traits'; -import { isUiLayoutScene } from '../uiLayout'; -import { GRID_MARGIN, gridLayout, gridPosition, isScrolling, VIEWPORT_HEIGHT, VIEWPORT_WIDTH } from '../world'; +import { GRID_MARGIN, gridLayout, gridPosition, VIEWPORT_HEIGHT, VIEWPORT_WIDTH } from '../world'; /** * Phaser 4.2 arm of the rendering benchmark. @@ -350,21 +349,7 @@ export const createPhaserAdapter = (): EngineAdapter => { }, coversArchetype(spec: ArchetypeSpec): boolean { - // This arm builds a fixed, viewport-sized scene with a static camera. A - // scrolling archetype would silently render as an ordinary fully-visible - // one here, i.e. a row that looks comparable and is not - so the arm sits - // the archetype out instead. - // - // Render-target archetypes remain out until their Phaser semantics are - // validated against the shared filter/mask contract. - // - // The UI-layout archetype is sat out for a capability reason rather than a - // policy one. Phaser 4 ships no layout engine: `Actions.GridAlign` places - // objects once on a fixed raster and `GameObjects.Grid` draws one, and - // neither re-solves a box when a child resizes. A cell here could only - // measure a flexbox implementation written inside the harness, which is - // the arm's missing feature rather than an adaptation to it. - return !isScrolling(spec) && !usesRenderTargets(spec) && !isUiLayoutScene(spec); + return phaserCovers(spec); }, async init(canvas: HTMLCanvasElement, target: Backend): Promise { diff --git a/packages/exojs-bench/src/rendering/adapters/pixi.ts b/packages/exojs-bench/src/rendering/adapters/pixi.ts index f5205ef13..5d7aa27c5 100644 --- a/packages/exojs-bench/src/rendering/adapters/pixi.ts +++ b/packages/exojs-bench/src/rendering/adapters/pixi.ts @@ -28,6 +28,7 @@ import { import { mutationSignature, selectMutationIndices, wobbleOffsetAt } from '../../shared/mutation'; import { BLUR_KERNEL_SIGMAS, BLUR_TAPS_PER_SIDE } from '../archetypes'; +import { pixiCulledCovers } from '../coverage'; import type { ArchetypeSpec, Backend, EngineAdapter, LayoutDigestReport } from '../EngineAdapter'; import { isParticleLifecycle, isParticles, PARTICLE_ALPHA, PARTICLE_LIFETIME, PARTICLE_PREROLL_STEPS, PARTICLE_STEP, particleSeedAt } from '../particles'; import { isPickingScene, PICK_RECT_SIZE, pickPointAt, pickRectAt } from '../picking'; @@ -825,14 +826,7 @@ export const createPixiAdapter = (config: PixiAdapterConfig = 'default'): Engine }, coversArchetype(spec: ArchetypeSpec): boolean { - // The stock arm runs everywhere; the culled variant only where culling can - // actually remove something. - // - // The tilemap scenes are the exception among the culling-enabled ones: the - // tile path decides chunk visibility itself, and `Culler.shared.cull` acts - // on `.cullable` scene nodes it never sees. The variant would therefore - // measure the stock arm a second time under another name. - return config === 'default' || (spec.cullingEnabled && !isTilemap(spec)); + return config === 'default' || pixiCulledCovers(spec); }, async init(canvas: HTMLCanvasElement, target: Backend): Promise { diff --git a/packages/exojs-bench/src/rendering/coverage.ts b/packages/exojs-bench/src/rendering/coverage.ts new file mode 100644 index 000000000..59043f421 --- /dev/null +++ b/packages/exojs-bench/src/rendering/coverage.ts @@ -0,0 +1,73 @@ +import type { ArchetypeSpec } from './EngineAdapter'; +import { isTilemap } from './tilemap'; +import { usesRenderTargets } from './traits'; +import { isUiLayoutScene } from './uiLayout'; +import { isScrolling } from './world'; + +/** + * Which archetypes each competitor arm covers. + * + * These predicates are consulted TWICE, from two processes: the driver builds + * the matrix from them in Node, and the adapter reports the same answer inside + * the harness page. Only the driver's copy decides which cells exist, so a + * divergence does not fail - it publishes a row. An arm that was gated out in + * its adapter alone still gets a cell, and its `buildScene` then falls through + * to the ordinary sprite scene: a row that looks like a hit-test or a tilemap + * comparison and is a sprite count under another name. + * + * That is the whole reason this module exists. One definition, imported by both. + * + * The general rule behind the individual clauses: an arm sits an archetype out + * when it has no public path that answers the archetype's question. Approximating + * one would mean writing the arm's missing feature and then publishing the + * harness's implementation under the library's name. + */ + +/** + * Phaser 4. + * + * - Scrolling: the adapter builds a fixed, viewport-sized scene with a static + * camera, so a scrolling archetype would render fully visible. + * - Render targets: no validated per-node equivalent for the shared filter and + * mask scenes, and `Filters.Blur` is an iterative step blur on a camera with + * no kernel to configure to the shared nine taps at sigma 2. + * - UI layout: Phaser 4 ships no layout engine. `Actions.GridAlign` places + * objects once on a fixed raster and `GameObjects.Grid` draws one; neither + * re-solves a box when a child resizes. + */ +export const phaserCovers = (spec: ArchetypeSpec): boolean => !isScrolling(spec) && !usesRenderTargets(spec) && !isUiLayoutScene(spec); + +/** + * Excalibur 0.32. + * + * Scrolling and render targets for the same reasons as Phaser - its + * `PostProcessor` chain is a full-SCREEN pass rather than a filtered subtree, + * and it ships no mask source. Beyond those it has no equivalent for three more + * archetypes, and the adapter implements none of them: + * + * - Tilemaps: its `TileMap` is a grid of `Tile`s each holding its own graphics + * list, drawn through the ordinary graphics path, so there is no dedicated + * tile submission path of the kind the other arms are compared on. + * - Particles: no emitter whose lifecycle matches the shared one. + * - UI layout: no layout container and no flexbox. + */ +export const excaliburCovers = (spec: ArchetypeSpec): boolean => + !isScrolling(spec) && + !usesRenderTargets(spec) && + !isTilemap(spec) && + !isUiLayoutScene(spec) && + spec.particles === undefined && + (spec.pointerQueriesPerFrame ?? 0) === 0; + +/** + * The second Pixi arm: stock Pixi plus the explicit per-frame + * `Culler.shared.cull` a Pixi application has to write itself. + * + * It runs only where culling can remove something, so the difference between the + * two Pixi arms is the measurement; on a fully visible archetype the cull call + * could only add cost over an identical visible set. The tilemap scenes are the + * exception among the culling-enabled ones: the tile path decides chunk + * visibility itself and `Culler.shared.cull` acts on `.cullable` scene nodes it + * never sees, so the variant would measure the stock arm a second time. + */ +export const pixiCulledCovers = (spec: ArchetypeSpec): boolean => spec.cullingEnabled && !isTilemap(spec); diff --git a/packages/exojs-bench/src/rendering/driver.ts b/packages/exojs-bench/src/rendering/driver.ts index 231a8fcae..f3128e822 100644 --- a/packages/exojs-bench/src/rendering/driver.ts +++ b/packages/exojs-bench/src/rendering/driver.ts @@ -18,11 +18,10 @@ import type { ViteDevServer } from '../shared/viteServer'; import { readEngineVersion, RENDERING_LIBRARY_ARMS, startViteServer as startPageServer } from '../shared/viteServer'; import type { RunPlan } from '../suite/plan'; import { buildMatrix } from './archetypes'; +import { excaliburCovers, phaserCovers, pixiCulledCovers } from './coverage'; import type { ArchetypeSpec, Backend, CellResult, CellSpec, EngineAdapter } from './EngineAdapter'; import type { MatrixSelection } from './selection'; import { applyPlan, applySelection } from './selection'; -import { usesRenderTargets } from './traits'; -import { isUiLayoutScene } from './uiLayout'; import { isScrolling, VIEWPORT_HEIGHT, VIEWPORT_WIDTH } from './world'; // Re-exported so the rendering barrel and the CLI keep importing the selection @@ -216,43 +215,28 @@ const ADAPTER_CAPABILITIES: readonly EngineAdapter[] = [ capabilityDescriptor('exojs', 'current', ['webgl2', 'webgpu']), capabilityDescriptor('exojs', 'retained', ['webgl2', 'webgpu']), // Pixi.js v8 is the direct renderer benchmark and the only other 2D library - // that ships WebGPU, so it runs on both backends. It is now a first-class, - // committed arm (pinned exact devDependency) rather than the old gitignored - // local-only reference; its version + provenance are stamped into the report - // header via `readLibraryProvenance`. + // that ships WebGPU, so it runs on both backends. It is a first-class, + // committed arm (pinned exact devDependency); its version + provenance are + // stamped into the report header via `readLibraryProvenance`. capabilityDescriptor('pixi', 'default', ['webgl2', 'webgpu']), - // Second Pixi arm: stock Pixi PLUS the explicit per-frame `Culler.shared.cull` - // a Pixi app that wants culling has to write itself. It runs only on - // archetypes with genuine off-screen content (`cullingEnabled`), where the - // difference between the two arms is the measurement; on a fully-visible - // archetype the cull call could only ever add cost over an identical visible - // set, so the variant would just duplicate `pixi default` across the matrix. - capabilityDescriptor('pixi', 'culled', ['webgl2', 'webgpu'], spec => spec.cullingEnabled), + capabilityDescriptor('pixi', 'culled', ['webgl2', 'webgpu'], pixiCulledCovers), // Phaser 4 and Excalibur are committed competitor arms (pinned exact // devDependencies). Both are WebGL2-only in this harness and never run WebGPU // (Phaser 4 ships no WebGPU renderer; Excalibur 0.32 has none). Phaser's // default renderer asks for WebGL1, so the adapter explicitly supplies and // verifies a WebGL2 context through Phaser's public context path; Excalibur // creates its own real WebGL2 context. + // // A missing (unlinked) competitor degrades gracefully: its per-cell dynamic // import fails in isolation (`runCellInPage` records that cell `unavailable` // and the run continues), and it is left out of Vite's pre-bundle set below. - // Both sit out the scrolling archetypes: neither arm implements a moving - // camera, so they would render a fixed, fully-visible scene under an id that - // promises off-screen content - a row that looks comparable and is not. - // - // Both also sit out the render-target archetypes (`filter-chain-*`, - // `mask-clip`): Phaser's adapter and Excalibur 0.32 have no validated - // per-node equivalent for the shared filter/mask scenes, so approximating - // the cell would violate the fairness rule. // - // Both sit out the UI-layout archetype as well, and that one is a capability - // finding rather than a policy: neither library ships a layout engine at all. - // Phaser 4's `Actions.GridAlign` places objects once on a fixed raster and - // `GameObjects.Grid` draws one; Excalibur 0.32 has no layout container. A cell - // on either arm could only measure a flexbox written inside the harness. - capabilityDescriptor('phaser', 'webgl2', ['webgl2'], spec => !isScrolling(spec) && !usesRenderTargets(spec) && !isUiLayoutScene(spec)), - capabilityDescriptor('excalibur', 'default', ['webgl2'], spec => !isScrolling(spec) && !usesRenderTargets(spec) && !isUiLayoutScene(spec)), + // Which archetypes each arm covers is `coverage.ts`, shared with the adapters + // rather than restated here - this list decides which cells EXIST, so a copy + // that drifted from an adapter's own answer would publish a row the adapter + // never implemented. + capabilityDescriptor('phaser', 'webgl2', ['webgl2'], phaserCovers), + capabilityDescriptor('excalibur', 'default', ['webgl2'], excaliburCovers), ]; /** diff --git a/packages/exojs-bench/src/run.ts b/packages/exojs-bench/src/run.ts index 48fb386a8..15589b9f1 100644 --- a/packages/exojs-bench/src/run.ts +++ b/packages/exojs-bench/src/run.ts @@ -479,6 +479,7 @@ const runRenderingDomain = async (args: Map, selector: DomainSel String(result.structural.drawCalls), result.status, ]), + { groupBy: 0 }, ); console.log(`\nReport written to ${outDir} (results.json, results.csv, results.md)`); @@ -638,6 +639,7 @@ const runPhysicsDomain = async (args: Map, selector: DomainSelec String(result.stepsPerSample), result.status, ]), + { groupBy: 0 }, ); // Notes are per cell and long enough that a column would set the table's width diff --git a/packages/exojs-bench/src/shared/table.ts b/packages/exojs-bench/src/shared/table.ts index b536d6c63..766622502 100644 --- a/packages/exojs-bench/src/shared/table.ts +++ b/packages/exojs-bench/src/shared/table.ts @@ -1,5 +1,5 @@ /** - * Fixed-width table rendering for the run summaries the CLI prints. + * Table rendering for the run summaries the CLI prints. * * The summaries used to be `key=value` runs padded by hand, which meant every * reader parsed a line before comparing two of them. A column that is a column @@ -19,33 +19,84 @@ export interface TableColumn { readonly align?: ColumnAlign; } -/** Two spaces between columns: enough to separate them, narrow enough to keep a wide matrix on one line. */ -const GUTTER = ' '; +/** Options for {@link formatTextTable}. */ +export interface TableOptions { + /** + * Column whose value starts a new group, drawing a rule wherever it changes. + * + * A matrix is many arms of a few scenarios, and the comparison a reader wants + * is inside one scenario. A rule between scenarios is what separates "these + * four numbers are comparable" from "these belong to another scene". Omit it + * and the body is one block. + */ + readonly groupBy?: number; +} + +/** + * Box-drawing characters, by the junctions each row of the frame needs. + * + * Non-ASCII on purpose, and the exception the source-hygiene rule makes for + * notation that IS the notation: these draw the table rather than decorate it. + */ +const BOX = { + topLeft: '┌', + topJoin: '┬', + topRight: '┐', + midLeft: '├', + midJoin: '┼', + midRight: '┤', + bottomLeft: '└', + bottomJoin: '┴', + bottomRight: '┘', + horizontal: '─', + vertical: '│', +} as const; + +/** One space of breathing room on each side of a cell's text. */ +const CELL_PADDING = ' '; const pad = (text: string, width: number, align: ColumnAlign): string => (align === 'right' ? text.padStart(width) : text.padEnd(width)); /** - * Render `rows` under `columns` as aligned text, header and rule included. + * Render `rows` under `columns` as a bordered table. * * Rows shorter than the column list are padded with empty cells, so a caller may * leave a trailing column off a row it has nothing to say in. Returns the lines * rather than printing them, which is what lets a caller indent a table inside a * larger block. */ -export const formatTextTable = (columns: readonly TableColumn[], rows: ReadonlyArray): readonly string[] => { +export const formatTextTable = (columns: readonly TableColumn[], rows: ReadonlyArray, options: TableOptions = {}): readonly string[] => { const widths = columns.map((column, index) => rows.reduce((width, row) => Math.max(width, (row[index] ?? '').length), column.header.length)); + const rule = (left: string, join: string, right: string): string => + left + widths.map(width => BOX.horizontal.repeat(width + CELL_PADDING.length * 2)).join(join) + right; const line = (cells: readonly string[]): string => - columns - .map((column, index) => pad(cells[index] ?? '', widths[index]!, column.align ?? 'left')) - .join(GUTTER) - .trimEnd(); + BOX.vertical + + columns.map((column, index) => CELL_PADDING + pad(cells[index] ?? '', widths[index]!, column.align ?? 'left') + CELL_PADDING).join(BOX.vertical) + + BOX.vertical; + + const body: string[] = []; + const { groupBy } = options; + + for (const [index, row] of rows.entries()) { + if (groupBy !== undefined && index > 0 && row[groupBy] !== rows[index - 1]?.[groupBy]) { + body.push(rule(BOX.midLeft, BOX.midJoin, BOX.midRight)); + } + + body.push(line(row)); + } - return [line(columns.map(column => column.header)), widths.map(width => '-'.repeat(width)).join(GUTTER), ...rows.map(row => line(row))]; + return [ + rule(BOX.topLeft, BOX.topJoin, BOX.topRight), + line(columns.map(column => column.header)), + rule(BOX.midLeft, BOX.midJoin, BOX.midRight), + ...body, + rule(BOX.bottomLeft, BOX.bottomJoin, BOX.bottomRight), + ]; }; /** Print a table produced by {@link formatTextTable}, each line indented by `indent`. */ -export const printTextTable = (columns: readonly TableColumn[], rows: ReadonlyArray, indent = ' '): void => { - for (const text of formatTextTable(columns, rows)) { +export const printTextTable = (columns: readonly TableColumn[], rows: ReadonlyArray, options: TableOptions = {}, indent = ' '): void => { + for (const text of formatTextTable(columns, rows, options)) { console.log(`${indent}${text}`); } }; From ef25056c9c69b5c28e7887bdb245602a88c8531d Mon Sep 17 00:00:00 2001 From: Exoridus Date: Sat, 12 Sep 2026 03:57:13 +0200 Subject: [PATCH 10/28] style(bench): format the phaser adapter --- packages/exojs-bench/src/rendering/adapters/phaser.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/exojs-bench/src/rendering/adapters/phaser.ts b/packages/exojs-bench/src/rendering/adapters/phaser.ts index d4dc45712..6760da972 100644 --- a/packages/exojs-bench/src/rendering/adapters/phaser.ts +++ b/packages/exojs-bench/src/rendering/adapters/phaser.ts @@ -17,15 +17,7 @@ import { } from '../sceneAssets'; import type { TilemapExtent } from '../tilemap'; import { isTilemap, isTilemapEditing, TILE_SIZE, tileIdAt, tilemapCameraAt, tilemapCameraFrameFor, tilemapEditsAt, tilemapExtent } from '../tilemap'; -import { - hasFullViewportLeaves, - isChurning, - isTextArchetype, - isTextUpdating, - leafAlpha, - pointerQueriesPerFrame, - textForLeaf, -} from '../traits'; +import { hasFullViewportLeaves, isChurning, isTextArchetype, isTextUpdating, leafAlpha, pointerQueriesPerFrame, textForLeaf } from '../traits'; import { GRID_MARGIN, gridLayout, gridPosition, VIEWPORT_HEIGHT, VIEWPORT_WIDTH } from '../world'; /** From d345efa9392633e2563106718301fd2aeb1a198c Mon Sep 17 00:00:00 2001 From: Exoridus Date: Sat, 12 Sep 2026 04:35:33 +0200 Subject: [PATCH 11/28] bench: measure the reference run on this machine, and let a drawless row publish Three pooled `bench:reference` runs on the RTX 5070 Ti, Chromium. The profile gains the seven rendering scenarios it never carried, plus `fx-blur` and `ui-layout-update`. `ui-layout-update` did not reach the table on the first attempt. The mechanism rule reads zero draw calls on both arms as a cell that failed to render its scene, which was true of every archetype until this one: it measures a box-tree solve and its leaves have no painted surface, so drawing nothing is the correct behaviour and the zeros are the evidence. A drawless archetype now keeps its counters and says so in its mechanism sentence. `fx-blur` publishes a loss: Pixi leads it by 1.3x on WebGPU and 1.4x on WebGL2. --- .../rtx-5070-ti-windows-11-chromium.json | 6588 +++++++++++++---- packages/exojs-bench/src/comparison/build.ts | 9 +- .../exojs-bench/src/comparison/mechanism.ts | 32 +- 3 files changed, 5298 insertions(+), 1331 deletions(-) diff --git a/packages/exojs-bench/results/rtx-5070-ti-windows-11-chromium.json b/packages/exojs-bench/results/rtx-5070-ti-windows-11-chromium.json index 9c8f25fd5..ba600eeb2 100644 --- a/packages/exojs-bench/results/rtx-5070-ti-windows-11-chromium.json +++ b/packages/exojs-bench/results/rtx-5070-ti-windows-11-chromium.json @@ -1,5 +1,5 @@ { - "schemaVersion": 6, + "schemaVersion": 7, "profile": { "slug": "rtx-5070-ti-windows-11-chromium", "gpu": "rtx-5070-ti", @@ -12,8 +12,8 @@ "prerelease": false }, "engineVersion": "0.17.0", - "measuredAt": "2026-09-09T15:39:44.282Z", - "runs": 4 + "measuredAt": "2026-09-12T02:29:18.798Z", + "runs": 3 }, "rendering": { "runs": [ @@ -35,59 +35,9 @@ "source": "assumed-stable", "evidence": "browser version '151.0.7922.34' carries no pre-release marker and none was declared; the operating system's own release status cannot be read at runtime" }, - "flags": [ - "--force-device-scale-factor=1" - ], - "headless": true, - "software": false, - "engineVersion": "0.17.0", - "timestamp": "2026-09-09T14:29:34.661Z" - }, - { - "backend": "webgpu", - "adapter": "nvidia blackwell", - "browser": "chromium", - "browserVersion": "151.0.7922.34", - "os": "win32 10.0.26200", - "platformVersion": { - "major": 11, - "source": "detected", - "evidence": "os.release() reported '10.0.26200'; build 26200 is Windows 11 (Windows 11 starts at build 22000)" - }, - "prerelease": { - "value": false, - "source": "assumed-stable", - "evidence": "browser version '151.0.7922.34' carries no pre-release marker and none was declared; the operating system's own release status cannot be read at runtime" - }, - "flags": [ - "--force-device-scale-factor=1", - "--enable-unsafe-webgpu" - ], - "headless": true, - "software": false, - "slotTier": 16, - "engineVersion": "0.17.0", - "timestamp": "2026-09-09T14:43:58.301Z" - } - ] - }, - { - "provenance": [ - { - "backend": "webgl2", - "adapter": "ANGLE (NVIDIA, NVIDIA GeForce RTX 5070 Ti (0x00002C05) Direct3D11 vs_5_0 ps_5_0, D3D11)", - "browser": "chromium", - "browserVersion": "151.0.7922.34", - "os": "win32 10.0.26200", - "platformVersion": { - "major": 11, - "source": "detected", - "evidence": "os.release() reported '10.0.26200'; build 26200 is Windows 11 (Windows 11 starts at build 22000)" - }, - "prerelease": { - "value": false, - "source": "assumed-stable", - "evidence": "browser version '151.0.7922.34' carries no pre-release marker and none was declared; the operating system's own release status cannot be read at runtime" + "clock": { + "resolutionMs": 0.004999995231628418, + "crossOriginIsolated": true }, "flags": [ "--force-device-scale-factor=1" @@ -95,7 +45,7 @@ "headless": true, "software": false, "engineVersion": "0.17.0", - "timestamp": "2026-09-09T14:48:09.791Z" + "timestamp": "2026-09-12T01:57:25.285Z" }, { "backend": "webgpu", @@ -113,6 +63,10 @@ "source": "assumed-stable", "evidence": "browser version '151.0.7922.34' carries no pre-release marker and none was declared; the operating system's own release status cannot be read at runtime" }, + "clock": { + "resolutionMs": 0.004999995231628418, + "crossOriginIsolated": true + }, "flags": [ "--force-device-scale-factor=1", "--enable-unsafe-webgpu" @@ -121,7 +75,7 @@ "software": false, "slotTier": 16, "engineVersion": "0.17.0", - "timestamp": "2026-09-09T15:02:35.495Z" + "timestamp": "2026-09-12T02:02:57.766Z" } ] }, @@ -143,13 +97,17 @@ "source": "assumed-stable", "evidence": "browser version '151.0.7922.34' carries no pre-release marker and none was declared; the operating system's own release status cannot be read at runtime" }, + "clock": { + "resolutionMs": 0.004999995231628418, + "crossOriginIsolated": true + }, "flags": [ "--force-device-scale-factor=1" ], "headless": true, "software": false, "engineVersion": "0.17.0", - "timestamp": "2026-09-09T15:06:47.979Z" + "timestamp": "2026-09-12T02:09:39.148Z" }, { "backend": "webgpu", @@ -167,6 +125,10 @@ "source": "assumed-stable", "evidence": "browser version '151.0.7922.34' carries no pre-release marker and none was declared; the operating system's own release status cannot be read at runtime" }, + "clock": { + "resolutionMs": 0.004999995231628418, + "crossOriginIsolated": true + }, "flags": [ "--force-device-scale-factor=1", "--enable-unsafe-webgpu" @@ -175,7 +137,7 @@ "software": false, "slotTier": 16, "engineVersion": "0.17.0", - "timestamp": "2026-09-09T15:21:11.773Z" + "timestamp": "2026-09-12T02:15:13.060Z" } ] }, @@ -197,13 +159,17 @@ "source": "assumed-stable", "evidence": "browser version '151.0.7922.34' carries no pre-release marker and none was declared; the operating system's own release status cannot be read at runtime" }, + "clock": { + "resolutionMs": 0.004999995231628418, + "crossOriginIsolated": true + }, "flags": [ "--force-device-scale-factor=1" ], "headless": true, "software": false, "engineVersion": "0.17.0", - "timestamp": "2026-09-09T15:25:21.159Z" + "timestamp": "2026-09-12T02:21:53.804Z" }, { "backend": "webgpu", @@ -221,6 +187,10 @@ "source": "assumed-stable", "evidence": "browser version '151.0.7922.34' carries no pre-release marker and none was declared; the operating system's own release status cannot be read at runtime" }, + "clock": { + "resolutionMs": 0.004999995231628418, + "crossOriginIsolated": true + }, "flags": [ "--force-device-scale-factor=1", "--enable-unsafe-webgpu" @@ -229,7 +199,7 @@ "software": false, "slotTier": 16, "engineVersion": "0.17.0", - "timestamp": "2026-09-09T15:39:44.282Z" + "timestamp": "2026-09-12T02:27:26.444Z" } ] } @@ -251,7 +221,7 @@ "backends": [ { "backend": "webgl2", - "headlineCount": 1000, + "headlineCount": null, "competitors": [ "excalibur", "phaser", @@ -265,38 +235,43 @@ "archetype": "static-heavy", "category": "Node scaling", "count": 1000, + "loadId": "1k", + "unit": "sprites", + "primary": false, "cells": [ { "competitor": "excalibur", - "referenceMs": 0.1549999974668026, - "referenceP95Ms": 0.19999999925494194, + "referenceMs": 0.06000000238418579, + "referenceP95Ms": 0.12000000476837158, + "referenceGpuMs": 0.006448000000000001, "referenceOverFrameBudget": false, - "competitorMs": 3.631249997764826, - "competitorP95Ms": 4.592499997466803, + "competitorMs": 2.5075000002980232, + "competitorP95Ms": 3.2999999970197678, + "competitorGpuMs": 0.015392, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.04268502514621991, - "factor": 23.42741972329745, - "label": "ExoJS leads clearly (23.43x)", + "ratio": 0.023928216301916102, + "factor": 41.79166501098216, + "label": "ExoJS leads clearly (41.79x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.14000000059604645, - "maxMs": 0.1550000011920929, - "ratio": 1.1071428609441738 + "minMs": 0.05500000715255737, + "maxMs": 0.07500000298023224, + "ratio": 1.3636362404862872 }, "competitor": { - "minMs": 3.5049999989569187, - "maxMs": 3.6950000002980232, - "ratio": 1.0542082742931949 + "minMs": 2.489999994635582, + "maxMs": 2.5549999997019768, + "ratio": 1.0261044197616185 }, "stable": true, "rungs": [ - "exojs-leads-clearly", "exojs-leads-clearly", "exojs-leads-clearly", "exojs-leads-clearly" @@ -305,48 +280,52 @@ }, { "competitor": "phaser", - "referenceMs": 0.1549999974668026, - "referenceP95Ms": 0.19999999925494194, + "referenceMs": 0.06000000238418579, + "referenceP95Ms": 0.12000000476837158, + "referenceGpuMs": 0.006448000000000001, "referenceOverFrameBudget": false, - "competitorMs": 0.5700000002980232, - "competitorP95Ms": 0.7650000005960464, + "competitorMs": 0.3799999952316284, + "competitorP95Ms": 0.5849999934434891, + "competitorGpuMs": 0.006304, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.27192981997501964, - "factor": 3.6774194168622745, - "label": "ExoJS leads (3.68x)", - "structural": false + "ratio": 0.15789474509759632, + "factor": 6.333333002196431, + "label": "ExoJS leads clearly (6.33x)", + "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.14000000059604645, - "maxMs": 0.1550000011920929, - "ratio": 1.1071428609441738 + "minMs": 0.05500000715255737, + "maxMs": 0.07500000298023224, + "ratio": 1.3636362404862872 }, "competitor": { - "minMs": 0.5699999965727329, - "maxMs": 0.5749999992549419, - "ratio": 1.0087719345829347 + "minMs": 0.3774999976158142, + "maxMs": 0.39000000059604645, + "ratio": 1.0331125908852419 }, "stable": true, "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" ] } }, { "competitor": "pixi", - "referenceMs": 0.1549999974668026, - "referenceP95Ms": 0.19999999925494194, + "referenceMs": 0.06000000238418579, + "referenceP95Ms": 0.12000000476837158, + "referenceGpuMs": 0.006448000000000001, "referenceOverFrameBudget": false, - "competitorMs": 0.13750000298023224, - "competitorP95Ms": 0.17500000074505806, + "competitorMs": 0.05250000208616257, + "competitorP95Ms": 0.10000000894069672, + "competitorGpuMs": 0.004224, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -356,23 +335,23 @@ "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.14000000059604645, - "maxMs": 0.1550000011920929, - "ratio": 1.1071428609441738 + "minMs": 0.05500000715255737, + "maxMs": 0.07500000298023224, + "ratio": 1.3636362404862872 }, "competitor": { - "minMs": 0.125, - "maxMs": 0.14500000327825546, - "ratio": 1.1600000262260437 + "minMs": 0.04999999701976776, + "maxMs": 0.05499999225139618, + "ratio": 1.0999999105930276 }, "stable": false, "rungs": [ - "level", - "level", "competitor-leads", + "level", "level" ] } @@ -380,41 +359,46 @@ ] }, { - "archetype": "dynamic-heavy", + "archetype": "static-heavy", "category": "Node scaling", - "count": 1000, + "count": 10000, + "loadId": "10k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "excalibur", - "referenceMs": 0.3374999985098839, - "referenceP95Ms": 0.4299999997019768, + "referenceMs": 0.0650000050663948, + "referenceP95Ms": 0.125, + "referenceGpuMs": 0.041776, "referenceOverFrameBudget": false, - "competitorMs": 3.7025000024586916, - "competitorP95Ms": 4.484999995678663, - "competitorOverFrameBudget": false, + "competitorMs": 31.47749999910593, + "competitorP95Ms": 32.61499999463558, + "competitorGpuMs": 0.105536, + "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.09115462479021277, - "factor": 10.97037042609131, - "label": "ExoJS leads clearly (10.97x)", + "ratio": 0.002064967200960719, + "factor": 484.26919300933855, + "label": "ExoJS leads clearly (484.27x)", "structural": true }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.3049999997019768, - "maxMs": 0.3449999988079071, - "ratio": 1.1311475381803782 + "minMs": 0.06499999761581421, + "maxMs": 0.07000000774860382, + "ratio": 1.0769232356336753 }, "competitor": { - "minMs": 3.6149999983608723, - "maxMs": 3.7274999991059303, - "ratio": 1.03112033217042 + "minMs": 31.375, + "maxMs": 31.622500002384186, + "ratio": 1.0078884462911295 }, "stable": true, "rungs": [ - "exojs-leads-clearly", "exojs-leads-clearly", "exojs-leads-clearly", "exojs-leads-clearly" @@ -423,72 +407,125 @@ }, { "competitor": "phaser", - "referenceMs": 0.3374999985098839, - "referenceP95Ms": 0.4299999997019768, + "referenceMs": 0.0650000050663948, + "referenceP95Ms": 0.125, + "referenceGpuMs": 0.041776, "referenceOverFrameBudget": false, - "competitorMs": 0.5675000008195639, - "competitorP95Ms": 0.8274999968707561, + "competitorMs": 4.6099999994039536, + "competitorP95Ms": 5.144999995827675, + "competitorGpuMs": 0.113472, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.594713652903045, - "factor": 1.681481491333827, - "label": "ExoJS leads (1.68x)", - "structural": false + "ratio": 0.014099784181084367, + "factor": 70.92307138584113, + "label": "ExoJS leads clearly (70.92x)", + "structural": true }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.3049999997019768, - "maxMs": 0.3449999988079071, - "ratio": 1.1311475381803782 + "minMs": 0.06499999761581421, + "maxMs": 0.07000000774860382, + "ratio": 1.0769232356336753 }, "competitor": { - "minMs": 0.5549999997019768, - "maxMs": 0.5874999985098839, - "ratio": 1.058558556442088 + "minMs": 4.185000002384186, + "maxMs": 4.705000005662441, + "ratio": 1.1242532862561558 }, "stable": true, "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" ] } }, { "competitor": "pixi", - "referenceMs": 0.3374999985098839, - "referenceP95Ms": 0.4299999997019768, + "referenceMs": 0.0650000050663948, + "referenceP95Ms": 0.125, + "referenceGpuMs": 0.041776, + "referenceOverFrameBudget": false, + "competitorMs": 0.05500000715255737, + "competitorP95Ms": 0.11499999463558197, + "competitorGpuMs": 0.012256, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.06499999761581421, + "maxMs": 0.07000000774860382, + "ratio": 1.0769232356336753 + }, + "competitor": { + "minMs": 0.050000011920928955, + "maxMs": 0.06000000238418579, + "ratio": 1.1999997615814777 + }, + "stable": false, + "rungs": [ + "competitor-leads", + "competitor-leads", + "level" + ] + } + } + ] + }, + { + "archetype": "static-heavy", + "category": "Node scaling", + "count": 100000, + "loadId": "100k", + "unit": "sprites", + "primary": false, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.10500000417232513, + "referenceP95Ms": 0.1550000011920929, + "referenceGpuMs": 0.419616, "referenceOverFrameBudget": false, - "competitorMs": 0.24499999731779099, - "competitorP95Ms": 0.33500000461935997, + "competitorMs": 0.0625, + "competitorP95Ms": 0.14499999582767487, + "competitorGpuMs": 0.074912, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.3775510294071986, - "factor": 1.3775510294071986, - "label": "competitor leads (1.38x)", + "ratio": 1.6800000667572021, + "factor": 1.6800000667572021, + "label": "competitor leads (1.68x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "competitor issues fewer draw calls (25 vs 1 per frame)", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.3049999997019768, - "maxMs": 0.3449999988079071, - "ratio": 1.1311475381803782 + "minMs": 0.10249999165534973, + "maxMs": 0.15000000596046448, + "ratio": 1.4634148114356027 }, "competitor": { - "minMs": 0.22999999672174454, - "maxMs": 0.25999999791383743, - "ratio": 1.1304347896508324 + "minMs": 0.054999999701976776, + "maxMs": 0.06499999761581421, + "ratio": 1.181818144873154 }, "stable": true, "rungs": [ - "competitor-leads", "competitor-leads", "competitor-leads", "competitor-leads" @@ -498,41 +535,46 @@ ] }, { - "archetype": "deep-hierarchy", + "archetype": "dynamic-heavy", "category": "Node scaling", - "count": 1000, + "count": 10000, + "loadId": "10k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "excalibur", - "referenceMs": 0.1837499998509884, - "referenceP95Ms": 0.23000000044703484, + "referenceMs": 1.0499999970197678, + "referenceP95Ms": 1.4100000113248825, + "referenceGpuMs": 0.066496, "referenceOverFrameBudget": false, - "competitorMs": 4.872500000521541, - "competitorP95Ms": 6.535000003874302, - "competitorOverFrameBudget": false, + "competitorMs": 32.9100000038743, + "competitorP95Ms": 35.07500000298023, + "competitorGpuMs": 0.11072, + "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.037711646963842015, - "factor": 26.51700682706331, - "label": "ExoJS leads clearly (26.52x)", + "ratio": 0.03190519589474802, + "factor": 31.3428572355079, + "label": "ExoJS leads clearly (31.34x)", "structural": true }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.16999999433755875, - "maxMs": 0.1900000013411045, - "ratio": 1.1176471039395033 + "minMs": 0.864999994635582, + "maxMs": 1.0974999964237213, + "ratio": 1.2687861309017578 }, "competitor": { - "minMs": 4.799999997019768, - "maxMs": 5.02250000089407, - "ratio": 1.0463541675025934 + "minMs": 32.67500000447035, + "maxMs": 33.99249999970198, + "ratio": 1.0403213464438066 }, "stable": true, "rungs": [ - "exojs-leads-clearly", "exojs-leads-clearly", "exojs-leads-clearly", "exojs-leads-clearly" @@ -541,35 +583,37 @@ }, { "competitor": "phaser", - "referenceMs": 0.1837499998509884, - "referenceP95Ms": 0.23000000044703484, + "referenceMs": 1.0499999970197678, + "referenceP95Ms": 1.4100000113248825, + "referenceGpuMs": 0.066496, "referenceOverFrameBudget": false, - "competitorMs": 0.5974999982863665, - "competitorP95Ms": 0.8500000014901161, + "competitorMs": 3.1174999997019768, + "competitorP95Ms": 3.3900000005960464, + "competitorGpuMs": 0.113568, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.3075313813857481, - "factor": 3.2517006735831706, - "label": "ExoJS leads (3.25x)", + "ratio": 0.33680833909226765, + "factor": 2.9690476271908843, + "label": "ExoJS leads (2.97x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.16999999433755875, - "maxMs": 0.1900000013411045, - "ratio": 1.1176471039395033 + "minMs": 0.864999994635582, + "maxMs": 1.0974999964237213, + "ratio": 1.2687861309017578 }, "competitor": { - "minMs": 0.5899999961256981, - "maxMs": 0.7874999977648258, - "ratio": 1.334745767688193 + "minMs": 3.1150000020861626, + "maxMs": 3.1724999994039536, + "ratio": 1.0184590681474421 }, "stable": true, "rungs": [ - "exojs-leads", "exojs-leads", "exojs-leads", "exojs-leads" @@ -578,35 +622,37 @@ }, { "competitor": "pixi", - "referenceMs": 0.1837499998509884, - "referenceP95Ms": 0.23000000044703484, + "referenceMs": 1.0499999970197678, + "referenceP95Ms": 1.4100000113248825, + "referenceGpuMs": 0.066496, "referenceOverFrameBudget": false, - "competitorMs": 0.14999999850988388, - "competitorP95Ms": 0.2150000035762787, + "competitorMs": 0.46000000834465027, + "competitorP95Ms": 0.6800000071525574, + "competitorGpuMs": 0.10848, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 2.2826086477656453, + "factor": 2.2826086477656453, + "label": "competitor leads (2.28x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.16999999433755875, - "maxMs": 0.1900000013411045, - "ratio": 1.1176471039395033 + "minMs": 0.864999994635582, + "maxMs": 1.0974999964237213, + "ratio": 1.2687861309017578 }, "competitor": { - "minMs": 0.13500000536441803, - "maxMs": 0.15000000223517418, - "ratio": 1.1111110835163693 + "minMs": 0.3500000089406967, + "maxMs": 0.4699999988079071, + "ratio": 1.3428571051480829 }, - "stable": false, + "stable": true, "rungs": [ - "level", "competitor-leads", "competitor-leads", "competitor-leads" @@ -616,115 +662,124 @@ ] }, { - "archetype": "lifecycle-churn", + "archetype": "dynamic-all", "category": "Node scaling", "count": 1000, + "loadId": "1k", + "unit": "sprites", + "primary": false, "cells": [ { "competitor": "excalibur", - "referenceMs": 1.1087500024586916, - "referenceP95Ms": 1.4750000014901161, + "referenceMs": 0.7225000038743019, + "referenceP95Ms": 0.9849999994039536, + "referenceGpuMs": 0.01408, "referenceOverFrameBudget": false, - "competitorMs": 8.625, - "competitorP95Ms": 14.514999996870756, + "competitorMs": 3.1400000005960464, + "competitorP95Ms": 3.6649999916553497, + "competitorGpuMs": 0.016064, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.12855072492274686, - "factor": 7.779030422434059, - "label": "ExoJS leads clearly (7.78x)", - "structural": true + "ratio": 0.2300955425914504, + "factor": 4.346020738765744, + "label": "ExoJS leads (4.35x)", + "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 1.012499999254942, - "maxMs": 1.1549999974668026, - "ratio": 1.1407407390782427 + "minMs": 0.7199999988079071, + "maxMs": 0.7850000038743019, + "ratio": 1.0902777849639087 }, "competitor": { - "minMs": 8.337500002235174, - "maxMs": 9.379999998956919, - "ratio": 1.1250374808326553 + "minMs": 2.8249999955296516, + "maxMs": 3.217500001192093, + "ratio": 1.138938055321608 }, "stable": true, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } }, { "competitor": "phaser", - "referenceMs": 1.1087500024586916, - "referenceP95Ms": 1.4750000014901161, + "referenceMs": 0.7225000038743019, + "referenceP95Ms": 0.9849999994039536, + "referenceGpuMs": 0.01408, "referenceOverFrameBudget": false, - "competitorMs": 1.4262499958276749, - "competitorP95Ms": 1.9100000001490116, + "competitorMs": 0.4675000011920929, + "competitorP95Ms": 0.7400000095367432, + "competitorGpuMs": 0.006432, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 1.5454545498010193, + "factor": 1.5454545498010193, + "label": "competitor leads (1.55x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 1.012499999254942, - "maxMs": 1.1549999974668026, - "ratio": 1.1407407390782427 + "minMs": 0.7199999988079071, + "maxMs": 0.7850000038743019, + "ratio": 1.0902777849639087 }, "competitor": { - "minMs": 1.385000005364418, - "maxMs": 1.4499999955296516, - "ratio": 1.046931400659548 + "minMs": 0.45000000298023224, + "maxMs": 0.4775000065565109, + "ratio": 1.0611111186536741 }, - "stable": false, + "stable": true, "rungs": [ - "level", - "exojs-leads", - "exojs-leads", - "exojs-leads" + "competitor-leads", + "competitor-leads", + "competitor-leads" ] } }, { "competitor": "pixi", - "referenceMs": 1.1087500024586916, - "referenceP95Ms": 1.4750000014901161, + "referenceMs": 0.7225000038743019, + "referenceP95Ms": 0.9849999994039536, + "referenceGpuMs": 0.01408, "referenceOverFrameBudget": false, - "competitorMs": 0.7225000001490116, - "competitorP95Ms": 0.864999994635582, + "competitorMs": 0.23000000417232513, + "competitorP95Ms": 0.39000000059604645, + "competitorGpuMs": 0.005056, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.5346020792110977, - "factor": 1.5346020792110977, - "label": "competitor leads (1.53x)", + "ratio": 3.1413043076859086, + "factor": 3.1413043076859086, + "label": "competitor leads (3.14x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 1.012499999254942, - "maxMs": 1.1549999974668026, - "ratio": 1.1407407390782427 + "minMs": 0.7199999988079071, + "maxMs": 0.7850000038743019, + "ratio": 1.0902777849639087 }, "competitor": { - "minMs": 0.6675000041723251, - "maxMs": 0.794999998062849, - "ratio": 1.191011225608334 + "minMs": 0.22500000894069672, + "maxMs": 0.23499999940395355, + "ratio": 1.0444444002928575 }, "stable": true, "rungs": [ - "competitor-leads", "competitor-leads", "competitor-leads", "competitor-leads" @@ -732,240 +787,213 @@ } } ] - } - ] - }, - { - "title": "Fill and GPU state", - "rows": [ + }, { - "archetype": "overdraw", - "category": "Fill and GPU state", - "count": 1000, + "archetype": "dynamic-all", + "category": "Node scaling", + "count": 10000, + "loadId": "10k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "excalibur", - "referenceMs": 0.0925000049173832, - "referenceP95Ms": 0.12999999895691872, + "referenceMs": 9.92000000178814, + "referenceP95Ms": 10.765000000596046, + "referenceGpuMs": 0.06816, "referenceOverFrameBudget": false, - "competitorMs": 4.763749999925494, - "competitorP95Ms": 56.24249999970198, - "competitorOverFrameBudget": false, + "competitorMs": 38.935000002384186, + "competitorP95Ms": 40.12000000476837, + "competitorGpuMs": 0.10528, + "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.019417476760709505, - "factor": 51.49999726140836, - "label": "ExoJS leads clearly (51.50x)", - "structural": true + "ratio": 0.2547836137454909, + "factor": 3.9248991930812416, + "label": "ExoJS leads (3.92x)", + "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.07750000059604645, - "maxMs": 0.09749999642372131, - "ratio": 1.2580644603078253 + "minMs": 9.782499998807907, + "maxMs": 10.337499998509884, + "ratio": 1.0567339636871564 }, "competitor": { - "minMs": 3.339999996125698, - "maxMs": 5.114999998360872, - "ratio": 1.5314371270341682 + "minMs": 38.09000000357628, + "maxMs": 41.54500000178814, + "ratio": 1.090706222050078 }, "stable": true, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } }, { "competitor": "phaser", - "referenceMs": 0.0925000049173832, - "referenceP95Ms": 0.12999999895691872, + "referenceMs": 9.92000000178814, + "referenceP95Ms": 10.765000000596046, + "referenceGpuMs": 0.06816, "referenceOverFrameBudget": false, - "competitorMs": 0.5400000009685755, - "competitorP95Ms": 0.7450000047683716, + "competitorMs": 3.5474999994039536, + "competitorP95Ms": 4.200000002980232, + "competitorGpuMs": 0.113568, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.17129630509531443, - "factor": 5.83783753796423, - "label": "ExoJS leads clearly (5.84x)", - "structural": true + "side": "competitor", + "ratio": 2.796335448472131, + "factor": 2.796335448472131, + "label": "competitor leads (2.80x)", + "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.07750000059604645, - "maxMs": 0.09749999642372131, - "ratio": 1.2580644603078253 + "minMs": 9.782499998807907, + "maxMs": 10.337499998509884, + "ratio": 1.0567339636871564 }, "competitor": { - "minMs": 0.5325000025331974, - "maxMs": 0.5500000007450581, - "ratio": 1.032863846250911 + "minMs": 3.377499997615814, + "maxMs": 3.8049999997019768, + "ratio": 1.1265729096633414 }, "stable": true, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" + "competitor-leads", + "competitor-leads", + "competitor-leads" ] } }, { "competitor": "pixi", - "referenceMs": 0.0925000049173832, - "referenceP95Ms": 0.12999999895691872, + "referenceMs": 9.92000000178814, + "referenceP95Ms": 10.765000000596046, + "referenceGpuMs": 0.06816, "referenceOverFrameBudget": false, - "competitorMs": 0.10625000298023224, - "competitorP95Ms": 0.14000000059604645, + "competitorMs": 2.365000009536743, + "competitorP95Ms": 2.895000010728836, + "competitorGpuMs": 0.108544, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 4.194503155089319, + "factor": 4.194503155089319, + "label": "competitor leads (4.19x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.07750000059604645, - "maxMs": 0.09749999642372131, - "ratio": 1.2580644603078253 + "minMs": 9.782499998807907, + "maxMs": 10.337499998509884, + "ratio": 1.0567339636871564 }, "competitor": { - "minMs": 0.08999999612569809, - "maxMs": 0.10999999940395355, - "ratio": 1.2222222682134625 + "minMs": 2.3299999982118607, + "maxMs": 2.4650000035762787, + "ratio": 1.057939916509881 }, - "stable": false, + "stable": true, "rungs": [ - "exojs-leads", - "level", - "level", - "level" + "competitor-leads", + "competitor-leads", + "competitor-leads" ] } } ] }, { - "archetype": "batch-breaking", - "category": "Fill and GPU state", - "count": 1000, + "archetype": "dynamic-all", + "category": "Node scaling", + "count": 100000, + "loadId": "100k", + "unit": "sprites", + "primary": false, "cells": [ { - "competitor": "excalibur", - "referenceMs": 0.4100000001490116, - "referenceP95Ms": 0.5099999979138374, - "referenceOverFrameBudget": false, - "competitorMs": 4.668749997392297, - "competitorP95Ms": 6.33500000089407, - "competitorOverFrameBudget": false, + "competitor": "phaser", + "referenceMs": 116.24624999985099, + "referenceP95Ms": 118.21999999135733, + "referenceGpuMs": 0.544832, + "referenceOverFrameBudget": true, + "competitorMs": 36.725000001490116, + "competitorP95Ms": 39.14249999821186, + "competitorGpuMs": 1.285488, + "competitorOverFrameBudget": true, "verdict": { - "side": "exojs", - "ratio": 0.08781793850131507, - "factor": 11.387195111452371, - "label": "ExoJS leads clearly (11.39x)", - "structural": true + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false }, - "mechanism": "ExoJS issues fewer buffer uploads (0 vs 63 per frame)", + "mechanism": "competitor issues fewer draw calls (25 vs 7 per frame)", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 2, "reference": { - "minMs": 0.3825000002980232, - "maxMs": 0.42499999701976776, - "ratio": 1.1111111024539368 + "minMs": 115.1224999949336, + "maxMs": 117.37000000476837, + "ratio": 1.0195226824463826 }, "competitor": { - "minMs": 4.357500001788139, - "maxMs": 4.887500002980232, - "ratio": 1.1216293748650839 + "minMs": 35.935000002384186, + "maxMs": 37.51500000059605, + "ratio": 1.0439682760013087 }, - "stable": true, + "stable": false, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" - ] - } - }, - { - "competitor": "phaser", - "referenceMs": 0.4100000001490116, - "referenceP95Ms": 0.5099999979138374, - "referenceOverFrameBudget": false, - "competitorMs": 0.9825000017881393, - "competitorP95Ms": 1.3224999979138374, - "competitorOverFrameBudget": false, - "verdict": { - "side": "exojs", - "ratio": 0.4173027983743675, - "factor": 2.396341466905016, - "label": "ExoJS leads (2.40x)", - "structural": false - }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 63/1000/0 vs 63/1000/1, none of them large enough to account for the gap); the difference is CPU-side", - "aggregate": { - "runs": 4, - "reference": { - "minMs": 0.3825000002980232, - "maxMs": 0.42499999701976776, - "ratio": 1.1111111024539368 - }, - "competitor": { - "minMs": 0.9424999989569187, - "maxMs": 1.0249999947845936, - "ratio": 1.087533152168678 - }, - "stable": true, - "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" + "competitor-leads", + "competitor-leads" ] } }, { "competitor": "pixi", - "referenceMs": 0.4100000001490116, - "referenceP95Ms": 0.5099999979138374, - "referenceOverFrameBudget": false, - "competitorMs": 0.31250000186264515, - "competitorP95Ms": 0.38500000536441803, - "competitorOverFrameBudget": false, + "referenceMs": 115.80499999970198, + "referenceP95Ms": 118.45499999821186, + "referenceGpuMs": 0.545504, + "referenceOverFrameBudget": true, + "competitorMs": 34.57750000059605, + "competitorP95Ms": 36.394999995827675, + "competitorGpuMs": 1.11088, + "competitorOverFrameBudget": true, "verdict": { "side": "competitor", - "ratio": 1.3119999926567079, - "factor": 1.3119999926567079, - "label": "competitor leads (1.31x)", + "ratio": 3.3491432289120304, + "factor": 3.3491432289120304, + "label": "competitor leads (3.35x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 63/1000/0 vs 63/1000/0, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "competitor issues fewer draw calls (25 vs 1 per frame)", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.3825000002980232, - "maxMs": 0.42499999701976776, - "ratio": 1.1111111024539368 + "minMs": 115.1224999949336, + "maxMs": 117.37000000476837, + "ratio": 1.0195226824463826 }, "competitor": { - "minMs": 0.3049999997019768, - "maxMs": 0.3200000002980232, - "ratio": 1.0491803298711584 + "minMs": 32.73749999701977, + "maxMs": 38.65999999642372, + "ratio": 1.1809087437936037 }, "stable": true, "rungs": [ - "competitor-leads", "competitor-leads", "competitor-leads", "competitor-leads" @@ -975,41 +1003,46 @@ ] }, { - "archetype": "mixed-blend", - "category": "Fill and GPU state", - "count": 1000, + "archetype": "deep-hierarchy", + "category": "Node scaling", + "count": 10000, + "loadId": "10k", + "unit": "nodes", + "primary": true, "cells": [ { "competitor": "excalibur", - "referenceMs": 0.16500000096857548, - "referenceP95Ms": 0.19999999925494194, + "referenceMs": 0.20999999344348907, + "referenceP95Ms": 0.3200000077486038, + "referenceGpuMs": 0.068736, "referenceOverFrameBudget": false, - "competitorMs": 3.4412500001490116, - "competitorP95Ms": 4.595000002533197, - "competitorOverFrameBudget": false, + "competitorMs": 45.23749999701977, + "competitorP95Ms": 46.82500000298023, + "competitorGpuMs": 0.11072, + "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.04794769370473831, - "factor": 20.856060484535412, - "label": "ExoJS leads clearly (20.86x)", + "ratio": 0.0046421661996645215, + "factor": 215.4166733781026, + "label": "ExoJS leads clearly (215.42x)", "structural": true }, - "mechanism": "competitor issues fewer draw calls (26 vs 1 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.14750000089406967, - "maxMs": 0.17499999701976776, - "ratio": 1.18644065056954 + "minMs": 0.20999999344348907, + "maxMs": 0.23499999940395355, + "ratio": 1.119047651147627 }, "competitor": { - "minMs": 3.369999997317791, - "maxMs": 3.5075000002980232, - "ratio": 1.040801187860436 + "minMs": 43.89750000089407, + "maxMs": 45.394999995827675, + "ratio": 1.034113559881613 }, "stable": true, "rungs": [ - "exojs-leads-clearly", "exojs-leads-clearly", "exojs-leads-clearly", "exojs-leads-clearly" @@ -1018,122 +1051,125 @@ }, { "competitor": "phaser", - "referenceMs": 0.16500000096857548, - "referenceP95Ms": 0.19999999925494194, + "referenceMs": 0.20999999344348907, + "referenceP95Ms": 0.3200000077486038, + "referenceGpuMs": 0.068736, "referenceOverFrameBudget": false, - "competitorMs": 0.5525000002235174, - "competitorP95Ms": 0.767500001937151, + "competitorMs": 3.907499998807907, + "competitorP95Ms": 4.204999998211861, + "competitorGpuMs": 0.121696, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.2986425355689116, - "factor": 3.3484848301833763, - "label": "ExoJS leads (3.35x)", - "structural": false + "ratio": 0.05374280064172885, + "factor": 18.60714343240879, + "label": "ExoJS leads clearly (18.61x)", + "structural": true }, - "mechanism": "competitor issues fewer draw calls (26 vs 1 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.14750000089406967, - "maxMs": 0.17499999701976776, - "ratio": 1.18644065056954 + "minMs": 0.20999999344348907, + "maxMs": 0.23499999940395355, + "ratio": 1.119047651147627 }, "competitor": { - "minMs": 0.494999997317791, - "maxMs": 0.6124999970197678, - "ratio": 1.2373737380579046 + "minMs": 3.457499995827675, + "maxMs": 4.417499989271164, + "ratio": 1.2776572652500262 }, "stable": true, "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" ] } }, { "competitor": "pixi", - "referenceMs": 0.16500000096857548, - "referenceP95Ms": 0.19999999925494194, + "referenceMs": 0.20999999344348907, + "referenceP95Ms": 0.3200000077486038, + "referenceGpuMs": 0.068736, "referenceOverFrameBudget": false, - "competitorMs": 0.11750000156462193, - "competitorP95Ms": 0.1550000011920929, + "competitorMs": 0.1850000023841858, + "competitorP95Ms": 0.38500000536441803, + "competitorGpuMs": 0.109216, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 1.1351350850654927, + "factor": 1, + "label": "level", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 26/0/0 vs 32/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.14750000089406967, - "maxMs": 0.17499999701976776, - "ratio": 1.18644065056954 + "minMs": 0.20999999344348907, + "maxMs": 0.23499999940395355, + "ratio": 1.119047651147627 }, "competitor": { - "minMs": 0.11500000208616257, - "maxMs": 0.125, - "ratio": 1.086956502021148 + "minMs": 0.18249999731779099, + "maxMs": 0.20000000298023224, + "ratio": 1.0958904433952847 }, - "stable": false, + "stable": true, "rungs": [ - "competitor-leads", "level", - "competitor-leads", - "competitor-leads" + "level", + "level" ] } } ] - } - ] - }, - { - "title": "Material variety", - "rows": [ + }, { - "archetype": "batch-breaking-atlased", - "category": "Material variety", - "count": 1000, + "archetype": "lifecycle-churn", + "category": "Node scaling", + "count": 5000, + "loadId": "5k", + "unit": "nodes", + "primary": true, "cells": [ { "competitor": "excalibur", - "referenceMs": 0.07249999791383743, - "referenceP95Ms": 0.0950000062584877, + "referenceMs": 2.882499996572733, + "referenceP95Ms": 3.5850000008940697, + "referenceGpuMs": 0.030080000000000003, "referenceOverFrameBudget": false, - "competitorMs": 3.3762500006705523, - "competitorP95Ms": 4.394999999552965, - "competitorOverFrameBudget": false, + "competitorMs": 53.23125000298023, + "competitorP95Ms": 60.61999999731779, + "competitorGpuMs": 0.055008, + "competitorOverFrameBudget": true, "verdict": { - "side": "exojs", - "ratio": 0.021473527700685172, - "factor": 46.56896686649638, - "label": "ExoJS leads clearly (46.57x)", - "structural": true + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 2, "reference": { - "minMs": 0.07000000029802322, - "maxMs": 0.07499999552965164, - "ratio": 1.0714285030048725 + "minMs": 2.7124999910593033, + "maxMs": 3.0525000020861626, + "ratio": 1.12534562659817 }, "competitor": { - "minMs": 3.342500001192093, - "maxMs": 3.4049999974668026, - "ratio": 1.018698577786812 + "minMs": 52.85750000178814, + "maxMs": 53.605000004172325, + "ratio": 1.0141417963838415 }, - "stable": true, + "stable": false, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", "exojs-leads-clearly", "exojs-leads-clearly" ] @@ -1141,35 +1177,37 @@ }, { "competitor": "phaser", - "referenceMs": 0.07249999791383743, - "referenceP95Ms": 0.0950000062584877, + "referenceMs": 2.75, + "referenceP95Ms": 3.5499999970197678, + "referenceGpuMs": 0.0304, "referenceOverFrameBudget": false, - "competitorMs": 0.6287500001490116, - "competitorP95Ms": 0.7800000011920929, + "competitorMs": 15.387499995529652, + "competitorP95Ms": 16.12000000476837, + "competitorGpuMs": 0.049792, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.11530814774815933, - "factor": 8.672414044704514, - "label": "ExoJS leads clearly (8.67x)", + "ratio": 0.17871649070992202, + "factor": 5.595454543828964, + "label": "ExoJS leads clearly (5.60x)", "structural": true }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.07000000029802322, - "maxMs": 0.07499999552965164, - "ratio": 1.0714285030048725 + "minMs": 2.7124999910593033, + "maxMs": 3.0525000020861626, + "ratio": 1.12534562659817 }, "competitor": { - "minMs": 0.505000002682209, - "maxMs": 0.7149999998509884, - "ratio": 1.415841576343377 + "minMs": 15.177500002086163, + "maxMs": 15.817500002682209, + "ratio": 1.0421676824581174 }, "stable": true, "rungs": [ - "exojs-leads-clearly", "exojs-leads-clearly", "exojs-leads-clearly", "exojs-leads-clearly" @@ -1178,38 +1216,40 @@ }, { "competitor": "pixi", - "referenceMs": 0.07249999791383743, - "referenceP95Ms": 0.0950000062584877, + "referenceMs": 2.75, + "referenceP95Ms": 3.5499999970197678, + "referenceGpuMs": 0.0304, "referenceOverFrameBudget": false, - "competitorMs": 0.08125000260770321, - "competitorP95Ms": 0.11249999701976776, + "competitorMs": 1.6850000023841858, + "competitorP95Ms": 2.145000010728836, + "competitorGpuMs": 0.045568, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 1.6320474754355463, + "factor": 1.6320474754355463, + "label": "competitor leads (1.63x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.07000000029802322, - "maxMs": 0.07499999552965164, - "ratio": 1.0714285030048725 + "minMs": 2.7124999910593033, + "maxMs": 3.0525000020861626, + "ratio": 1.12534562659817 }, "competitor": { - "minMs": 0.07999999821186066, - "maxMs": 0.08999999612569809, - "ratio": 1.1249999767169352 + "minMs": 1.657499998807907, + "maxMs": 1.8200000002980232, + "ratio": 1.0980392166557997 }, - "stable": false, + "stable": true, "rungs": [ - "level", - "level", - "level", - "exojs-leads" + "competitor-leads", + "competitor-leads", + "competitor-leads" ] } } @@ -1218,44 +1258,49 @@ ] }, { - "title": "Text", + "title": "Fill and GPU state", "rows": [ { - "archetype": "text-static", - "category": "Text", - "count": 1000, + "archetype": "fill-layers", + "category": "Fill and GPU state", + "count": 32, + "loadId": "32", + "unit": "layers", + "primary": true, "cells": [ { "competitor": "excalibur", - "referenceMs": 0.11750000156462193, - "referenceP95Ms": 0.1549999974668026, + "referenceMs": 0.030000001192092896, + "referenceP95Ms": 0.06499999761581421, + "referenceGpuMs": 0.122208, "referenceOverFrameBudget": false, - "competitorMs": 13.134999999776483, - "competitorP95Ms": 16.672500003129244, + "competitorMs": 0.19749999791383743, + "competitorP95Ms": 0.2800000011920929, + "competitorGpuMs": 0.208128, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.008945565402864212, - "factor": 111.78723255209981, - "label": "ExoJS leads clearly (111.79x)", + "ratio": 0.15189874181761198, + "factor": 6.58333300219643, + "label": "ExoJS leads clearly (6.58x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.11499999463558197, - "maxMs": 0.13499999791383743, - "ratio": 1.1739130800973732 + "minMs": 0.030000001192092896, + "maxMs": 0.03500000387430191, + "ratio": 1.1666667494508922 }, "competitor": { - "minMs": 12.792500000447035, - "maxMs": 13.327499996870756, - "ratio": 1.0418213794336546 + "minMs": 0.1899999976158142, + "maxMs": 0.20000000298023224, + "ratio": 1.0526316078415872 }, "stable": true, "rungs": [ - "exojs-leads-clearly", "exojs-leads-clearly", "exojs-leads-clearly", "exojs-leads-clearly" @@ -1264,462 +1309,465 @@ }, { "competitor": "phaser", - "referenceMs": 0.11750000156462193, - "referenceP95Ms": 0.1549999974668026, + "referenceMs": 0.030000001192092896, + "referenceP95Ms": 0.06499999761581421, + "referenceGpuMs": 0.122208, "referenceOverFrameBudget": false, - "competitorMs": 2.280000001192093, - "competitorP95Ms": 3.405000001192093, + "competitorMs": 0.08249999582767487, + "competitorP95Ms": 0.13500000536441803, + "competitorGpuMs": 0.120848, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.05153508837859093, - "factor": 19.40425507090868, - "label": "ExoJS leads clearly (19.40x)", - "structural": true + "ratio": 0.36363639647638996, + "factor": 2.7499997516473234, + "label": "ExoJS leads (2.75x)", + "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.11499999463558197, - "maxMs": 0.13499999791383743, - "ratio": 1.1739130800973732 + "minMs": 0.030000001192092896, + "maxMs": 0.03500000387430191, + "ratio": 1.1666667494508922 }, "competitor": { - "minMs": 2.1424999982118607, - "maxMs": 2.3975000008940697, - "ratio": 1.1190198379906806 + "minMs": 0.059999994933605194, + "maxMs": 0.0925000011920929, + "ratio": 1.5416668167130942 }, "stable": true, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } }, { "competitor": "pixi", - "referenceMs": 0.11750000156462193, - "referenceP95Ms": 0.1549999974668026, + "referenceMs": 0.030000001192092896, + "referenceP95Ms": 0.06499999761581421, + "referenceGpuMs": 0.122208, "referenceOverFrameBudget": false, - "competitorMs": 0.09999999776482582, - "competitorP95Ms": 0.13000000268220901, + "competitorMs": 0.030000001192092896, + "competitorP95Ms": 0.06000000238418579, + "competitorGpuMs": 0.121536, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 1, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.11499999463558197, - "maxMs": 0.13499999791383743, - "ratio": 1.1739130800973732 + "minMs": 0.030000001192092896, + "maxMs": 0.03500000387430191, + "ratio": 1.1666667494508922 }, "competitor": { - "minMs": 0.0925000011920929, - "maxMs": 0.10499999672174454, - "ratio": 1.1351350850654927 + "minMs": 0.030000001192092896, + "maxMs": 0.030000001192092896, + "ratio": 1 }, - "stable": false, + "stable": true, "rungs": [ - "competitor-leads", "level", - "competitor-leads", - "competitor-leads" + "level", + "level" ] } } ] }, { - "archetype": "text-dynamic", - "category": "Text", - "count": 1000, + "archetype": "batch-breaking", + "category": "Fill and GPU state", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "excalibur", - "referenceMs": 3.724999997764826, - "referenceP95Ms": 5.092499997466803, + "referenceMs": 1.1299999952316284, + "referenceP95Ms": 1.8500000089406967, + "referenceGpuMs": 5.932832, "referenceOverFrameBudget": false, - "competitorMs": 13.507500002160668, - "competitorP95Ms": 17.554999999701977, - "competitorOverFrameBudget": false, + "competitorMs": 18.029999993741512, + "competitorP95Ms": 19.45499999821186, + "competitorGpuMs": 4.507328, + "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.27577271865030334, - "factor": 3.6261744994002147, - "label": "ExoJS leads (3.63x)", - "structural": false + "ratio": 0.06267332199799604, + "factor": 15.955752274180947, + "label": "ExoJS leads clearly (15.96x)", + "structural": true }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "ExoJS issues fewer buffer uploads (0 vs 313 per frame)", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 3.557499997317791, - "maxMs": 3.7825000025331974, - "ratio": 1.0632466634954454 + "minMs": 1.0950000062584877, + "maxMs": 1.1400000005960464, + "ratio": 1.0410958850048955 }, "competitor": { - "minMs": 13.269999999552965, - "maxMs": 13.697499997913837, - "ratio": 1.0322155236153183 + "minMs": 17.76749999076128, + "maxMs": 18.649999998509884, + "ratio": 1.049669340549176 }, "stable": true, "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" ] } }, { "competitor": "phaser", - "referenceMs": 3.724999997764826, - "referenceP95Ms": 5.092499997466803, + "referenceMs": 1.1299999952316284, + "referenceP95Ms": 1.8500000089406967, + "referenceGpuMs": 5.932832, "referenceOverFrameBudget": false, - "competitorMs": 2.642500001937151, - "competitorP95Ms": 3.814999997615814, + "competitorMs": 3.9325000047683716, + "competitorP95Ms": 4.230000004172325, + "competitorGpuMs": 4.191232, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 1.4096499508170752, - "factor": 1.4096499508170752, - "label": "competitor leads (1.41x)", + "side": "exojs", + "ratio": 0.2873490130607607, + "factor": 3.4800885144802893, + "label": "ExoJS leads (3.48x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 313/5000/0 vs 313/5000/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 3.557499997317791, - "maxMs": 3.7825000025331974, - "ratio": 1.0632466634954454 + "minMs": 1.0950000062584877, + "maxMs": 1.1400000005960464, + "ratio": 1.0410958850048955 }, "competitor": { - "minMs": 2.425000000745058, - "maxMs": 2.905000001192093, - "ratio": 1.1979381444534265 + "minMs": 3.852500006556511, + "maxMs": 3.972500003874302, + "ratio": 1.0311486040528397 }, "stable": true, "rungs": [ - "competitor-leads", - "competitor-leads", - "competitor-leads", - "competitor-leads" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } }, { "competitor": "pixi", - "referenceMs": 3.724999997764826, - "referenceP95Ms": 5.092499997466803, + "referenceMs": 1.1299999952316284, + "referenceP95Ms": 1.8500000089406967, + "referenceGpuMs": 5.932832, "referenceOverFrameBudget": false, - "competitorMs": 7.403750000521541, - "competitorP95Ms": 10.432500001043081, + "competitorMs": 0.760000005364418, + "competitorP95Ms": 1.0249999910593033, + "competitorGpuMs": 5.786704, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.5031234168498973, - "factor": 1.9875838939501036, - "label": "ExoJS leads (1.99x)", + "side": "competitor", + "ratio": 1.486842088494192, + "factor": 1.486842088494192, + "label": "competitor leads (1.49x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 313/5000/0 vs 313/5000/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 3.557499997317791, - "maxMs": 3.7825000025331974, - "ratio": 1.0632466634954454 + "minMs": 1.0950000062584877, + "maxMs": 1.1400000005960464, + "ratio": 1.0410958850048955 }, "competitor": { - "minMs": 7.285000000149012, - "maxMs": 7.767500001937151, - "ratio": 1.066231983771897 + "minMs": 0.7524999976158142, + "maxMs": 0.7949999943375587, + "ratio": 1.0564784011380726 }, "stable": true, "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" + "competitor-leads", + "competitor-leads", + "competitor-leads" ] } } ] - } - ] - }, - { - "title": "Render targets", - "rows": [ + }, { - "archetype": "filter-chain-1", - "category": "Render targets", - "count": 1000, + "archetype": "mixed-blend", + "category": "Fill and GPU state", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, "cells": [ { - "competitor": "pixi", - "referenceMs": 0.3200000002980232, - "referenceP95Ms": 0.4049999974668026, + "competitor": "excalibur", + "referenceMs": 0.1850000023841858, + "referenceP95Ms": 0.25, + "referenceGpuMs": 0.116608, "referenceOverFrameBudget": false, - "competitorMs": 0.45749999955296516, - "competitorP95Ms": 0.5399999991059303, + "competitorMs": 14.157499998807907, + "competitorP95Ms": 15.675000011920929, + "competitorGpuMs": 0.054528, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.699453553247439, - "factor": 1.429687497271516, - "label": "ExoJS leads (1.43x)", - "structural": false + "ratio": 0.01306727899698133, + "factor": 76.52702603434194, + "label": "ExoJS leads clearly (76.53x)", + "structural": true }, - "mechanism": "competitor issues fewer texture binds (9 vs 3 per frame)", + "mechanism": "competitor issues fewer draw calls (120 vs 1 per frame)", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.30000000447034836, - "maxMs": 0.33500000089406967, - "ratio": 1.116666653007269 + "minMs": 0.18000000715255737, + "maxMs": 0.19999999552965164, + "ratio": 1.1111110421242565 }, "competitor": { - "minMs": 0.4399999976158142, - "maxMs": 0.4699999988079071, - "ratio": 1.0681818212605705 + "minMs": 13.879999995231628, + "maxMs": 14.175000004470348, + "ratio": 1.021253602978391 }, "stable": true, "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" ] } - } - ] - }, - { - "archetype": "filter-chain-2", - "category": "Render targets", - "count": 1000, - "cells": [ + }, { - "competitor": "pixi", - "referenceMs": 0.2875000014901161, - "referenceP95Ms": 0.3499999977648258, + "competitor": "phaser", + "referenceMs": 0.1850000023841858, + "referenceP95Ms": 0.25, + "referenceGpuMs": 0.116608, "referenceOverFrameBudget": false, - "competitorMs": 0.44749999791383743, - "competitorP95Ms": 0.5350000001490116, + "competitorMs": 2.552499994635582, + "competitorP95Ms": 2.810000002384186, + "competitorGpuMs": 0.060608, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.642458106883549, - "factor": 1.5565217238067455, - "label": "ExoJS leads (1.56x)", - "structural": false + "ratio": 0.07247796386796783, + "factor": 13.797297090487904, + "label": "ExoJS leads clearly (13.80x)", + "structural": true }, - "mechanism": "competitor issues fewer texture binds (11 vs 3 per frame)", + "mechanism": "competitor issues fewer draw calls (120 vs 1 per frame)", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.25999999791383743, - "maxMs": 0.29499999806284904, - "ratio": 1.13461538626862 + "minMs": 0.18000000715255737, + "maxMs": 0.19999999552965164, + "ratio": 1.1111110421242565 }, "competitor": { - "minMs": 0.4350000023841858, - "maxMs": 0.4624999985098839, - "ratio": 1.0632183815516638 + "minMs": 2.527499996125698, + "maxMs": 2.560000002384186, + "ratio": 1.0128585583811298 }, "stable": true, "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" ] } - } - ] - }, - { - "archetype": "filter-chain-4", - "category": "Render targets", - "count": 1000, - "cells": [ + }, { "competitor": "pixi", - "referenceMs": 0.24500000104308128, - "referenceP95Ms": 0.29250000417232513, + "referenceMs": 0.1850000023841858, + "referenceP95Ms": 0.25, + "referenceGpuMs": 0.116608, "referenceOverFrameBudget": false, - "competitorMs": 0.3974999990314245, - "competitorP95Ms": 0.4649999998509884, + "competitorMs": 0.09999999403953552, + "competitorP95Ms": 0.1550000011920929, + "competitorGpuMs": 2.138176, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.6163522053838111, - "factor": 1.6224489687309323, - "label": "ExoJS leads (1.62x)", + "side": "competitor", + "ratio": 1.8500001341104588, + "factor": 1.8500001341104588, + "label": "competitor leads (1.85x)", "structural": false }, - "mechanism": "competitor issues fewer texture binds (15 vs 7 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 120/0/0 vs 158/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.22499999403953552, - "maxMs": 0.25, - "ratio": 1.1111111405455043 + "minMs": 0.18000000715255737, + "maxMs": 0.19999999552965164, + "ratio": 1.1111110421242565 }, "competitor": { - "minMs": 0.3725000023841858, - "maxMs": 0.4100000038743019, - "ratio": 1.1006711442955635 + "minMs": 0.0949999988079071, + "maxMs": 0.10000000894069672, + "ratio": 1.0526316862687524 }, "stable": true, "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads", - "exojs-leads" + "competitor-leads", + "competitor-leads", + "competitor-leads" ] } } ] - }, + } + ] + }, + { + "title": "Material variety", + "rows": [ { - "archetype": "composite", - "category": "Render targets", - "count": 1000, + "archetype": "batch-breaking-atlased", + "category": "Material variety", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, "cells": [ { - "competitor": "pixi", - "referenceMs": 0.36499999836087227, - "referenceP95Ms": 0.45749999955296516, + "competitor": "excalibur", + "referenceMs": 0.030000001192092896, + "referenceP95Ms": 0.07000000774860382, + "referenceGpuMs": 0.022144, "referenceOverFrameBudget": false, - "competitorMs": 0.36249999701976776, - "competitorP95Ms": 0.4750000052154064, + "competitorMs": 13.730000004172325, + "competitorP95Ms": 15.429999992251396, + "competitorGpuMs": 0.054592, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": 1.0068965554804354, - "factor": 1, - "label": "level", - "structural": false + "side": "exojs", + "ratio": 0.002184996444499372, + "factor": 457.66664861970554, + "label": "ExoJS leads clearly (457.67x)", + "structural": true }, - "mechanism": "ExoJS issues fewer draw calls (5 vs 8 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.33500000089406967, - "maxMs": 0.375, - "ratio": 1.1194029820870919 + "minMs": 0.030000001192092896, + "maxMs": 0.04999999701976776, + "ratio": 1.6666665010982156 }, "competitor": { - "minMs": 0.35750000178813934, - "maxMs": 0.3724999986588955, - "ratio": 1.041958032995047 + "minMs": 13.550000004470348, + "maxMs": 14.375, + "ratio": 1.0608856085060865 }, "stable": true, "rungs": [ - "level", - "level", - "level", - "level" + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" ] } - } - ] - }, - { - "archetype": "mask-clip", - "category": "Render targets", - "count": 1000, - "cells": [ + }, { - "competitor": "pixi", - "referenceMs": 0.24249999970197678, - "referenceP95Ms": 0.29500000178813934, + "competitor": "phaser", + "referenceMs": 0.030000001192092896, + "referenceP95Ms": 0.07000000774860382, + "referenceGpuMs": 0.022144, "referenceOverFrameBudget": false, - "competitorMs": 0.14000000059604645, - "competitorP95Ms": 0.1875, + "competitorMs": 2.3350000083446503, + "competitorP95Ms": 2.730000004172325, + "competitorGpuMs": 0.049696, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 1.7321428476395655, - "factor": 1.7321428476395655, - "label": "competitor leads (1.73x)", - "structural": false + "side": "exojs", + "ratio": 0.012847966203375208, + "factor": 77.83333051866967, + "label": "ExoJS leads clearly (77.83x)", + "structural": true }, - "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.2199999988079071, - "maxMs": 0.24500000476837158, - "ratio": 1.1136363913451346 + "minMs": 0.030000001192092896, + "maxMs": 0.04999999701976776, + "ratio": 1.6666665010982156 }, "competitor": { - "minMs": 0.13500000536441803, - "maxMs": 0.14249999821186066, - "ratio": 1.055555500366072 + "minMs": 2.3224999979138374, + "maxMs": 2.3400000035762787, + "ratio": 1.0075349862984544 }, "stable": true, "rungs": [ - "competitor-leads", - "competitor-leads", - "competitor-leads", - "competitor-leads" + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" ] } - } - ] - }, - { - "archetype": "mask-clip-animated", - "category": "Render targets", - "count": 1000, - "cells": [ + }, { "competitor": "pixi", - "referenceMs": 0.2587500009685755, - "referenceP95Ms": 0.32750000059604645, + "referenceMs": 0.030000001192092896, + "referenceP95Ms": 0.07000000774860382, + "referenceGpuMs": 0.022144, "referenceOverFrameBudget": false, - "competitorMs": 0.19500000029802322, - "competitorP95Ms": 0.24249999970197678, + "competitorMs": 0.030000001192092896, + "competitorP95Ms": 0.06999999284744263, + "competitorGpuMs": 0.009216, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 1.326923079862162, - "factor": 1.326923079862162, - "label": "competitor leads (1.33x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, - "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.23499999940395355, - "maxMs": 0.2850000038743019, - "ratio": 1.2127659770092203 + "minMs": 0.030000001192092896, + "maxMs": 0.04999999701976776, + "ratio": 1.6666665010982156 }, "competitor": { - "minMs": 0.1899999976158142, - "maxMs": 0.20000000298023224, - "ratio": 1.0526316078415872 + "minMs": 0.030000001192092896, + "maxMs": 0.03999999910593033, + "ratio": 1.3333332505491078 }, - "stable": true, + "stable": false, "rungs": [ + "exojs-leads", "competitor-leads", - "competitor-leads", - "competitor-leads", - "competitor-leads" + "level" ] } } @@ -1728,149 +1776,103 @@ ] }, { - "title": "Camera and world", + "title": "Text", "rows": [ { - "archetype": "scrolling-world", - "category": "Camera and world", + "archetype": "text-static", + "category": "Text", "count": 1000, + "loadId": "1k", + "unit": "labels", + "primary": true, "cells": [ { - "competitor": "pixi", - "referenceMs": 0.16499999910593033, - "referenceP95Ms": 0.39499999582767487, + "competitor": "excalibur", + "referenceMs": 0.04500000178813934, + "referenceP95Ms": 0.09999999403953552, + "referenceGpuMs": 0.074016, "referenceOverFrameBudget": false, - "competitorMs": 0.09999999776482582, - "competitorP95Ms": 0.1275000013411045, + "competitorMs": 12.0625, + "competitorP95Ms": 13.550000011920929, + "competitorGpuMs": 0.12656, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 1.6500000279396778, - "factor": 1.6500000279396778, - "label": "competitor leads (1.65x)", - "structural": false + "side": "exojs", + "ratio": 0.0037305700964260597, + "factor": 268.0555449039852, + "label": "ExoJS leads clearly (268.06x)", + "structural": true }, - "mechanism": "competitor issues fewer draw calls (120 vs 1 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.14249999821186066, - "maxMs": 0.17249999940395355, - "ratio": 1.2105263267967952 + "minMs": 0.040000006556510925, + "maxMs": 0.04749999940395355, + "ratio": 1.1874997904524551 }, "competitor": { - "minMs": 0.0949999988079071, - "maxMs": 0.10999999940395355, - "ratio": 1.1578947450975963 + "minMs": 11.759999997913837, + "maxMs": 12.214999996125698, + "ratio": 1.038690476045287 }, "stable": true, "rungs": [ - "competitor-leads", - "competitor-leads", - "competitor-leads", - "competitor-leads" + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" ] } - } - ] - } - ] - } - ], - "excluded": [ - { - "archetype": "split-screen", - "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" - }, - { - "archetype": "mixed-material", - "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" - }, - { - "archetype": "instanced-batch", - "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" - }, - { - "archetype": "mixed-sprite-mesh-static", - "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" - }, - { - "archetype": "mixed-sprite-mesh-array", - "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" - }, - { - "archetype": "mixed-material-atlased", - "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" - } - ], - "webgl1": [] - }, - { - "backend": "webgpu", - "headlineCount": 5000, - "competitors": [ - "pixi" - ], - "sections": [ - { - "title": "Node scaling", - "rows": [ - { - "archetype": "static-heavy", - "category": "Node scaling", - "count": 5000, - "cells": [ + }, { - "competitor": "pixi", - "referenceMs": 0.24625000171363354, - "referenceP95Ms": 0.32500000298023224, + "competitor": "phaser", + "referenceMs": 0.04500000178813934, + "referenceP95Ms": 0.09999999403953552, + "referenceGpuMs": 0.074016, "referenceOverFrameBudget": false, - "competitorMs": 0.28500000201165676, - "competitorP95Ms": 0.3500000014901161, + "competitorMs": 2.0950000062584877, + "competitorP95Ms": 2.4549999982118607, + "competitorGpuMs": 0.13784000000000002, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", - "structural": false + "side": "exojs", + "ratio": 0.02147971439317843, + "factor": 46.55555384468156, + "label": "ExoJS leads clearly (46.56x)", + "structural": true }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.22999999672174454, - "maxMs": 0.29250000044703484, - "ratio": 1.2717391505048723 + "minMs": 0.040000006556510925, + "maxMs": 0.04749999940395355, + "ratio": 1.1874997904524551 }, "competitor": { - "minMs": 0.2849999964237213, - "maxMs": 0.29999999701976776, - "ratio": 1.0526315816991987 + "minMs": 2.0650000125169754, + "maxMs": 2.3050000071525574, + "ratio": 1.1162227569882928 }, - "stable": false, + "stable": true, "rungs": [ - "level", - "level", - "level", - "exojs-leads" + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" ] } - } - ] - }, - { - "archetype": "dynamic-heavy", - "category": "Node scaling", - "count": 5000, - "cells": [ + }, { "competitor": "pixi", - "referenceMs": 0.6887500025331974, - "referenceP95Ms": 0.8749999962747097, + "referenceMs": 0.04500000178813934, + "referenceP95Ms": 0.09999999403953552, + "referenceGpuMs": 0.074016, "referenceOverFrameBudget": false, - "competitorMs": 0.5300000011920929, - "competitorP95Ms": 0.642500001937151, + "competitorMs": 0.03999999165534973, + "competitorP95Ms": 0.07500000298023224, + "competitorGpuMs": 0.014592, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -1879,66 +1881,149 @@ "label": "unstable across runs", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.6225000023841858, - "maxMs": 0.7124999985098839, - "ratio": 1.1445783064754964 + "minMs": 0.040000006556510925, + "maxMs": 0.04749999940395355, + "ratio": 1.1874997904524551 }, "competitor": { - "minMs": 0.5199999958276749, - "maxMs": 0.5449999943375587, - "ratio": 1.0480769205970701 + "minMs": 0.034999996423721313, + "maxMs": 0.040000006556510925, + "ratio": 1.1428574469625044 }, "stable": false, "rungs": [ "level", "competitor-leads", - "competitor-leads", - "competitor-leads" + "level" ] } } ] }, { - "archetype": "deep-hierarchy", - "category": "Node scaling", + "archetype": "text-static", + "category": "Text", "count": 5000, + "loadId": "5k", + "unit": "labels", + "primary": false, "cells": [ + { + "competitor": "excalibur", + "referenceMs": 0.04500000178813934, + "referenceP95Ms": 0.08499999344348907, + "referenceGpuMs": 0.336128, + "referenceOverFrameBudget": false, + "competitorMs": 61.45999999344349, + "competitorP95Ms": 63.75, + "competitorGpuMs": 0.614016, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.000732183563178326, + "factor": 1365.7777233609468, + "label": "ExoJS leads clearly (1365.78x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 3/4/3, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.040000006556510925, + "maxMs": 0.04500000178813934, + "ratio": 1.1249998603016367 + }, + "competitor": { + "minMs": 61.354999996721745, + "maxMs": 62.41999999433756, + "ratio": 1.0173579984951955 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "phaser", + "referenceMs": 0.04500000178813934, + "referenceP95Ms": 0.08499999344348907, + "referenceGpuMs": 0.336128, + "referenceOverFrameBudget": false, + "competitorMs": 10.622500002384186, + "competitorP95Ms": 10.990000009536743, + "competitorGpuMs": 0.759776, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.004236291059358836, + "factor": 236.05554622853282, + "label": "ExoJS leads clearly (236.06x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 4/0/4, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.040000006556510925, + "maxMs": 0.04500000178813934, + "ratio": 1.1249998603016367 + }, + "competitor": { + "minMs": 9.764999993145466, + "maxMs": 10.935000002384186, + "ratio": 1.119815669232975 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, { "competitor": "pixi", - "referenceMs": 0.3049999978393316, - "referenceP95Ms": 0.375, + "referenceMs": 0.04500000178813934, + "referenceP95Ms": 0.08499999344348907, + "referenceGpuMs": 0.336128, "referenceOverFrameBudget": false, - "competitorMs": 0.2999999988824129, - "competitorP95Ms": 0.35999999940395355, + "competitorMs": 0.040000006556510925, + "competitorP95Ms": 0.07999999821186066, + "competitorGpuMs": 0.051168, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.0166666632518173, + "ratio": 1.1249998603016367, "factor": 1, "label": "level", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.29500000178813934, - "maxMs": 0.3100000023841858, - "ratio": 1.0508474593394037 + "minMs": 0.040000006556510925, + "maxMs": 0.04500000178813934, + "ratio": 1.1249998603016367 }, "competitor": { - "minMs": 0.2875000014901161, - "maxMs": 0.3099999986588955, - "ratio": 1.0782608593118665 + "minMs": 0.040000006556510925, + "maxMs": 0.04500000178813934, + "ratio": 1.1249998603016367 }, "stable": true, "rungs": [ - "level", "level", "level", "level" @@ -1948,320 +2033,305 @@ ] }, { - "archetype": "lifecycle-churn", - "category": "Node scaling", - "count": 5000, + "archetype": "text-dynamic", + "category": "Text", + "count": 1000, + "loadId": "1k", + "unit": "labels", + "primary": true, "cells": [ { - "competitor": "pixi", - "referenceMs": 3.555000001564622, - "referenceP95Ms": 4.704999998211861, + "competitor": "excalibur", + "referenceMs": 2.802500009536743, + "referenceP95Ms": 3.385000005364418, + "referenceGpuMs": 0.204928, "referenceOverFrameBudget": false, - "competitorMs": 2.551249999552965, - "competitorP95Ms": 2.9200000017881393, + "competitorMs": 12.525000005960464, + "competitorP95Ms": 14.240000009536743, + "competitorGpuMs": 0.126592, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 1.393434591744257, - "factor": 1.393434591744257, - "label": "competitor leads (1.39x)", + "side": "exojs", + "ratio": 0.22375249566491612, + "factor": 4.469223894144023, + "label": "ExoJS leads (4.47x)", "structural": false }, - "mechanism": "competitor issues fewer buffer uploads (13 vs 3 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 3.4024999998509884, - "maxMs": 3.662499997764826, - "ratio": 1.0764144005658263 + "minMs": 2.739999994635582, + "maxMs": 2.837499998509884, + "ratio": 1.035583943089485 }, "competitor": { - "minMs": 2.544999998062849, - "maxMs": 2.6150000020861626, - "ratio": 1.027504913193161 + "minMs": 12.342500001192093, + "maxMs": 12.689999997615814, + "ratio": 1.0281547495556134 }, "stable": true, "rungs": [ - "competitor-leads", - "competitor-leads", - "competitor-leads", - "competitor-leads" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } - } - ] - } - ] - }, - { - "title": "Fill and GPU state", - "rows": [ - { - "archetype": "overdraw", - "category": "Fill and GPU state", - "count": 5000, - "cells": [ + }, { - "competitor": "pixi", - "referenceMs": 0.16375000402331352, - "referenceP95Ms": 0.2199999988079071, + "competitor": "phaser", + "referenceMs": 2.802500009536743, + "referenceP95Ms": 3.385000005364418, + "referenceGpuMs": 0.204928, "referenceOverFrameBudget": false, - "competitorMs": 0.1849999986588955, - "competitorP95Ms": 0.2524999976158142, + "competitorMs": 2.2999999970197678, + "competitorP95Ms": 2.6749999970197678, + "competitorGpuMs": 0.137712, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.8851351632993096, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.16250000149011612, - "maxMs": 0.17249999940395355, - "ratio": 1.0615384481362338 + "minMs": 2.739999994635582, + "maxMs": 2.837499998509884, + "ratio": 1.035583943089485 }, "competitor": { - "minMs": 0.17999999970197678, - "maxMs": 0.19500000029802322, - "ratio": 1.0833333367826763 + "minMs": 2.2725000083446503, + "maxMs": 2.394999995827675, + "ratio": 1.0539053848330926 }, - "stable": true, + "stable": false, "rungs": [ "level", - "level", - "level", - "level" + "competitor-leads", + "competitor-leads" ] } - } - ] - }, - { - "archetype": "batch-breaking", - "category": "Fill and GPU state", - "count": 5000, - "cells": [ + }, { "competitor": "pixi", - "referenceMs": 0.8775000013411045, - "referenceP95Ms": 1.2575000002980232, + "referenceMs": 2.802500009536743, + "referenceP95Ms": 3.385000005364418, + "referenceGpuMs": 0.204928, "referenceOverFrameBudget": false, - "competitorMs": 0.5937500018626451, - "competitorP95Ms": 0.7925000004470348, + "competitorMs": 6.125, + "competitorP95Ms": 7.280000001192093, + "competitorGpuMs": 0.121024, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 1.477894734464524, - "factor": 1.477894734464524, - "label": "competitor leads (1.48x)", + "side": "exojs", + "ratio": 0.45755102196518255, + "factor": 2.185548609868683, + "label": "ExoJS leads (2.19x)", "structural": false }, - "mechanism": "competitor issues fewer texture binds (626 vs 314 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.8075000010430813, - "maxMs": 0.8900000005960464, - "ratio": 1.1021671819769616 + "minMs": 2.739999994635582, + "maxMs": 2.837499998509884, + "ratio": 1.035583943089485 }, "competitor": { - "minMs": 0.5824999995529652, - "maxMs": 0.5975000038743019, - "ratio": 1.0257510803997396 + "minMs": 6.0775000005960464, + "maxMs": 6.13000001013279, + "ratio": 1.0086384219714677 }, "stable": true, "rungs": [ - "competitor-leads", - "competitor-leads", - "competitor-leads", - "competitor-leads" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } } ] }, { - "archetype": "mixed-blend", - "category": "Fill and GPU state", + "archetype": "text-dynamic", + "category": "Text", "count": 5000, + "loadId": "5k", + "unit": "labels", + "primary": false, "cells": [ { - "competitor": "pixi", - "referenceMs": 0.3374999985098839, - "referenceP95Ms": 0.4675000011920929, + "competitor": "excalibur", + "referenceMs": 14.677499994635582, + "referenceP95Ms": 18.509999990463257, + "referenceGpuMs": 0.855296, "referenceOverFrameBudget": false, - "competitorMs": 0.37124999798834324, - "competitorP95Ms": 0.5324999988079071, - "competitorOverFrameBudget": false, + "competitorMs": 63.75999999791384, + "competitorP95Ms": 66.38500000536442, + "competitorGpuMs": 0.614208, + "competitorOverFrameBudget": true, "verdict": { - "side": "neither", - "ratio": 0.909090910003132, - "factor": 1, - "label": "level", + "side": "exojs", + "ratio": 0.23019918436505357, + "factor": 4.344064045049716, + "label": "ExoJS leads (4.34x)", "structural": false }, - "mechanism": "competitor issues fewer texture binds (240 vs 159 per frame)", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 3/4/3, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.3150000050663948, - "maxMs": 0.3449999988079071, - "ratio": 1.095238073838091 + "minMs": 14.390000000596046, + "maxMs": 15.112499997019768, + "ratio": 1.0502084778591936 }, "competitor": { - "minMs": 0.36500000208616257, - "maxMs": 0.39000000059604645, - "ratio": 1.0684931462109482 + "minMs": 63.63500000536442, + "maxMs": 64.18250000476837, + "ratio": 1.008603755784675 }, "stable": true, "rungs": [ - "level", - "level", - "level", - "level" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } - } - ] - } - ] - }, - { - "title": "Material variety", - "rows": [ - { - "archetype": "batch-breaking-atlased", - "category": "Material variety", - "count": 5000, - "cells": [ + }, { - "competitor": "pixi", - "referenceMs": 0.16250000149011612, - "referenceP95Ms": 0.20749999955296516, + "competitor": "phaser", + "referenceMs": 14.677499994635582, + "referenceP95Ms": 18.509999990463257, + "referenceGpuMs": 0.855296, "referenceOverFrameBudget": false, - "competitorMs": 0.17999999970197678, - "competitorP95Ms": 0.23000000044703484, + "competitorMs": 11.987500004470348, + "competitorP95Ms": 12.604999989271164, + "competitorGpuMs": 0.75968, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.902777787550916, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 4/0/4, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.1599999964237213, - "maxMs": 0.16499999910593033, - "ratio": 1.0312500174622987 + "minMs": 14.390000000596046, + "maxMs": 15.112499997019768, + "ratio": 1.0502084778591936 }, "competitor": { - "minMs": 0.17999999970197678, - "maxMs": 0.1850000023841858, - "ratio": 1.0277777927249303 + "minMs": 11.229999996721745, + "maxMs": 12.497500002384186, + "ratio": 1.1128673202166028 }, - "stable": true, + "stable": false, "rungs": [ + "competitor-leads", "level", - "level", - "level", - "level" + "competitor-leads" ] } - } - ] - } - ] - }, - { - "title": "Text", - "rows": [ - { - "archetype": "text-static", - "category": "Text", - "count": 5000, - "cells": [ + }, { "competitor": "pixi", - "referenceMs": 0.2374999988824129, - "referenceP95Ms": 0.29500000178813934, + "referenceMs": 14.677499994635582, + "referenceP95Ms": 18.509999990463257, + "referenceGpuMs": 0.855296, "referenceOverFrameBudget": false, - "competitorMs": 0.21749999932944775, - "competitorP95Ms": 0.26999999955296516, - "competitorOverFrameBudget": false, + "competitorMs": 33.45500000566244, + "competitorP95Ms": 34.78000000119209, + "competitorGpuMs": 0.658176, + "competitorOverFrameBudget": true, "verdict": { - "side": "neither", - "ratio": 1.0919540212166674, - "factor": 1, - "label": "level", + "side": "exojs", + "ratio": 0.43872365841133865, + "factor": 2.2793391257291615, + "label": "ExoJS leads (2.28x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.22500000149011612, - "maxMs": 0.24500000104308128, - "ratio": 1.0888888863133797 + "minMs": 14.390000000596046, + "maxMs": 15.112499997019768, + "ratio": 1.0502084778591936 }, "competitor": { - "minMs": 0.19999999552965164, - "maxMs": 0.23499999940395355, - "ratio": 1.1750000232830649 + "minMs": 33.44749999791384, + "maxMs": 34.17999999970198, + "ratio": 1.0218999925804269 }, "stable": true, "rungs": [ - "level", - "level", - "level", - "level" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } } ] - }, + } + ] + }, + { + "title": "Render targets", + "rows": [ { - "archetype": "text-dynamic", - "category": "Text", + "archetype": "filter-chain-1", + "category": "Render targets", "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 18.540000000968575, - "referenceP95Ms": 22.15249999985099, - "referenceOverFrameBudget": true, - "competitorMs": 37.40625, - "competitorP95Ms": 40.60249999910593, - "competitorOverFrameBudget": true, + "referenceMs": 0.13499999791383743, + "referenceP95Ms": 0.2150000035762787, + "referenceGpuMs": 0.039008, + "referenceOverFrameBudget": false, + "competitorMs": 0.45750000327825546, + "competitorP95Ms": 0.6449999958276749, + "competitorGpuMs": 0.015296, + "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.49563909777025433, - "factor": 2.0175970872732365, - "label": "ExoJS leads (2.02x)", + "ratio": 0.2950819605387615, + "factor": 3.3888889655409535, + "label": "ExoJS leads (3.39x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/3 vs 1/2/3, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "competitor issues fewer texture binds (9 vs 3 per frame)", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 18.277499996125698, - "maxMs": 18.87000000104308, - "ratio": 1.0324169063079183 + "minMs": 0.13499999046325684, + "maxMs": 0.14249999821186066, + "ratio": 1.055555616877211 }, "competitor": { - "minMs": 36.75499999523163, - "maxMs": 39.872499998658895, - "ratio": 1.0848183921597527 + "minMs": 0.4100000038743019, + "maxMs": 0.4725000113248825, + "ratio": 1.1524390411219163 }, "stable": true, "rungs": [ - "exojs-leads", "exojs-leads", "exojs-leads", "exojs-leads" @@ -2269,48 +2339,48 @@ } } ] - } - ] - }, - { - "title": "Render targets", - "rows": [ + }, { - "archetype": "filter-chain-1", + "archetype": "filter-chain-2", "category": "Render targets", "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 0.3925000000745058, - "referenceP95Ms": 0.47749999910593033, + "referenceMs": 0.125, + "referenceP95Ms": 0.19999998807907104, + "referenceGpuMs": 0.047855999999999996, "referenceOverFrameBudget": false, - "competitorMs": 1.0974999982863665, - "competitorP95Ms": 1.3250000029802322, + "competitorMs": 0.375, + "competitorP95Ms": 0.5050000101327896, + "competitorGpuMs": 0.02144, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.35763098012515193, - "factor": 2.7961783390523185, - "label": "ExoJS leads (2.80x)", + "ratio": 0.3333333333333333, + "factor": 3, + "label": "ExoJS leads (3.00x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/6/3 vs 2/4/2, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "competitor issues fewer texture binds (11 vs 3 per frame)", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.3674999997019768, - "maxMs": 0.3999999985098839, - "ratio": 1.088435370977586 + "minMs": 0.125, + "maxMs": 0.12999999523162842, + "ratio": 1.0399999618530273 }, "competitor": { - "minMs": 1.0649999976158142, - "maxMs": 1.1099999994039536, - "ratio": 1.0422535229003564 + "minMs": 0.3500000089406967, + "maxMs": 0.3799999952316284, + "ratio": 1.0857142443559618 }, "stable": true, "rungs": [ - "exojs-leads", "exojs-leads", "exojs-leads", "exojs-leads" @@ -2320,41 +2390,46 @@ ] }, { - "archetype": "filter-chain-2", + "archetype": "filter-chain-4", "category": "Render targets", "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 0.35999999940395355, - "referenceP95Ms": 0.4700000025331974, + "referenceMs": 0.12999999523162842, + "referenceP95Ms": 0.2199999988079071, + "referenceGpuMs": 0.05656, "referenceOverFrameBudget": false, - "competitorMs": 0.8799999989569187, - "competitorP95Ms": 1.0700000040233135, + "competitorMs": 0.35999999940395355, + "competitorP95Ms": 0.4950000047683716, + "competitorGpuMs": 0.036416, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.4090909088984871, - "factor": 2.4444444455942254, - "label": "ExoJS leads (2.44x)", + "ratio": 0.3611110984635206, + "factor": 2.7692308662205796, + "label": "ExoJS leads (2.77x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 4/8/3 vs 3/6/3, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "competitor issues fewer texture binds (15 vs 7 per frame)", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.3425000011920929, - "maxMs": 0.380000002682209, - "ratio": 1.109489055064511 + "minMs": 0.12999999523162842, + "maxMs": 0.14000000059604645, + "ratio": 1.0769231210093542 }, "competitor": { - "minMs": 0.7100000008940697, - "maxMs": 0.8974999971687794, - "ratio": 1.2640845014628166 + "minMs": 0.3499999940395355, + "maxMs": 0.369999997317791, + "ratio": 1.0571428674824386 }, "stable": true, "rungs": [ - "exojs-leads", "exojs-leads", "exojs-leads", "exojs-leads" @@ -2364,41 +2439,46 @@ ] }, { - "archetype": "filter-chain-4", + "archetype": "composite", "category": "Render targets", "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 0.4087500013411045, - "referenceP95Ms": 0.5075000002980232, + "referenceMs": 0.11999999731779099, + "referenceP95Ms": 0.2150000035762787, + "referenceGpuMs": 0.11075199999999999, "referenceOverFrameBudget": false, - "competitorMs": 1.0762499999254942, - "competitorP95Ms": 1.2525000013411045, + "competitorMs": 0.17000000178813934, + "competitorP95Ms": 0.26500000059604645, + "competitorGpuMs": 0.035424, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.37979094203893254, - "factor": 2.6330275141145667, - "label": "ExoJS leads (2.63x)", + "ratio": 0.7058823297386766, + "factor": 1.4166667132327964, + "label": "ExoJS leads (1.42x)", "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 6/12/3 vs 5/10/5, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "ExoJS issues fewer draw calls (5 vs 8 per frame)", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.3674999997019768, - "maxMs": 0.41499999910593033, - "ratio": 1.1292516991631933 + "minMs": 0.11999998986721039, + "maxMs": 0.125, + "ratio": 1.0416667546249172 }, "competitor": { - "minMs": 0.8125, - "maxMs": 1.094999998807907, - "ratio": 1.3476923062251165 + "minMs": 0.17000000178813934, + "maxMs": 0.17999999225139618, + "ratio": 1.0588234726945427 }, "stable": true, "rungs": [ - "exojs-leads", "exojs-leads", "exojs-leads", "exojs-leads" @@ -2408,85 +2488,96 @@ ] }, { - "archetype": "composite", + "archetype": "mask-clip", "category": "Render targets", "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 0.39375000074505806, - "referenceP95Ms": 0.494999997317791, + "referenceMs": 0.10999999940395355, + "referenceP95Ms": 0.20000000298023224, + "referenceGpuMs": 0.016704, "referenceOverFrameBudget": false, - "competitorMs": 0.4874999988824129, - "competitorP95Ms": 0.6150000020861626, + "competitorMs": 0.06999999284744263, + "competitorP95Ms": 0.11500000953674316, + "competitorGpuMs": 0.0168, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 1.5714287234812523, + "factor": 1.5714287234812523, + "label": "competitor leads (1.57x)", "structural": false }, - "mechanism": "ExoJS issues fewer draw calls (5 vs 8 per frame)", + "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.3799999952316284, - "maxMs": 0.4049999974668026, - "ratio": 1.065789480391797 + "minMs": 0.10249999910593033, + "maxMs": 0.10999999940395355, + "ratio": 1.0731707352531021 }, "competitor": { - "minMs": 0.47999999672174454, - "maxMs": 0.5225000008940697, - "ratio": 1.0885416759637236 + "minMs": 0.06999999284744263, + "maxMs": 0.07000000029802322, + "ratio": 1.0000001064368766 }, - "stable": false, + "stable": true, "rungs": [ - "level", - "level", - "exojs-leads", - "exojs-leads" + "competitor-leads", + "competitor-leads", + "competitor-leads" ] } } ] }, { - "archetype": "mask-clip", + "archetype": "fx-blur", "category": "Render targets", - "count": 5000, + "count": 720, + "loadId": "720", + "unit": "viewport", + "primary": true, + "label": "1280 x 720", "cells": [ { "competitor": "pixi", - "referenceMs": 0.35249999910593033, - "referenceP95Ms": 0.4299999997019768, + "referenceMs": 0.11499999463558197, + "referenceP95Ms": 0.19500000774860382, + "referenceGpuMs": 0.130448, "referenceOverFrameBudget": false, - "competitorMs": 0.2512499988079071, - "competitorP95Ms": 0.3199999965727329, + "competitorMs": 0.07999999821186066, + "competitorP95Ms": 0.14000000059604645, + "competitorGpuMs": 0.034432000000000004, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 1.4374999650754026, + "factor": 1.4374999650754026, + "label": "competitor leads (1.44x)", "structural": false }, - "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", + "mechanism": "competitor issues fewer texture binds (11 vs 3 per frame)", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.32999999821186066, - "maxMs": 0.369999997317791, - "ratio": 1.1212121191596196 + "minMs": 0.11499999463558197, + "maxMs": 0.11500000208616257, + "ratio": 1.0000000647876603 }, "competitor": { - "minMs": 0.24500000476837158, - "maxMs": 0.3049999997019768, - "ratio": 1.2448979337381259 + "minMs": 0.07999999821186066, + "maxMs": 0.08500000834465027, + "ratio": 1.062500128056857 }, - "stable": false, + "stable": true, "rungs": [ - "level", "competitor-leads", "competitor-leads", "competitor-leads" @@ -2499,38 +2590,43 @@ "archetype": "mask-clip-animated", "category": "Render targets", "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 0.35624999925494194, - "referenceP95Ms": 0.4424999989569187, + "referenceMs": 0.11499999463558197, + "referenceP95Ms": 0.1899999976158142, + "referenceGpuMs": 0.016815999999999998, "referenceOverFrameBudget": false, - "competitorMs": 0.32124999910593033, - "competitorP95Ms": 0.4050000049173832, + "competitorMs": 0.11999998986721039, + "competitorP95Ms": 0.19500000774860382, + "competitorGpuMs": 0.052912, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 0.9583333695514366, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", + "timer": "resolved", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.32750000059604645, - "maxMs": 0.41499999910593033, - "ratio": 1.2671755674828544 + "minMs": 0.10999999940395355, + "maxMs": 0.12000000476837158, + "ratio": 1.0909091401691282 }, "competitor": { - "minMs": 0.3199999965727329, - "maxMs": 0.32999999821186066, - "ratio": 1.0312500054569682 + "minMs": 0.11500000208616257, + "maxMs": 0.13500000536441803, + "ratio": 1.1739130688299524 }, - "stable": false, + "stable": true, "rungs": [ - "competitor-leads", "level", "level", "level" @@ -2547,59 +2643,2588 @@ { "archetype": "scrolling-world", "category": "Camera and world", - "count": 5000, + "count": 10000, + "loadId": "10k", + "unit": "sprites", + "primary": true, "cells": [ { "competitor": "pixi", - "referenceMs": 0.23000000044703484, - "referenceP95Ms": 0.4299999997019768, + "referenceMs": 0.0650000050663948, + "referenceP95Ms": 0.36499999463558197, + "referenceGpuMs": 0.008352, "referenceOverFrameBudget": false, - "competitorMs": 0.19250000081956387, - "competitorP95Ms": 0.24000000208616257, + "competitorMs": 0.034999996423721313, + "competitorP95Ms": 0.08499999344348907, + "competitorGpuMs": 0.006752, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 1.8571431916587549, + "factor": 1.8571431916587549, + "label": "competitor leads (1.86x)", "structural": false }, "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", + "timer": "limited", "aggregate": { - "runs": 4, + "runs": 3, "reference": { - "minMs": 0.2150000035762787, - "maxMs": 0.23499999940395355, - "ratio": 1.0930232348604552 + "minMs": 0.06499999761581421, + "maxMs": 0.06749999523162842, + "ratio": 1.0384615031925166 }, "competitor": { - "minMs": 0.1899999976158142, - "maxMs": 0.19500000029802322, - "ratio": 1.0263158039207936 + "minMs": 0.034999996423721313, + "maxMs": 0.03999999910593033, + "ratio": 1.1428572340887513 }, - "stable": false, + "stable": true, "rungs": [ - "level", "competitor-leads", - "level", - "level" + "competitor-leads", + "competitor-leads" ] } } ] } ] - } - ], - "excluded": [ - { - "archetype": "split-screen", - "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" }, { - "archetype": "mixed-material", - "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" - }, + "title": "Tilemaps", + "rows": [ + { + "archetype": "tilemap-scroll", + "category": "Tilemaps", + "count": 10000, + "loadId": "10k", + "unit": "tiles", + "primary": false, + "cells": [ + { + "competitor": "phaser", + "referenceMs": 0.08249999582767487, + "referenceP95Ms": 0.3349999934434891, + "referenceGpuMs": 0.013583999999999999, + "referenceOverFrameBudget": false, + "competitorMs": 0.04999999701976776, + "competitorP95Ms": 0.0949999988079071, + "competitorGpuMs": 0.007296, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.650000014901162, + "factor": 1.650000014901162, + "label": "competitor leads (1.65x)", + "structural": false + }, + "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.08249999582767487, + "maxMs": 0.08500000089406967, + "ratio": 1.0303030932464141 + }, + "competitor": { + "minMs": 0.04500000178813934, + "maxMs": 0.05499999225139618, + "ratio": 1.2222220014642873 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 0.08249999582767487, + "referenceP95Ms": 0.3349999934434891, + "referenceGpuMs": 0.013583999999999999, + "referenceOverFrameBudget": false, + "competitorMs": 0.07750000059604645, + "competitorP95Ms": 0.14000000059604645, + "competitorGpuMs": 0.010528, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 1.0645160670086948, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "ExoJS issues fewer draw calls (90 vs 282 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.08249999582767487, + "maxMs": 0.08500000089406967, + "ratio": 1.0303030932464141 + }, + "competitor": { + "minMs": 0.07500000298023224, + "maxMs": 0.07750000059604645, + "ratio": 1.0333333002196432 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + } + ] + }, + { + "archetype": "tilemap-scroll", + "category": "Tilemaps", + "count": 100000, + "loadId": "100k", + "unit": "tiles", + "primary": true, + "cells": [ + { + "competitor": "phaser", + "referenceMs": 0.07999999821186066, + "referenceP95Ms": 0.17499999701976776, + "referenceGpuMs": 0.014112, + "referenceOverFrameBudget": false, + "competitorMs": 0.04999999701976776, + "competitorP95Ms": 0.0949999988079071, + "competitorGpuMs": 0.006944, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.6000000596046484, + "factor": 1.6000000596046484, + "label": "competitor leads (1.60x)", + "structural": false + }, + "mechanism": "competitor issues fewer draw calls (30 vs 1 per frame)", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.06749999523162842, + "maxMs": 0.09000000357627869, + "ratio": 1.333333480505306 + }, + "competitor": { + "minMs": 0.04500000178813934, + "maxMs": 0.05250000208616257, + "ratio": 1.1666666666666667 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 0.07999999821186066, + "referenceP95Ms": 0.17499999701976776, + "referenceGpuMs": 0.014112, + "referenceOverFrameBudget": false, + "competitorMs": 0.07750000059604645, + "competitorP95Ms": 0.16499999165534973, + "competitorGpuMs": 0.009728, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 1.0322580335043474, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "competitor issues fewer draw calls (30 vs 2 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.06749999523162842, + "maxMs": 0.09000000357627869, + "ratio": 1.333333480505306 + }, + "competitor": { + "minMs": 0.07500000298023224, + "maxMs": 0.08249999582767487, + "ratio": 1.0999999006589294 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + } + ] + }, + { + "archetype": "tilemap-edit", + "category": "Tilemaps", + "count": 100000, + "loadId": "100k", + "unit": "tiles", + "primary": true, + "cells": [ + { + "competitor": "phaser", + "referenceMs": 0.2724999934434891, + "referenceP95Ms": 0.36000001430511475, + "referenceGpuMs": 0.013344, + "referenceOverFrameBudget": false, + "competitorMs": 1.5250000059604645, + "competitorP95Ms": 1.6199999898672104, + "competitorGpuMs": 0.030992, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.1786885195924082, + "factor": 5.596330431753637, + "label": "ExoJS leads clearly (5.60x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/3/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.26249999552965164, + "maxMs": 0.2775000035762787, + "ratio": 1.057142888769812 + }, + "competitor": { + "minMs": 1.467500001192093, + "maxMs": 1.5549999997019768, + "ratio": 1.0596252118833425 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 0.2724999934434891, + "referenceP95Ms": 0.36000001430511475, + "referenceGpuMs": 0.013344, + "referenceOverFrameBudget": false, + "competitorMs": 0.5750000029802322, + "competitorP95Ms": 0.864999994635582, + "competitorGpuMs": 0.055952, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.47391302961933596, + "factor": 2.110091804825953, + "label": "ExoJS leads (2.11x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 2/0/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.26249999552965164, + "maxMs": 0.2775000035762787, + "ratio": 1.057142888769812 + }, + "competitor": { + "minMs": 0.5700000002980232, + "maxMs": 0.5799999982118607, + "ratio": 1.0175438559800156 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + } + ] + }, + { + "title": "Particles", + "rows": [ + { + "archetype": "particles-draw", + "category": "Particles", + "count": 1000, + "loadId": "1k", + "unit": "particles", + "primary": false, + "cells": [ + { + "competitor": "phaser", + "referenceMs": 0.06999999284744263, + "referenceP95Ms": 0.125, + "referenceGpuMs": 0.003104, + "referenceOverFrameBudget": false, + "competitorMs": 0.26500000059604645, + "competitorP95Ms": 0.35499998927116394, + "competitorGpuMs": 0.00584, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.26415091581130723, + "factor": 3.7857146810512554, + "label": "ExoJS leads (3.79x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.0650000125169754, + "maxMs": 0.06999999284744263, + "ratio": 1.0769227595019528 + }, + "competitor": { + "minMs": 0.2524999976158142, + "maxMs": 0.2749999910593033, + "ratio": 1.0891088857661038 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 0.06999999284744263, + "referenceP95Ms": 0.125, + "referenceGpuMs": 0.003104, + "referenceOverFrameBudget": false, + "competitorMs": 0.04499998688697815, + "competitorP95Ms": 0.08999998867511749, + "competitorGpuMs": 0.003568, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.555555849899566, + "factor": 1.555555849899566, + "label": "competitor leads (1.56x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.0650000125169754, + "maxMs": 0.06999999284744263, + "ratio": 1.0769227595019528 + }, + "competitor": { + "minMs": 0.040000006556510925, + "maxMs": 0.04500000178813934, + "ratio": 1.1249998603016367 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "particles-draw", + "category": "Particles", + "count": 10000, + "loadId": "10k", + "unit": "particles", + "primary": true, + "cells": [ + { + "competitor": "phaser", + "referenceMs": 0.26749999076128006, + "referenceP95Ms": 0.4050000011920929, + "referenceGpuMs": 0.039279999999999995, + "referenceOverFrameBudget": false, + "competitorMs": 2.165000006556511, + "competitorP95Ms": 2.4000000059604645, + "competitorGpuMs": 0.113344, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.12355657734465589, + "factor": 8.093458247961514, + "label": "ExoJS leads clearly (8.09x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.2549999952316284, + "maxMs": 0.2775000035762787, + "ratio": 1.0882353284917219 + }, + "competitor": { + "minMs": 2.1649999916553497, + "maxMs": 2.185000002384186, + "ratio": 1.009237880279872 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 0.26749999076128006, + "referenceP95Ms": 0.4050000011920929, + "referenceGpuMs": 0.039279999999999995, + "referenceOverFrameBudget": false, + "competitorMs": 0.04500000178813934, + "competitorP95Ms": 0.0949999988079071, + "competitorGpuMs": 0.008752, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 5.9444440029285746, + "factor": 5.9444440029285746, + "label": "competitor leads clearly (5.94x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.2549999952316284, + "maxMs": 0.2775000035762787, + "ratio": 1.0882353284917219 + }, + "competitor": { + "minMs": 0.04500000178813934, + "maxMs": 0.04500000178813934, + "ratio": 1 + }, + "stable": true, + "rungs": [ + "competitor-leads-clearly", + "competitor-leads-clearly", + "competitor-leads-clearly" + ] + } + } + ] + }, + { + "archetype": "particles-draw", + "category": "Particles", + "count": 100000, + "loadId": "100k", + "unit": "particles", + "primary": false, + "cells": [ + { + "competitor": "phaser", + "referenceMs": 1.7849999964237213, + "referenceP95Ms": 2.3599999994039536, + "referenceGpuMs": 0.463248, + "referenceOverFrameBudget": false, + "competitorMs": 20.315000005066395, + "competitorP95Ms": 20.599999994039536, + "competitorGpuMs": 1.283648, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.08786610858865651, + "factor": 11.380952406592634, + "label": "ExoJS leads clearly (11.38x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 7/0/7, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 1.6924999952316284, + "maxMs": 1.8200000002980232, + "ratio": 1.075332351802427 + }, + "competitor": { + "minMs": 19.842500001192093, + "maxMs": 23.372500002384186, + "ratio": 1.1779009701892411 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 1.7849999964237213, + "referenceP95Ms": 2.3599999994039536, + "referenceGpuMs": 0.463248, + "referenceOverFrameBudget": false, + "competitorMs": 0.07500000298023224, + "competitorP95Ms": 0.125, + "competitorGpuMs": 0.054272, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 23.799999006589292, + "factor": 23.799999006589292, + "label": "competitor leads clearly (23.80x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 1.6924999952316284, + "maxMs": 1.8200000002980232, + "ratio": 1.075332351802427 + }, + "competitor": { + "minMs": 0.07000000029802322, + "maxMs": 0.07749999314546585, + "ratio": 1.1071427545073085 + }, + "stable": true, + "rungs": [ + "competitor-leads-clearly", + "competitor-leads-clearly", + "competitor-leads-clearly" + ] + } + } + ] + }, + { + "archetype": "particles-lifecycle", + "category": "Particles", + "count": 10000, + "loadId": "10k", + "unit": "particles", + "primary": true, + "cells": [ + { + "competitor": "phaser", + "referenceMs": 0.6724999994039536, + "referenceP95Ms": 0.8700000047683716, + "referenceGpuMs": 0.038928000000000004, + "referenceOverFrameBudget": false, + "competitorMs": 3.8250000029802322, + "competitorP95Ms": 4.079999998211861, + "competitorGpuMs": 0.120064, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.1758169931712361, + "factor": 5.6877323514801255, + "label": "ExoJS leads clearly (5.69x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.6424999982118607, + "maxMs": 0.6799999997019768, + "ratio": 1.0583657612365482 + }, + "competitor": { + "minMs": 3.4249999970197678, + "maxMs": 3.885000005364418, + "ratio": 1.1343065718963257 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 0.6724999994039536, + "referenceP95Ms": 0.8700000047683716, + "referenceGpuMs": 0.038928000000000004, + "referenceOverFrameBudget": false, + "competitorMs": 0.7450000047683716, + "competitorP95Ms": 1.1449999958276749, + "competitorGpuMs": 0.051808, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 0.9026845571806955, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.6424999982118607, + "maxMs": 0.6799999997019768, + "ratio": 1.0583657612365482 + }, + "competitor": { + "minMs": 0.7100000008940697, + "maxMs": 0.7899999916553497, + "ratio": 1.112676043183859 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + } + ] + } + ] + }, + { + "title": "Interaction", + "rows": [ + { + "archetype": "interaction-picking", + "category": "Interaction", + "count": 10000, + "loadId": "10k", + "unit": "rects", + "primary": true, + "cells": [ + { + "competitor": "phaser", + "referenceMs": 0.24500000476837158, + "referenceP95Ms": 0.39499999582767487, + "referenceGpuMs": 0.056992, + "referenceOverFrameBudget": false, + "competitorMs": 62.15250000357628, + "competitorP95Ms": 63.21500000357628, + "competitorGpuMs": 0.11776, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.003941917135340882, + "factor": 253.68366854660525, + "label": "ExoJS leads clearly (253.68x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.23500000685453415, + "maxMs": 0.2549999952316284, + "ratio": 1.085106331037149 + }, + "competitor": { + "minMs": 62.13000000268221, + "maxMs": 69, + "ratio": 1.1105746015937745 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "pixi", + "referenceMs": 0.24500000476837158, + "referenceP95Ms": 0.39499999582767487, + "referenceGpuMs": 0.056992, + "referenceOverFrameBudget": false, + "competitorMs": 20.21500000357628, + "competitorP95Ms": 21.070000007748604, + "competitorGpuMs": 0.027872, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.012119713318082023, + "factor": 82.510202490355, + "label": "ExoJS leads clearly (82.51x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.23500000685453415, + "maxMs": 0.2549999952316284, + "ratio": 1.085106331037149 + }, + "competitor": { + "minMs": 20.03999999910593, + "maxMs": 20.775000005960464, + "ratio": 1.0366766470502657 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + } + ] + }, + { + "archetype": "ui-layout-update", + "category": "Interaction", + "count": 1000, + "loadId": "1k", + "unit": "widgets", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 11.582499995827675, + "referenceP95Ms": 11.939999997615814, + "referenceGpuMs": 0.001408, + "referenceOverFrameBudget": false, + "competitorMs": 82.75249999016523, + "competitorP95Ms": 84.2749999910593, + "competitorGpuMs": 0.001408, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.13996555991908646, + "factor": 7.144614722208064, + "label": "ExoJS leads clearly (7.14x)", + "structural": true + }, + "mechanism": "neither arm submits anything for this scene (draw/bind/upload per frame: 0/0/0 vs 0/0/0); the whole difference is the per-frame work itself", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 11.155000001192093, + "maxMs": 11.700000002980232, + "ratio": 1.0488570149466514 + }, + "competitor": { + "minMs": 82.23750000447035, + "maxMs": 82.75249999761581, + "ratio": 1.0062623498175098 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + } + ] + } + ] + } + ], + "excluded": [ + { + "archetype": "overdraw", + "reason": "not measured in this run" + }, + { + "archetype": "split-screen", + "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" + }, + { + "archetype": "mixed-material", + "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" + }, + { + "archetype": "instanced-batch", + "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" + }, + { + "archetype": "mixed-sprite-mesh-static", + "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" + }, + { + "archetype": "mixed-sprite-mesh-array", + "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" + }, + { + "archetype": "mixed-material-atlased", + "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" + } + ], + "webgl1": [] + }, + { + "backend": "webgpu", + "headlineCount": null, + "competitors": [ + "pixi" + ], + "sections": [ + { + "title": "Node scaling", + "rows": [ + { + "archetype": "static-heavy", + "category": "Node scaling", + "count": 1000, + "loadId": "1k", + "unit": "sprites", + "primary": false, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.11500000953674316, + "referenceP95Ms": 0.17499999701976776, + "referenceGpuMs": 0.004288, + "referenceOverFrameBudget": false, + "competitorMs": 0.125, + "competitorP95Ms": 0.18000000715255737, + "competitorGpuMs": 0.00528, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 0.9200000762939453, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.11500000208616257, + "maxMs": 0.12999999523162842, + "ratio": 1.1304347206378942 + }, + "competitor": { + "minMs": 0.125, + "maxMs": 0.125, + "ratio": 1 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + } + ] + }, + { + "archetype": "static-heavy", + "category": "Node scaling", + "count": 10000, + "loadId": "10k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.16499999910593033, + "referenceP95Ms": 0.24000000953674316, + "referenceGpuMs": 0.012032, + "referenceOverFrameBudget": false, + "competitorMs": 0.13500000536441803, + "competitorP95Ms": 0.19500000774860382, + "competitorGpuMs": 0.011712, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.1574999988079071, + "maxMs": 0.16999999433755875, + "ratio": 1.0793650515826168 + }, + "competitor": { + "minMs": 0.1300000101327896, + "maxMs": 0.14000000059604645, + "ratio": 1.0769229975677868 + }, + "stable": false, + "rungs": [ + "level", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "static-heavy", + "category": "Node scaling", + "count": 100000, + "loadId": "100k", + "unit": "sprites", + "primary": false, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.10999999940395355, + "referenceP95Ms": 0.17000000178813934, + "referenceGpuMs": 0.09272, + "referenceOverFrameBudget": false, + "competitorMs": 0.14750000089406967, + "competitorP95Ms": 0.26500000059604645, + "competitorGpuMs": 0.07353599999999999, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.7457627033029812, + "factor": 1.3409091063028526, + "label": "ExoJS leads (1.34x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.10250000655651093, + "maxMs": 0.11249999701976776, + "ratio": 1.0975608763277842 + }, + "competitor": { + "minMs": 0.14000000059604645, + "maxMs": 0.1599999964237213, + "ratio": 1.14285711244661 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + }, + { + "archetype": "dynamic-heavy", + "category": "Node scaling", + "count": 10000, + "loadId": "10k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.9199999943375587, + "referenceP95Ms": 1.1500000059604645, + "referenceGpuMs": 0.012512, + "referenceOverFrameBudget": false, + "competitorMs": 0.5425000041723251, + "competitorP95Ms": 0.6650000065565109, + "competitorGpuMs": 0.011552, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.695852511081863, + "factor": 1.695852511081863, + "label": "competitor leads (1.70x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.8449999988079071, + "maxMs": 0.9224999994039536, + "ratio": 1.0917159771661307 + }, + "competitor": { + "minMs": 0.5324999988079071, + "maxMs": 0.5549999922513962, + "ratio": 1.0422535089086558 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "dynamic-all", + "category": "Node scaling", + "count": 1000, + "loadId": "1k", + "unit": "sprites", + "primary": false, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.8149999976158142, + "referenceP95Ms": 0.9849999994039536, + "referenceGpuMs": 0.005024, + "referenceOverFrameBudget": false, + "competitorMs": 0.3374999910593033, + "competitorP95Ms": 0.44999998807907104, + "competitorGpuMs": 0.005216, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 2.4148148717213083, + "factor": 2.4148148717213083, + "label": "competitor leads (2.41x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.8100000023841858, + "maxMs": 0.8199999928474426, + "ratio": 1.01234566720225 + }, + "competitor": { + "minMs": 0.3199999928474426, + "maxMs": 0.3374999985098839, + "ratio": 1.0546875189174902 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "dynamic-all", + "category": "Node scaling", + "count": 10000, + "loadId": "10k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 9.604999996721745, + "referenceP95Ms": 10.679999992251396, + "referenceGpuMs": 0.012608, + "referenceOverFrameBudget": false, + "competitorMs": 2.552499994635582, + "competitorP95Ms": 3.2100000083446503, + "competitorGpuMs": 0.011552, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 3.762977479689688, + "factor": 3.762977479689688, + "label": "competitor leads (3.76x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 9.462500005960464, + "maxMs": 9.655000001192093, + "ratio": 1.0203434605136457 + }, + "competitor": { + "minMs": 2.5150000005960464, + "maxMs": 2.649999998509884, + "ratio": 1.053677931563357 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "dynamic-all", + "category": "Node scaling", + "count": 100000, + "loadId": "100k", + "unit": "sprites", + "primary": false, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 96.9050000011921, + "referenceP95Ms": 98.3200000077486, + "referenceGpuMs": 0.09304, + "referenceOverFrameBudget": true, + "competitorMs": 32.82000000029802, + "competitorP95Ms": 34.10999999940395, + "competitorGpuMs": 0.073728, + "competitorOverFrameBudget": true, + "verdict": { + "side": "competitor", + "ratio": 2.9526203534525335, + "factor": 2.9526203534525335, + "label": "competitor leads (2.95x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 95.46500000357628, + "maxMs": 97.50999999791384, + "ratio": 1.0214214633034195 + }, + "competitor": { + "minMs": 32.73249999433756, + "maxMs": 37.75500000268221, + "ratio": 1.153440770158512 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "deep-hierarchy", + "category": "Node scaling", + "count": 10000, + "loadId": "10k", + "unit": "nodes", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.24499999731779099, + "referenceP95Ms": 0.33500000834465027, + "referenceGpuMs": 0.015136, + "referenceOverFrameBudget": false, + "competitorMs": 0.21249999850988388, + "competitorP95Ms": 0.28999999165534973, + "competitorGpuMs": 0.01456, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.23499999940395355, + "maxMs": 0.2524999976158142, + "ratio": 1.074468077686158 + }, + "competitor": { + "minMs": 0.21000000089406967, + "maxMs": 0.2200000137090683, + "ratio": 1.0476191084401134 + }, + "stable": false, + "rungs": [ + "level", + "level", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "lifecycle-churn", + "category": "Node scaling", + "count": 5000, + "loadId": "5k", + "unit": "nodes", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 2.7899999916553497, + "referenceP95Ms": 3.0850000083446503, + "referenceGpuMs": 0.009952, + "referenceOverFrameBudget": false, + "competitorMs": 1.9400000050663948, + "competitorP95Ms": 2.2749999910593033, + "competitorGpuMs": 0.008672, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.4381443218397643, + "factor": 1.4381443218397643, + "label": "competitor leads (1.44x)", + "structural": false + }, + "mechanism": "competitor issues fewer buffer uploads (13 vs 3 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 2.787500001490116, + "maxMs": 2.9375, + "ratio": 1.0538116586294881 + }, + "competitor": { + "minMs": 1.8725000023841858, + "maxMs": 2.2050000056624413, + "ratio": 1.1775700949825876 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + } + ] + }, + { + "title": "Fill and GPU state", + "rows": [ + { + "archetype": "fill-layers", + "category": "Fill and GPU state", + "count": 32, + "loadId": "32", + "unit": "layers", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.08500000834465027, + "referenceP95Ms": 0.12999999523162842, + "referenceGpuMs": 0.140064, + "referenceOverFrameBudget": false, + "competitorMs": 0.09999999403953552, + "competitorP95Ms": 0.14000000059604645, + "competitorGpuMs": 0.141584, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 0.8500001341104587, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.08499999344348907, + "maxMs": 0.08500000834465027, + "ratio": 1.0000001753077923 + }, + "competitor": { + "minMs": 0.0949999988079071, + "maxMs": 0.09999999403953552, + "ratio": 1.052631529414422 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + } + ] + }, + { + "archetype": "batch-breaking", + "category": "Fill and GPU state", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.7374999970197678, + "referenceP95Ms": 0.9849999994039536, + "referenceGpuMs": 0.010336, + "referenceOverFrameBudget": false, + "competitorMs": 0.44999999552965164, + "competitorP95Ms": 0.5450000017881393, + "competitorGpuMs": 0.008832, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.6388888985470491, + "factor": 1.6388888985470491, + "label": "competitor leads (1.64x)", + "structural": false + }, + "mechanism": "competitor issues fewer texture binds (626 vs 314 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.7149999961256981, + "maxMs": 0.7375000044703484, + "ratio": 1.0314685433098867 + }, + "competitor": { + "minMs": 0.4399999976158142, + "maxMs": 0.4699999988079071, + "ratio": 1.0681818212605705 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "mixed-blend", + "category": "Fill and GPU state", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.22500000894069672, + "referenceP95Ms": 0.28999999165534973, + "referenceGpuMs": 0.018432, + "referenceOverFrameBudget": false, + "competitorMs": 0.2850000038743019, + "competitorP95Ms": 0.3400000035762787, + "competitorGpuMs": 0.023280000000000002, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "competitor issues fewer texture binds (240 vs 159 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.2199999988079071, + "maxMs": 0.22500000894069672, + "ratio": 1.0227273189085577 + }, + "competitor": { + "minMs": 0.2800000011920929, + "maxMs": 0.28999999165534973, + "ratio": 1.0357142515024362 + }, + "stable": false, + "rungs": [ + "level", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + } + ] + }, + { + "title": "Material variety", + "rows": [ + { + "archetype": "batch-breaking-atlased", + "category": "Material variety", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.08499999344348907, + "referenceP95Ms": 0.14499999582767487, + "referenceGpuMs": 0.009184, + "referenceOverFrameBudget": false, + "competitorMs": 0.0950000062584877, + "competitorP95Ms": 0.14000000059604645, + "competitorGpuMs": 0.008768, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 0.8947367141451616, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.08249999582767487, + "maxMs": 0.08999998867511749, + "ratio": 1.0909090088090252 + }, + "competitor": { + "minMs": 0.0949999988079071, + "maxMs": 0.09999999403953552, + "ratio": 1.052631529414422 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + } + ] + } + ] + }, + { + "title": "Text", + "rows": [ + { + "archetype": "text-static", + "category": "Text", + "count": 1000, + "loadId": "1k", + "unit": "labels", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.10500000417232513, + "referenceP95Ms": 0.1600000113248825, + "referenceGpuMs": 0.15051199999999998, + "referenceOverFrameBudget": false, + "competitorMs": 0.11499999463558197, + "competitorP95Ms": 0.2199999988079071, + "competitorGpuMs": 0.014304, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 0.9130435571328039, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.10499998927116394, + "maxMs": 0.10999999940395355, + "ratio": 1.0476191489875015 + }, + "competitor": { + "minMs": 0.10499998927116394, + "maxMs": 0.11500000953674316, + "ratio": 1.095238297975003 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + } + ] + }, + { + "archetype": "text-static", + "category": "Text", + "count": 5000, + "loadId": "5k", + "unit": "labels", + "primary": false, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.10500000417232513, + "referenceP95Ms": 0.20000000298023224, + "referenceGpuMs": 0.726528, + "referenceOverFrameBudget": false, + "competitorMs": 0.10500000417232513, + "competitorP95Ms": 0.1599999964237213, + "competitorGpuMs": 0.051744, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 1, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.10499999672174454, + "maxMs": 0.10750000178813934, + "ratio": 1.023809572804273 + }, + "competitor": { + "minMs": 0.10500000417232513, + "maxMs": 0.10500000417232513, + "ratio": 1 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + } + ] + }, + { + "archetype": "text-dynamic", + "category": "Text", + "count": 1000, + "loadId": "1k", + "unit": "labels", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 3.0550000071525574, + "referenceP95Ms": 3.5499999970197678, + "referenceGpuMs": 0.150688, + "referenceOverFrameBudget": false, + "competitorMs": 6.450000002980232, + "competitorP95Ms": 7.655000001192093, + "competitorGpuMs": 0.014144, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.4736434117427891, + "factor": 2.111292958389227, + "label": "ExoJS leads (2.11x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/3 vs 1/2/3, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 3.002500005066395, + "maxMs": 3.0900000035762787, + "ratio": 1.0291423808034095 + }, + "competitor": { + "minMs": 6.41499999910593, + "maxMs": 6.465000003576279, + "ratio": 1.0077942329660665 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + }, + { + "archetype": "text-dynamic", + "category": "Text", + "count": 5000, + "loadId": "5k", + "unit": "labels", + "primary": false, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 18.1875, + "referenceP95Ms": 20.984999999403954, + "referenceGpuMs": 0.726208, + "referenceOverFrameBudget": true, + "competitorMs": 33.28000000119209, + "competitorP95Ms": 33.99000000953674, + "competitorGpuMs": 0.051712, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.5464993990188859, + "factor": 1.8298281787597026, + "label": "ExoJS leads (1.83x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/3 vs 1/2/3, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 17.655000001192093, + "maxMs": 18.612499997019768, + "ratio": 1.0542339278257165 + }, + "competitor": { + "minMs": 33.0625, + "maxMs": 33.742499992251396, + "ratio": 1.0205671075161102 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + } + ] + }, + { + "title": "Render targets", + "rows": [ + { + "archetype": "filter-chain-1", + "category": "Render targets", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.23250000178813934, + "referenceP95Ms": 0.3199999928474426, + "referenceGpuMs": 0.024704, + "referenceOverFrameBudget": false, + "competitorMs": 0.7150000035762787, + "competitorP95Ms": 0.8700000047683716, + "competitorGpuMs": 0.016128, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.3251748260492637, + "factor": 3.0752688089344926, + "label": "ExoJS leads (3.08x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/6/3 vs 2/4/2, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.22750000655651093, + "maxMs": 0.26499999314546585, + "ratio": 1.1648351011349969 + }, + "competitor": { + "minMs": 0.7150000035762787, + "maxMs": 0.7349999994039536, + "ratio": 1.0279720219966981 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + }, + { + "archetype": "filter-chain-2", + "category": "Render targets", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.23499999940395355, + "referenceP95Ms": 0.3050000071525574, + "referenceGpuMs": 0.03008, + "referenceOverFrameBudget": false, + "competitorMs": 0.7025000005960464, + "competitorP95Ms": 0.8250000029802322, + "competitorGpuMs": 0.02144, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.33451957182144393, + "factor": 2.9893617122461484, + "label": "ExoJS leads (2.99x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 4/8/3 vs 3/6/3, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.22999999672174454, + "maxMs": 0.24499999731779099, + "ratio": 1.0652173948254162 + }, + "competitor": { + "minMs": 0.6174999997019768, + "maxMs": 0.7325000017881393, + "ratio": 1.1862348212820484 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + }, + { + "archetype": "filter-chain-4", + "category": "Render targets", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.2749999910593033, + "referenceP95Ms": 0.3450000137090683, + "referenceGpuMs": 0.041104, + "referenceOverFrameBudget": false, + "competitorMs": 0.7124999985098839, + "competitorP95Ms": 0.8400000035762787, + "competitorGpuMs": 0.031712, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.385964900539559, + "factor": 2.590909169725153, + "label": "ExoJS leads (2.59x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 6/12/3 vs 5/10/5, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.26999999582767487, + "maxMs": 0.2750000059604645, + "ratio": 1.0185185563335373 + }, + "competitor": { + "minMs": 0.6800000071525574, + "maxMs": 0.760000005364418, + "ratio": 1.1176470549564461 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + }, + { + "archetype": "composite", + "category": "Render targets", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.24500000476837158, + "referenceP95Ms": 0.3149999976158142, + "referenceGpuMs": 0.03304, + "referenceOverFrameBudget": false, + "competitorMs": 0.35249999910593033, + "competitorP95Ms": 0.5049999952316284, + "competitorGpuMs": 0.044896000000000005, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.6950354762830687, + "factor": 1.438775478552303, + "label": "ExoJS leads (1.44x)", + "structural": false + }, + "mechanism": "ExoJS issues fewer draw calls (5 vs 8 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.24249999970197678, + "maxMs": 0.2475000023841858, + "ratio": 1.020618567787026 + }, + "competitor": { + "minMs": 0.3499999940395355, + "maxMs": 0.35749999433755875, + "ratio": 1.0214285726449928 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + }, + { + "archetype": "mask-clip", + "category": "Render targets", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.23499999940395355, + "referenceP95Ms": 0.29999999701976776, + "referenceGpuMs": 0.024944, + "referenceOverFrameBudget": false, + "competitorMs": 0.1525000035762787, + "competitorP95Ms": 0.2150000035762787, + "competitorGpuMs": 0.01232, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.5409835665112581, + "factor": 1.5409835665112581, + "label": "competitor leads (1.54x)", + "structural": false + }, + "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.2199999988079071, + "maxMs": 0.23499999940395355, + "ratio": 1.0681818212605705 + }, + "competitor": { + "minMs": 0.14999999850988388, + "maxMs": 0.1600000038743019, + "ratio": 1.0666667030917276 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "fx-blur", + "category": "Render targets", + "count": 720, + "loadId": "720", + "unit": "viewport", + "primary": true, + "label": "1280 x 720", + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.24500000476837158, + "referenceP95Ms": 0.3199999928474426, + "referenceGpuMs": 0.04704, + "referenceOverFrameBudget": false, + "competitorMs": 0.1899999976158142, + "competitorP95Ms": 0.2750000059604645, + "competitorGpuMs": 0.035232, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.2894737254879816, + "factor": 1.2894737254879816, + "label": "competitor leads (1.29x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 4/8/5 vs 3/6/3, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.23499999940395355, + "maxMs": 0.25, + "ratio": 1.0638297899323061 + }, + "competitor": { + "minMs": 0.1899999976158142, + "maxMs": 0.1899999976158142, + "ratio": 1 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "mask-clip-animated", + "category": "Render targets", + "count": 5000, + "loadId": "5k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.22499999403953552, + "referenceP95Ms": 0.2849999964237213, + "referenceGpuMs": 0.024944, + "referenceOverFrameBudget": false, + "competitorMs": 0.1850000023841858, + "competitorP95Ms": 0.24500000476837158, + "competitorGpuMs": 0.012288, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.2199999988079071, + "maxMs": 0.23000000417232513, + "ratio": 1.045454570084564 + }, + "competitor": { + "minMs": 0.1850000023841858, + "maxMs": 0.19750000536441803, + "ratio": 1.0675675828061544 + }, + "stable": false, + "rungs": [ + "competitor-leads", + "level", + "level" + ] + } + } + ] + } + ] + }, + { + "title": "Camera and world", + "rows": [ + { + "archetype": "scrolling-world", + "category": "Camera and world", + "count": 10000, + "loadId": "10k", + "unit": "sprites", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.12999999523162842, + "referenceP95Ms": 0.32500000298023224, + "referenceGpuMs": 0.00648, + "referenceOverFrameBudget": false, + "competitorMs": 0.10499999672174454, + "competitorP95Ms": 0.14500001072883606, + "competitorGpuMs": 0.006688, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.11499999463558197, + "maxMs": 0.13500000536441803, + "ratio": 1.1739131448850337 + }, + "competitor": { + "minMs": 0.10499998927116394, + "maxMs": 0.10500000417232513, + "ratio": 1.0000001419158353 + }, + "stable": false, + "rungs": [ + "competitor-leads", + "level", + "competitor-leads" + ] + } + } + ] + } + ] + }, + { + "title": "Tilemaps", + "rows": [ + { + "archetype": "tilemap-scroll", + "category": "Tilemaps", + "count": 10000, + "loadId": "10k", + "unit": "tiles", + "primary": false, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.1549999937415123, + "referenceP95Ms": 0.4650000035762787, + "referenceGpuMs": 0.008128, + "referenceOverFrameBudget": false, + "competitorMs": 0.1850000023841858, + "competitorP95Ms": 0.2550000101327896, + "competitorGpuMs": 0.007552, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "ExoJS issues fewer buffer uploads (326 vs 652 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.14500000327825546, + "maxMs": 0.16500000655651093, + "ratio": 1.137931053972981 + }, + "competitor": { + "minMs": 0.1850000023841858, + "maxMs": 0.1899999976158142, + "ratio": 1.0270270009037352 + }, + "stable": false, + "rungs": [ + "exojs-leads", + "level", + "level" + ] + } + } + ] + }, + { + "archetype": "tilemap-scroll", + "category": "Tilemaps", + "count": 100000, + "loadId": "100k", + "unit": "tiles", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.13499999791383743, + "referenceP95Ms": 0.29500000178813934, + "referenceGpuMs": 0.007616, + "referenceOverFrameBudget": false, + "competitorMs": 0.18000000715255737, + "competitorP95Ms": 0.24500000476837158, + "competitorGpuMs": 0.00712, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.7499999586078873, + "factor": 1.3333334069193157, + "label": "ExoJS leads (1.33x)", + "structural": false + }, + "mechanism": "competitor issues fewer draw calls (60 vs 2 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.1275000050663948, + "maxMs": 0.14249999821186066, + "ratio": 1.1176470003876056 + }, + "competitor": { + "minMs": 0.17749999463558197, + "maxMs": 0.1849999949336052, + "ratio": 1.0422535240827537 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + }, + { + "archetype": "tilemap-edit", + "category": "Tilemaps", + "count": 100000, + "loadId": "100k", + "unit": "tiles", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.32249999046325684, + "referenceP95Ms": 0.3799999952316284, + "referenceGpuMs": 0.007408, + "referenceOverFrameBudget": false, + "competitorMs": 0.7725000008940697, + "competitorP95Ms": 0.9800000041723251, + "competitorGpuMs": 0.00592, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.4174757153268666, + "factor": 2.3953489108151844, + "label": "ExoJS leads (2.40x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/4/2 vs 2/4/3, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.3174999952316284, + "maxMs": 0.32500000298023224, + "ratio": 1.0236220720039138 + }, + "competitor": { + "minMs": 0.7674999982118607, + "maxMs": 0.7725000008940697, + "ratio": 1.0065146614903688 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + } + ] + }, + { + "title": "Particles", + "rows": [ + { + "archetype": "particles-draw", + "category": "Particles", + "count": 1000, + "loadId": "1k", + "unit": "particles", + "primary": false, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.14499999582767487, + "referenceP95Ms": 0.20499999821186066, + "referenceGpuMs": 0.003104, + "referenceOverFrameBudget": false, + "competitorMs": 0.1300000101327896, + "competitorP95Ms": 0.1850000023841858, + "competitorGpuMs": 0.004672, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.14499999582767487, + "maxMs": 0.1599999964237213, + "ratio": 1.103448282949423 + }, + "competitor": { + "minMs": 0.125, + "maxMs": 0.1300000101327896, + "ratio": 1.040000081062317 + }, + "stable": false, + "rungs": [ + "level", + "competitor-leads", + "level" + ] + } + } + ] + }, + { + "archetype": "particles-draw", + "category": "Particles", + "count": 10000, + "loadId": "10k", + "unit": "particles", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.317500002682209, + "referenceP95Ms": 0.42499999701976776, + "referenceGpuMs": 0.010208, + "referenceOverFrameBudget": false, + "competitorMs": 0.125, + "competitorP95Ms": 0.17999999225139618, + "competitorGpuMs": 0.007968, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 2.540000021457672, + "factor": 2.540000021457672, + "label": "competitor leads (2.54x)", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.3100000023841858, + "maxMs": 0.32999999821186066, + "ratio": 1.0645161150769564 + }, + "competitor": { + "minMs": 0.125, + "maxMs": 0.12999999523162842, + "ratio": 1.0399999618530273 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "particles-draw", + "category": "Particles", + "count": 100000, + "loadId": "100k", + "unit": "particles", + "primary": false, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 2.002500005066395, + "referenceP95Ms": 3.054999992251396, + "referenceGpuMs": 0.089712, + "referenceOverFrameBudget": false, + "competitorMs": 0.14750000089406967, + "competitorP95Ms": 0.21000000834465027, + "competitorGpuMs": 0.052944000000000005, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 13.576271138496695, + "factor": 13.576271138496695, + "label": "competitor leads clearly (13.58x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 1.987500011920929, + "maxMs": 2.065000005066395, + "ratio": 1.0389937070091193 + }, + "competitor": { + "minMs": 0.14500000327825546, + "maxMs": 0.1550000011920929, + "ratio": 1.0689655012948338 + }, + "stable": true, + "rungs": [ + "competitor-leads-clearly", + "competitor-leads-clearly", + "competitor-leads-clearly" + ] + } + } + ] + }, + { + "archetype": "particles-lifecycle", + "category": "Particles", + "count": 10000, + "loadId": "10k", + "unit": "particles", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.33249999582767487, + "referenceP95Ms": 0.4049999862909317, + "referenceGpuMs": 0.009728, + "referenceOverFrameBudget": false, + "competitorMs": 0.9549999982118607, + "competitorP95Ms": 1.155000001192093, + "competitorGpuMs": 0.007712, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.3481675355499968, + "factor": 2.872180481791072, + "label": "ExoJS leads (2.87x)", + "structural": false + }, + "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.3200000077486038, + "maxMs": 0.3400000035762787, + "ratio": 1.0624999854480852 + }, + "competitor": { + "minMs": 0.9299999922513962, + "maxMs": 0.9549999982118607, + "ratio": 1.0268817270631831 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] + } + ] + }, + { + "title": "Interaction", + "rows": [ + { + "archetype": "interaction-picking", + "category": "Interaction", + "count": 10000, + "loadId": "10k", + "unit": "rects", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.29250000417232513, + "referenceP95Ms": 0.42499999701976776, + "referenceGpuMs": 0.026112, + "referenceOverFrameBudget": false, + "competitorMs": 22.832499995827675, + "competitorP95Ms": 24.980000004172325, + "competitorGpuMs": 0.02608, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.012810686706483105, + "factor": 78.05982793209125, + "label": "ExoJS leads clearly (78.06x)", + "structural": true + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.2900000065565109, + "maxMs": 0.29500000178813934, + "ratio": 1.0172413624778802 + }, + "competitor": { + "minMs": 22.097500003874302, + "maxMs": 23.93749999254942, + "ratio": 1.0832673374070603 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + } + ] + }, + { + "archetype": "ui-layout-update", + "category": "Interaction", + "count": 1000, + "loadId": "1k", + "unit": "widgets", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 11.46000000089407, + "referenceP95Ms": 12.179999992251396, + "referenceGpuMs": 0.001472, + "referenceOverFrameBudget": false, + "competitorMs": 82.41750000417233, + "competitorP95Ms": 83.31499999761581, + "competitorGpuMs": 0.001472, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.1390481390519479, + "factor": 7.191753926504571, + "label": "ExoJS leads clearly (7.19x)", + "structural": true + }, + "mechanism": "neither arm submits anything for this scene (draw/bind/upload per frame: 0/0/0 vs 0/0/1); the whole difference is the per-frame work itself", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 11.407499998807907, + "maxMs": 11.760000005364418, + "ratio": 1.0309007237863992 + }, + "competitor": { + "minMs": 82.07000000029802, + "maxMs": 82.75000000745058, + "ratio": 1.0082856099323758 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + } + ] + } + ] + } + ], + "excluded": [ + { + "archetype": "overdraw", + "reason": "not measured in this run" + }, + { + "archetype": "split-screen", + "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" + }, + { + "archetype": "mixed-material", + "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" + }, { "archetype": "instanced-batch", "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" @@ -2621,8 +5246,1321 @@ } ] }, + "physics": { + "runs": [ + { + "browser": "chromium", + "browserVersion": "151.0.7922.34", + "host": { + "cpu": "AMD Ryzen 7 3700X 8-Core Processor", + "cpuCount": 16, + "os": "win32 10.0.26200", + "platformVersion": { + "major": 11, + "source": "detected", + "evidence": "os.release() reported '10.0.26200'; build 26200 is Windows 11 (Windows 11 starts at build 22000)" + }, + "arch": "x64" + }, + "prerelease": { + "value": false, + "source": "assumed-stable", + "evidence": "browser version '151.0.7922.34' carries no pre-release marker and none was declared; the operating system's own release status cannot be read at runtime" + }, + "fixedDelta": 0.016666666666666666, + "clock": { + "resolutionMs": 0.004999995231628418, + "crossOriginIsolated": true + }, + "caveats": [ + "Step time is CPU wall-clock per step() over the timed window (median/p95), measured in one chromium page (same-run discipline). No number here was taken in Node.", + "The page's performance.now() resolves to 5.0us (cross-origin isolated: true). A cell whose step is too fast to time individually at that resolution batches steps per timing sample and records the batch as stepsPerSample; median and p95 are then per-step averages over that batch.", + "Scenes are warmed to steady state before timing; the per-cell warmupSteps/timedSteps counts are recorded for honesty.", + "All arms build the byte-identical scene (bodies, positions, shapes, sizes, static/dynamic split, gravity, perturbed-body set) from the shared deterministic RNG, and the perturbed-body selection is asserted equal across arms before each cell is timed.", + "Each arm runs at its own engine defaults for solver iterations, contact model and sleeping - those engine differences are the measured quantity in a native-vs-adapter comparison, disclosed per arm below.", + "Arm roles: matter-js, planck and nape-js are the JAVASCRIPT PEERS exojs-physics is compared against; rapier is a Rust/WASM engine and stands as the REFERENCE CEILING for what leaving JavaScript buys, not as a peer a JS solver is expected to match.", + "exojs-physics arm (native runtime, pure JS): TGS-Soft solver, 4 sub-steps per fixed step, sleeping ON by default (resting bodies deactivate). Contact count = solid contacts in the world contact graph.", + "matter-js arm (pure-JS peer): constraint solver at matter defaults (6 position / 4 velocity / 2 constraint iterations), sleeping OFF by default (a settled stack keeps paying full solve cost); matter's default per-step air drag (frictionAir) is zeroed so all arms integrate the same pure-gravity field; gravity (px/s^2) and perturbation velocity (px/s) are mapped into matter's px-per-step unit model. Contact count = active colliding pairs (engine.pairs.collisionActive), a pair-level proxy, not identical in semantics to the exojs solid-contact count.", + "planck arm (pure-JS peer): Box2D port at planck defaults (8 velocity / 3 position iterations per step), sleeping ON by default; Settings.lengthUnitsPerMeter is set to 30 so planck's absolute MKS tolerances are interpreted at the scene's pixel scale, which is the knob planck gives a pixel-coordinate game - positions, gravity (px/s^2) and velocity (px/s) then carry over unconverted. Contact count = the world contact list filtered by isTouching(), a touching collider-pair count. Rays are answered from planck's dynamic tree, but World.rayCast is Box2D's non-solid ray (an origin inside a fixture is not a hit). Continuous collision runs for every body (planck's default), where exojs and rapier restrict it to bullets and matter has none.", + "rapier arm (WASM reference ceiling, not a pure-JS peer): TGS-Soft solver at rapier defaults (4 solver / 1 internal PGS iterations), auto-sleeping ON; default lengthUnit=1 is fed a px-scale world (tuned for ~1-unit objects), exactly what attaching rapier with pixel coordinates yields. Contact count = collider pairs with a solid narrow-phase manifold (numContacts > 0), deduped.", + "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." + ], + "engineVersion": "0.17.0", + "timestamp": "2026-09-12T02:04:51.251Z" + }, + { + "browser": "chromium", + "browserVersion": "151.0.7922.34", + "host": { + "cpu": "AMD Ryzen 7 3700X 8-Core Processor", + "cpuCount": 16, + "os": "win32 10.0.26200", + "platformVersion": { + "major": 11, + "source": "detected", + "evidence": "os.release() reported '10.0.26200'; build 26200 is Windows 11 (Windows 11 starts at build 22000)" + }, + "arch": "x64" + }, + "prerelease": { + "value": false, + "source": "assumed-stable", + "evidence": "browser version '151.0.7922.34' carries no pre-release marker and none was declared; the operating system's own release status cannot be read at runtime" + }, + "fixedDelta": 0.016666666666666666, + "clock": { + "resolutionMs": 0.004999995231628418, + "crossOriginIsolated": true + }, + "caveats": [ + "Step time is CPU wall-clock per step() over the timed window (median/p95), measured in one chromium page (same-run discipline). No number here was taken in Node.", + "The page's performance.now() resolves to 5.0us (cross-origin isolated: true). A cell whose step is too fast to time individually at that resolution batches steps per timing sample and records the batch as stepsPerSample; median and p95 are then per-step averages over that batch.", + "Scenes are warmed to steady state before timing; the per-cell warmupSteps/timedSteps counts are recorded for honesty.", + "All arms build the byte-identical scene (bodies, positions, shapes, sizes, static/dynamic split, gravity, perturbed-body set) from the shared deterministic RNG, and the perturbed-body selection is asserted equal across arms before each cell is timed.", + "Each arm runs at its own engine defaults for solver iterations, contact model and sleeping - those engine differences are the measured quantity in a native-vs-adapter comparison, disclosed per arm below.", + "Arm roles: matter-js, planck and nape-js are the JAVASCRIPT PEERS exojs-physics is compared against; rapier is a Rust/WASM engine and stands as the REFERENCE CEILING for what leaving JavaScript buys, not as a peer a JS solver is expected to match.", + "exojs-physics arm (native runtime, pure JS): TGS-Soft solver, 4 sub-steps per fixed step, sleeping ON by default (resting bodies deactivate). Contact count = solid contacts in the world contact graph.", + "matter-js arm (pure-JS peer): constraint solver at matter defaults (6 position / 4 velocity / 2 constraint iterations), sleeping OFF by default (a settled stack keeps paying full solve cost); matter's default per-step air drag (frictionAir) is zeroed so all arms integrate the same pure-gravity field; gravity (px/s^2) and perturbation velocity (px/s) are mapped into matter's px-per-step unit model. Contact count = active colliding pairs (engine.pairs.collisionActive), a pair-level proxy, not identical in semantics to the exojs solid-contact count.", + "planck arm (pure-JS peer): Box2D port at planck defaults (8 velocity / 3 position iterations per step), sleeping ON by default; Settings.lengthUnitsPerMeter is set to 30 so planck's absolute MKS tolerances are interpreted at the scene's pixel scale, which is the knob planck gives a pixel-coordinate game - positions, gravity (px/s^2) and velocity (px/s) then carry over unconverted. Contact count = the world contact list filtered by isTouching(), a touching collider-pair count. Rays are answered from planck's dynamic tree, but World.rayCast is Box2D's non-solid ray (an origin inside a fixture is not a hit). Continuous collision runs for every body (planck's default), where exojs and rapier restrict it to bullets and matter has none.", + "rapier arm (WASM reference ceiling, not a pure-JS peer): TGS-Soft solver at rapier defaults (4 solver / 1 internal PGS iterations), auto-sleeping ON; default lengthUnit=1 is fed a px-scale world (tuned for ~1-unit objects), exactly what attaching rapier with pixel coordinates yields. Contact count = collider pairs with a solid narrow-phase manifold (numContacts > 0), deduped.", + "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." + ], + "engineVersion": "0.17.0", + "timestamp": "2026-09-12T02:17:06.299Z" + }, + { + "browser": "chromium", + "browserVersion": "151.0.7922.34", + "host": { + "cpu": "AMD Ryzen 7 3700X 8-Core Processor", + "cpuCount": 16, + "os": "win32 10.0.26200", + "platformVersion": { + "major": 11, + "source": "detected", + "evidence": "os.release() reported '10.0.26200'; build 26200 is Windows 11 (Windows 11 starts at build 22000)" + }, + "arch": "x64" + }, + "prerelease": { + "value": false, + "source": "assumed-stable", + "evidence": "browser version '151.0.7922.34' carries no pre-release marker and none was declared; the operating system's own release status cannot be read at runtime" + }, + "fixedDelta": 0.016666666666666666, + "clock": { + "resolutionMs": 0.004999995231628418, + "crossOriginIsolated": true + }, + "caveats": [ + "Step time is CPU wall-clock per step() over the timed window (median/p95), measured in one chromium page (same-run discipline). No number here was taken in Node.", + "The page's performance.now() resolves to 5.0us (cross-origin isolated: true). A cell whose step is too fast to time individually at that resolution batches steps per timing sample and records the batch as stepsPerSample; median and p95 are then per-step averages over that batch.", + "Scenes are warmed to steady state before timing; the per-cell warmupSteps/timedSteps counts are recorded for honesty.", + "All arms build the byte-identical scene (bodies, positions, shapes, sizes, static/dynamic split, gravity, perturbed-body set) from the shared deterministic RNG, and the perturbed-body selection is asserted equal across arms before each cell is timed.", + "Each arm runs at its own engine defaults for solver iterations, contact model and sleeping - those engine differences are the measured quantity in a native-vs-adapter comparison, disclosed per arm below.", + "Arm roles: matter-js, planck and nape-js are the JAVASCRIPT PEERS exojs-physics is compared against; rapier is a Rust/WASM engine and stands as the REFERENCE CEILING for what leaving JavaScript buys, not as a peer a JS solver is expected to match.", + "exojs-physics arm (native runtime, pure JS): TGS-Soft solver, 4 sub-steps per fixed step, sleeping ON by default (resting bodies deactivate). Contact count = solid contacts in the world contact graph.", + "matter-js arm (pure-JS peer): constraint solver at matter defaults (6 position / 4 velocity / 2 constraint iterations), sleeping OFF by default (a settled stack keeps paying full solve cost); matter's default per-step air drag (frictionAir) is zeroed so all arms integrate the same pure-gravity field; gravity (px/s^2) and perturbation velocity (px/s) are mapped into matter's px-per-step unit model. Contact count = active colliding pairs (engine.pairs.collisionActive), a pair-level proxy, not identical in semantics to the exojs solid-contact count.", + "planck arm (pure-JS peer): Box2D port at planck defaults (8 velocity / 3 position iterations per step), sleeping ON by default; Settings.lengthUnitsPerMeter is set to 30 so planck's absolute MKS tolerances are interpreted at the scene's pixel scale, which is the knob planck gives a pixel-coordinate game - positions, gravity (px/s^2) and velocity (px/s) then carry over unconverted. Contact count = the world contact list filtered by isTouching(), a touching collider-pair count. Rays are answered from planck's dynamic tree, but World.rayCast is Box2D's non-solid ray (an origin inside a fixture is not a hit). Continuous collision runs for every body (planck's default), where exojs and rapier restrict it to bullets and matter has none.", + "rapier arm (WASM reference ceiling, not a pure-JS peer): TGS-Soft solver at rapier defaults (4 solver / 1 internal PGS iterations), auto-sleeping ON; default lengthUnit=1 is fed a px-scale world (tuned for ~1-unit objects), exactly what attaching rapier with pixel coordinates yields. Contact count = collider pairs with a solid narrow-phase manifold (numContacts > 0), deduped.", + "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." + ], + "engineVersion": "0.17.0", + "timestamp": "2026-09-12T02:29:18.798Z" + } + ], + "libraries": [ + { + "name": "@codexo/exojs-physics", + "version": "0.17.0" + }, + { + "name": "matter-js", + "version": "0.20.0" + }, + { + "name": "planck", + "version": "1.5.0" + }, + { + "name": "@dimforge/rapier2d-compat", + "version": "0.19.3" + }, + { + "name": "@newkrok/nape-js", + "version": "3.42.0" + } + ], + "section": { + "title": "Physics", + "rows": [ + { + "archetype": "box-stack", + "category": "Physics", + "count": 5500, + "loadId": "5500", + "unit": "bodies", + "primary": true, + "cells": [ + { + "competitor": "matter-js", + "referenceMs": 8.944999992847443, + "referenceP95Ms": 9.445000007748604, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 25.97749999165535, + "competitorP95Ms": 27.650000005960464, + "competitorGpuMs": null, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.3443364448357544, + "factor": 2.904136390433466, + "label": "ExoJS leads (2.90x)", + "structural": false + }, + "mechanism": "ExoJS resolves fewer solved contacts (5501 vs 12381); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 8.842500001192093, + "maxMs": 8.974999994039536, + "ratio": 1.0149844492880498 + }, + "competitor": { + "minMs": 25.667499996721745, + "maxMs": 27.46000000089407, + "ratio": 1.0698353951261814 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "nape-js", + "referenceMs": 8.944999992847443, + "referenceP95Ms": 9.445000007748604, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 33.375, + "competitorP95Ms": 36.59499999880791, + "competitorGpuMs": null, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.2680149810590994, + "factor": 3.7311347151131535, + "label": "ExoJS leads (3.73x)", + "structural": false + }, + "mechanism": "identical scene (5501 bodies, 5501 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 8.842500001192093, + "maxMs": 8.974999994039536, + "ratio": 1.0149844492880498 + }, + "competitor": { + "minMs": 30.732499986886978, + "maxMs": 34.13500000536442, + "ratio": 1.1107134147866013 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "planck", + "referenceMs": 8.944999992847443, + "referenceP95Ms": 9.445000007748604, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 66.82500000298023, + "competitorP95Ms": 91, + "competitorGpuMs": null, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.1338570892996411, + "factor": 7.470654002952993, + "label": "ExoJS leads clearly (7.47x)", + "structural": true + }, + "mechanism": "ExoJS resolves fewer solved contacts (5501 vs 10677); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 8.842500001192093, + "maxMs": 8.974999994039536, + "ratio": 1.0149844492880498 + }, + "competitor": { + "minMs": 66.48000000417233, + "maxMs": 67.20250000059605, + "ratio": 1.010867930150096 + }, + "stable": true, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "rapier", + "referenceMs": 8.944999992847443, + "referenceP95Ms": 9.445000007748604, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 8.22749999910593, + "competitorP95Ms": 11.774999991059303, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 1.0872075349522312, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "ExoJS resolves fewer solved contacts (5501 vs 10045); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 8.842500001192093, + "maxMs": 8.974999994039536, + "ratio": 1.0149844492880498 + }, + "competitor": { + "minMs": 8.182499997317791, + "maxMs": 8.3125, + "ratio": 1.0158875652581514 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + } + ] + }, + { + "archetype": "many-dynamic", + "category": "Physics", + "count": 1500, + "loadId": "1500", + "unit": "bodies", + "primary": true, + "cells": [ + { + "competitor": "matter-js", + "referenceMs": 9.769999995827675, + "referenceP95Ms": 10.21000000834465, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 9.775000005960464, + "competitorP95Ms": 10.459999993443489, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 0.9994884900123028, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "identical scene (1504 bodies, 4182 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 9.6875, + "maxMs": 9.927500002086163, + "ratio": 1.024774193763733 + }, + "competitor": { + "minMs": 9.719999998807907, + "maxMs": 10.069999992847443, + "ratio": 1.0360082298438746 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + }, + { + "competitor": "nape-js", + "referenceMs": 9.769999995827675, + "referenceP95Ms": 10.21000000834465, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 13.515000000596046, + "competitorP95Ms": 14.560000002384186, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.7229004806064959, + "factor": 1.3833162749608692, + "label": "ExoJS leads (1.38x)", + "structural": false + }, + "mechanism": "identical scene (1504 bodies, 4182 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 9.6875, + "maxMs": 9.927500002086163, + "ratio": 1.024774193763733 + }, + "competitor": { + "minMs": 13.07999999821186, + "maxMs": 14.092500001192093, + "ratio": 1.0774082571191628 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "planck", + "referenceMs": 9.769999995827675, + "referenceP95Ms": 10.21000000834465, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 14.252499997615814, + "competitorP95Ms": 15.815000012516975, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.6854937728442041, + "factor": 1.4588024568784455, + "label": "ExoJS leads (1.46x)", + "structural": false + }, + "mechanism": "identical scene (1504 bodies, 4182 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 9.6875, + "maxMs": 9.927500002086163, + "ratio": 1.024774193763733 + }, + "competitor": { + "minMs": 14.232499994337559, + "maxMs": 17.720000006258488, + "ratio": 1.2450377666122214 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "rapier", + "referenceMs": 9.769999995827675, + "referenceP95Ms": 10.21000000834465, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 2.6700000017881393, + "competitorP95Ms": 2.7950000017881393, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 3.659176025949273, + "factor": 3.659176025949273, + "label": "competitor leads (3.66x)", + "structural": false + }, + "mechanism": "identical scene (1504 bodies, 4182 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 9.6875, + "maxMs": 9.927500002086163, + "ratio": 1.024774193763733 + }, + "competitor": { + "minMs": 2.6599999964237213, + "maxMs": 2.7049999982118607, + "ratio": 1.0169172939280602 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "mixed-static-dynamic", + "category": "Physics", + "count": 1700, + "loadId": "1700", + "unit": "bodies", + "primary": true, + "cells": [ + { + "competitor": "matter-js", + "referenceMs": 7.8950000032782555, + "referenceP95Ms": 8.365000009536743, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 3.9824999943375587, + "competitorP95Ms": 4.385000005364418, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.9824231047090044, + "factor": 1.9824231047090044, + "label": "competitor leads (1.98x)", + "structural": false + }, + "mechanism": "identical scene (1783 bodies, 2174 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 7.820000007748604, + "maxMs": 7.912500001490116, + "ratio": 1.01182864368924 + }, + "competitor": { + "minMs": 3.8200000002980232, + "maxMs": 4.032499998807907, + "ratio": 1.0556282718568861 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + }, + { + "competitor": "nape-js", + "referenceMs": 7.8950000032782555, + "referenceP95Ms": 8.365000009536743, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 10.850000001490116, + "competitorP95Ms": 12.840000003576279, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.727649769787463, + "factor": 1.374287523367302, + "label": "ExoJS leads (1.37x)", + "structural": false + }, + "mechanism": "identical scene (1783 bodies, 2174 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 7.820000007748604, + "maxMs": 7.912500001490116, + "ratio": 1.01182864368924 + }, + "competitor": { + "minMs": 10.324999995529652, + "maxMs": 10.879999995231628, + "ratio": 1.0537530266287913 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "planck", + "referenceMs": 7.8950000032782555, + "referenceP95Ms": 8.365000009536743, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 18.137500002980232, + "competitorP95Ms": 22.799999997019768, + "competitorGpuMs": null, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.4352860097577396, + "factor": 2.2973400880872656, + "label": "ExoJS leads (2.30x)", + "structural": false + }, + "mechanism": "identical scene (1783 bodies, 2174 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 7.820000007748604, + "maxMs": 7.912500001490116, + "ratio": 1.01182864368924 + }, + "competitor": { + "minMs": 17.992500007152557, + "maxMs": 18.825000002980232, + "ratio": 1.0462692786159085 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "rapier", + "referenceMs": 7.8950000032782555, + "referenceP95Ms": 8.365000009536743, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 3.230000004172325, + "competitorP95Ms": 3.655000001192093, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 2.444272443678005, + "factor": 2.444272443678005, + "label": "competitor leads (2.44x)", + "structural": false + }, + "mechanism": "identical scene (1783 bodies, 2174 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 7.820000007748604, + "maxMs": 7.912500001490116, + "ratio": 1.01182864368924 + }, + "competitor": { + "minMs": 3.219999998807907, + "maxMs": 3.2349999994039536, + "ratio": 1.00465838528 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "raycast", + "category": "Physics", + "count": 1700, + "loadId": "1700", + "unit": "bodies", + "primary": true, + "cells": [ + { + "competitor": "matter-js", + "referenceMs": 11.799999997019768, + "referenceP95Ms": 12.45499999821186, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 9.865000002086163, + "competitorP95Ms": 12.459999993443489, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "identical scene (1783 bodies, 2174 contacts, 0 joints on both arms) with 30 vs 46 ray hits per step; the difference is query and solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 11.76749999821186, + "maxMs": 12.044999994337559, + "ratio": 1.0235818989732623 + }, + "competitor": { + "minMs": 9.649999991059303, + "maxMs": 9.969999998807907, + "ratio": 1.0331606225953454 + }, + "stable": false, + "rungs": [ + "level", + "competitor-leads", + "competitor-leads" + ] + } + }, + { + "competitor": "nape-js", + "referenceMs": 11.799999997019768, + "referenceP95Ms": 12.45499999821186, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 28.259999997913837, + "competitorP95Ms": 31.315000012516975, + "competitorGpuMs": null, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.41755130919642064, + "factor": 2.3949152546653596, + "label": "ExoJS leads (2.39x)", + "structural": false + }, + "mechanism": "identical scene (1783 bodies, 2174 contacts, 0 joints on both arms) with 30 vs 32 ray hits per step; the difference is query and solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 11.76749999821186, + "maxMs": 12.044999994337559, + "ratio": 1.0235818989732623 + }, + "competitor": { + "minMs": 27.802500009536743, + "maxMs": 28.6875, + "ratio": 1.0318316694599294 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "planck", + "referenceMs": 11.799999997019768, + "referenceP95Ms": 12.45499999821186, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 18.63000000268221, + "competitorP95Ms": 23.200000002980232, + "competitorGpuMs": null, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.6333870099474446, + "factor": 1.578813559948088, + "label": "ExoJS leads (1.58x)", + "structural": false + }, + "mechanism": "identical scene (1783 bodies, 2174 contacts, 0 joints on both arms) with 30 vs 36 ray hits per step; the difference is query and solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 11.76749999821186, + "maxMs": 12.044999994337559, + "ratio": 1.0235818989732623 + }, + "competitor": { + "minMs": 18.62999999523163, + "maxMs": 18.655000001192093, + "ratio": 1.0013419219520592 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "rapier", + "referenceMs": 11.799999997019768, + "referenceP95Ms": 12.45499999821186, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 3.344999998807907, + "competitorP95Ms": 3.770000010728836, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 3.527653214118104, + "factor": 3.527653214118104, + "label": "competitor leads (3.53x)", + "structural": false + }, + "mechanism": "identical scene (1783 bodies, 2174 contacts, 0 joints on both arms) with 30 vs 39 ray hits per step; the difference is query and solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 11.76749999821186, + "maxMs": 12.044999994337559, + "ratio": 1.0235818989732623 + }, + "competitor": { + "minMs": 3.3400000035762787, + "maxMs": 3.355000004172325, + "ratio": 1.0044910181377202 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + }, + { + "archetype": "body-churn", + "category": "Physics", + "count": 1500, + "loadId": "1500", + "unit": "bodies", + "primary": true, + "cells": [ + { + "competitor": "matter-js", + "referenceMs": 8.917499996721745, + "referenceP95Ms": 9.45499999821186, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 11.242499999701977, + "competitorP95Ms": 12.885000005364418, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "ExoJS resolves fewer solved contacts (1415 vs 3840); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 8.89249999821186, + "maxMs": 9.012500002980232, + "ratio": 1.01349451839106 + }, + "competitor": { + "minMs": 11.137499995529652, + "maxMs": 11.282499998807907, + "ratio": 1.0130190799853158 + }, + "stable": false, + "rungs": [ + "exojs-leads", + "exojs-leads", + "level" + ] + } + }, + { + "competitor": "nape-js", + "referenceMs": 8.917499996721745, + "referenceP95Ms": 9.45499999821186, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 6.945000007748604, + "competitorP95Ms": 7.579999998211861, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 1.284017276713089, + "factor": 1.284017276713089, + "label": "competitor leads (1.28x)", + "structural": false + }, + "mechanism": "identical scene (1504 bodies, 1415 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 8.89249999821186, + "maxMs": 9.012500002980232, + "ratio": 1.01349451839106 + }, + "competitor": { + "minMs": 6.084999993443489, + "maxMs": 6.947499997913837, + "ratio": 1.1417419893836782 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + }, + { + "competitor": "planck", + "referenceMs": 8.917499996721745, + "referenceP95Ms": 9.45499999821186, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 12.042500004172325, + "competitorP95Ms": 13.280000001192093, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "exojs", + "ratio": 0.7405023868492528, + "factor": 1.3504345397924749, + "label": "ExoJS leads (1.35x)", + "structural": false + }, + "mechanism": "ExoJS resolves fewer solved contacts (1415 vs 3163); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 8.89249999821186, + "maxMs": 9.012500002980232, + "ratio": 1.01349451839106 + }, + "competitor": { + "minMs": 11.837499998509884, + "maxMs": 12.145000003278255, + "ratio": 1.0259767691494894 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "rapier", + "referenceMs": 8.917499996721745, + "referenceP95Ms": 9.45499999821186, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 1.280000001192093, + "competitorP95Ms": 1.3250000029802322, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 6.966796865950528, + "factor": 6.966796865950528, + "label": "competitor leads clearly (6.97x)", + "structural": true + }, + "mechanism": "identical scene (1504 bodies, 1415 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 8.89249999821186, + "maxMs": 9.012500002980232, + "ratio": 1.01349451839106 + }, + "competitor": { + "minMs": 1.2749999910593033, + "maxMs": 1.280000001192093, + "ratio": 1.0039215766022362 + }, + "stable": true, + "rungs": [ + "competitor-leads-clearly", + "competitor-leads-clearly", + "competitor-leads-clearly" + ] + } + } + ] + }, + { + "archetype": "joints", + "category": "Physics", + "count": 9000, + "loadId": "9k", + "unit": "bodies", + "primary": true, + "cells": [ + { + "competitor": "matter-js", + "referenceMs": 11.875, + "referenceP95Ms": 12.385000005364418, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 8.675000004470348, + "competitorP95Ms": 9.584999993443489, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "competitor resolves fewer solved contacts (7875 vs 0); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 10.352500006556511, + "maxMs": 12.049999997019768, + "ratio": 1.163970054517091 + }, + "competitor": { + "minMs": 8.120000004768372, + "maxMs": 10.649999998509884, + "ratio": 1.3115763537260838 + }, + "stable": false, + "rungs": [ + "competitor-leads", + "level", + "competitor-leads" + ] + } + }, + { + "competitor": "nape-js", + "referenceMs": 11.875, + "referenceP95Ms": 12.385000005364418, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 0.0004999995231628418, + "competitorP95Ms": 0.0005000010132789612, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 23750.022649786613, + "factor": 23750.022649786613, + "label": "competitor leads clearly (23750.02x)", + "structural": true + }, + "mechanism": "competitor resolves fewer solved contacts (7875 vs 0); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "limited", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 10.352500006556511, + "maxMs": 12.049999997019768, + "ratio": 1.163970054517091 + }, + "competitor": { + "minMs": 0.0004999995231628418, + "maxMs": 0.0004999995231628418, + "ratio": 1 + }, + "stable": true, + "rungs": [ + "competitor-leads-clearly", + "competitor-leads-clearly", + "competitor-leads-clearly" + ] + } + }, + { + "competitor": "planck", + "referenceMs": 11.875, + "referenceP95Ms": 12.385000005364418, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 0.42000000178813934, + "competitorP95Ms": 0.5, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 28.273809403434498, + "factor": 28.273809403434498, + "label": "competitor leads clearly (28.27x)", + "structural": true + }, + "mechanism": "competitor resolves fewer solved contacts (7875 vs 0); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 10.352500006556511, + "maxMs": 12.049999997019768, + "ratio": 1.163970054517091 + }, + "competitor": { + "minMs": 0.39500001072883606, + "maxMs": 0.5700000077486038, + "ratio": 1.4430379551050283 + }, + "stable": true, + "rungs": [ + "competitor-leads-clearly", + "competitor-leads-clearly", + "competitor-leads-clearly" + ] + } + }, + { + "competitor": "rapier", + "referenceMs": 11.875, + "referenceP95Ms": 12.385000005364418, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 0.3674999997019768, + "competitorP95Ms": 0.4099999964237213, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 32.31292519627211, + "factor": 32.31292519627211, + "label": "competitor leads clearly (32.31x)", + "structural": true + }, + "mechanism": "competitor resolves fewer solved contacts (7875 vs 1125); the counters are not semantically identical across arms (see the run's caveats)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 10.352500006556511, + "maxMs": 12.049999997019768, + "ratio": 1.163970054517091 + }, + "competitor": { + "minMs": 0.3674999997019768, + "maxMs": 0.375, + "ratio": 1.0204081640928035 + }, + "stable": true, + "rungs": [ + "competitor-leads-clearly", + "competitor-leads-clearly", + "competitor-leads-clearly" + ] + } + } + ] + }, + { + "archetype": "settling-pile", + "category": "Physics", + "count": 3000, + "loadId": "3k", + "unit": "bodies", + "primary": true, + "cells": [ + { + "competitor": "matter-js", + "referenceMs": 8.85750000178814, + "referenceP95Ms": 9.614999994635582, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 26.162499994039536, + "competitorP95Ms": 27.515000000596046, + "competitorGpuMs": null, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.3385570952243133, + "factor": 2.9537115426201397, + "label": "ExoJS leads (2.95x)", + "structural": false + }, + "mechanism": "identical scene (3004 bodies, 6216 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 8.805000007152557, + "maxMs": 9.35750000178814, + "ratio": 1.0627484377270608 + }, + "competitor": { + "minMs": 26.04500000178814, + "maxMs": 26.625, + "ratio": 1.022269149478673 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "nape-js", + "referenceMs": 8.85750000178814, + "referenceP95Ms": 9.614999994635582, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 35.72249999642372, + "competitorP95Ms": 37.64000000059605, + "competitorGpuMs": null, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.2479529708915918, + "factor": 4.033022860763434, + "label": "ExoJS leads (4.03x)", + "structural": false + }, + "mechanism": "identical scene (3004 bodies, 6216 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 8.805000007152557, + "maxMs": 9.35750000178814, + "ratio": 1.0627484377270608 + }, + "competitor": { + "minMs": 34.32999999821186, + "maxMs": 35.94500000029802, + "ratio": 1.0470434023352835 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + }, + { + "competitor": "planck", + "referenceMs": 8.85750000178814, + "referenceP95Ms": 9.614999994635582, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 44.9125000089407, + "competitorP95Ms": 47.63500000536442, + "competitorGpuMs": null, + "competitorOverFrameBudget": true, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "identical scene (3004 bodies, 6216 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 8.805000007152557, + "maxMs": 9.35750000178814, + "ratio": 1.0627484377270608 + }, + "competitor": { + "minMs": 44.747500002384186, + "maxMs": 46.25749999284744, + "ratio": 1.033744901734908 + }, + "stable": false, + "rungs": [ + "exojs-leads-clearly", + "exojs-leads", + "exojs-leads-clearly" + ] + } + }, + { + "competitor": "rapier", + "referenceMs": 8.85750000178814, + "referenceP95Ms": 9.614999994635582, + "referenceGpuMs": null, + "referenceOverFrameBudget": false, + "competitorMs": 4.14750000089407, + "competitorP95Ms": 4.5150000005960464, + "competitorGpuMs": null, + "competitorOverFrameBudget": false, + "verdict": { + "side": "competitor", + "ratio": 2.1356238697718486, + "factor": 2.1356238697718486, + "label": "competitor leads (2.14x)", + "structural": false + }, + "mechanism": "identical scene (3004 bodies, 6216 contacts, 0 joints on both arms); the difference is solver cost", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 8.805000007152557, + "maxMs": 9.35750000178814, + "ratio": 1.0627484377270608 + }, + "competitor": { + "minMs": 4.119999997317791, + "maxMs": 4.1875, + "ratio": 1.0163834958073188 + }, + "stable": true, + "rungs": [ + "competitor-leads", + "competitor-leads", + "competitor-leads" + ] + } + } + ] + } + ] + } + }, "signature": { "algorithm": "sha256", - "value": "cb202ad3f559b846de40a2326a448fade95ae78e30a01dea4866a65d29623885" + "value": "bfa886e0a623e1f287b0900ad54386741b4300f786ce924aad13462c89a51dd6" } } diff --git a/packages/exojs-bench/src/comparison/build.ts b/packages/exojs-bench/src/comparison/build.ts index c1f0168d3..1c238fbd1 100644 --- a/packages/exojs-bench/src/comparison/build.ts +++ b/packages/exojs-bench/src/comparison/build.ts @@ -2,6 +2,7 @@ import { PHYSICS_ARCHETYPES } from '../physics/archetypes'; import type { PhysicsCellResult } from '../physics/PhysicsAdapter'; import { ARCHETYPES } from '../rendering/archetypes'; import type { ArchetypeCategory, Backend, CellResult, StructuralCounters } from '../rendering/EngineAdapter'; +import { isUiLayoutScene } from '../rendering/uiLayout'; import type { ClockReport } from '../shared/clock'; import { exceedsFrameBudget } from '../shared/frameBudget'; import type { TimerCheck } from '../shared/timerCheck'; @@ -352,8 +353,12 @@ const buildBackend = (backend: Backend, results: readonly CellResult[]): Backend continue; } - const counters = (result: CellResult): StructuralCounters | null => (result.structural.drawCalls > 0 ? result.structural : null); - const mechanism = renderingMechanism(counters(reference), counters(competitorCell)); + // A drawless archetype keeps its counters: zero draws is what it is + // supposed to report, and dropping them would leave the row with no + // evidence and take it out of the table. + const drawless = isUiLayoutScene(archetype); + const counters = (result: CellResult): StructuralCounters | null => (drawless || result.structural.drawCalls > 0 ? result.structural : null); + const mechanism = renderingMechanism(counters(reference), counters(competitorCell), { drawless }); cells.push({ competitor, diff --git a/packages/exojs-bench/src/comparison/mechanism.ts b/packages/exojs-bench/src/comparison/mechanism.ts index 1487b052a..b078f6025 100644 --- a/packages/exojs-bench/src/comparison/mechanism.ts +++ b/packages/exojs-bench/src/comparison/mechanism.ts @@ -65,16 +65,40 @@ const dominantCounter = (pairs: readonly CounterPair[]): CounterPair | null => { return null; }; +/** How a rendering row's counters are to be read. */ +export interface RenderingMechanismOptions { + /** + * Whether the archetype submits nothing by design. + * + * The zero-draw check below rests on every scene drawing something, so a cell + * at zero has to be a cell that failed to build its scene. One archetype + * breaks that: the UI-layout scene measures a box-tree solve and its leaves + * are layout boxes with no painted surface, so both arms correctly draw + * nothing. There the zeros are the evidence rather than the absence of it - + * neither arm rendered, so all of the difference is the solve. + */ + readonly drawless?: boolean; +} + /** Mechanism sentence for a rendering row, or `null` when neither arm reported counters. */ -export const renderingMechanism = (exojs: StructuralCounters | null, competitor: StructuralCounters | null): string | null => { +export const renderingMechanism = ( + exojs: StructuralCounters | null, + competitor: StructuralCounters | null, + options: RenderingMechanismOptions = {}, +): string | null => { if (exojs === null || competitor === null) { return null; } - // A cell that issued no draw call at all did not render the archetype, so its - // counters describe nothing. if (exojs.drawCalls === 0 && competitor.drawCalls === 0) { - return null; + // A cell that issued no draw call at all did not render the archetype, so + // its counters describe nothing - unless the archetype draws nothing on + // purpose (see `drawless`). + if (options.drawless !== true) { + return null; + } + + return `neither arm submits anything for this scene (draw/bind/upload per frame: ${exojs.drawCalls}/${exojs.textureBinds}/${exojs.bufferUploads} vs ${competitor.drawCalls}/${competitor.textureBinds}/${competitor.bufferUploads}); the whole difference is the per-frame work itself`; } const dominant = dominantCounter([ From b0e49138943f2cf7abe88c143bf8179a3b9504cf Mon Sep 17 00:00:00 2001 From: Exoridus Date: Sat, 12 Sep 2026 04:36:41 +0200 Subject: [PATCH 12/28] site: name the UI-layout scenario on the benchmarks page The page falls back to the raw archetype id when it has no title, so the new row would have shipped as `ui-layout-update` beside `Hit testing` and `Blur effect`. The fallback stays - the page has to render a profile from a newer harness than it knows - and a test now keeps the repository's own profiles away from it. --- site/src/lib/bench-profiles.ts | 2 ++ test/site/bench-archetype-labels.test.ts | 33 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 test/site/bench-archetype-labels.test.ts diff --git a/site/src/lib/bench-profiles.ts b/site/src/lib/bench-profiles.ts index 7fb874d6c..16073d602 100644 --- a/site/src/lib/bench-profiles.ts +++ b/site/src/lib/bench-profiles.ts @@ -476,6 +476,7 @@ const ARCHETYPE_DESCRIPTIONS: Readonly> = { 'particles-lifecycle': 'A steady particle effect: ageing, movement, fading and respawning.', 'interaction-picking': 'A block of point queries against a field of interactive rectangles; stresses the hit-test index.', 'fx-blur': 'A separable two-pass Gaussian over a fixed area; stresses the target passes a filter runs.', + 'ui-layout-update': 'Nested boxes of fixed-size widgets re-solved after a tenth of them resize; stresses the layout engine, and nothing is drawn.', }; /** @@ -517,6 +518,7 @@ const ARCHETYPE_TITLES: Readonly> = { 'particles-lifecycle': 'Particle effect', 'interaction-picking': 'Hit testing', 'fx-blur': 'Blur effect', + 'ui-layout-update': 'Interface layout', 'box-stack': 'Box stack', 'many-dynamic': 'Many active bodies', 'mixed-static-dynamic': 'Static level, falling bodies', diff --git a/test/site/bench-archetype-labels.test.ts b/test/site/bench-archetype-labels.test.ts new file mode 100644 index 000000000..4c3b7c835 --- /dev/null +++ b/test/site/bench-archetype-labels.test.ts @@ -0,0 +1,33 @@ +/** + * Every scenario a published profile carries has to have a name and a sentence + * on the benchmarks page. + * + * `archetypeTitle` falls back to the raw archetype id, which is a reasonable + * thing for a renderer to do and a bad thing to ship: a row reading + * `ui-layout-update` next to rows reading `Hit testing` and `Blur effect` looks + * like a defect in the page rather than a label nobody wrote. The fallback stays + * - the page must render a profile measured by a newer harness than it knows - + * and this test is what stops the repository's own profiles reaching it. + */ + +import { describe, expect, it } from 'vitest'; + +import { archetypeDescription, archetypeTitle, benchProfiles } from '../../site/src/lib/bench-profiles'; + +const publishedArchetypes = (): readonly string[] => [ + ...new Set( + benchProfiles.flatMap(profile => + (profile.rendering?.backends ?? []).flatMap(backend => backend.sections.flatMap(section => section.rows.map(row => row.archetype))), + ), + ), +]; + +describe('benchmark page labels', () => { + it('names every rendering scenario the published profiles carry', () => { + expect(publishedArchetypes().filter(archetype => archetypeTitle(archetype) === archetype)).toStrictEqual([]); + }); + + it('describes every rendering scenario the published profiles carry', () => { + expect(publishedArchetypes().filter(archetype => archetypeDescription(archetype) === undefined)).toStrictEqual([]); + }); +}); From 28850aec7678f767fa54e03e7006847ce7c57ed9 Mon Sep 17 00:00:00 2001 From: Exoridus Date: Sat, 12 Sep 2026 04:45:07 +0200 Subject: [PATCH 13/28] site(home): offer the latest release beside the install command Not everyone installs from npm, and nothing on the landing page pointed at the release bundles. The link targets `/releases/latest` rather than a tag built from the package version: GitHub resolves it to whatever is newest, while the version in `package.json` is the one being developed and is ahead of the last release for most of a cycle. --- site/src/components/pages/HomePage.astro | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/site/src/components/pages/HomePage.astro b/site/src/components/pages/HomePage.astro index 34e4b1ede..0ae3f1c86 100644 --- a/site/src/components/pages/HomePage.astro +++ b/site/src/components/pages/HomePage.astro @@ -5,6 +5,7 @@ import EnglishFallbackNotice from '../EnglishFallbackNotice.astro'; import ExampleThumb from '../../components/ExampleThumb.astro'; import { GuideExamplePreview } from '../../components/GuideExamplePreview'; import InstallCommand from '../../components/InstallCommand.astro'; +import { appInfo } from '../../lib/app-info'; import { getAllExamples, getExamplesForChapter } from '../../lib/examples-catalog'; import { getExampleExecutionSource } from '../../lib/example-sources'; import { GUIDE_PARTS } from '../../lib/guide-structure'; @@ -155,6 +156,10 @@ const examples = [
@@ -451,6 +456,37 @@ const examples = [ .hero-install { margin-top: var(--s-5); max-width: 420px; + display: grid; + gap: var(--s-3); + justify-items: start; + } + + /* + * `/releases/latest` rather than a tag built from the package version: + * GitHub resolves it to whatever is newest, while the version in + * `package.json` is the one being developed and is ahead of the last + * release for most of a cycle - which would link a tag that does not exist + * yet. + */ + .hero-download { + display: grid; + gap: 2px; + text-decoration: none; + color: var(--fg-muted); + font-size: 13px; + } + + .hero-download-label { + color: var(--accent); + font-weight: 500; + } + + .hero-download:hover .hero-download-label { + text-decoration: underline; + } + + .hero-download-note { + color: var(--fg-faint); } .home .btn { From 50f8d12670b955a0d11e924a783b9df694fb94e2 Mon Sep 17 00:00:00 2001 From: Exoridus Date: Sat, 12 Sep 2026 05:38:38 +0200 Subject: [PATCH 14/28] release: summarize a changelog entry and fold the rest, and ship a stable ZIP name A squash commit's body is the pull request description, and the changelog put all of it under the bullet. Fifteen of those end to end made the v0.17.0 release page several thousand words of unbroken prose - the entries stopped being scannable, which is the one thing release notes are for. An entry now leads with its opening paragraph and keeps the rest in a `
` fold: nothing is lost, neither on the release page nor in `CHANGELOG.md`. The release also uploads a second copy of the Full ZIP under a version-less name. GitHub serves the newest release's assets at `/releases/latest/download/`, which is a permanent direct-download URL only for a name that does not move with the version - so the site's download button can link the archive itself rather than the release page. --- .github/workflows/release.yml | 10 ++++++ scripts/release/changelogFromCommits.ts | 35 ++++++++++++++++++++- site/src/components/pages/HomePage.astro | 16 +++++----- test/release/changelog-from-commits.test.ts | 19 +++++++++++ 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 967e80f03..c4bbedcc8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -162,6 +162,15 @@ jobs: --repo "${GITHUB_REPOSITORY}" \ --out ".release/release-notes.md" + # A second copy under a version-less name. GitHub serves the assets of the + # newest release at /releases/latest/download/, which is a + # permanent direct-download URL only for a name that does not move with + # the version - so the site can link the archive itself instead of the + # release page. The versioned name stays: it is what a downloaded file + # should be called once it is on disk. + - name: Add the version-less Full ZIP alias + run: cp ".release/exojs-${TARGET_TAG}-full.zip" .release/exojs-full.zip + - name: Create GitHub release with Full ZIP uses: softprops/action-gh-release@v3 with: @@ -170,5 +179,6 @@ jobs: files: | .release/exojs-${{ env.TARGET_TAG }}-full.zip .release/exojs-${{ env.TARGET_TAG }}-full.zip.sha256 + .release/exojs-full.zip .release/artifacts/checksums.sha256 .release/artifacts/release-manifest.json diff --git a/scripts/release/changelogFromCommits.ts b/scripts/release/changelogFromCommits.ts index 7360cae7f..b4dd86e87 100644 --- a/scripts/release/changelogFromCommits.ts +++ b/scripts/release/changelogFromCommits.ts @@ -111,12 +111,45 @@ const indentBody = (body: string): string => .join('\n') .replace(/\n{3,}/gu, '\n\n'); +/** + * Split a body into its opening paragraph and everything after it. + * + * A pull request description is written to be read once, by a reviewer with the + * diff open. A release is read by someone deciding whether to upgrade, and + * pasting a dozen such descriptions end to end leaves them several thousand + * words of prose with no shape - the entries stop being scannable and the + * release page becomes a wall. The opening paragraph is the part that answers + * "what changed"; the rest is the reasoning, the measurements and the + * follow-ups, which belong in the release but not in front of it. + */ +const splitSummary = (body: string): { summary: string; detail: string } => { + const trimmed = body.trim(); + const separator = trimmed.indexOf('\n\n'); + + return separator === -1 ? { summary: trimmed, detail: '' } : { summary: trimmed.slice(0, separator).trim(), detail: trimmed.slice(separator).trim() }; +}; + const renderEntry = (entry: ChangelogEntry, repoUrl: string): string => { const link = entry.pullRequest !== null ? ` ([#${entry.pullRequest}](${repoUrl}/pull/${entry.pullRequest}))` : ''; const flag = entry.breaking ? 'BREAKING: ' : ''; const head = `- **${flag}${entry.title}.**${link}`; - return entry.body.length > 0 ? `${head}\n${indentBody(entry.body)}` : head; + if (entry.body.length === 0) { + return head; + } + + const { summary, detail } = splitSummary(entry.body); + const rendered = [head, indentBody(summary)]; + + if (detail.length > 0) { + // A fold rather than a cut: everything the description said is still in the + // release, one click away, and still in `CHANGELOG.md` for anyone reading + // the file. The blank line after the `` tag is what makes the + // Markdown inside render instead of appearing as source. + rendered.push(indentBody(['
', 'Details', '', detail, '', '
'].join('\n'))); + } + + return rendered.join('\n'); }; /** diff --git a/site/src/components/pages/HomePage.astro b/site/src/components/pages/HomePage.astro index 0ae3f1c86..e1a08db9d 100644 --- a/site/src/components/pages/HomePage.astro +++ b/site/src/components/pages/HomePage.astro @@ -156,9 +156,9 @@ const examples = [
@@ -462,11 +462,13 @@ const examples = [ } /* - * `/releases/latest` rather than a tag built from the package version: - * GitHub resolves it to whatever is newest, while the version in - * `package.json` is the one being developed and is ahead of the last - * release for most of a cycle - which would link a tag that does not exist - * yet. + * The archive itself, through GitHub's `/releases/latest/download/` + * redirect, so the button downloads instead of landing on release notes. + * It resolves against whatever release is newest, which is why the release + * workflow uploads a second copy of the Full ZIP under a version-less name: + * a URL carrying the version would have to be rebuilt for every release, + * and the version in `package.json` is the one being developed and is ahead + * of the last release for most of a cycle. */ .hero-download { display: grid; diff --git a/test/release/changelog-from-commits.test.ts b/test/release/changelog-from-commits.test.ts index 8ec0e843a..56bfa4be8 100644 --- a/test/release/changelog-from-commits.test.ts +++ b/test/release/changelog-from-commits.test.ts @@ -62,6 +62,25 @@ describe('renderUnreleasedEntries', () => { expect(rendered).toContain(' It does things.'); }); + test('keeps the opening paragraph in front and folds the rest away', () => { + const body = ['It does things.', 'And here is the long reasoning.', 'With a second paragraph of it.'].join('\n\n'); + const rendered = renderUnreleasedEntries([classifyCommit(commit('feat: add a thing (#1)', body))!], '', REPO); + + expect(rendered).toContain(' It does things.'); + expect(rendered.indexOf('It does things.')).toBeLessThan(rendered.indexOf('
')); + expect(rendered).toContain(' Details'); + expect(rendered).toContain(' And here is the long reasoning.'); + expect(rendered).toContain(' With a second paragraph of it.'); + expect(rendered).toContain('
'); + }); + + test('leaves a single-paragraph body unfolded', () => { + const rendered = renderUnreleasedEntries([classifyCommit(commit('feat: add a thing (#1)', 'It does things.'))!], '', REPO); + + expect(rendered).toContain(' It does things.'); + expect(rendered).not.toContain('
'); + }); + test('skips entries whose pull request the section already names', () => { const entries = [classifyCommit(commit('feat: add a thing (#1)'))!, classifyCommit(commit('fix: fix a thing (#2)'))!]; const rendered = renderUnreleasedEntries(entries, '- **Hand-written.** ([#1](x/pull/1))', REPO); From dbfcf7d0ef95eafd2e609f2ac418408552014ef4 Mon Sep 17 00:00:00 2001 From: Exoridus Date: Sat, 12 Sep 2026 06:01:06 +0200 Subject: [PATCH 15/28] docs: one line per changelog entry, long-line prose, subject-only commits --- AGENTS.md | 2 + CONTRIBUTING.md | 17 +++--- scripts/release/changelogFromCommits.ts | 57 +++++---------------- site/scripts/capture-screenshots.ts | 29 +++++++++++ test/release/changelog-from-commits.test.ts | 21 ++------ 5 files changed, 57 insertions(+), 69 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8b34975c6..ab4eda344 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,8 @@ Use `CONTRIBUTING.md` when a task touches repository conventions such as imports package boundaries, distribution, build constants, or public API conventions. Read package-local documentation when working inside that package. +Prose is written in long lines: pull request descriptions, guides, `docs/`, READMEs, commit bodies. Break a line at the end of a paragraph or where the break carries meaning, never at a column width. Commits are their Conventional Commits subject (with `!` for a breaking change); the detail goes into the pull request description, which the changelog links to and does not copy. + `.workspace/` is private working context, not repository authority. Plans, research, reviews, and temporary design artifacts belong there by default. Agents may use relevant files there as context, but they are not part of the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eedf8fd9c..24e3eace4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,14 +14,15 @@ A release cuts by merging `next` into `main` and tagging; see ## Changelog: written by the release cut, not by pull requests -`CHANGELOG.md` is not edited in a pull request. Its `## [Unreleased]` section -is assembled at the release cut from the squash commits since the last tag: -the Conventional Commits type files the entry (`feat` under Added, `fix` and -`perf` under Fixed, `refactor` and any `!` under Changed, `docs` under -Documentation; `ci`, `build`, `test`, `chore` and `style` stay out), the -subject becomes the headline, and the pull request description becomes the -prose. That makes the pull request description the release text - write it -for a reader of the changelog, not for the reviewer alone. +`CHANGELOG.md` is not edited in a pull request. Its `## [Unreleased]` section is assembled at the release cut from the squash commits since the last tag: the Conventional Commits type files the entry (`feat` under Added, `fix` and `perf` under Fixed, `refactor` and any `!` under Changed, `docs` under Documentation; `ci`, `build`, `test`, `chore` and `style` stay out), a `!` marks it breaking, and the subject becomes the line, linked to its pull request. The description is not copied in: the changelog is the quick read, the pull request is where the detail lives. + +## Commit subjects, pull request descriptions, and prose + +A commit is its Conventional Commits subject. It carries the scope, the `!` for a breaking change and the pull request number, and that is the whole of what the changelog and the release notes ever read from it. A body is optional and short; the reasoning belongs in the pull request. + +The pull request description is where the detail goes - what changed, why, what was measured, what a breaking change breaks and what to do about it. It is linked from the changelog line, so nothing needs saying twice. + +Prose - pull request descriptions, guides, `docs/`, READMEs, commit bodies where there is one - is written in long lines. Break a line where a paragraph ends or where a break carries meaning, never at a column. A wall of text wrapped at seventy characters reads as a wall of text everywhere it is rendered, and a diff over it shows every reflowed line as a change. Code comments follow the formatter; this rule is about text people read. Two pull requests that both edited the top of the same section conflicted on every second merge; the `lint:changelog` gate now fails a branch that touches diff --git a/scripts/release/changelogFromCommits.ts b/scripts/release/changelogFromCommits.ts index b4dd86e87..38ec2668a 100644 --- a/scripts/release/changelogFromCommits.ts +++ b/scripts/release/changelogFromCommits.ts @@ -3,12 +3,11 @@ * release instead of edited by hand in every pull request. * * Every squash merge carries a Conventional Commits subject with the pull - * request number and the pull request description as its body, so the - * changelog can be assembled at the cut: the type files the entry under a - * Keep a Changelog heading, the subject becomes the headline, the body the - * prose. Pull requests never touch `CHANGELOG.md`, which is what used to make - * two of them conflict whenever both landed an entry at the top of the same - * section. + * request number, so the changelog can be assembled at the cut: the type files + * the entry under a Keep a Changelog heading, a `!` marks it breaking, the + * subject becomes the line, and the pull request link is where the detail lives. + * Pull requests never touch `CHANGELOG.md`, which is what used to make two of + * them conflict whenever both landed an entry at the top of the same section. * * Kept apart from the cut script so it can be tested; nothing here touches git * except {@link readCommitsSince}. @@ -104,52 +103,20 @@ export const mentionedPullRequests = (section: string): Set => { return found; }; -const indentBody = (body: string): string => - body - .split('\n') - .map(line => (line.trim().length === 0 ? '' : ` ${line}`)) - .join('\n') - .replace(/\n{3,}/gu, '\n\n'); - /** - * Split a body into its opening paragraph and everything after it. + * One line per entry: the headline and the pull request it came from. * - * A pull request description is written to be read once, by a reviewer with the - * diff open. A release is read by someone deciding whether to upgrade, and - * pasting a dozen such descriptions end to end leaves them several thousand - * words of prose with no shape - the entries stop being scannable and the - * release page becomes a wall. The opening paragraph is the part that answers - * "what changed"; the rest is the reasoning, the measurements and the - * follow-ups, which belong in the release but not in front of it. + * The body is not rendered on purpose. A squash commit's body is the pull + * request description, and a changelog that inlines it stops being a changelog: + * fifteen descriptions end to end are several thousand words with no shape, and + * the one thing release notes are for - a quick read of what changed - is gone. + * The description stays where it was written, one click away behind the link. */ -const splitSummary = (body: string): { summary: string; detail: string } => { - const trimmed = body.trim(); - const separator = trimmed.indexOf('\n\n'); - - return separator === -1 ? { summary: trimmed, detail: '' } : { summary: trimmed.slice(0, separator).trim(), detail: trimmed.slice(separator).trim() }; -}; - const renderEntry = (entry: ChangelogEntry, repoUrl: string): string => { const link = entry.pullRequest !== null ? ` ([#${entry.pullRequest}](${repoUrl}/pull/${entry.pullRequest}))` : ''; const flag = entry.breaking ? 'BREAKING: ' : ''; - const head = `- **${flag}${entry.title}.**${link}`; - - if (entry.body.length === 0) { - return head; - } - - const { summary, detail } = splitSummary(entry.body); - const rendered = [head, indentBody(summary)]; - - if (detail.length > 0) { - // A fold rather than a cut: everything the description said is still in the - // release, one click away, and still in `CHANGELOG.md` for anyone reading - // the file. The blank line after the `` tag is what makes the - // Markdown inside render instead of appearing as source. - rendered.push(indentBody(['
', 'Details', '', detail, '', '
'].join('\n'))); - } - return rendered.join('\n'); + return `- **${flag}${entry.title}.**${link}`; }; /** diff --git a/site/scripts/capture-screenshots.ts b/site/scripts/capture-screenshots.ts index 666efa8dc..d4a78d20c 100644 --- a/site/scripts/capture-screenshots.ts +++ b/site/scripts/capture-screenshots.ts @@ -244,6 +244,27 @@ const buildTaskUrl = (baseUrl: string, route: string): string => { return new URL(routeWithoutLeadingSlash, baseUrl).toString(); }; +/** + * Open every collapsed disclosure on the page. + * + * A review pass needs both states: the page as a visitor first meets it, and the + * page with everything it can show already showing. The second is not reachable + * by scrolling, so a capture that only ever saw the default state reviews half + * the layout - and a `
` that breaks its surroundings once open breaks + * it for every reader who clicks. + * + * The disclosures are opened directly rather than clicked: a click scrolls the + * summary into view and fires the page's own handlers, which would reorder the + * captures against each other. + */ +const openEveryDisclosure = async (page: Page): Promise => { + await page.evaluate(() => { + for (const details of document.querySelectorAll('details')) { + details.open = true; + } + }); +}; + const main = async (): Promise => { const parsed = parseArgs({ args: process.argv.slice(2), @@ -260,6 +281,7 @@ const main = async (): Promise => { 'delay-ms': { type: 'string' }, 'timeout-ms': { type: 'string' }, mobile: { type: 'boolean' }, + expand: { type: 'boolean' }, help: { type: 'boolean', short: 'h' }, }, allowPositionals: false, @@ -279,6 +301,7 @@ Options: --viewport Single viewport WIDTHxHEIGHT (default: 1440x1000) --viewports Comma-separated viewport list WIDTHxHEIGHT,... --mobile Include extra 390x844 viewport + --expand Open every
before capturing --concurrency Max concurrent pages (default: 3) --max-height Max panel split height (default: 1568) --delay-ms Extra stabilization delay after load (default: 300) @@ -293,6 +316,7 @@ Options: const maxHeight = Math.max(256, Number.parseInt(parsed.values['max-height'] ?? String(DEFAULT_MAX_PANEL_HEIGHT), 10) || DEFAULT_MAX_PANEL_HEIGHT); const delayMs = Math.max(0, Number.parseInt(parsed.values['delay-ms'] ?? String(DEFAULT_STABLE_DELAY_MS), 10) || DEFAULT_STABLE_DELAY_MS); const timeoutMs = Math.max(5_000, Number.parseInt(parsed.values['timeout-ms'] ?? String(DEFAULT_NAVIGATION_TIMEOUT_MS), 10) || DEFAULT_NAVIGATION_TIMEOUT_MS); + const expandDisclosures = parsed.values.expand === true; const routesFromList = parsed.values.routes ? parsed.values.routes @@ -396,6 +420,11 @@ Options: } await page.waitForLoadState('networkidle', { timeout: timeoutMs }).catch(() => undefined); await applyStabilization(page); + + if (expandDisclosures) { + await openEveryDisclosure(page); + } + await waitForVisualReady(page, delayMs); const routeSlug = routeToFileSlug(task.route); diff --git a/test/release/changelog-from-commits.test.ts b/test/release/changelog-from-commits.test.ts index 56bfa4be8..0fe60b16e 100644 --- a/test/release/changelog-from-commits.test.ts +++ b/test/release/changelog-from-commits.test.ts @@ -59,26 +59,15 @@ describe('renderUnreleasedEntries', () => { expect(rendered.indexOf('### Changed')).toBeLessThan(rendered.indexOf('### Added')); expect(rendered.indexOf('BREAKING: Change a contract')).toBeLessThan(rendered.indexOf('Rename a thing')); expect(rendered).toContain(`([#1](${REPO}/pull/1))`); - expect(rendered).toContain(' It does things.'); }); - test('keeps the opening paragraph in front and folds the rest away', () => { - const body = ['It does things.', 'And here is the long reasoning.', 'With a second paragraph of it.'].join('\n\n'); + test('renders the headline and the link, never the body', () => { + const body = ['It does things.', 'And here is the long reasoning.'].join('\n\n'); const rendered = renderUnreleasedEntries([classifyCommit(commit('feat: add a thing (#1)', body))!], '', REPO); - expect(rendered).toContain(' It does things.'); - expect(rendered.indexOf('It does things.')).toBeLessThan(rendered.indexOf('
')); - expect(rendered).toContain(' Details'); - expect(rendered).toContain(' And here is the long reasoning.'); - expect(rendered).toContain(' With a second paragraph of it.'); - expect(rendered).toContain('
'); - }); - - test('leaves a single-paragraph body unfolded', () => { - const rendered = renderUnreleasedEntries([classifyCommit(commit('feat: add a thing (#1)', 'It does things.'))!], '', REPO); - - expect(rendered).toContain(' It does things.'); - expect(rendered).not.toContain('
'); + expect(rendered).toContain(`- **Add a thing.** ([#1](${REPO}/pull/1))`); + expect(rendered).not.toContain('It does things.'); + expect(rendered).not.toContain('long reasoning'); }); test('skips entries whose pull request the section already names', () => { From 3c58b4df183cda235e9f9ec806043b732d59af10 Mon Sep 17 00:00:00 2001 From: Dima Giesler Date: Sat, 12 Sep 2026 05:48:20 +0200 Subject: [PATCH 16/28] bench: re-measure the macOS reference run on the current arms --- .../results/m3-max-macos-27-beta-webkit.json | 3676 +++++++++-------- 1 file changed, 1856 insertions(+), 1820 deletions(-) diff --git a/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json b/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json index 62385fd87..cfb1cdc71 100644 --- a/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json +++ b/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json @@ -12,7 +12,7 @@ "prerelease": true }, "engineVersion": "0.17.0", - "measuredAt": "2026-09-12T02:40:41.625Z", + "measuredAt": "2026-09-12T03:46:13.242Z", "runs": 3 }, "rendering": { @@ -43,7 +43,7 @@ "headless": true, "software": false, "engineVersion": "0.17.0", - "timestamp": "2026-09-12T02:06:06.620Z" + "timestamp": "2026-09-12T03:11:55.632Z" }, { "backend": "webgpu", @@ -70,7 +70,7 @@ "software": false, "slotTier": 16, "engineVersion": "0.17.0", - "timestamp": "2026-09-12T02:13:25.798Z" + "timestamp": "2026-09-12T03:19:13.432Z" } ] }, @@ -100,7 +100,7 @@ "headless": true, "software": false, "engineVersion": "0.17.0", - "timestamp": "2026-09-12T02:18:15.735Z" + "timestamp": "2026-09-12T03:24:03.163Z" }, { "backend": "webgpu", @@ -127,7 +127,7 @@ "software": false, "slotTier": 16, "engineVersion": "0.17.0", - "timestamp": "2026-09-12T02:25:50.325Z" + "timestamp": "2026-09-12T03:31:20.194Z" } ] }, @@ -157,7 +157,7 @@ "headless": true, "software": false, "engineVersion": "0.17.0", - "timestamp": "2026-09-12T02:30:39.988Z" + "timestamp": "2026-09-12T03:36:09.267Z" }, { "backend": "webgpu", @@ -184,7 +184,7 @@ "software": false, "slotTier": 16, "engineVersion": "0.17.0", - "timestamp": "2026-09-12T02:38:00.061Z" + "timestamp": "2026-09-12T03:43:31.391Z" } ] } @@ -226,19 +226,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.21000000000003638, - "referenceP95Ms": 0.459999999999809, - "referenceGpuMs": 16.679999999999836, + "referenceMs": 0.20000000000004547, + "referenceP95Ms": 0.42000000000007276, + "referenceGpuMs": 16.660000000000082, "referenceOverFrameBudget": false, - "competitorMs": 2.2700000000000955, - "competitorP95Ms": 8.340000000000146, + "competitorMs": 2.240000000000009, + "competitorP95Ms": 8.180000000000291, "competitorGpuMs": 16.660000000000082, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.09251101321587117, - "factor": 10.809523809522391, - "label": "ExoJS leads clearly (10.81x)", + "ratio": 0.08928571428573423, + "factor": 11.199999999997498, + "label": "ExoJS leads clearly (11.20x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -247,13 +247,13 @@ "runs": 3, "reference": { "minMs": 0.20000000000004547, - "maxMs": 0.21000000000003638, - "ratio": 1.0499999999999432 + "maxMs": 0.23999999999995225, + "ratio": 1.1999999999994884 }, "competitor": { - "minMs": 2.259999999999877, - "maxMs": 2.480000000000018, - "ratio": 1.0973451327434305 + "minMs": 2.2199999999999704, + "maxMs": 2.509999999999991, + "ratio": 1.1306306306306415 }, "stable": true, "rungs": [ @@ -265,19 +265,19 @@ }, { "competitor": "phaser", - "referenceMs": 0.21000000000003638, - "referenceP95Ms": 0.459999999999809, - "referenceGpuMs": 16.679999999999836, + "referenceMs": 0.20000000000004547, + "referenceP95Ms": 0.42000000000007276, + "referenceGpuMs": 16.660000000000082, "referenceOverFrameBudget": false, - "competitorMs": 1.2100000000000364, + "competitorMs": 1.2700000000000955, "competitorP95Ms": 1.7200000000000273, - "competitorGpuMs": 16.660000000000082, + "competitorGpuMs": 16.66000000000031, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.1735537190082893, - "factor": 5.761904761903937, - "label": "ExoJS leads clearly (5.76x)", + "ratio": 0.1574803149606539, + "factor": 6.349999999999033, + "label": "ExoJS leads clearly (6.35x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -286,13 +286,13 @@ "runs": 3, "reference": { "minMs": 0.20000000000004547, - "maxMs": 0.21000000000003638, - "ratio": 1.0499999999999432 + "maxMs": 0.23999999999995225, + "ratio": 1.1999999999994884 }, "competitor": { - "minMs": 1.1700000000000728, - "maxMs": 1.2600000000001046, - "ratio": 1.0769230769230993 + "minMs": 1.259999999999991, + "maxMs": 1.2799999999999727, + "ratio": 1.0158730158730016 }, "stable": true, "rungs": [ @@ -304,19 +304,19 @@ }, { "competitor": "pixi", - "referenceMs": 0.21000000000003638, - "referenceP95Ms": 0.459999999999809, - "referenceGpuMs": 16.679999999999836, + "referenceMs": 0.20000000000004547, + "referenceP95Ms": 0.42000000000007276, + "referenceGpuMs": 16.660000000000082, "referenceOverFrameBudget": false, "competitorMs": 0.18000000000017735, - "competitorP95Ms": 0.37999999999988177, - "competitorGpuMs": 16.660000000000025, + "competitorP95Ms": 0.3999999999998636, + "competitorGpuMs": 16.67999999999995, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.1666666666657193, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -325,17 +325,17 @@ "runs": 3, "reference": { "minMs": 0.20000000000004547, - "maxMs": 0.21000000000003638, - "ratio": 1.0499999999999432 + "maxMs": 0.23999999999995225, + "ratio": 1.1999999999994884 }, "competitor": { - "minMs": 0.18000000000006366, - "maxMs": 0.22000000000002728, - "ratio": 1.2222222222219414 + "minMs": 0.1799999999998363, + "maxMs": 0.1999999999998181, + "ratio": 1.1111111111111112 }, - "stable": true, + "stable": false, "rungs": [ - "level", + "competitor-leads", "level", "level" ] @@ -357,15 +357,15 @@ "referenceP95Ms": 0.25999999999976353, "referenceGpuMs": 16.66000000000031, "referenceOverFrameBudget": false, - "competitorMs": 20.329999999999927, - "competitorP95Ms": 24.90000000000009, - "competitorGpuMs": 21.199999999999818, + "competitorMs": 19.84999999999991, + "competitorP95Ms": 24.079999999999927, + "competitorGpuMs": 20.960000000000036, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.006886374815537293, - "factor": 145.2142857144173, - "label": "ExoJS leads clearly (145.21x)", + "ratio": 0.007052896725434424, + "factor": 141.7857142858426, + "label": "ExoJS leads clearly (141.79x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -373,14 +373,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.12000000000034561, - "maxMs": 0.13999999999987267, - "ratio": 1.1666666666622456 + "minMs": 0.13999999999987267, + "maxMs": 0.17000000000007276, + "ratio": 1.2142857142873384 }, "competitor": { - "minMs": 20.02000000000021, - "maxMs": 20.86999999999989, - "ratio": 1.0424575424575262 + "minMs": 19.420000000000073, + "maxMs": 19.98999999999978, + "ratio": 1.02935118434602 }, "stable": true, "rungs": [ @@ -396,15 +396,15 @@ "referenceP95Ms": 0.25999999999976353, "referenceGpuMs": 16.66000000000031, "referenceOverFrameBudget": false, - "competitorMs": 3.0499999999999545, - "competitorP95Ms": 8.279999999999745, + "competitorMs": 3.0800000000001546, + "competitorP95Ms": 9.159999999999854, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.04590163934422123, - "factor": 21.785714285733775, - "label": "ExoJS leads clearly (21.79x)", + "ratio": 0.04545454545450183, + "factor": 22.000000000021114, + "label": "ExoJS leads clearly (22.00x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -412,14 +412,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.12000000000034561, - "maxMs": 0.13999999999987267, - "ratio": 1.1666666666622456 + "minMs": 0.13999999999987267, + "maxMs": 0.17000000000007276, + "ratio": 1.2142857142873384 }, "competitor": { - "minMs": 2.9400000000000546, - "maxMs": 3.1399999999998727, - "ratio": 1.0680272108842905 + "minMs": 3.0799999999999272, + "maxMs": 3.080000000000382, + "ratio": 1.0000000000001477 }, "stable": true, "rungs": [ @@ -436,14 +436,14 @@ "referenceGpuMs": 16.66000000000031, "referenceOverFrameBudget": false, "competitorMs": 0.13999999999987267, - "competitorP95Ms": 0.2800000000002001, + "competitorP95Ms": 0.2600000000002183, "competitorGpuMs": 16.679999999999836, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -451,18 +451,18 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.12000000000034561, - "maxMs": 0.13999999999987267, - "ratio": 1.1666666666622456 + "minMs": 0.13999999999987267, + "maxMs": 0.17000000000007276, + "ratio": 1.2142857142873384 }, "competitor": { "minMs": 0.13999999999987267, - "maxMs": 0.15999999999985448, - "ratio": 1.1428571428571428 + "maxMs": 0.13999999999987267, + "ratio": 1 }, - "stable": true, + "stable": false, "rungs": [ - "level", + "competitor-leads", "level", "level" ] @@ -479,15 +479,15 @@ "primary": false, "cells": [ { - "competitor": "phaser", - "referenceMs": 0.05999999999994543, - "referenceP95Ms": 0.22999999999956344, - "referenceGpuMs": 16.659999999999854, + "competitor": "pixi", + "referenceMs": 0.06000000000040018, + "referenceP95Ms": 0.2599999999993088, + "referenceGpuMs": 16.67999999999938, "referenceOverFrameBudget": false, - "competitorMs": 17.925000000000182, - "competitorP95Ms": 23.389999999999873, - "competitorGpuMs": 18.829999999999927, - "competitorOverFrameBudget": true, + "competitorMs": 0.04999999999972715, + "competitorP95Ms": 0.2600000000002183, + "competitorGpuMs": 16.660000000000764, + "competitorOverFrameBudget": false, "verdict": { "side": "neither", "ratio": null, @@ -495,63 +495,62 @@ "label": "unstable across runs", "structural": false }, - "mechanism": "competitor issues fewer draw calls (25 vs 7 per frame)", + "mechanism": "competitor issues fewer draw calls (25 vs 1 per frame)", "timer": "limited", "aggregate": { - "runs": 2, + "runs": 3, "reference": { "minMs": 0.05999999999949068, "maxMs": 0.06000000000040018, "ratio": 1.0000000000151583 }, "competitor": { - "minMs": 17.7800000000002, - "maxMs": 18.070000000000164, - "ratio": 1.0163104611923488 + "minMs": 0.03999999999996362, + "maxMs": 0.06999999999970896, + "ratio": 1.7499999999943157 }, "stable": false, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly" + "competitor-leads", + "competitor-leads", + "level" ] } }, { - "competitor": "pixi", + "competitor": "phaser", "referenceMs": 0.06000000000040018, - "referenceP95Ms": 0.23999999999978172, - "referenceGpuMs": 16.659999999999854, + "referenceP95Ms": 0.2599999999993088, + "referenceGpuMs": 16.67999999999938, "referenceOverFrameBudget": false, - "competitorMs": 0.06000000000040018, - "competitorP95Ms": 0.2600000000002183, - "competitorGpuMs": 16.67999999999938, - "competitorOverFrameBudget": false, + "competitorMs": 16.88000000000011, + "competitorP95Ms": 21.82000000000062, + "competitorGpuMs": 18.05999999999949, + "competitorOverFrameBudget": true, "verdict": { "side": "neither", - "ratio": 1, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, - "mechanism": "competitor issues fewer draw calls (25 vs 1 per frame)", + "mechanism": "competitor issues fewer draw calls (25 vs 7 per frame)", "timer": "limited", "aggregate": { - "runs": 3, + "runs": 1, "reference": { - "minMs": 0.05999999999949068, - "maxMs": 0.07000000000016371, - "ratio": 1.1666666666792986 + "minMs": 0.06000000000040018, + "maxMs": 0.06000000000040018, + "ratio": 1 }, "competitor": { - "minMs": 0.05999999999949068, - "maxMs": 0.06000000000040018, - "ratio": 1.0000000000151583 + "minMs": 16.88000000000011, + "maxMs": 16.88000000000011, + "ratio": 1 }, - "stable": true, + "stable": false, "rungs": [ - "level", - "level", - "level" + "exojs-leads-clearly" ] } } @@ -567,19 +566,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 1.699999999999818, - "referenceP95Ms": 2.399999999999636, - "referenceGpuMs": 16.660000000000764, + "referenceMs": 1.7200000000002547, + "referenceP95Ms": 2.480000000000473, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 26.090000000001965, - "competitorP95Ms": 30.040000000000873, - "competitorGpuMs": 27.279999999998836, + "competitorMs": 26.140000000001237, + "competitorP95Ms": 35.37999999999738, + "competitorGpuMs": 27.260000000002037, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.06515906477576427, - "factor": 15.34705882353221, - "label": "ExoJS leads clearly (15.35x)", + "ratio": 0.06579954093344198, + "factor": 15.19767441860312, + "label": "ExoJS leads clearly (15.20x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -587,14 +586,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.6900000000000546, - "maxMs": 1.7599999999997635, - "ratio": 1.0414201183430216 + "minMs": 1.5399999999999636, + "maxMs": 1.980000000000473, + "ratio": 1.285714285714623 }, "competitor": { - "minMs": 25.539999999999054, - "maxMs": 28.3700000000008, - "ratio": 1.1108065779170655 + "minMs": 25.790000000000873, + "maxMs": 26.600000000002183, + "ratio": 1.031407522295513 }, "stable": true, "rungs": [ @@ -606,19 +605,19 @@ }, { "competitor": "phaser", - "referenceMs": 1.699999999999818, - "referenceP95Ms": 2.399999999999636, - "referenceGpuMs": 16.660000000000764, + "referenceMs": 1.7200000000002547, + "referenceP95Ms": 2.480000000000473, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 3.0299999999999727, - "competitorP95Ms": 8.180000000000291, - "competitorGpuMs": 16.660000000003492, + "competitorMs": 2.9900000000016007, + "competitorP95Ms": 7.960000000000036, + "competitorGpuMs": 16.660000000000082, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5610561056105061, - "factor": 1.7823529411766452, - "label": "ExoJS leads (1.78x)", + "ratio": 0.5752508361201786, + "factor": 1.738372093023929, + "label": "ExoJS leads (1.74x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -626,14 +625,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.6900000000000546, - "maxMs": 1.7599999999997635, - "ratio": 1.0414201183430216 + "minMs": 1.5399999999999636, + "maxMs": 1.980000000000473, + "ratio": 1.285714285714623 }, "competitor": { - "minMs": 2.9500000000007276, - "maxMs": 3.2399999999943248, - "ratio": 1.098305084743568 + "minMs": 2.990000000000009, + "maxMs": 3.259999999999991, + "ratio": 1.0903010033444753 }, "stable": true, "rungs": [ @@ -645,19 +644,19 @@ }, { "competitor": "pixi", - "referenceMs": 1.699999999999818, - "referenceP95Ms": 2.399999999999636, - "referenceGpuMs": 16.660000000000764, + "referenceMs": 1.7200000000002547, + "referenceP95Ms": 2.480000000000473, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 1.3100000000004002, - "competitorP95Ms": 2.0399999999999636, - "competitorGpuMs": 16.660000000000764, + "competitorMs": 1.319999999999709, + "competitorP95Ms": 1.9200000000000728, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 1.3030303030307833, + "factor": 1.3030303030307833, + "label": "competitor leads (1.30x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -665,20 +664,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.6900000000000546, - "maxMs": 1.7599999999997635, - "ratio": 1.0414201183430216 + "minMs": 1.5399999999999636, + "maxMs": 1.980000000000473, + "ratio": 1.285714285714623 }, "competitor": { - "minMs": 1.300000000000182, - "maxMs": 1.4299999999998363, - "ratio": 1.09999999999972 + "minMs": 1.2599999999997635, + "maxMs": 1.319999999999709, + "ratio": 1.0476190476190133 }, - "stable": false, + "stable": true, "rungs": [ "competitor-leads", "competitor-leads", - "level" + "competitor-leads" ] } } @@ -694,19 +693,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 1.5799999999999272, - "referenceP95Ms": 2.160000000000764, - "referenceGpuMs": 16.67999999999938, + "referenceMs": 1.6599999999998545, + "referenceP95Ms": 2.2200000000002547, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 3.390000000001237, - "competitorP95Ms": 8.360000000000582, + "competitorMs": 3.4200000000000728, + "competitorP95Ms": 6.600000000002183, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.4660766961650002, - "factor": 2.1455696202540464, - "label": "ExoJS leads (2.15x)", + "ratio": 0.48538011695901145, + "factor": 2.0602409638556463, + "label": "ExoJS leads (2.06x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -714,14 +713,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.5399999999999636, - "maxMs": 1.6399999999998727, - "ratio": 1.0649350649350073 + "minMs": 1.5600000000004002, + "maxMs": 1.7600000000002183, + "ratio": 1.1282051282049788 }, "competitor": { - "minMs": 3.319999999999709, - "maxMs": 3.430000000000291, - "ratio": 1.0331325301206602 + "minMs": 3.390000000001237, + "maxMs": 3.4899999999997817, + "ratio": 1.0294985250733062 }, "stable": true, "rungs": [ @@ -733,19 +732,19 @@ }, { "competitor": "phaser", - "referenceMs": 1.5799999999999272, - "referenceP95Ms": 2.160000000000764, - "referenceGpuMs": 16.67999999999938, + "referenceMs": 1.6599999999998545, + "referenceP95Ms": 2.2200000000002547, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 1.0299999999988358, - "competitorP95Ms": 1.3800000000046566, - "competitorGpuMs": 16.679999999993015, + "competitorMs": 1.2100000000000364, + "competitorP95Ms": 1.6599999999998545, + "competitorGpuMs": 16.679999999999836, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.533980582525935, - "factor": 1.533980582525935, - "label": "competitor leads (1.53x)", + "ratio": 1.3719008264461194, + "factor": 1.3719008264461194, + "label": "competitor leads (1.37x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -753,14 +752,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.5399999999999636, - "maxMs": 1.6399999999998727, - "ratio": 1.0649350649350073 + "minMs": 1.5600000000004002, + "maxMs": 1.7600000000002183, + "ratio": 1.1282051282049788 }, "competitor": { - "minMs": 0.9800000000104774, - "maxMs": 1.2199999999998, - "ratio": 1.2448979591701599 + "minMs": 1.0399999999972351, + "maxMs": 1.240000000000009, + "ratio": 1.1923076923108709 }, "stable": true, "rungs": [ @@ -772,19 +771,19 @@ }, { "competitor": "pixi", - "referenceMs": 1.5799999999999272, - "referenceP95Ms": 2.160000000000764, - "referenceGpuMs": 16.67999999999938, + "referenceMs": 1.6599999999998545, + "referenceP95Ms": 2.2200000000002547, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 0.9800000000004729, - "competitorP95Ms": 1.360000000000582, + "competitorMs": 0.9800000000000182, + "competitorP95Ms": 1.339999999999236, "competitorGpuMs": 16.660000000000764, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.6122448979583315, - "factor": 1.6122448979583315, - "label": "competitor leads (1.61x)", + "ratio": 1.6938775510202282, + "factor": 1.6938775510202282, + "label": "competitor leads (1.69x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -792,14 +791,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.5399999999999636, - "maxMs": 1.6399999999998727, - "ratio": 1.0649350649350073 + "minMs": 1.5600000000004002, + "maxMs": 1.7600000000002183, + "ratio": 1.1282051282049788 }, "competitor": { - "minMs": 0.9600000000000364, - "maxMs": 1, - "ratio": 1.0416666666666272 + "minMs": 0.9699999999997999, + "maxMs": 0.9900000000002365, + "ratio": 1.0206185567014852 }, "stable": true, "rungs": [ @@ -821,19 +820,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 4.020000000000437, + "referenceMs": 4.059999999999491, "referenceP95Ms": 5.199999999998909, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 30.80000000000109, - "competitorP95Ms": 34.94000000000233, - "competitorGpuMs": 31.840000000000146, + "competitorMs": 31.779999999998836, + "competitorP95Ms": 35.45999999999913, + "competitorGpuMs": 32.60000000000218, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.13051948051949006, - "factor": 7.661691542287998, - "label": "ExoJS leads clearly (7.66x)", + "ratio": 0.12775330396474635, + "factor": 7.827586206897248, + "label": "ExoJS leads clearly (7.83x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -841,14 +840,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.9899999999997817, - "maxMs": 4.0600000000004, - "ratio": 1.0175438596492787 + "minMs": 4.019999999998618, + "maxMs": 4.299999999999272, + "ratio": 1.0696517412937192 }, "competitor": { - "minMs": 29.900000000001455, - "maxMs": 34.54999999999927, - "ratio": 1.1555183946487488 + "minMs": 31.55999999999949, + "maxMs": 32.25, + "ratio": 1.021863117870739 }, "stable": true, "rungs": [ @@ -860,19 +859,19 @@ }, { "competitor": "phaser", - "referenceMs": 4.020000000000437, + "referenceMs": 4.059999999999491, "referenceP95Ms": 5.199999999998909, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 3.139999999999418, - "competitorP95Ms": 7.8399999999965075, - "competitorGpuMs": 16.660000000003492, + "competitorMs": 2.930000000000291, + "competitorP95Ms": 7.6599999999998545, + "competitorGpuMs": 16.660000000000764, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.28025477707044, - "factor": 1.28025477707044, - "label": "competitor leads (1.28x)", + "ratio": 1.3856655290099273, + "factor": 1.3856655290099273, + "label": "competitor leads (1.39x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -880,14 +879,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.9899999999997817, - "maxMs": 4.0600000000004, - "ratio": 1.0175438596492787 + "minMs": 4.019999999998618, + "maxMs": 4.299999999999272, + "ratio": 1.0696517412937192 }, "competitor": { - "minMs": 2.9600000000000364, - "maxMs": 3.1999999999970896, - "ratio": 1.0810810810800846 + "minMs": 2.8400000000001455, + "maxMs": 2.9600000000000364, + "ratio": 1.0422535211267199 }, "stable": true, "rungs": [ @@ -899,19 +898,19 @@ }, { "competitor": "pixi", - "referenceMs": 4.020000000000437, + "referenceMs": 4.059999999999491, "referenceP95Ms": 5.199999999998909, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 2.5999999999994543, - "competitorP95Ms": 10.180000000000291, - "competitorGpuMs": 16.660000000001673, + "competitorMs": 2.6700000000000728, + "competitorP95Ms": 11.5, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.5461538461543385, - "factor": 1.5461538461543385, - "label": "competitor leads (1.55x)", + "ratio": 1.5205992509360975, + "factor": 1.5205992509360975, + "label": "competitor leads (1.52x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -919,14 +918,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.9899999999997817, - "maxMs": 4.0600000000004, - "ratio": 1.0175438596492787 + "minMs": 4.019999999998618, + "maxMs": 4.299999999999272, + "ratio": 1.0696517412937192 }, "competitor": { - "minMs": 2.5499999999992724, - "maxMs": 2.8599999999996726, - "ratio": 1.121568627451172 + "minMs": 2.5799999999999272, + "maxMs": 2.839999999999236, + "ratio": 1.1007751937981844 }, "stable": true, "rungs": [ @@ -948,13 +947,13 @@ "cells": [ { "competitor": "phaser", - "referenceMs": 36.82000000000062, - "referenceP95Ms": 43, - "referenceGpuMs": 38.26000000000022, + "referenceMs": 37.57000000000062, + "referenceP95Ms": 43.31999999999971, + "referenceGpuMs": 38.70000000000073, "referenceOverFrameBudget": true, - "competitorMs": 21.439999999995052, - "competitorP95Ms": 26.020000000004075, - "competitorGpuMs": 22.15999999998894, + "competitorMs": 20.53999999999951, + "competitorP95Ms": 25.23999999999978, + "competitorGpuMs": 21.300000000000182, "competitorOverFrameBudget": true, "verdict": { "side": "neither", @@ -968,13 +967,13 @@ "aggregate": { "runs": 1, "reference": { - "minMs": 36.82000000000062, - "maxMs": 36.82000000000062, + "minMs": 37.57000000000062, + "maxMs": 37.57000000000062, "ratio": 1 }, "competitor": { - "minMs": 21.439999999995052, - "maxMs": 21.439999999995052, + "minMs": 20.53999999999951, + "maxMs": 20.53999999999951, "ratio": 1 }, "stable": false, @@ -985,19 +984,19 @@ }, { "competitor": "pixi", - "referenceMs": 38.22999999999956, - "referenceP95Ms": 43.63999999999942, - "referenceGpuMs": 39.280000000000655, + "referenceMs": 38.100000000000364, + "referenceP95Ms": 44.18000000000029, + "referenceGpuMs": 40.23999999999978, "referenceOverFrameBudget": true, - "competitorMs": 28.43000000000029, - "competitorP95Ms": 32.20000000000073, - "competitorGpuMs": 30.219999999999345, + "competitorMs": 29.01000000000022, + "competitorP95Ms": 32.13999999999942, + "competitorGpuMs": 30.200000000000728, "competitorOverFrameBudget": true, "verdict": { "side": "competitor", - "ratio": 1.3447062961659926, - "factor": 1.3447062961659926, - "label": "competitor leads (1.34x)", + "ratio": 1.3133402275077586, + "factor": 1.3133402275077586, + "label": "competitor leads (1.31x)", "structural": false }, "mechanism": "competitor issues fewer draw calls (25 vs 1 per frame)", @@ -1005,14 +1004,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 36.82000000000062, - "maxMs": 40.81999999999971, - "ratio": 1.1086366105377248 + "minMs": 37.57000000000062, + "maxMs": 39.88999999999942, + "ratio": 1.061751397391503 }, "competitor": { - "minMs": 28.18000000000029, - "maxMs": 29.05999999999949, - "ratio": 1.0312278211497228 + "minMs": 28.31999999999971, + "maxMs": 29.159999999999854, + "ratio": 1.029661016949158 }, "stable": true, "rungs": [ @@ -1034,19 +1033,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.5700000000006185, - "referenceP95Ms": 0.7999999999992724, - "referenceGpuMs": 16.679999999998472, + "referenceMs": 0.5599999999994907, + "referenceP95Ms": 0.7800000000006548, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 37.330000000001746, - "competitorP95Ms": 56.81999999999971, - "competitorGpuMs": 39.86000000000058, + "competitorMs": 28.299999999999272, + "competitorP95Ms": 33.599999999998545, + "competitorGpuMs": 29.659999999996217, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.0152692204661289, - "factor": 65.49122807010744, - "label": "ExoJS leads clearly (65.49x)", + "ratio": 0.019787985865706893, + "factor": 50.53571428575895, + "label": "ExoJS leads clearly (50.54x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1054,14 +1053,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5399999999999636, - "maxMs": 0.5799999999999272, - "ratio": 1.0740740740740118 + "minMs": 0.5500000000001819, + "maxMs": 0.6199999999989814, + "ratio": 1.1272727272705023 }, "competitor": { - "minMs": 26.970000000001164, - "maxMs": 38.12000000000262, - "ratio": 1.4134223210975518 + "minMs": 27.539999999997235, + "maxMs": 28.80999999999767, + "ratio": 1.046114742193194 }, "stable": true, "rungs": [ @@ -1073,19 +1072,19 @@ }, { "competitor": "phaser", - "referenceMs": 0.5700000000006185, - "referenceP95Ms": 0.7999999999992724, - "referenceGpuMs": 16.679999999998472, + "referenceMs": 0.5599999999994907, + "referenceP95Ms": 0.7800000000006548, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 3.0900000000001455, - "competitorP95Ms": 7.459999999999809, + "competitorMs": 3.0600000000000023, + "competitorP95Ms": 10.060000000012224, "competitorGpuMs": 16.660000000000082, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.1844660194176672, - "factor": 5.421052631573321, - "label": "ExoJS leads clearly (5.42x)", + "ratio": 0.18300653594754585, + "factor": 5.464285714290688, + "label": "ExoJS leads clearly (5.46x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1093,14 +1092,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5399999999999636, - "maxMs": 0.5799999999999272, - "ratio": 1.0740740740740118 + "minMs": 0.5500000000001819, + "maxMs": 0.6199999999989814, + "ratio": 1.1272727272705023 }, "competitor": { - "minMs": 3.0699999999999363, - "maxMs": 3.1700000000000728, - "ratio": 1.0325732899023252 + "minMs": 3.0599999999999454, + "maxMs": 3.109999999993306, + "ratio": 1.0163398692788763 }, "stable": true, "rungs": [ @@ -1112,17 +1111,17 @@ }, { "competitor": "pixi", - "referenceMs": 0.5700000000006185, - "referenceP95Ms": 0.7999999999992724, - "referenceGpuMs": 16.679999999998472, + "referenceMs": 0.5599999999994907, + "referenceP95Ms": 0.7800000000006548, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 0.5799999999999272, - "competitorP95Ms": 0.7999999999992724, - "competitorGpuMs": 16.68000000000029, + "competitorMs": 0.5599999999994907, + "competitorP95Ms": 0.8000000000010914, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9827586206908447, + "ratio": 1, "factor": 1, "label": "level", "structural": false @@ -1132,14 +1131,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5399999999999636, - "maxMs": 0.5799999999999272, - "ratio": 1.0740740740740118 + "minMs": 0.5500000000001819, + "maxMs": 0.6199999999989814, + "ratio": 1.1272727272705023 }, "competitor": { - "minMs": 0.5599999999994907, - "maxMs": 0.6000000000003638, - "ratio": 1.0714285714301954 + "minMs": 0.5400000000008731, + "maxMs": 0.5799999999999272, + "ratio": 1.0740740740722028 }, "stable": true, "rungs": [ @@ -1161,19 +1160,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 2.8299999999999272, - "referenceP95Ms": 4.479999999999563, + "referenceMs": 2.889999999999418, + "referenceP95Ms": 6.240000000001601, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 25.139999999999418, - "competitorP95Ms": 30.04000000000815, - "competitorGpuMs": 26.679999999993015, + "competitorMs": 25.400000000001455, + "competitorP95Ms": 31.079999999987194, + "competitorGpuMs": 26.84000000001106, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.11256961018297505, - "factor": 8.883392226148432, - "label": "ExoJS leads clearly (8.88x)", + "ratio": 0.11377952755902568, + "factor": 8.788927335642413, + "label": "ExoJS leads clearly (8.79x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1181,14 +1180,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.5600000000013097, - "maxMs": 2.8400000000001455, - "ratio": 1.1093749999994893 + "minMs": 2.8400000000001455, + "maxMs": 2.9500000000025466, + "ratio": 1.0387323943670406 }, "competitor": { - "minMs": 25.06999999999971, - "maxMs": 25.52999999999156, - "ratio": 1.0183486238528863 + "minMs": 25.360000000000582, + "maxMs": 25.44000000000233, + "ratio": 1.003154574132561 }, "stable": true, "rungs": [ @@ -1200,19 +1199,19 @@ }, { "competitor": "phaser", - "referenceMs": 2.8299999999999272, - "referenceP95Ms": 4.479999999999563, + "referenceMs": 2.889999999999418, + "referenceP95Ms": 6.240000000001601, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 10.56000000000131, - "competitorP95Ms": 11.200000000000728, + "competitorMs": 10.860000000000582, + "competitorP95Ms": 11.380000000001019, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.2679924242423841, - "factor": 3.731448763251442, - "label": "ExoJS leads (3.73x)", + "ratio": 0.2661141804787535, + "factor": 3.757785467128986, + "label": "ExoJS leads (3.76x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1220,14 +1219,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.5600000000013097, - "maxMs": 2.8400000000001455, - "ratio": 1.1093749999994893 + "minMs": 2.8400000000001455, + "maxMs": 2.9500000000025466, + "ratio": 1.0387323943670406 }, "competitor": { - "minMs": 10.550000000001091, - "maxMs": 10.659999999999854, - "ratio": 1.0104265402842418 + "minMs": 10.590000000000146, + "maxMs": 10.880000000001019, + "ratio": 1.0273843248348318 }, "stable": true, "rungs": [ @@ -1239,12 +1238,12 @@ }, { "competitor": "pixi", - "referenceMs": 2.8299999999999272, - "referenceP95Ms": 4.479999999999563, + "referenceMs": 2.889999999999418, + "referenceP95Ms": 6.240000000001601, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 2.400000000001455, - "competitorP95Ms": 8.040000000000873, + "competitorMs": 2.5100000000002183, + "competitorP95Ms": 7.580000000001746, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { @@ -1259,14 +1258,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.5600000000013097, - "maxMs": 2.8400000000001455, - "ratio": 1.1093749999994893 + "minMs": 2.8400000000001455, + "maxMs": 2.9500000000025466, + "ratio": 1.0387323943670406 }, "competitor": { - "minMs": 2.3400000000001455, - "maxMs": 2.5600000000013097, - "ratio": 1.0940170940175857 + "minMs": 2.3699999999989814, + "maxMs": 2.6199999999989814, + "ratio": 1.105485232067556 }, "stable": false, "rungs": [ @@ -1294,18 +1293,18 @@ { "competitor": "excalibur", "referenceMs": 0.07999999999992724, - "referenceP95Ms": 0.12000000000080036, - "referenceGpuMs": 16.68000000000029, + "referenceP95Ms": 0.13999999999941792, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 0.6399999999994179, - "competitorP95Ms": 0.8600000000005821, - "competitorGpuMs": 16.639999999999418, + "competitorMs": 0.6200000000026193, + "competitorP95Ms": 0.8799999999973807, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.125, - "factor": 8, - "label": "ExoJS leads clearly (8.00x)", + "ratio": 0.12903225806385366, + "factor": 7.7500000000397895, + "label": "ExoJS leads clearly (7.75x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1318,9 +1317,9 @@ "ratio": 1 }, "competitor": { - "minMs": 0.4500000000007276, - "maxMs": 0.6599999999962165, - "ratio": 1.4666666666558874 + "minMs": 0.6199999999989814, + "maxMs": 0.6599999999998545, + "ratio": 1.0645161290337724 }, "stable": true, "rungs": [ @@ -1333,18 +1332,18 @@ { "competitor": "phaser", "referenceMs": 0.07999999999992724, - "referenceP95Ms": 0.12000000000080036, - "referenceGpuMs": 16.68000000000029, + "referenceP95Ms": 0.13999999999941792, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 0.27999999999974534, + "competitorMs": 0.2600000000002183, "competitorP95Ms": 0.4600000000000364, - "competitorGpuMs": 16.659999999999854, + "competitorGpuMs": 16.679999999999836, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.2857142857142857, - "factor": 3.5, - "label": "ExoJS leads (3.50x)", + "ratio": 0.30769230769176953, + "factor": 3.2500000000056843, + "label": "ExoJS leads (3.25x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1357,9 +1356,9 @@ "ratio": 1 }, "competitor": { - "minMs": 0.25999999999976353, - "maxMs": 0.2800000000002001, - "ratio": 1.076923076924826 + "minMs": 0.20000000001164153, + "maxMs": 0.27999999999974534, + "ratio": 1.399999999917236 }, "stable": true, "rungs": [ @@ -1372,18 +1371,18 @@ { "competitor": "pixi", "referenceMs": 0.07999999999992724, - "referenceP95Ms": 0.12000000000080036, - "referenceGpuMs": 16.68000000000029, + "referenceP95Ms": 0.13999999999941792, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 0.09999999999854481, + "competitorMs": 0.1000000000003638, "competitorP95Ms": 0.21999999999934516, - "competitorGpuMs": 16.679999999998472, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "exojs", + "ratio": 0.7999999999963621, + "factor": 1.2500000000056843, + "label": "ExoJS leads (1.25x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -1396,14 +1395,14 @@ "ratio": 1 }, "competitor": { - "minMs": 0.09999999999854481, + "minMs": 0.1000000000003638, "maxMs": 0.1000000000003638, - "ratio": 1.00000000001819 + "ratio": 1 }, - "stable": false, + "stable": true, "rungs": [ - "level", - "level", + "exojs-leads", + "exojs-leads", "exojs-leads" ] } @@ -1420,19 +1419,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 1.4099999999998545, + "referenceMs": 1.4000000000014552, "referenceP95Ms": 1.819999999999709, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 19.31000000000131, - "competitorP95Ms": 23.5, - "competitorGpuMs": 28.459999999999127, - "competitorOverFrameBudget": true, + "competitorMs": 16.159999999999854, + "competitorP95Ms": 17.919999999998254, + "competitorGpuMs": 26.89999999999418, + "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.07301916105643495, - "factor": 13.69503546099525, - "label": "ExoJS leads clearly (13.70x)", + "ratio": 0.08663366336642746, + "factor": 11.542857142845042, + "label": "ExoJS leads clearly (11.54x)", "structural": true }, "mechanism": "ExoJS issues fewer buffer uploads (0 vs 313 per frame)", @@ -1440,14 +1439,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.3700000000008004, - "maxMs": 1.4200000000000728, - "ratio": 1.0364963503644111 + "minMs": 1.3799999999991996, + "maxMs": 1.4099999999998545, + "ratio": 1.0217391304352696 }, "competitor": { - "minMs": 19.30000000000291, - "maxMs": 20.780000000002474, - "ratio": 1.0766839378238 + "minMs": 15.869999999998981, + "maxMs": 19.55000000000291, + "ratio": 1.2318840579712769 }, "stable": true, "rungs": [ @@ -1459,19 +1458,19 @@ }, { "competitor": "phaser", - "referenceMs": 1.4099999999998545, + "referenceMs": 1.4000000000014552, "referenceP95Ms": 1.819999999999709, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 2.7600000000002183, - "competitorP95Ms": 7.199999999999818, + "competitorMs": 2.8899999999998727, + "competitorP95Ms": 6.819999999999709, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5108695652172982, - "factor": 1.957446808510995, - "label": "ExoJS leads (1.96x)", + "ratio": 0.48442906574446953, + "factor": 2.0642857142834776, + "label": "ExoJS leads (2.06x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 313/5000/0 vs 313/5000/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1479,14 +1478,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.3700000000008004, - "maxMs": 1.4200000000000728, - "ratio": 1.0364963503644111 + "minMs": 1.3799999999991996, + "maxMs": 1.4099999999998545, + "ratio": 1.0217391304352696 }, "competitor": { - "minMs": 2.7200000000002547, - "maxMs": 2.9200000000000728, - "ratio": 1.0735294117646321 + "minMs": 2.7800000000002, + "maxMs": 3, + "ratio": 1.0791366906474043 }, "stable": true, "rungs": [ @@ -1498,19 +1497,19 @@ }, { "competitor": "pixi", - "referenceMs": 1.4099999999998545, + "referenceMs": 1.4000000000014552, "referenceP95Ms": 1.819999999999709, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 1.2200000000002547, + "competitorMs": 1.180000000000291, "competitorP95Ms": 2, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.1557377049176722, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 313/5000/0 vs 313/5000/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -1518,20 +1517,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.3700000000008004, - "maxMs": 1.4200000000000728, - "ratio": 1.0364963503644111 + "minMs": 1.3799999999991996, + "maxMs": 1.4099999999998545, + "ratio": 1.0217391304352696 }, "competitor": { - "minMs": 1.2200000000002547, - "maxMs": 1.3499999999985448, - "ratio": 1.1065573770477566 + "minMs": 1.139999999999418, + "maxMs": 1.4799999999995634, + "ratio": 1.2982456140353678 }, - "stable": true, + "stable": false, "rungs": [ "level", "level", - "level" + "competitor-leads" ] } } @@ -1547,19 +1546,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.5399999999990541, - "referenceP95Ms": 0.7200000000011642, - "referenceGpuMs": 16.679999999996653, + "referenceMs": 0.4599999999991269, + "referenceP95Ms": 0.7399999999979627, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 10.080000000001746, - "competitorP95Ms": 13.480000000003201, + "competitorMs": 9.779999999995198, + "competitorP95Ms": 12.940000000002328, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.053571428571325456, - "factor": 18.666666666702596, - "label": "ExoJS leads clearly (18.67x)", + "ratio": 0.04703476482610969, + "factor": 21.260869565247308, + "label": "ExoJS leads clearly (21.26x)", "structural": true }, "mechanism": "competitor issues fewer draw calls (120 vs 1 per frame)", @@ -1567,14 +1566,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.47000000000116415, - "maxMs": 0.5399999999990541, - "ratio": 1.1489361702079077 + "minMs": 0.38000000000101863, + "maxMs": 0.5400000000008731, + "ratio": 1.4210526315774357 }, "competitor": { - "minMs": 9.999999999996362, - "maxMs": 10.349999999998545, - "ratio": 1.035000000000231 + "minMs": 9.590000000000146, + "maxMs": 9.909999999996217, + "ratio": 1.0333680917618422 }, "stable": true, "rungs": [ @@ -1586,13 +1585,13 @@ }, { "competitor": "phaser", - "referenceMs": 0.5399999999990541, - "referenceP95Ms": 0.7200000000011642, - "referenceGpuMs": 16.679999999996653, + "referenceMs": 0.4599999999991269, + "referenceP95Ms": 0.7399999999979627, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 2.4099999999998545, - "competitorP95Ms": 4.699999999999818, - "competitorGpuMs": 16.660000000000764, + "competitorMs": 2.6699999999982538, + "competitorP95Ms": 4.859999999999673, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -1606,38 +1605,38 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.47000000000116415, - "maxMs": 0.5399999999990541, - "ratio": 1.1489361702079077 + "minMs": 0.38000000000101863, + "maxMs": 0.5400000000008731, + "ratio": 1.4210526315774357 }, "competitor": { - "minMs": 2.399999999999636, - "maxMs": 2.6400000000003274, - "ratio": 1.1000000000003032 + "minMs": 2.6299999999996544, + "maxMs": 2.6700000000000728, + "ratio": 1.0152091254754463 }, "stable": false, "rungs": [ - "exojs-leads", - "exojs-leads", - "exojs-leads-clearly" + "exojs-leads-clearly", + "exojs-leads-clearly", + "exojs-leads" ] } }, { "competitor": "pixi", - "referenceMs": 0.5399999999990541, - "referenceP95Ms": 0.7200000000011642, - "referenceGpuMs": 16.679999999996653, + "referenceMs": 0.4599999999991269, + "referenceP95Ms": 0.7399999999979627, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, "competitorMs": 0.31999999999970896, - "competitorP95Ms": 0.42000000000189175, - "competitorGpuMs": 16.68000000000029, + "competitorP95Ms": 0.4400000000023283, + "competitorGpuMs": 16.679999999996653, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 1.687499999998579, - "factor": 1.687499999998579, - "label": "competitor leads (1.69x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 120/0/0 vs 158/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -1645,19 +1644,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.47000000000116415, - "maxMs": 0.5399999999990541, - "ratio": 1.1489361702079077 + "minMs": 0.38000000000101863, + "maxMs": 0.5400000000008731, + "ratio": 1.4210526315774357 }, "competitor": { "minMs": 0.3000000000029104, "maxMs": 0.31999999999970896, "ratio": 1.0666666666553486 }, - "stable": true, + "stable": false, "rungs": [ "competitor-leads", - "competitor-leads", + "level", "competitor-leads" ] } @@ -1680,18 +1679,18 @@ { "competitor": "excalibur", "referenceMs": 0.08000000000174623, - "referenceP95Ms": 0.13999999999941792, - "referenceGpuMs": 16.68000000000029, + "referenceP95Ms": 0.12000000000261934, + "referenceGpuMs": 16.679999999996653, "referenceOverFrameBudget": false, - "competitorMs": 10.569999999999709, - "competitorP95Ms": 14.520000000004075, + "competitorMs": 9.659999999999854, + "competitorP95Ms": 14.37999999999738, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.007568590350212718, - "factor": 132.12499999711235, - "label": "ExoJS leads clearly (132.12x)", + "ratio": 0.008281573499145697, + "factor": 120.74999999736247, + "label": "ExoJS leads clearly (120.75x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1704,9 +1703,9 @@ "ratio": 1 }, "competitor": { - "minMs": 10.130000000001019, - "maxMs": 10.700000000000728, - "ratio": 1.0562685093780506 + "minMs": 9.650000000001455, + "maxMs": 10.049999999999272, + "ratio": 1.04145077720184 }, "stable": true, "rungs": [ @@ -1719,18 +1718,18 @@ { "competitor": "phaser", "referenceMs": 0.08000000000174623, - "referenceP95Ms": 0.13999999999941792, - "referenceGpuMs": 16.68000000000029, + "referenceP95Ms": 0.12000000000261934, + "referenceGpuMs": 16.679999999996653, "referenceOverFrameBudget": false, - "competitorMs": 2.730000000000018, - "competitorP95Ms": 5, - "competitorGpuMs": 16.659999999999854, + "competitorMs": 2.600000000000364, + "competitorP95Ms": 4.059999999997672, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.029304029304668754, - "factor": 34.12499999925535, - "label": "ExoJS leads clearly (34.12x)", + "ratio": 0.03076923076989809, + "factor": 32.49999999929514, + "label": "ExoJS leads clearly (32.50x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1743,9 +1742,9 @@ "ratio": 1 }, "competitor": { - "minMs": 2.5200000000004366, - "maxMs": 2.9600000000000364, - "ratio": 1.1746031746029855 + "minMs": 2.4399999999996, + "maxMs": 2.889999999992142, + "ratio": 1.1844262295051704 }, "stable": true, "rungs": [ @@ -1758,12 +1757,12 @@ { "competitor": "pixi", "referenceMs": 0.08000000000174623, - "referenceP95Ms": 0.13999999999941792, - "referenceGpuMs": 16.68000000000029, + "referenceP95Ms": 0.12000000000261934, + "referenceGpuMs": 16.679999999996653, "referenceOverFrameBudget": false, "competitorMs": 0.09999999999854481, - "competitorP95Ms": 0.21999999999752617, - "competitorGpuMs": 16.659999999999854, + "competitorP95Ms": 0.18000000000029104, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -1782,9 +1781,9 @@ "ratio": 1 }, "competitor": { - "minMs": 0.09999999999854481, - "maxMs": 0.1000000000003638, - "ratio": 1.00000000001819 + "minMs": 0.08000000000174623, + "maxMs": 0.09999999999854481, + "ratio": 1.2499999999545253 }, "stable": true, "rungs": [ @@ -1815,15 +1814,15 @@ "referenceP95Ms": 0.22000000000116415, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 6.579999999998108, - "competitorP95Ms": 9.160000000003492, + "competitorMs": 6.619999999995343, + "competitorP95Ms": 9.440000000002328, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.01519756838939385, - "factor": 65.7999999985448, - "label": "ExoJS leads clearly (65.80x)", + "ratio": 0.015105740181609234, + "factor": 66.19999999850843, + "label": "ExoJS leads clearly (66.20x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1836,9 +1835,9 @@ "ratio": 1.0000000000363798 }, "competitor": { - "minMs": 6.519999999996799, - "maxMs": 7.1599999999998545, - "ratio": 1.0981595092029708 + "minMs": 6.580000000001746, + "maxMs": 6.700000000000728, + "ratio": 1.0182370820667097 }, "stable": true, "rungs": [ @@ -1854,15 +1853,15 @@ "referenceP95Ms": 0.22000000000116415, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 2.9200000000000728, - "competitorP95Ms": 3.9799999999995634, - "competitorGpuMs": 16.659999999999854, + "competitorMs": 2.970000000001164, + "competitorP95Ms": 4.220000000001164, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.03424657534321243, - "factor": 29.199999999363353, - "label": "ExoJS leads clearly (29.20x)", + "ratio": 0.03367003367075542, + "factor": 29.699999999363353, + "label": "ExoJS leads clearly (29.70x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1875,9 +1874,9 @@ "ratio": 1.0000000000363798 }, "competitor": { - "minMs": 2.8900000000003274, - "maxMs": 2.9399999999986903, - "ratio": 1.0173010380617153 + "minMs": 2.8599999999996726, + "maxMs": 3.039999999999054, + "ratio": 1.062937062936854 }, "stable": true, "rungs": [ @@ -1893,13 +1892,13 @@ "referenceP95Ms": 0.22000000000116415, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 0.10000000000218279, - "competitorP95Ms": 0.22000000000116415, + "competitorMs": 0.09999999999854481, + "competitorP95Ms": 0.21999999999752617, "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1, + "ratio": 1.0000000000363798, "factor": 1, "label": "level", "structural": false @@ -1915,8 +1914,8 @@ }, "competitor": { "minMs": 0.09999999999854481, - "maxMs": 0.11999999999898137, - "ratio": 1.200000000007276 + "maxMs": 0.09999999999854481, + "ratio": 1 }, "stable": true, "rungs": [ @@ -1938,19 +1937,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.09999999999854481, + "referenceMs": 0.08000000000174623, "referenceP95Ms": 0.15999999999985448, - "referenceGpuMs": 16.68000000000029, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 32.89999999999782, - "competitorP95Ms": 38.400000000001455, - "competitorGpuMs": 34.54000000000087, + "competitorMs": 33.390000000003056, + "competitorP95Ms": 38.18000000000029, + "competitorGpuMs": 34.63999999999942, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.003039513677767521, - "factor": 329.00000000476575, - "label": "ExoJS leads clearly (329.00x)", + "ratio": 0.0023959269242808897, + "factor": 417.3749999909278, + "label": "ExoJS leads clearly (417.37x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 3/4/3, none of them large enough to account for the gap); the difference is CPU-side", @@ -1958,14 +1957,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.09000000000014552, + "minMs": 0.08000000000174623, "maxMs": 0.09999999999854481, - "ratio": 1.1111111110931458 + "ratio": 1.2499999999545253 }, "competitor": { - "minMs": 32.58999999999651, - "maxMs": 47.19000000000233, - "ratio": 1.4479901810373546 + "minMs": 32.85000000000218, + "maxMs": 33.43000000000029, + "ratio": 1.0176560121765013 }, "stable": true, "rungs": [ @@ -1977,19 +1976,19 @@ }, { "competitor": "phaser", - "referenceMs": 0.09999999999854481, + "referenceMs": 0.08000000000174623, "referenceP95Ms": 0.15999999999985448, - "referenceGpuMs": 16.68000000000029, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 6.710000000000946, - "competitorP95Ms": 8.119999999998981, + "competitorMs": 6.8400000000037835, + "competitorP95Ms": 7.959999999999127, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.014903129657009047, - "factor": 67.1000000009859, - "label": "ExoJS leads clearly (67.10x)", + "ratio": 0.011695906432997366, + "factor": 85.49999999818101, + "label": "ExoJS leads clearly (85.50x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 4/0/4, none of them large enough to account for the gap); the difference is CPU-side", @@ -1997,14 +1996,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.09000000000014552, + "minMs": 0.08000000000174623, "maxMs": 0.09999999999854481, - "ratio": 1.1111111110931458 + "ratio": 1.2499999999545253 }, "competitor": { - "minMs": 6.6899999999996, - "maxMs": 6.760000000000218, - "ratio": 1.0104633781764758 + "minMs": 6.5, + "maxMs": 6.899999999999636, + "ratio": 1.0615384615384056 }, "stable": true, "rungs": [ @@ -2016,17 +2015,17 @@ }, { "competitor": "pixi", - "referenceMs": 0.09999999999854481, + "referenceMs": 0.08000000000174623, "referenceP95Ms": 0.15999999999985448, - "referenceGpuMs": 16.68000000000029, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, "competitorMs": 0.09999999999854481, - "competitorP95Ms": 0.21999999999752617, - "competitorGpuMs": 16.659999999999854, + "competitorP95Ms": 0.2000000000007276, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1, + "ratio": 0.8000000000291039, "factor": 1, "label": "level", "structural": false @@ -2036,14 +2035,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.09000000000014552, + "minMs": 0.08000000000174623, "maxMs": 0.09999999999854481, - "ratio": 1.1111111110931458 + "ratio": 1.2499999999545253 }, "competitor": { - "minMs": 0.09000000000014552, - "maxMs": 0.09999999999854481, - "ratio": 1.1111111110931458 + "minMs": 0.09999999999854481, + "maxMs": 0.10000000000218279, + "ratio": 1.0000000000363798 }, "stable": true, "rungs": [ @@ -2065,19 +2064,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 2.539999999997235, - "referenceP95Ms": 7.1599999999998545, + "referenceMs": 2.5900000000019645, + "referenceP95Ms": 6.400000000001455, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 7.019999999996799, - "competitorP95Ms": 9.30000000000291, + "competitorMs": 7.130000000001019, + "competitorP95Ms": 9.660000000003492, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.36182336182313296, - "factor": 2.7637795275608035, - "label": "ExoJS leads (2.76x)", + "ratio": 0.3632538569427201, + "factor": 2.7528957528940583, + "label": "ExoJS leads (2.75x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -2085,14 +2084,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.4899999999997817, - "maxMs": 2.5799999999981083, - "ratio": 1.036144578312584 + "minMs": 2.460000000000946, + "maxMs": 2.640000000001237, + "ratio": 1.0731707317074073 }, "competitor": { - "minMs": 6.930000000000291, - "maxMs": 7.1599999999998545, - "ratio": 1.0331890331889688 + "minMs": 7.120000000002619, + "maxMs": 7.5900000000001455, + "ratio": 1.0660112359546845 }, "stable": true, "rungs": [ @@ -2104,19 +2103,19 @@ }, { "competitor": "phaser", - "referenceMs": 2.539999999997235, - "referenceP95Ms": 7.1599999999998545, + "referenceMs": 2.5900000000019645, + "referenceP95Ms": 6.400000000001455, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 2.8200000000006185, - "competitorP95Ms": 5.600000000000364, + "competitorMs": 3.0600000000004, + "competitorP95Ms": 4.960000000006403, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.900709219856978, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -2124,18 +2123,18 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.4899999999997817, - "maxMs": 2.5799999999981083, - "ratio": 1.036144578312584 + "minMs": 2.460000000000946, + "maxMs": 2.640000000001237, + "ratio": 1.0731707317074073 }, "competitor": { - "minMs": 2.7799999999997453, - "maxMs": 3.0200000000004366, - "ratio": 1.0863309352520552 + "minMs": 3.030000000000655, + "maxMs": 3.210000000006403, + "ratio": 1.0594059405959435 }, - "stable": true, + "stable": false, "rungs": [ - "level", + "exojs-leads", "level", "level" ] @@ -2143,19 +2142,19 @@ }, { "competitor": "pixi", - "referenceMs": 2.539999999997235, - "referenceP95Ms": 7.1599999999998545, + "referenceMs": 2.5900000000019645, + "referenceP95Ms": 6.400000000001455, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 4.43999999999869, - "competitorP95Ms": 5.900000000001455, + "competitorMs": 4.450000000000728, + "competitorP95Ms": 6, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5720720720716181, - "factor": 1.7480314960643792, - "label": "ExoJS leads (1.75x)", + "ratio": 0.5820224719104586, + "factor": 1.718146718145696, + "label": "ExoJS leads (1.72x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -2163,14 +2162,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.4899999999997817, - "maxMs": 2.5799999999981083, - "ratio": 1.036144578312584 + "minMs": 2.460000000000946, + "maxMs": 2.640000000001237, + "ratio": 1.0731707317074073 }, "competitor": { - "minMs": 4.230000000001382, - "maxMs": 4.5, - "ratio": 1.0638297872336948 + "minMs": 4.3700000000008, + "maxMs": 4.459999999999127, + "ratio": 1.0205949656746705 }, "stable": true, "rungs": [ @@ -2192,19 +2191,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 7.130000000001019, - "referenceP95Ms": 9.139999999999418, + "referenceMs": 7.4099999999980355, + "referenceP95Ms": 8.840000000000146, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 34.44000000000233, - "competitorP95Ms": 39.16000000000349, - "competitorGpuMs": 36.19999999999709, + "competitorMs": 34.94999999999709, + "competitorP95Ms": 39.4800000000032, + "competitorGpuMs": 36.419999999998254, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.2070267131242897, - "factor": 4.830294530153914, - "label": "ExoJS leads (4.83x)", + "ratio": 0.21201716738193568, + "factor": 4.716599190284259, + "label": "ExoJS leads (4.72x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 3/4/3, none of them large enough to account for the gap); the difference is CPU-side", @@ -2212,14 +2211,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 7.1299999999992, - "maxMs": 7.570000000001528, - "ratio": 1.0617110799442324 + "minMs": 7.099999999998545, + "maxMs": 7.469999999999345, + "ratio": 1.0521126760564614 }, "competitor": { - "minMs": 34.31999999999971, - "maxMs": 35.36000000000058, - "ratio": 1.030303030303056 + "minMs": 34.22999999999956, + "maxMs": 36.279999999998836, + "ratio": 1.059888986269334 }, "stable": true, "rungs": [ @@ -2231,17 +2230,17 @@ }, { "competitor": "phaser", - "referenceMs": 7.130000000001019, - "referenceP95Ms": 9.139999999999418, + "referenceMs": 7.4099999999980355, + "referenceP95Ms": 8.840000000000146, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 7, - "competitorP95Ms": 8.55999999999949, + "competitorMs": 6.9399999999996, + "competitorP95Ms": 8.220000000001164, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.0185714285715741, + "ratio": 1.0677233429392599, "factor": 1, "label": "level", "structural": false @@ -2251,14 +2250,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 7.1299999999992, - "maxMs": 7.570000000001528, - "ratio": 1.0617110799442324 + "minMs": 7.099999999998545, + "maxMs": 7.469999999999345, + "ratio": 1.0521126760564614 }, "competitor": { - "minMs": 6.989999999999782, - "maxMs": 7.130000000000109, - "ratio": 1.020028612303338 + "minMs": 6.930000000007567, + "maxMs": 7.050000000000182, + "ratio": 1.0173160173149327 }, "stable": true, "rungs": [ @@ -2270,19 +2269,19 @@ }, { "competitor": "pixi", - "referenceMs": 7.130000000001019, - "referenceP95Ms": 9.139999999999418, + "referenceMs": 7.4099999999980355, + "referenceP95Ms": 8.840000000000146, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 18.910000000001673, - "competitorP95Ms": 24.299999999999272, - "competitorGpuMs": 20.979999999999563, + "competitorMs": 18.779999999998836, + "competitorP95Ms": 22.600000000002183, + "competitorGpuMs": 20.860000000000582, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.37704918032788937, - "factor": 2.652173913043334, - "label": "ExoJS leads (2.65x)", + "ratio": 0.3945686900957665, + "factor": 2.534412955466102, + "label": "ExoJS leads (2.53x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -2290,14 +2289,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 7.1299999999992, - "maxMs": 7.570000000001528, - "ratio": 1.0617110799442324 + "minMs": 7.099999999998545, + "maxMs": 7.469999999999345, + "ratio": 1.0521126760564614 }, "competitor": { - "minMs": 18.860000000000582, - "maxMs": 19.290000000000873, - "ratio": 1.02279957582186 + "minMs": 18.770000000002256, + "maxMs": 18.93999999999869, + "ratio": 1.0090570058602246 }, "stable": true, "rungs": [ @@ -2324,19 +2323,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.3600000000005821, - "referenceP95Ms": 0.6399999999994179, + "referenceMs": 0.37999999999919964, + "referenceP95Ms": 0.680000000000291, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 2.100000000000364, - "competitorP95Ms": 3.540000000000873, + "competitorMs": 2.039999999999054, + "competitorP95Ms": 3.959999999999127, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.1714285714288189, - "factor": 5.833333333324912, - "label": "ExoJS leads clearly (5.83x)", + "ratio": 0.1862745098036156, + "factor": 5.3684210526403975, + "label": "ExoJS leads clearly (5.37x)", "structural": true }, "mechanism": "competitor issues fewer texture binds (9 vs 3 per frame)", @@ -2344,14 +2343,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.3400000000001455, + "minMs": 0.36999999999898137, "maxMs": 0.38000000000101863, - "ratio": 1.117647058826047 + "ratio": 1.0270270270326076 }, "competitor": { - "minMs": 2.0099999999983993, - "maxMs": 2.3099999999994907, - "ratio": 1.1492537313439455 + "minMs": 2.0100000000002183, + "maxMs": 2.0900000000001455, + "ratio": 1.039800995024835 }, "stable": true, "rungs": [ @@ -2373,19 +2372,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.31999999999970896, + "referenceMs": 0.3100000000013097, "referenceP95Ms": 0.5, - "referenceGpuMs": 16.660000000003492, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 2.609999999996944, - "competitorP95Ms": 4.1599999999962165, + "competitorMs": 2.4099999999998545, + "competitorP95Ms": 4.580000000001746, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.12260536398470637, - "factor": 8.156249999997868, - "label": "ExoJS leads clearly (8.16x)", + "ratio": 0.12863070539474208, + "factor": 7.774193548353782, + "label": "ExoJS leads clearly (7.77x)", "structural": true }, "mechanism": "competitor issues fewer texture binds (11 vs 3 per frame)", @@ -2393,14 +2392,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.3100000000013097, + "minMs": 0.3000000000029104, "maxMs": 0.31999999999970896, - "ratio": 1.0322580645108292 + "ratio": 1.0666666666553486 }, "competitor": { - "minMs": 2.099999999998545, - "maxMs": 2.7599999999965803, - "ratio": 1.3142857142849966 + "minMs": 2.349999999998545, + "maxMs": 2.640000000003056, + "ratio": 1.123404255321145 }, "stable": true, "rungs": [ @@ -2422,19 +2421,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.27999999999883585, - "referenceP95Ms": 0.5, - "referenceGpuMs": 16.660000000003492, + "referenceMs": 0.2999999999992724, + "referenceP95Ms": 0.4599999999991269, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 2.570000000003347, - "competitorP95Ms": 4.220000000001164, - "competitorGpuMs": 16.68000000000029, + "competitorMs": 2.7600000000020373, + "competitorP95Ms": 4.440000000002328, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.10894941634181758, - "factor": 9.178571428621543, - "label": "ExoJS leads clearly (9.18x)", + "ratio": 0.10869565217356919, + "factor": 9.200000000029103, + "label": "ExoJS leads clearly (9.20x)", "structural": true }, "mechanism": "competitor issues fewer texture binds (15 vs 7 per frame)", @@ -2442,14 +2441,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.27999999999883585, - "maxMs": 0.2999999999956344, - "ratio": 1.0714285714174347 + "minMs": 0.26000000000203727, + "maxMs": 0.31999999999970896, + "ratio": 1.2307692307584674 }, "competitor": { - "minMs": 2.3100000000013097, - "maxMs": 2.599999999998545, - "ratio": 1.1255411255398575 + "minMs": 2.399999999997817, + "maxMs": 2.959999999999127, + "ratio": 1.2333333333340912 }, "stable": true, "rungs": [ @@ -2471,19 +2470,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.31999999999970896, - "referenceP95Ms": 0.6000000000058208, - "referenceGpuMs": 16.68000000000029, + "referenceMs": 0.3400000000037835, + "referenceP95Ms": 0.5600000000049477, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, "competitorMs": 0.4599999999991269, - "competitorP95Ms": 0.7200000000011642, - "competitorGpuMs": 16.68000000000029, + "competitorP95Ms": 0.7600000000020373, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.6956521739137312, - "factor": 1.4374999999985787, - "label": "ExoJS leads (1.44x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (5 vs 8 per frame)", @@ -2492,19 +2491,19 @@ "runs": 3, "reference": { "minMs": 0.31999999999970896, - "maxMs": 0.32999999999810825, - "ratio": 1.0312499999950262 + "maxMs": 0.3599999999933061, + "ratio": 1.1249999999801048 }, "competitor": { - "minMs": 0.41999999999825377, - "maxMs": 0.47999999999592546, - "ratio": 1.1428571428521932 + "minMs": 0.42000000000552973, + "maxMs": 0.5599999999976717, + "ratio": 1.333333333310235 }, - "stable": true, + "stable": false, "rungs": [ "exojs-leads", "exojs-leads", - "exojs-leads" + "level" ] } } @@ -2520,19 +2519,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.250000000003638, - "referenceP95Ms": 0.4400000000023283, - "referenceGpuMs": 16.68000000000029, + "referenceMs": 0.2400000000052387, + "referenceP95Ms": 0.43999999999505235, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.15999999999985448, - "competitorP95Ms": 0.27999999999883585, - "competitorGpuMs": 16.660000000003492, + "competitorMs": 0.16000000000349246, + "competitorP95Ms": 0.26000000000203727, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.5625000000241585, - "factor": 1.5625000000241585, - "label": "competitor leads (1.56x)", + "ratio": 1.5, + "factor": 1.5, + "label": "competitor leads (1.50x)", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", @@ -2540,9 +2539,9 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.22999999999956344, - "maxMs": 0.26000000000203727, - "ratio": 1.130434782619699 + "minMs": 0.23999999999796273, + "maxMs": 0.25, + "ratio": 1.041666666675509 }, "competitor": { "minMs": 0.13999999999941792, @@ -2573,15 +2572,15 @@ "referenceP95Ms": 0.4400000000023283, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.4099999999998545, - "competitorP95Ms": 0.6399999999994179, - "competitorGpuMs": 16.660000000003492, + "competitorMs": 0.4000000000014552, + "competitorP95Ms": 0.5800000000017462, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.6341463414686087, - "factor": 1.576923076910161, - "label": "ExoJS leads (1.58x)", + "ratio": 0.6500000000027285, + "factor": 1.5384615384550804, + "label": "ExoJS leads (1.54x)", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", @@ -2589,14 +2588,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.26000000000203727, - "maxMs": 0.26000000000203727, - "ratio": 1 + "minMs": 0.2400000000016007, + "maxMs": 0.27999999999883585, + "ratio": 1.1666666666540348 }, "competitor": { - "minMs": 0.4099999999998545, - "maxMs": 0.4399999999986903, - "ratio": 1.0731707317045036 + "minMs": 0.3900000000030559, + "maxMs": 0.4000000000014552, + "ratio": 1.0256410256367203 }, "stable": true, "rungs": [ @@ -2623,19 +2622,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.18999999999869033, - "referenceP95Ms": 1.4400000000023283, - "referenceGpuMs": 16.659999999999854, + "referenceMs": 0.18000000000029104, + "referenceP95Ms": 1.6000000000021828, + "referenceGpuMs": 16.679999999996653, "referenceOverFrameBudget": false, - "competitorMs": 0.11999999999898137, + "competitorMs": 0.11000000000058208, "competitorP95Ms": 0.22000000000116415, - "competitorGpuMs": 16.68000000000029, + "competitorGpuMs": 16.679999999996653, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.5833333333358597, - "factor": 1.5833333333358597, - "label": "competitor leads (1.58x)", + "ratio": 1.6363636363576233, + "factor": 1.6363636363576233, + "label": "competitor leads (1.64x)", "structural": false }, "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", @@ -2644,8 +2643,8 @@ "runs": 3, "reference": { "minMs": 0.17999999999847205, - "maxMs": 0.1999999999989086, - "ratio": 1.1111111111144796 + "maxMs": 0.18000000000029104, + "ratio": 1.0000000000101055 }, "competitor": { "minMs": 0.10000000000218279, @@ -2678,18 +2677,18 @@ { "competitor": "excalibur", "referenceMs": 0.18000000000029104, - "referenceP95Ms": 1.180000000000291, - "referenceGpuMs": 16.68000000000029, + "referenceP95Ms": 1.1600000000034925, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 20.790000000000873, - "competitorP95Ms": 25.69999999999709, - "competitorGpuMs": 21.94000000000233, + "competitorMs": 20.810000000004948, + "competitorP95Ms": 25.90000000000873, + "competitorGpuMs": 22, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.008658008658022293, - "factor": 115.4999999998181, - "label": "ExoJS leads clearly (115.50x)", + "ratio": 0.008649687650180118, + "factor": 115.61111111095167, + "label": "ExoJS leads clearly (115.61x)", "structural": true }, "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", @@ -2697,14 +2696,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.1599999999962165, - "maxMs": 0.18000000000029104, - "ratio": 1.1250000000284217 + "minMs": 0.18000000000029104, + "maxMs": 0.20000000000436557, + "ratio": 1.1111111111335679 }, "competitor": { - "minMs": 20.60000000000582, - "maxMs": 21.860000000007858, - "ratio": 1.061165048543771 + "minMs": 20.580000000001746, + "maxMs": 21.060000000004948, + "ratio": 1.0233236151605034 }, "stable": true, "rungs": [ @@ -2717,18 +2716,18 @@ { "competitor": "phaser", "referenceMs": 0.18000000000029104, - "referenceP95Ms": 1.180000000000291, - "referenceGpuMs": 16.68000000000029, + "referenceP95Ms": 1.1600000000034925, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.20999999999912689, - "competitorP95Ms": 0.38000000000101863, + "competitorMs": 0.2000000000007276, + "competitorP95Ms": 0.42000000000189175, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 0.899999999998181, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", @@ -2736,19 +2735,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.1599999999962165, - "maxMs": 0.18000000000029104, - "ratio": 1.1250000000284217 + "minMs": 0.18000000000029104, + "maxMs": 0.20000000000436557, + "ratio": 1.1111111111335679 }, "competitor": { - "minMs": 0.2000000000007276, - "maxMs": 0.21999999999752617, - "ratio": 1.0999999999836292 + "minMs": 0.1999999999989086, + "maxMs": 0.20999999999912689, + "ratio": 1.0500000000013643 }, - "stable": false, + "stable": true, "rungs": [ "level", - "exojs-leads", + "level", "level" ] } @@ -2756,18 +2755,18 @@ { "competitor": "pixi", "referenceMs": 0.18000000000029104, - "referenceP95Ms": 1.180000000000291, - "referenceGpuMs": 16.68000000000029, + "referenceP95Ms": 1.1600000000034925, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, "competitorMs": 0.22000000000116415, - "competitorP95Ms": 0.39999999999417923, - "competitorGpuMs": 16.660000000003492, + "competitorP95Ms": 0.37999999999738066, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 0.8181818181788116, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (90 vs 282 per frame)", @@ -2775,19 +2774,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.1599999999962165, - "maxMs": 0.18000000000029104, - "ratio": 1.1250000000284217 + "minMs": 0.18000000000029104, + "maxMs": 0.20000000000436557, + "ratio": 1.1111111111335679 }, "competitor": { - "minMs": 0.2199999999938882, - "maxMs": 0.23999999999796273, - "ratio": 1.090909090930137 + "minMs": 0.20000000000436557, + "maxMs": 0.22000000000116415, + "ratio": 1.0999999999818102 }, - "stable": false, + "stable": true, "rungs": [ "level", - "exojs-leads", + "level", "level" ] } @@ -2805,18 +2804,18 @@ { "competitor": "excalibur", "referenceMs": 0.13999999999941792, - "referenceP95Ms": 0.7399999999979627, - "referenceGpuMs": 16.68000000000029, + "referenceP95Ms": 0.7799999999988358, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 186.42000000000553, - "competitorP95Ms": 196.44000000000233, - "competitorGpuMs": 193.6999999999971, + "competitorMs": 180.13999999999942, + "competitorP95Ms": 189.39999999999418, + "competitorGpuMs": 183.11999999999534, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.0007509923827884013, - "factor": 1331.5714285770043, - "label": "ExoJS leads clearly (1331.57x)", + "ratio": 0.0007771733096448227, + "factor": 1286.7142857196313, + "label": "ExoJS leads clearly (1286.71x)", "structural": true }, "mechanism": "competitor issues fewer draw calls (30 vs 5 per frame)", @@ -2824,14 +2823,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.12000000000261934, - "maxMs": 0.16000000000349246, - "ratio": 1.3333333333333333 + "minMs": 0.11999999999898137, + "maxMs": 0.18000000000029104, + "ratio": 1.5000000000151583 }, "competitor": { - "minMs": 182.55999999999767, - "maxMs": 189.13999999999942, - "ratio": 1.0360429447852861 + "minMs": 178.79000000000087, + "maxMs": 182.29000000000087, + "ratio": 1.0195760389283517 }, "stable": true, "rungs": [ @@ -2844,12 +2843,12 @@ { "competitor": "phaser", "referenceMs": 0.13999999999941792, - "referenceP95Ms": 0.7399999999979627, - "referenceGpuMs": 16.68000000000029, + "referenceP95Ms": 0.7799999999988358, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.19000000000050932, - "competitorP95Ms": 0.3400000000001455, - "competitorGpuMs": 16.68000000000029, + "competitorMs": 0.16000000000349246, + "competitorP95Ms": 0.38000000000101863, + "competitorGpuMs": 16.700000000000728, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -2863,18 +2862,18 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.12000000000261934, - "maxMs": 0.16000000000349246, - "ratio": 1.3333333333333333 + "minMs": 0.11999999999898137, + "maxMs": 0.18000000000029104, + "ratio": 1.5000000000151583 }, "competitor": { - "minMs": 0.18000000000029104, - "maxMs": 0.2000000000007276, - "ratio": 1.1111111111133567 + "minMs": 0.15999999999985448, + "maxMs": 0.17000000000189175, + "ratio": 1.0625000000127898 }, "stable": false, "rungs": [ - "exojs-leads", + "level", "exojs-leads", "level" ] @@ -2883,11 +2882,11 @@ { "competitor": "pixi", "referenceMs": 0.13999999999941792, - "referenceP95Ms": 0.7399999999979627, - "referenceGpuMs": 16.68000000000029, + "referenceP95Ms": 0.7799999999988358, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.18000000000029104, - "competitorP95Ms": 0.41999999999825377, + "competitorMs": 0.22000000000116415, + "competitorP95Ms": 0.3600000000005821, "competitorGpuMs": 16.659999999996217, "competitorOverFrameBudget": false, "verdict": { @@ -2902,14 +2901,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.12000000000261934, - "maxMs": 0.16000000000349246, - "ratio": 1.3333333333333333 + "minMs": 0.11999999999898137, + "maxMs": 0.18000000000029104, + "ratio": 1.5000000000151583 }, "competitor": { "minMs": 0.18000000000029104, - "maxMs": 0.21999999999752617, - "ratio": 1.2222222222065025 + "maxMs": 0.22000000000116415, + "ratio": 1.2222222222267136 }, "stable": false, "rungs": [ @@ -2931,19 +2930,19 @@ "cells": [ { "competitor": "phaser", - "referenceMs": 0.7800000000024738, + "referenceMs": 0.750000000003638, "referenceP95Ms": 1.319999999999709, - "referenceGpuMs": 16.68000000000029, + "referenceGpuMs": 16.659999999996217, "referenceOverFrameBudget": false, - "competitorMs": 1.7199999999993452, - "competitorP95Ms": 5.680000000000291, + "competitorMs": 1.8000000000010914, + "competitorP95Ms": 6, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.45348837209463416, - "factor": 2.205128205120372, - "label": "ExoJS leads (2.21x)", + "ratio": 0.4166666666684351, + "factor": 2.399999999989814, + "label": "ExoJS leads (2.40x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/3/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -2951,14 +2950,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.75, - "maxMs": 0.7900000000008731, - "ratio": 1.0533333333344974 + "minMs": 0.7000000000007276, + "maxMs": 0.8100000000013097, + "ratio": 1.1571428571435254 }, "competitor": { - "minMs": 1.6200000000008004, - "maxMs": 2.4499999999989086, - "ratio": 1.5123456790109249 + "minMs": 1.68000000000211, + "maxMs": 2.400000000001455, + "ratio": 1.4285714285705005 }, "stable": true, "rungs": [ @@ -2970,19 +2969,19 @@ }, { "competitor": "pixi", - "referenceMs": 0.7800000000024738, + "referenceMs": 0.750000000003638, "referenceP95Ms": 1.319999999999709, - "referenceGpuMs": 16.68000000000029, + "referenceGpuMs": 16.659999999996217, "referenceOverFrameBudget": false, - "competitorMs": 0.9799999999995634, - "competitorP95Ms": 1.9399999999950523, + "competitorMs": 1.1299999999973807, + "competitorP95Ms": 1.9000000000014552, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "exojs", + "ratio": 0.6637168141640499, + "factor": 1.5066666666558661, + "label": "ExoJS leads (1.51x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 2/0/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -2990,20 +2989,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.75, - "maxMs": 0.7900000000008731, - "ratio": 1.0533333333344974 + "minMs": 0.7000000000007276, + "maxMs": 0.8100000000013097, + "ratio": 1.1571428571435254 }, "competitor": { - "minMs": 0.7300000000032014, - "maxMs": 1, - "ratio": 1.3698630136926226 + "minMs": 1.110000000000582, + "maxMs": 1.2600000000020373, + "ratio": 1.1351351351363752 }, - "stable": false, + "stable": true, "rungs": [ "exojs-leads", "exojs-leads", - "level" + "exojs-leads" ] } } @@ -3024,19 +3023,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.27999999999883585, - "referenceP95Ms": 0.3800000000046566, + "referenceMs": 0.26000000000203727, + "referenceP95Ms": 0.41999999999825377, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 2.4099999999962165, - "competitorP95Ms": 6.580000000001746, + "competitorMs": 2.390000000006694, + "competitorP95Ms": 7.419999999998254, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.11618257261380723, - "factor": 8.607142857165131, - "label": "ExoJS leads clearly (8.61x)", + "ratio": 0.10878661087920881, + "factor": 9.19230769226141, + "label": "ExoJS leads clearly (9.19x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3044,14 +3043,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.2599999999947613, + "minMs": 0.26000000000203727, "maxMs": 0.27999999999883585, - "ratio": 1.0769230769402982 + "ratio": 1.076923076910161 }, "competitor": { - "minMs": 2.290000000000873, - "maxMs": 2.4400000000023283, - "ratio": 1.065502183406724 + "minMs": 2.389999999992142, + "maxMs": 2.4199999999982538, + "ratio": 1.0125523012578286 }, "stable": true, "rungs": [ @@ -3063,19 +3062,19 @@ }, { "competitor": "phaser", - "referenceMs": 0.27999999999883585, - "referenceP95Ms": 0.3800000000046566, + "referenceMs": 0.26000000000203727, + "referenceP95Ms": 0.41999999999825377, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.8800000000010186, - "competitorP95Ms": 1.0600000000013097, + "competitorMs": 0.8400000000001455, + "competitorP95Ms": 1.0800000000017462, "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.318181818180127, - "factor": 3.1428571428738477, - "label": "ExoJS leads (3.14x)", + "ratio": 0.30952380952618125, + "factor": 3.230769230744475, + "label": "ExoJS leads (3.23x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3083,14 +3082,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.2599999999947613, + "minMs": 0.26000000000203727, "maxMs": 0.27999999999883585, - "ratio": 1.0769230769402982 + "ratio": 1.076923076910161 }, "competitor": { - "minMs": 0.7999999999992724, - "maxMs": 0.9199999999982538, - "ratio": 1.149999999998863 + "minMs": 0.8400000000001455, + "maxMs": 0.8799999999901047, + "ratio": 1.047619047607086 }, "stable": true, "rungs": [ @@ -3102,19 +3101,19 @@ }, { "competitor": "pixi", - "referenceMs": 0.27999999999883585, - "referenceP95Ms": 0.3800000000046566, + "referenceMs": 0.26000000000203727, + "referenceP95Ms": 0.41999999999825377, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, "competitorMs": 0.13999999999941792, - "competitorP95Ms": 0.2599999999947613, - "competitorGpuMs": 16.68000000000029, + "competitorP95Ms": 0.27999999999883585, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 2, - "factor": 2, - "label": "competitor leads (2.00x)", + "ratio": 1.8571428571651305, + "factor": 1.8571428571651305, + "label": "competitor leads (1.86x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -3122,9 +3121,9 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.2599999999947613, + "minMs": 0.26000000000203727, "maxMs": 0.27999999999883585, - "ratio": 1.0769230769402982 + "ratio": 1.076923076910161 }, "competitor": { "minMs": 0.13999999999941792, @@ -3151,19 +3150,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.4599999999991269, - "referenceP95Ms": 0.5999999999985448, - "referenceGpuMs": 16.660000000003492, + "referenceMs": 0.46000000000276486, + "referenceP95Ms": 0.5800000000017462, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 25.42000000000553, - "competitorP95Ms": 28, - "competitorGpuMs": 26.24000000000524, + "competitorMs": 24.489999999997963, + "competitorP95Ms": 26.979999999995925, + "competitorGpuMs": 25.460000000006403, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.018095987411448734, - "factor": 55.2608695653343, - "label": "ExoJS leads clearly (55.26x)", + "ratio": 0.018783176806974403, + "factor": 53.23913043445818, + "label": "ExoJS leads clearly (53.24x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3172,13 +3171,13 @@ "runs": 3, "reference": { "minMs": 0.4500000000007276, - "maxMs": 0.4800000000032014, - "ratio": 1.0666666666720563 + "maxMs": 0.47999999999956344, + "ratio": 1.066666666663972 }, "competitor": { - "minMs": 24.700000000004366, - "maxMs": 25.499999999992724, - "ratio": 1.0323886639671342 + "minMs": 14.69999999999709, + "maxMs": 25.69000000000233, + "ratio": 1.7476190476195521 }, "stable": true, "rungs": [ @@ -3190,19 +3189,19 @@ }, { "competitor": "phaser", - "referenceMs": 0.4599999999991269, - "referenceP95Ms": 0.5999999999985448, - "referenceGpuMs": 16.660000000003492, + "referenceMs": 0.46000000000276486, + "referenceP95Ms": 0.5800000000017462, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 2.8099999999994907, - "competitorP95Ms": 3.8800000000010186, + "competitorMs": 2.9800000000032014, + "competitorP95Ms": 3.779999999998836, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.1637010676153773, - "factor": 6.108695652184401, - "label": "ExoJS leads clearly (6.11x)", + "ratio": 0.15436241610814452, + "factor": 6.478260869533239, + "label": "ExoJS leads clearly (6.48x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3211,13 +3210,13 @@ "runs": 3, "reference": { "minMs": 0.4500000000007276, - "maxMs": 0.4800000000032014, - "ratio": 1.0666666666720563 + "maxMs": 0.47999999999956344, + "ratio": 1.066666666663972 }, "competitor": { - "minMs": 2.7600000000002183, - "maxMs": 3.100000000000364, - "ratio": 1.1231884057971444 + "minMs": 2.7999999999992724, + "maxMs": 3.2000000000007276, + "ratio": 1.1428571428576997 }, "stable": true, "rungs": [ @@ -3229,18 +3228,18 @@ }, { "competitor": "pixi", - "referenceMs": 0.4599999999991269, - "referenceP95Ms": 0.5999999999985448, - "referenceGpuMs": 16.660000000003492, + "referenceMs": 0.46000000000276486, + "referenceP95Ms": 0.5800000000017462, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 0.11999999999534339, + "competitorMs": 0.11999999999898137, "competitorP95Ms": 0.2400000000052387, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 3.83333333347481, - "factor": 3.83333333347481, + "ratio": 3.8333333333889135, + "factor": 3.8333333333889135, "label": "competitor leads (3.83x)", "structural": false }, @@ -3250,8 +3249,8 @@ "runs": 3, "reference": { "minMs": 0.4500000000007276, - "maxMs": 0.4800000000032014, - "ratio": 1.0666666666720563 + "maxMs": 0.47999999999956344, + "ratio": 1.066666666663972 }, "competitor": { "minMs": 0.11999999999534339, @@ -3278,19 +3277,19 @@ "cells": [ { "competitor": "phaser", - "referenceMs": 1.6299999999973807, - "referenceP95Ms": 2.599999999998545, + "referenceMs": 1.6699999999982538, + "referenceP95Ms": 3.1000000000058208, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 9.469999999999345, - "competitorP95Ms": 9.80000000000291, + "competitorMs": 9.510000000002037, + "competitorP95Ms": 10.659999999999854, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.17212249207998875, - "factor": 5.809815950929179, - "label": "ExoJS leads clearly (5.81x)", + "ratio": 0.1756046267085064, + "factor": 5.694610778450288, + "label": "ExoJS leads clearly (5.69x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 7/0/7, none of them large enough to account for the gap); the difference is CPU-side", @@ -3298,14 +3297,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.6199999999989814, - "maxMs": 1.8099999999976717, - "ratio": 1.1172839506165493 + "minMs": 1.5800000000017462, + "maxMs": 1.8799999999973807, + "ratio": 1.1898734177185462 }, "competitor": { - "minMs": 9.380000000001019, - "maxMs": 9.5, - "ratio": 1.0127931769721714 + "minMs": 9.449999999998909, + "maxMs": 9.6299999999992, + "ratio": 1.019047619047652 }, "stable": true, "rungs": [ @@ -3317,19 +3316,19 @@ }, { "competitor": "pixi", - "referenceMs": 1.6299999999973807, - "referenceP95Ms": 2.599999999998545, + "referenceMs": 1.6699999999982538, + "referenceP95Ms": 3.1000000000058208, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.1499999999978172, + "competitorMs": 0.13999999999941792, "competitorP95Ms": 0.27999999999883585, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 10.866666666807335, - "factor": 10.866666666807335, - "label": "competitor leads clearly (10.87x)", + "ratio": 11.928571428608551, + "factor": 11.928571428608551, + "label": "competitor leads clearly (11.93x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -3337,14 +3336,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.6199999999989814, - "maxMs": 1.8099999999976717, - "ratio": 1.1172839506165493 + "minMs": 1.5800000000017462, + "maxMs": 1.8799999999973807, + "ratio": 1.1898734177185462 }, "competitor": { - "minMs": 0.13000000000101863, - "maxMs": 0.15999999999985448, - "ratio": 1.2307692307584674 + "minMs": 0.13999999999941792, + "maxMs": 0.1499999999978172, + "ratio": 1.0714285714174347 }, "stable": true, "rungs": [ @@ -3353,6 +3352,43 @@ "competitor-leads-clearly" ] } + }, + { + "competitor": "excalibur", + "referenceMs": 1.8799999999973807, + "referenceP95Ms": 3.1599999999962165, + "referenceGpuMs": 16.660000000003492, + "referenceOverFrameBudget": false, + "competitorMs": 191.07000000000698, + "competitorP95Ms": 195.77999999999884, + "competitorGpuMs": 196.6600000000035, + "competitorOverFrameBudget": true, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 5/6/5, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 1, + "reference": { + "minMs": 1.8799999999973807, + "maxMs": 1.8799999999973807, + "ratio": 1 + }, + "competitor": { + "minMs": 191.07000000000698, + "maxMs": 191.07000000000698, + "ratio": 1 + }, + "stable": false, + "rungs": [ + "exojs-leads-clearly" + ] + } } ] }, @@ -3366,19 +3402,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 1.4899999999979627, - "referenceP95Ms": 2.280000000006112, + "referenceMs": 1.360000000000582, + "referenceP95Ms": 2.279999999998836, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 25.089999999996508, - "competitorP95Ms": 27.29999999998836, - "competitorGpuMs": 26.479999999995925, + "competitorMs": 24.959999999999127, + "competitorP95Ms": 27.360000000000582, + "competitorGpuMs": 26.05999999999767, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.05938620964520407, - "factor": 16.838926174517322, - "label": "ExoJS leads clearly (16.84x)", + "ratio": 0.05448717948720471, + "factor": 18.352941176462092, + "label": "ExoJS leads clearly (18.35x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3386,14 +3422,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.4599999999991269, - "maxMs": 1.4900000000016007, - "ratio": 1.0205479452071862 + "minMs": 1.3400000000001455, + "maxMs": 1.6599999999962165, + "ratio": 1.2388059701462957 }, "competitor": { - "minMs": 24.70000000001164, - "maxMs": 26.580000000001746, - "ratio": 1.07611336032345 + "minMs": 23.999999999992724, + "maxMs": 25.179999999993015, + "ratio": 1.0491666666666937 }, "stable": true, "rungs": [ @@ -3405,19 +3441,19 @@ }, { "competitor": "phaser", - "referenceMs": 1.4899999999979627, - "referenceP95Ms": 2.280000000006112, + "referenceMs": 1.360000000000582, + "referenceP95Ms": 2.279999999998836, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 3.1599999999998545, - "competitorP95Ms": 6.5, + "competitorMs": 3.1600000000034925, + "competitorP95Ms": 6.139999999999418, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.47151898734114917, - "factor": 2.120805369130319, - "label": "ExoJS leads (2.12x)", + "ratio": 0.43037974683515157, + "factor": 2.3235294117662795, + "label": "ExoJS leads (2.32x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3425,14 +3461,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.4599999999991269, - "maxMs": 1.4900000000016007, - "ratio": 1.0205479452071862 + "minMs": 1.3400000000001455, + "maxMs": 1.6599999999962165, + "ratio": 1.2388059701462957 }, "competitor": { - "minMs": 3.139999999999418, - "maxMs": 3.2299999999995634, - "ratio": 1.0286624203822172 + "minMs": 3.1199999999989814, + "maxMs": 3.360000000000582, + "ratio": 1.076923076923615 }, "stable": true, "rungs": [ @@ -3444,8 +3480,8 @@ }, { "competitor": "pixi", - "referenceMs": 1.4899999999979627, - "referenceP95Ms": 2.280000000006112, + "referenceMs": 1.360000000000582, + "referenceP95Ms": 2.279999999998836, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, "competitorMs": 0.9000000000014552, @@ -3454,9 +3490,9 @@ "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.6555555555506152, - "factor": 1.6555555555506152, - "label": "competitor leads (1.66x)", + "ratio": 1.5111111111093145, + "factor": 1.5111111111093145, + "label": "competitor leads (1.51x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3464,14 +3500,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.4599999999991269, - "maxMs": 1.4900000000016007, - "ratio": 1.0205479452071862 + "minMs": 1.3400000000001455, + "maxMs": 1.6599999999962165, + "ratio": 1.2388059701462957 }, "competitor": { - "minMs": 0.8799999999973807, - "maxMs": 0.9700000000048021, - "ratio": 1.1022727272814652 + "minMs": 0.8900000000030559, + "maxMs": 0.9199999999982538, + "ratio": 1.033707865163028 }, "stable": true, "rungs": [ @@ -3498,19 +3534,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.5400000000008731, - "referenceP95Ms": 0.9000000000014552, + "referenceMs": 0.569999999999709, + "referenceP95Ms": 0.9400000000023283, "referenceGpuMs": 16.679999999993015, "referenceOverFrameBudget": false, - "competitorMs": 24.04000000000815, - "competitorP95Ms": 26.139999999999418, - "competitorGpuMs": 25.160000000003492, + "competitorMs": 24.62999999999738, + "competitorP95Ms": 27.920000000012806, + "competitorGpuMs": 25.720000000001164, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.02246256239603536, - "factor": 44.51851851846163, - "label": "ExoJS leads clearly (44.52x)", + "ratio": 0.02314250913519162, + "factor": 43.21052631580694, + "label": "ExoJS leads clearly (43.21x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3519,13 +3555,13 @@ "runs": 3, "reference": { "minMs": 0.5400000000008731, - "maxMs": 0.569999999996071, - "ratio": 1.055555555546573 + "maxMs": 0.5799999999944703, + "ratio": 1.074074074062097 }, "competitor": { - "minMs": 22.64999999999418, - "maxMs": 26.610000000015134, - "ratio": 1.1748344370870627 + "minMs": 24.510000000002037, + "maxMs": 25.810000000004948, + "ratio": 1.053039575683509 }, "stable": true, "rungs": [ @@ -3537,19 +3573,19 @@ }, { "competitor": "phaser", - "referenceMs": 0.5400000000008731, - "referenceP95Ms": 0.9000000000014552, + "referenceMs": 0.569999999999709, + "referenceP95Ms": 0.9400000000023283, "referenceGpuMs": 16.679999999993015, "referenceOverFrameBudget": false, - "competitorMs": 32.42000000000189, - "competitorP95Ms": 35.31999999999971, - "competitorGpuMs": 33.340000000000146, + "competitorMs": 32.67000000000007, + "competitorP95Ms": 35.43999999999869, + "competitorGpuMs": 33.5199999999968, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.01665638494758919, - "factor": 60.037037036943474, - "label": "ExoJS leads clearly (60.04x)", + "ratio": 0.017447199265372137, + "factor": 57.3157894737136, + "label": "ExoJS leads clearly (57.32x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3558,13 +3594,13 @@ "runs": 3, "reference": { "minMs": 0.5400000000008731, - "maxMs": 0.569999999996071, - "ratio": 1.055555555546573 + "maxMs": 0.5799999999944703, + "ratio": 1.074074074062097 }, "competitor": { - "minMs": 32.400000000001455, - "maxMs": 33.219999999999345, - "ratio": 1.0253086419752424 + "minMs": 32.51000000000022, + "maxMs": 32.90000000000873, + "ratio": 1.0119963088283146 }, "stable": true, "rungs": [ @@ -3576,19 +3612,19 @@ }, { "competitor": "pixi", - "referenceMs": 0.5400000000008731, - "referenceP95Ms": 0.9000000000014552, + "referenceMs": 0.569999999999709, + "referenceP95Ms": 0.9400000000023283, "referenceGpuMs": 16.679999999993015, "referenceOverFrameBudget": false, - "competitorMs": 18.089999999996508, - "competitorP95Ms": 21.760000000002037, - "competitorGpuMs": 18.860000000000582, + "competitorMs": 17.169999999998254, + "competitorP95Ms": 21.87999999999738, + "competitorGpuMs": 17.839999999996508, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.029850746268710746, - "factor": 33.49999999993936, - "label": "ExoJS leads clearly (33.50x)", + "ratio": 0.03319743739078433, + "factor": 30.122807017556173, + "label": "ExoJS leads clearly (30.12x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -3597,13 +3633,13 @@ "runs": 3, "reference": { "minMs": 0.5400000000008731, - "maxMs": 0.569999999996071, - "ratio": 1.055555555546573 + "maxMs": 0.5799999999944703, + "ratio": 1.074074074062097 }, "competitor": { - "minMs": 16.589999999996508, - "maxMs": 18.36000000000422, - "ratio": 1.106690777577341 + "minMs": 16.56999999999971, + "maxMs": 17.5, + "ratio": 1.0561255280627826 }, "stable": true, "rungs": [ @@ -3674,19 +3710,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.2799999999999727, - "referenceP95Ms": 0.6000000000000227, - "referenceGpuMs": 0.072354, + "referenceMs": 0.2999999999999545, + "referenceP95Ms": 0.5799999999999272, + "referenceGpuMs": 0.072688, "referenceOverFrameBudget": false, - "competitorMs": 0.3000000000000682, - "competitorP95Ms": 0.5799999999999272, - "competitorGpuMs": 0.072292, + "competitorMs": 0.2999999999999545, + "competitorP95Ms": 0.5800000000000409, + "competitorGpuMs": 0.07325, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9333333333330301, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3694,19 +3730,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.2600000000002183, - "maxMs": 0.3100000000000591, - "ratio": 1.1923076923069187 + "minMs": 0.2599999999998772, + "maxMs": 0.32000000000010687, + "ratio": 1.230769230770223 }, "competitor": { - "minMs": 0.2999999999999545, - "maxMs": 0.3000000000001819, - "ratio": 1.0000000000007578 + "minMs": 0.27999999999985903, + "maxMs": 0.4600000000000364, + "ratio": 1.6428571428580998 }, - "stable": true, + "stable": false, "rungs": [ "level", - "level", + "exojs-leads", "level" ] } @@ -3723,13 +3759,13 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.1999999999998181, + "referenceMs": 0.20000000000004547, "referenceP95Ms": 0.36000000000012733, - "referenceGpuMs": 0.206208, + "referenceGpuMs": 0.2054165, "referenceOverFrameBudget": false, - "competitorMs": 0.25999999999976353, - "competitorP95Ms": 0.4399999999995998, - "competitorGpuMs": 0.190458, + "competitorMs": 0.23999999999978172, + "competitorP95Ms": 0.4600000000000364, + "competitorGpuMs": 0.18993749999999998, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -3743,14 +3779,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.18000000000029104, - "maxMs": 0.20000000000027285, - "ratio": 1.1111111111108305 + "minMs": 0.14000000000032742, + "maxMs": 0.21000000000003638, + "ratio": 1.4999999999967517 }, "competitor": { - "minMs": 0.23999999999978172, - "maxMs": 0.2600000000002183, - "ratio": 1.0833333333352282 + "minMs": 0.19000000000005457, + "maxMs": 0.2800000000002001, + "ratio": 1.4736842105269456 }, "stable": false, "rungs": [ @@ -3772,19 +3808,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.13999999999941792, - "referenceP95Ms": 0.22000000000025466, - "referenceGpuMs": 0.4222295, + "referenceMs": 0.15000000000009095, + "referenceP95Ms": 0.3000000000001819, + "referenceGpuMs": 0.429021, "referenceOverFrameBudget": false, - "competitorMs": 0.0999999999994543, - "competitorP95Ms": 0.6000000000003638, - "competitorGpuMs": 0.2353335, + "competitorMs": 0.13000000000010914, + "competitorP95Ms": 0.5, + "competitorGpuMs": 0.274937, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 1.400000000001819, - "factor": 1.400000000001819, - "label": "competitor leads (1.40x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3792,20 +3828,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.1299999999996544, - "maxMs": 0.14000000000032742, - "ratio": 1.0769230769284586 + "minMs": 0.05999999999994543, + "maxMs": 0.15999999999985448, + "ratio": 2.6666666666666665 }, "competitor": { "minMs": 0.09000000000014552, - "maxMs": 0.10000000000081855, - "ratio": 1.1111111111184095 + "maxMs": 0.3599999999996726, + "ratio": 3.9999999999898943 }, - "stable": true, + "stable": false, "rungs": [ "competitor-leads", - "competitor-leads", - "competitor-leads" + "exojs-leads", + "exojs-leads" ] } } @@ -3821,19 +3857,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 1.8400000000001455, - "referenceP95Ms": 2.399999999999636, - "referenceGpuMs": 0.20729150000000002, + "referenceMs": 1.8900000000003274, + "referenceP95Ms": 2.4600000000000364, + "referenceGpuMs": 0.2075835, "referenceOverFrameBudget": false, - "competitorMs": 1.3800000000001091, - "competitorP95Ms": 2.0600000000004, - "competitorGpuMs": 0.19085449999999998, + "competitorMs": 1.300000000000182, + "competitorP95Ms": 1.8400000000001455, + "competitorGpuMs": 0.1915835, "competitorOverFrameBudget": false, - "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "verdict": { + "side": "competitor", + "ratio": 1.4538461538462022, + "factor": 1.4538461538462022, + "label": "competitor leads (1.45x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -3841,20 +3877,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.4200000000000728, - "maxMs": 1.9299999999998363, - "ratio": 1.3591549295772798 + "minMs": 1.8599999999996726, + "maxMs": 1.900000000000091, + "ratio": 1.0215053763443147 }, "competitor": { - "minMs": 1.2400000000006912, - "maxMs": 1.5700000000001637, - "ratio": 1.2661290322574907 + "minMs": 1.0899999999996908, + "maxMs": 1.4200000000000728, + "ratio": 1.302752293578418 }, - "stable": false, + "stable": true, "rungs": [ "competitor-leads", "competitor-leads", - "level" + "competitor-leads" ] } } @@ -3870,19 +3906,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 1.6900000000000546, - "referenceP95Ms": 2.2600000000002183, - "referenceGpuMs": 0.0739165, + "referenceMs": 1.7199999999993452, + "referenceP95Ms": 2.199999999999818, + "referenceGpuMs": 0.073708, "referenceOverFrameBudget": false, - "competitorMs": 1.0799999999999272, - "competitorP95Ms": 1.360000000000582, - "competitorGpuMs": 0.07185449999999999, + "competitorMs": 0.9799999999995634, + "competitorP95Ms": 1.319999999999709, + "competitorGpuMs": 0.0729795, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 1.7551020408164402, + "factor": 1.7551020408164402, + "label": "competitor leads (1.76x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -3890,20 +3926,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.8899999999998727, - "maxMs": 1.7399999999997817, - "ratio": 1.9550561797753154 + "minMs": 1.7100000000000364, + "maxMs": 1.7499999999995453, + "ratio": 1.0233918128652093 }, "competitor": { - "minMs": 0.9699999999997999, - "maxMs": 1.0899999999996908, - "ratio": 1.1237113402060985 + "minMs": 0.9399999999995998, + "maxMs": 1.2300000000000182, + "ratio": 1.3085106382984488 }, - "stable": false, + "stable": true, "rungs": [ "competitor-leads", "competitor-leads", - "level" + "competitor-leads" ] } } @@ -3919,19 +3955,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 4.1200000000008, - "referenceP95Ms": 5.139999999999418, - "referenceGpuMs": 0.21029150000000002, + "referenceMs": 4.220000000001164, + "referenceP95Ms": 10.44000000000051, + "referenceGpuMs": 0.211333, "referenceOverFrameBudget": false, - "competitorMs": 2.7200000000002547, - "competitorP95Ms": 10.979999999999563, - "competitorGpuMs": 0.193062, + "competitorMs": 2.7600000000002183, + "competitorP95Ms": 9.219999999999345, + "competitorGpuMs": 0.1934165, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.5147058823530937, - "factor": 1.5147058823530937, - "label": "competitor leads (1.51x)", + "ratio": 1.5289855072466776, + "factor": 1.5289855072466776, + "label": "competitor leads (1.53x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -3939,14 +3975,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.780000000000655, - "maxMs": 4.199999999999818, - "ratio": 1.1111111111108705 + "minMs": 3.7399999999997817, + "maxMs": 4.400000000000546, + "ratio": 1.1764705882355087 }, "competitor": { - "minMs": 2.599999999998545, - "maxMs": 2.730000000000473, - "ratio": 1.0500000000007697 + "minMs": 2.7100000000000364, + "maxMs": 2.780000000000655, + "ratio": 1.025830258302811 }, "stable": true, "rungs": [ @@ -3968,19 +4004,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 37.26000000000022, - "referenceP95Ms": 41.220000000001164, - "referenceGpuMs": 0.9720415, + "referenceMs": 38.039999999999964, + "referenceP95Ms": 41.659999999999854, + "referenceGpuMs": 0.970521, "referenceOverFrameBudget": true, - "competitorMs": 28.49000000000069, - "competitorP95Ms": 32.039999999999054, - "competitorGpuMs": 0.550104, + "competitorMs": 28.61999999999898, + "competitorP95Ms": 31.81999999999971, + "competitorGpuMs": 0.7028755, "competitorOverFrameBudget": true, "verdict": { "side": "competitor", - "ratio": 1.3078273078272837, - "factor": 1.3078273078272837, - "label": "competitor leads (1.31x)", + "ratio": 1.329140461215979, + "factor": 1.329140461215979, + "label": "competitor leads (1.33x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -3988,14 +4024,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 37.100000000000364, - "maxMs": 37.44999999999982, - "ratio": 1.0094339622641362 + "minMs": 37, + "maxMs": 39.590000000000146, + "ratio": 1.0700000000000038 }, "competitor": { - "minMs": 28.18000000000029, - "maxMs": 28.51000000000022, - "ratio": 1.0117104329311541 + "minMs": 28.220000000000255, + "maxMs": 28.789999999999964, + "ratio": 1.0201984408221014 }, "stable": true, "rungs": [ @@ -4017,19 +4053,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.5200000000004366, - "referenceP95Ms": 0.7200000000011642, - "referenceGpuMs": 0.2291455, + "referenceMs": 0.47000000000025466, + "referenceP95Ms": 0.6599999999998545, + "referenceGpuMs": 0.13700050000000003, "referenceOverFrameBudget": false, "competitorMs": 0.5, - "competitorP95Ms": 0.8000000000010914, - "competitorGpuMs": 0.21816649999999999, + "competitorP95Ms": 0.7400000000016007, + "competitorGpuMs": 0.1317705, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.0400000000008731, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -4037,19 +4073,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5, - "maxMs": 0.5600000000004002, - "ratio": 1.1200000000008004 + "minMs": 0.42000000000007276, + "maxMs": 0.47999999999956344, + "ratio": 1.1428571428559053 }, "competitor": { - "minMs": 0.5, - "maxMs": 0.5399999999999636, - "ratio": 1.0799999999999272 + "minMs": 0.4400000000005093, + "maxMs": 0.7399999999997817, + "ratio": 1.681818181815739 }, - "stable": true, + "stable": false, "rungs": [ "level", - "level", + "exojs-leads", "level" ] } @@ -4066,19 +4102,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 2.819999999999709, - "referenceP95Ms": 6.779999999998836, - "referenceGpuMs": 0.1425, + "referenceMs": 2.680000000000291, + "referenceP95Ms": 6.479999999999563, + "referenceGpuMs": 0.142021, "referenceOverFrameBudget": false, - "competitorMs": 2.459999999999127, - "competitorP95Ms": 8.720000000001164, - "competitorGpuMs": 0.157458, + "competitorMs": 2.5799999999981083, + "competitorP95Ms": 7.440000000002328, + "competitorGpuMs": 0.1590205, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 1.038759689923355, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "competitor issues fewer buffer uploads (13 vs 3 per frame)", @@ -4086,19 +4122,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.680000000000291, - "maxMs": 3.0600000000013097, - "ratio": 1.141791044776484 + "minMs": 2.5099999999983993, + "maxMs": 2.7300000000013824, + "ratio": 1.0876494023916827 }, "competitor": { - "minMs": 2.430000000000291, - "maxMs": 2.5199999999986176, - "ratio": 1.037037037036344 + "minMs": 2.539999999999054, + "maxMs": 2.5899999999983265, + "ratio": 1.0196850393697996 }, - "stable": false, + "stable": true, "rungs": [ "level", - "competitor-leads", + "level", "level" ] } @@ -4120,19 +4156,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.15999999999985448, - "referenceP95Ms": 0.27999999999883585, - "referenceGpuMs": 0.5884795, + "referenceMs": 0.13999999999941792, + "referenceP95Ms": 0.23999999999978172, + "referenceGpuMs": 0.589167, "referenceOverFrameBudget": false, "competitorMs": 0.2000000000007276, - "competitorP95Ms": 0.37999999999919964, - "competitorGpuMs": 0.7117709999999999, + "competitorP95Ms": 0.3999999999996362, + "competitorGpuMs": 0.6553745, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "exojs", + "ratio": 0.699999999994543, + "factor": 1.4285714285825653, + "label": "ExoJS leads (1.43x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -4140,20 +4176,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.1400000000012369, - "maxMs": 0.18999999999959982, - "ratio": 1.3571428571280082 + "minMs": 0.1000000000003638, + "maxMs": 0.1400000000012369, + "ratio": 1.4000000000072759 }, "competitor": { - "minMs": 0.2000000000007276, - "maxMs": 0.21999999999934516, - "ratio": 1.0999999999927241 + "minMs": 0.15999999999985448, + "maxMs": 0.38000000000101863, + "ratio": 2.3750000000085265 }, - "stable": false, + "stable": true, "rungs": [ "exojs-leads", "exojs-leads", - "level" + "exojs-leads" ] } } @@ -4169,19 +4205,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.819999999999709, - "referenceP95Ms": 1.5800000000017462, - "referenceGpuMs": 0.220604, + "referenceMs": 0.570000000001528, + "referenceP95Ms": 1, + "referenceGpuMs": 0.22197899999999998, "referenceOverFrameBudget": false, - "competitorMs": 0.6599999999998545, - "competitorP95Ms": 1.0400000000008731, - "competitorGpuMs": 0.076938, + "competitorMs": 0.7600000000011278, + "competitorP95Ms": 1.0799999999999272, + "competitorGpuMs": 0.1250205, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "exojs", + "ratio": 0.7500000000008975, + "factor": 1.3333333333317379, + "label": "ExoJS leads (1.33x)", "structural": false }, "mechanism": "competitor issues fewer texture binds (626 vs 314 per frame)", @@ -4189,20 +4225,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5300000000006548, - "maxMs": 0.9799999999995634, - "ratio": 1.8490566037704765 + "minMs": 0.5400000000008731, + "maxMs": 0.5799999999999272, + "ratio": 1.0740740740722028 }, "competitor": { - "minMs": 0.6599999999998545, - "maxMs": 0.7000000000007276, - "ratio": 1.0606060606073968 + "minMs": 0.7399999999979627, + "maxMs": 0.7999999999992724, + "ratio": 1.0810810810830742 }, - "stable": false, + "stable": true, "rungs": [ - "level", - "level", - "competitor-leads" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } } @@ -4219,18 +4255,18 @@ { "competitor": "pixi", "referenceMs": 0.42000000000189175, - "referenceP95Ms": 0.5400000000008731, - "referenceGpuMs": 0.1192915, + "referenceP95Ms": 0.5199999999967986, + "referenceGpuMs": 0.11856249999999999, "referenceOverFrameBudget": false, - "competitorMs": 0.5600000000013097, - "competitorP95Ms": 0.7799999999988358, - "competitorGpuMs": 0.1186245, + "competitorMs": 0.5499999999992724, + "competitorP95Ms": 0.7600000000020373, + "competitorGpuMs": 0.119646, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.7500000000016241, - "factor": 1.333333333330446, - "label": "ExoJS leads (1.33x)", + "ratio": 0.7636363636408134, + "factor": 1.3095238095161787, + "label": "ExoJS leads (1.31x)", "structural": false }, "mechanism": "competitor issues fewer texture binds (240 vs 159 per frame)", @@ -4238,14 +4274,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.38999999999759893, - "maxMs": 0.43000000000029104, - "ratio": 1.1025641025716368 + "minMs": 0.41999999999825377, + "maxMs": 0.42000000000189175, + "ratio": 1.000000000008662 }, "competitor": { "minMs": 0.5400000000008731, - "maxMs": 0.569999999999709, - "ratio": 1.0555555555533098 + "maxMs": 0.7200000000011642, + "ratio": 1.3333333333333333 }, "stable": true, "rungs": [ @@ -4272,18 +4308,18 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.12000000000080036, - "referenceP95Ms": 0.22000000000116415, - "referenceGpuMs": 0.1255835, + "referenceMs": 0.11999999999898137, + "referenceP95Ms": 0.2000000000007276, + "referenceGpuMs": 0.12593749999999998, "referenceOverFrameBudget": false, "competitorMs": 0.18000000000029104, - "competitorP95Ms": 0.31999999999970896, - "competitorGpuMs": 0.11510400000000001, + "competitorP95Ms": 0.3600000000005821, + "competitorGpuMs": 0.1145, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.6666666666700352, - "factor": 1.499999999992421, + "ratio": 0.6666666666599297, + "factor": 1.5000000000151583, "label": "ExoJS leads (1.50x)", "structural": false }, @@ -4293,13 +4329,13 @@ "runs": 3, "reference": { "minMs": 0.11999999999898137, - "maxMs": 0.12000000000261934, - "ratio": 1.0000000000303164 + "maxMs": 0.11999999999898137, + "ratio": 1 }, "competitor": { "minMs": 0.15999999999985448, - "maxMs": 0.2000000000007276, - "ratio": 1.2500000000056843 + "maxMs": 0.3999999999978172, + "ratio": 2.4999999999886313 }, "stable": true, "rungs": [ @@ -4327,18 +4363,18 @@ { "competitor": "pixi", "referenceMs": 0.18000000000029104, - "referenceP95Ms": 0.3600000000005821, - "referenceGpuMs": 0.650812, + "referenceP95Ms": 0.2999999999992724, + "referenceGpuMs": 1.414479, "referenceOverFrameBudget": false, - "competitorMs": 0.19000000000050932, + "competitorMs": 0.18000000000029104, "competitorP95Ms": 0.31999999999970896, - "competitorGpuMs": 0.246, + "competitorGpuMs": 0.2457295, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9473684210516238, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -4346,19 +4382,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.18000000000029104, - "maxMs": 0.18000000000029104, - "ratio": 1 + "minMs": 0.13999999999941792, + "maxMs": 0.19999999999708962, + "ratio": 1.4285714285565796 }, "competitor": { - "minMs": 0.18000000000029104, - "maxMs": 0.2000000000007276, - "ratio": 1.1111111111133567 + "minMs": 0.17000000000007276, + "maxMs": 0.37999999999738066, + "ratio": 2.235294117630694 }, - "stable": true, + "stable": false, "rungs": [ "level", - "level", + "exojs-leads", "level" ] } @@ -4375,13 +4411,13 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.15999999999985448, - "referenceP95Ms": 0.2599999999983993, - "referenceGpuMs": 3.5047085, + "referenceMs": 0.1500000000014552, + "referenceP95Ms": 0.27999999999883585, + "referenceGpuMs": 3.4957295000000004, "referenceOverFrameBudget": false, "competitorMs": 0.18000000000029104, "competitorP95Ms": 0.3400000000001455, - "competitorGpuMs": 0.371521, + "competitorGpuMs": 0.366375, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -4395,20 +4431,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.13999999999941792, - "maxMs": 0.18000000000029104, - "ratio": 1.28571428572171 + "minMs": 0.11000000000058208, + "maxMs": 0.15999999999985448, + "ratio": 1.4545454545364347 }, "competitor": { - "minMs": 0.18000000000029104, - "maxMs": 0.18000000000029104, - "ratio": 1 + "minMs": 0.13999999999941792, + "maxMs": 0.3400000000001455, + "ratio": 2.4285714285825653 }, "stable": false, "rungs": [ "level", "exojs-leads", - "level" + "exojs-leads" ] } } @@ -4424,19 +4460,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 2.6700000000000728, - "referenceP95Ms": 7.540000000000873, - "referenceGpuMs": 1.4847085, + "referenceMs": 2.7000000000007276, + "referenceP95Ms": 7.3400000000001455, + "referenceGpuMs": 1.49175, "referenceOverFrameBudget": false, - "competitorMs": 4.5, - "competitorP95Ms": 5.779999999998836, - "competitorGpuMs": 0.291625, + "competitorMs": 4.319999999999709, + "competitorP95Ms": 5.919999999998254, + "competitorGpuMs": 0.29112499999999997, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5933333333333495, - "factor": 1.6853932584269204, - "label": "ExoJS leads (1.69x)", + "ratio": 0.6250000000002105, + "factor": 1.5999999999994612, + "label": "ExoJS leads (1.60x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/3 vs 1/2/3, none of them large enough to account for the gap); the difference is CPU-side", @@ -4444,14 +4480,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.600000000000364, - "maxMs": 3.399999999997817, - "ratio": 1.3076923076912852 + "minMs": 2.6700000000000728, + "maxMs": 3.1300000000010186, + "ratio": 1.172284644195106 }, "competitor": { - "minMs": 4.430000000000291, - "maxMs": 4.699999999998909, - "ratio": 1.0609480812637924 + "minMs": 4.249999999998181, + "maxMs": 4.700000000000728, + "ratio": 1.105882352941821 }, "stable": true, "rungs": [ @@ -4473,19 +4509,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 7.899999999999636, - "referenceP95Ms": 9.639999999999418, - "referenceGpuMs": 3.821021, + "referenceMs": 7.290000000000873, + "referenceP95Ms": 9.399999999997817, + "referenceGpuMs": 3.8783125, "referenceOverFrameBudget": false, - "competitorMs": 19.209999999997308, - "competitorP95Ms": 22.739999999997963, - "competitorGpuMs": 0.8035625, + "competitorMs": 19.57999999999811, + "competitorP95Ms": 24.42000000000189, + "competitorGpuMs": 0.800354, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.4112441436752079, - "factor": 2.431645569620024, - "label": "ExoJS leads (2.43x)", + "ratio": 0.3723186925434922, + "factor": 2.6858710562408454, + "label": "ExoJS leads (2.69x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/3 vs 1/2/3, none of them large enough to account for the gap); the difference is CPU-side", @@ -4493,14 +4529,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 7.249999999998181, - "maxMs": 7.970000000001164, - "ratio": 1.0993103448280226 + "minMs": 7.240000000001601, + "maxMs": 8.089999999998327, + "ratio": 1.117403314916649 }, "competitor": { - "minMs": 19.07999999999811, - "maxMs": 19.670000000000073, - "ratio": 1.030922431865934 + "minMs": 19, + "maxMs": 19.7599999999984, + "ratio": 1.0399999999999157 }, "stable": true, "rungs": [ @@ -4527,19 +4563,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.5600000000013097, - "referenceP95Ms": 1.0600000000013097, - "referenceGpuMs": 0.2069795, + "referenceMs": 0.5400000000008731, + "referenceP95Ms": 0.8000000000029104, + "referenceGpuMs": 0.223082, "referenceOverFrameBudget": false, - "competitorMs": 2.1599999999998545, - "competitorP95Ms": 3.720000000001164, - "competitorGpuMs": 0.28029200000000004, + "competitorMs": 1.8999999999996362, + "competitorP95Ms": 2.680000000000291, + "competitorGpuMs": 0.2800835, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.2592592592598831, - "factor": 3.857142857133576, - "label": "ExoJS leads (3.86x)", + "ratio": 0.28421052631630345, + "factor": 3.5185185185121557, + "label": "ExoJS leads (3.52x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/6/3 vs 2/4/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -4547,14 +4583,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5500000000010914, - "maxMs": 0.5800000000017462, - "ratio": 1.054545454546537 + "minMs": 0.41999999999825377, + "maxMs": 0.5400000000008731, + "ratio": 1.28571428572171 }, "competitor": { - "minMs": 2.030000000000655, - "maxMs": 2.4099999999980355, - "ratio": 1.1871921182252503 + "minMs": 1.8400000000001455, + "maxMs": 2.3799999999991996, + "ratio": 1.2934782608690278 }, "stable": true, "rungs": [ @@ -4577,12 +4613,12 @@ { "competitor": "pixi", "referenceMs": 0.4400000000023283, - "referenceP95Ms": 0.7000000000043656, - "referenceGpuMs": 0.2582715, + "referenceP95Ms": 0.680000000000291, + "referenceGpuMs": 0.235958, "referenceOverFrameBudget": false, - "competitorMs": 2.290000000000873, - "competitorP95Ms": 3.3399999999965075, - "competitorGpuMs": 0.4509175, + "competitorMs": 2.1700000000018917, + "competitorP95Ms": 3.779999999998836, + "competitorGpuMs": 0.449771, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -4596,19 +4632,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.4099999999998545, - "maxMs": 0.4599999999991269, - "ratio": 1.1219512195104637 + "minMs": 0.3600000000005821, + "maxMs": 0.4500000000007276, + "ratio": 1.25 }, "competitor": { - "minMs": 1.9799999999995634, - "maxMs": 2.2999999999992724, - "ratio": 1.1616161616160503 + "minMs": 2, + "maxMs": 2.400000000001455, + "ratio": 1.2000000000007276 }, "stable": false, "rungs": [ "exojs-leads", - "exojs-leads", + "exojs-leads-clearly", "exojs-leads-clearly" ] } @@ -4625,13 +4661,13 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.4400000000023283, - "referenceP95Ms": 0.680000000000291, - "referenceGpuMs": 0.3397715, + "referenceMs": 0.42000000000189175, + "referenceP95Ms": 0.6200000000026193, + "referenceGpuMs": 0.30972900000000003, "referenceOverFrameBudget": false, - "competitorMs": 2.1600000000034925, - "competitorP95Ms": 3.860000000000582, - "competitorGpuMs": 0.7453745, + "competitorMs": 2.1699999999982538, + "competitorP95Ms": 3.720000000001164, + "competitorGpuMs": 0.8490215, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -4645,20 +4681,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.43999999999505235, - "maxMs": 0.47999999999592546, - "ratio": 1.0909090909120975 + "minMs": 0.3600000000005821, + "maxMs": 0.4400000000023283, + "ratio": 1.2222222222267136 }, "competitor": { - "minMs": 2.150000000005093, - "maxMs": 2.319999999999709, - "ratio": 1.079069767439169 + "minMs": 1.9199999999982538, + "maxMs": 2.9099999999998545, + "ratio": 1.5156250000013027 }, "stable": false, "rungs": [ "exojs-leads", "exojs-leads-clearly", - "exojs-leads" + "exojs-leads-clearly" ] } } @@ -4674,19 +4710,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.4800000000032014, - "referenceP95Ms": 0.7200000000011642, - "referenceGpuMs": 0.316583, + "referenceMs": 0.4599999999991269, + "referenceP95Ms": 0.6999999999970896, + "referenceGpuMs": 0.3076675, "referenceOverFrameBudget": false, - "competitorMs": 0.7600000000020373, - "competitorP95Ms": 1.0800000000017462, - "competitorGpuMs": 0.4396245, + "competitorMs": 0.75, + "competitorP95Ms": 1.180000000000291, + "competitorGpuMs": 0.47770799999999997, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.6315789473709404, - "factor": 1.5833333333270174, - "label": "ExoJS leads (1.58x)", + "ratio": 0.6133333333321692, + "factor": 1.6304347826117902, + "label": "ExoJS leads (1.63x)", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (5 vs 8 per frame)", @@ -4694,14 +4730,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.46000000000276486, - "maxMs": 0.5099999999983993, - "ratio": 1.1086956521637694 + "minMs": 0.4500000000007276, + "maxMs": 0.5199999999967986, + "ratio": 1.1555555555465729 }, "competitor": { - "minMs": 0.6599999999962165, - "maxMs": 0.7999999999992724, - "ratio": 1.2121212121270584 + "minMs": 0.6999999999970896, + "maxMs": 1.3699999999989814, + "ratio": 1.957142857149539 }, "stable": true, "rungs": [ @@ -4723,19 +4759,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.3400000000001455, - "referenceP95Ms": 0.5400000000008731, - "referenceGpuMs": 0.3265625, + "referenceMs": 0.3600000000005821, + "referenceP95Ms": 0.5200000000040745, + "referenceGpuMs": 0.3171875, "referenceOverFrameBudget": false, "competitorMs": 0.3000000000029104, - "competitorP95Ms": 0.4800000000032014, - "competitorGpuMs": 0.153188, + "competitorP95Ms": 0.5199999999967986, + "competitorGpuMs": 0.153042, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.1333333333228237, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", @@ -4743,20 +4779,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.3100000000013097, - "maxMs": 0.3600000000005821, - "ratio": 1.1612903225776168 + "minMs": 0.3000000000029104, + "maxMs": 0.3899999999994179, + "ratio": 1.2999999999854481 }, "competitor": { - "minMs": 0.2900000000008731, - "maxMs": 0.31999999999970896, - "ratio": 1.103448275857743 + "minMs": 0.2400000000016007, + "maxMs": 0.5200000000004366, + "ratio": 2.166666666654035 }, - "stable": true, + "stable": false, "rungs": [ - "level", - "level", - "level" + "competitor-leads", + "exojs-leads", + "competitor-leads" ] } } @@ -4772,19 +4808,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.3600000000005821, + "referenceMs": 0.3500000000021828, "referenceP95Ms": 0.5, - "referenceGpuMs": 0.32695799999999997, + "referenceGpuMs": 0.32100049999999997, "referenceOverFrameBudget": false, - "competitorMs": 0.41999999999825377, - "competitorP95Ms": 0.6399999999994179, - "competitorGpuMs": 0.15181250000000002, + "competitorMs": 0.4599999999991269, + "competitorP95Ms": 0.7599999999947613, + "competitorGpuMs": 0.121666, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.8571428571478068, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", @@ -4792,19 +4828,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.3600000000005821, - "maxMs": 0.37999999999738066, - "ratio": 1.055555555546573 + "minMs": 0.33999999999650754, + "maxMs": 0.3600000000005821, + "ratio": 1.0588235294243529 }, "competitor": { - "minMs": 0.37999999999738066, - "maxMs": 0.4500000000007276, - "ratio": 1.1842105263258669 + "minMs": 0.3400000000001455, + "maxMs": 0.6300000000010186, + "ratio": 1.8529411764727912 }, - "stable": true, + "stable": false, "rungs": [ - "level", - "level", + "exojs-leads", + "exojs-leads", "level" ] } @@ -4826,13 +4862,13 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.2400000000016007, + "referenceMs": 0.23999999999978172, "referenceP95Ms": 1.4600000000027649, - "referenceGpuMs": 0.10002050000000001, + "referenceGpuMs": 0.101021, "referenceOverFrameBudget": false, - "competitorMs": 0.2000000000007276, + "competitorMs": 0.19999999999708962, "competitorP95Ms": 0.3600000000005821, - "competitorGpuMs": 0.0978125, + "competitorGpuMs": 0.097458, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -4842,24 +4878,24 @@ "structural": false }, "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", - "timer": "resolved", + "timer": "limited", "aggregate": { "runs": 3, "reference": { - "minMs": 0.23999999999796273, - "maxMs": 0.25, - "ratio": 1.041666666675509 + "minMs": 0.18000000000029104, + "maxMs": 0.2400000000016007, + "ratio": 1.3333333333400703 }, "competitor": { - "minMs": 0.2000000000007276, - "maxMs": 0.2000000000007276, - "ratio": 1 + "minMs": 0.17000000000189175, + "maxMs": 0.36999999999898137, + "ratio": 2.1764705882050825 }, "stable": false, "rungs": [ - "level", "competitor-leads", - "competitor-leads" + "exojs-leads", + "level" ] } } @@ -4880,18 +4916,18 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.2599999999947613, - "referenceP95Ms": 1.319999999999709, - "referenceGpuMs": 0.06943750000000001, + "referenceMs": 0.2599999999983993, + "referenceP95Ms": 1.4799999999959255, + "referenceGpuMs": 0.067771, "referenceOverFrameBudget": false, "competitorMs": 0.41999999999825377, - "competitorP95Ms": 0.7799999999988358, - "competitorGpuMs": 0.077208, + "competitorP95Ms": 0.8399999999965075, + "competitorGpuMs": 0.076708, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.6190476190377198, - "factor": 1.6153846154104472, + "ratio": 0.6190476190463816, + "factor": 1.6153846153878444, "label": "ExoJS leads (1.62x)", "structural": false }, @@ -4900,14 +4936,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.2400000000052387, + "minMs": 0.22000000000116415, "maxMs": 0.26000000000203727, - "ratio": 1.0833333333181752 + "ratio": 1.1818181818211884 }, "competitor": { - "minMs": 0.4099999999998545, - "maxMs": 0.43000000000029104, - "ratio": 1.0487804878059601 + "minMs": 0.37999999999738066, + "maxMs": 0.6399999999994179, + "ratio": 1.6842105263258669 }, "stable": true, "rungs": [ @@ -4929,34 +4965,34 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.22000000000116415, - "referenceP95Ms": 0.8799999999973807, - "referenceGpuMs": 0.06697900000000001, + "referenceMs": 0.20999999999912689, + "referenceP95Ms": 0.8400000000037835, + "referenceGpuMs": 0.06789600000000001, "referenceOverFrameBudget": false, - "competitorMs": 0.38000000000101863, - "competitorP95Ms": 0.6200000000026193, - "competitorGpuMs": 0.07572899999999999, + "competitorMs": 0.4400000000023283, + "competitorP95Ms": 0.6999999999970896, + "competitorGpuMs": 0.0767505, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5789473684225642, - "factor": 1.7272727272682176, - "label": "ExoJS leads (1.73x)", + "ratio": 0.4772727272682174, + "factor": 2.0952380952578937, + "label": "ExoJS leads (2.10x)", "structural": false }, "mechanism": "competitor issues fewer draw calls (60 vs 2 per frame)", - "timer": "limited", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.18999999999869033, - "maxMs": 0.22000000000116415, - "ratio": 1.1578947368562138 + "minMs": 0.2000000000007276, + "maxMs": 0.22999999999956344, + "ratio": 1.1499999999936334 }, "competitor": { - "minMs": 0.3600000000005821, - "maxMs": 0.3999999999978172, - "ratio": 1.1111111111032512 + "minMs": 0.31999999999970896, + "maxMs": 0.48999999999796273, + "ratio": 1.5312499999950262 }, "stable": true, "rungs": [ @@ -4978,19 +5014,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.7999999999992724, - "referenceP95Ms": 1.2600000000020373, - "referenceGpuMs": 0.067771, + "referenceMs": 0.6899999999986903, + "referenceP95Ms": 1.319999999999709, + "referenceGpuMs": 0.0669585, "referenceOverFrameBudget": false, - "competitorMs": 1.7799999999988358, - "competitorP95Ms": 3.180000000000291, - "competitorGpuMs": 0.06868750000000001, + "competitorMs": 2.3300000000017462, + "competitorP95Ms": 3.3800000000046566, + "competitorGpuMs": 0.069917, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.4494382022470762, - "factor": 2.2250000000005685, - "label": "ExoJS leads (2.23x)", + "ratio": 0.29613733905500994, + "factor": 3.376811594211839, + "label": "ExoJS leads (3.38x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/4/2 vs 2/4/3, none of them large enough to account for the gap); the difference is CPU-side", @@ -4998,14 +5034,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5999999999985448, - "maxMs": 0.9400000000023283, - "ratio": 1.566666666674347 + "minMs": 0.6699999999982538, + "maxMs": 0.9099999999998545, + "ratio": 1.3582089552272034 }, "competitor": { - "minMs": 1.5900000000001455, - "maxMs": 1.8699999999953434, - "ratio": 1.1761006289277813 + "minMs": 1.7000000000007276, + "maxMs": 2.390000000003056, + "ratio": 1.4058823529423723 }, "stable": true, "rungs": [ @@ -5032,19 +5068,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.27999999999883585, - "referenceP95Ms": 0.4599999999991269, - "referenceGpuMs": 0.070687, + "referenceMs": 0.2800000000024738, + "referenceP95Ms": 0.4400000000023283, + "referenceGpuMs": 0.07079150000000001, "referenceOverFrameBudget": false, "competitorMs": 0.26000000000203727, - "competitorP95Ms": 0.43999999999505235, - "competitorGpuMs": 0.069, + "competitorP95Ms": 0.5, + "competitorGpuMs": 0.0683335, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.076923076910161, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -5052,19 +5088,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.27999999999883585, - "maxMs": 0.2999999999956344, - "ratio": 1.0714285714174347 + "minMs": 0.2599999999947613, + "maxMs": 0.3000000000029104, + "ratio": 1.1538461538805962 }, "competitor": { - "minMs": 0.2599999999983993, - "maxMs": 0.26000000000203727, - "ratio": 1.0000000000139921 + "minMs": 0.22999999999956344, + "maxMs": 0.4400000000023283, + "ratio": 1.9130434782746237 }, - "stable": true, + "stable": false, "rungs": [ "level", - "level", + "exojs-leads", "level" ] } @@ -5081,19 +5117,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.46000000000640284, - "referenceP95Ms": 0.6000000000058208, - "referenceGpuMs": 0.1444375, + "referenceMs": 0.4599999999991269, + "referenceP95Ms": 0.5799999999944703, + "referenceGpuMs": 0.146333, "referenceOverFrameBudget": false, - "competitorMs": 0.22000000000116415, - "competitorP95Ms": 0.43999999999505235, - "competitorGpuMs": 0.232792, + "competitorMs": 0.23999999999796273, + "competitorP95Ms": 0.41999999999825377, + "competitorGpuMs": 0.232042, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 2.0909090909271306, - "factor": 2.0909090909271306, - "label": "competitor leads (2.09x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -5101,19 +5137,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.4599999999991269, - "maxMs": 0.4900000000016007, - "ratio": 1.0652173913098495 + "minMs": 0.43999999999505235, + "maxMs": 0.47999999999592546, + "ratio": 1.0909090909120975 }, "competitor": { "minMs": 0.22000000000116415, - "maxMs": 0.23999999999796273, - "ratio": 1.090909090894058 + "maxMs": 0.4599999999991269, + "ratio": 2.090909090894058 }, - "stable": true, + "stable": false, "rungs": [ "competitor-leads", - "competitor-leads", + "level", "competitor-leads" ] } @@ -5130,19 +5166,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 2.210000000002765, - "referenceP95Ms": 2.680000000000291, - "referenceGpuMs": 0.3617295, + "referenceMs": 2.109999999996944, + "referenceP95Ms": 2.5199999999967986, + "referenceGpuMs": 0.38845850000000004, "referenceOverFrameBudget": false, - "competitorMs": 0.27000000000043656, - "competitorP95Ms": 0.41999999999825377, - "competitorGpuMs": 0.25004149999999997, + "competitorMs": 0.26000000000203727, + "competitorP95Ms": 0.4000000000014552, + "competitorGpuMs": 0.261021, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 8.18518518518219, - "factor": 8.18518518518219, - "label": "competitor leads clearly (8.19x)", + "ratio": 8.115384615309273, + "factor": 8.115384615309273, + "label": "competitor leads clearly (8.12x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -5150,14 +5186,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.7800000000024738, - "maxMs": 2.2299999999995634, - "ratio": 1.2528089887620586 + "minMs": 2.0200000000004366, + "maxMs": 2.179999999996653, + "ratio": 1.079207920790189 }, "competitor": { - "minMs": 0.250000000003638, - "maxMs": 0.2999999999956344, - "ratio": 1.1999999999650754 + "minMs": 0.20999999999912689, + "maxMs": 0.37999999999738066, + "ratio": 1.80952380951886 }, "stable": true, "rungs": [ @@ -5179,19 +5215,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.6899999999986903, - "referenceP95Ms": 0.8999999999941792, - "referenceGpuMs": 0.2272915, + "referenceMs": 0.680000000000291, + "referenceP95Ms": 0.8600000000005821, + "referenceGpuMs": 0.22912500000000002, "referenceOverFrameBudget": false, - "competitorMs": 0.9200000000055297, + "competitorMs": 0.9200000000018917, "competitorP95Ms": 1.180000000000291, - "competitorGpuMs": 0.22462500000000002, + "competitorGpuMs": 0.138229, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.7499999999940685, - "factor": 1.3333333333438782, - "label": "ExoJS leads (1.33x)", + "ratio": 0.7391304347814052, + "factor": 1.3529411764727912, + "label": "ExoJS leads (1.35x)", "structural": false }, "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", @@ -5199,14 +5235,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.680000000000291, + "minMs": 0.6600000000034925, "maxMs": 0.6999999999970896, - "ratio": 1.0294117647011618 + "ratio": 1.0606060605960386 }, "competitor": { - "minMs": 0.9200000000055297, - "maxMs": 0.9800000000032014, - "ratio": 1.0652173913014251 + "minMs": 0.9199999999982538, + "maxMs": 1.0799999999981083, + "ratio": 1.1739130434784328 }, "stable": true, "rungs": [ @@ -5233,19 +5269,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.6399999999994179, - "referenceP95Ms": 0.9799999999959255, - "referenceGpuMs": 0.3017085, + "referenceMs": 0.6100000000005821, + "referenceP95Ms": 0.8600000000005821, + "referenceGpuMs": 0.30239649999999996, "referenceOverFrameBudget": false, - "competitorMs": 16.350000000002183, - "competitorP95Ms": 20.099999999998545, - "competitorGpuMs": 0.29231300000000005, + "competitorMs": 16.660000000003492, + "competitorP95Ms": 21, + "competitorGpuMs": 0.294688, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.03914373088680933, - "factor": 25.546875000026642, - "label": "ExoJS leads clearly (25.55x)", + "ratio": 0.0366146458583706, + "factor": 27.31147540981573, + "label": "ExoJS leads clearly (27.31x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -5253,14 +5289,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5999999999985448, - "maxMs": 0.6599999999962165, - "ratio": 1.099999999996362 + "minMs": 0.5, + "maxMs": 0.6200000000026193, + "ratio": 1.2400000000052387 }, "competitor": { - "minMs": 16.029999999998836, - "maxMs": 16.429999999996653, - "ratio": 1.0249532127260041 + "minMs": 16.280000000002474, + "maxMs": 17.74000000000524, + "ratio": 1.089680589680746 }, "stable": true, "rungs": [ @@ -5353,7 +5389,7 @@ "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." ], "engineVersion": "0.17.0", - "timestamp": "2026-09-12T02:16:07.435Z" + "timestamp": "2026-09-12T03:21:55.258Z" }, { "browser": "webkit", @@ -5393,7 +5429,7 @@ "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." ], "engineVersion": "0.17.0", - "timestamp": "2026-09-12T02:28:32.450Z" + "timestamp": "2026-09-12T03:34:01.941Z" }, { "browser": "webkit", @@ -5433,7 +5469,7 @@ "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." ], "engineVersion": "0.17.0", - "timestamp": "2026-09-12T02:40:41.625Z" + "timestamp": "2026-09-12T03:46:13.242Z" } ], "libraries": [ @@ -5471,19 +5507,19 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 5.480000000000018, - "referenceP95Ms": 5.919999999999845, + "referenceMs": 5.139999999999873, + "referenceP95Ms": 6.019999999999982, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 10.339999999998327, - "competitorP95Ms": 12.059999999997672, + "competitorMs": 11.139999999999418, + "competitorP95Ms": 12.099999999998545, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5299806576403197, - "factor": 1.8868613138683015, - "label": "ExoJS leads (1.89x)", + "ratio": 0.46140035906643995, + "factor": 2.1673151750972166, + "label": "ExoJS leads (2.17x)", "structural": false }, "mechanism": "ExoJS resolves fewer solved contacts (5501 vs 12297); the counters are not semantically identical across arms (see the run's caveats)", @@ -5491,14 +5527,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.379999999999654, - "maxMs": 5.5, - "ratio": 1.0223048327138202 + "minMs": 5.059999999999945, + "maxMs": 5.400000000000091, + "ratio": 1.0671936758893577 }, "competitor": { - "minMs": 10.30000000000291, - "maxMs": 10.399999999997817, - "ratio": 1.0097087378635805 + "minMs": 10.380000000001019, + "maxMs": 11.200000000000728, + "ratio": 1.0789980732176907 }, "stable": true, "rungs": [ @@ -5510,19 +5546,19 @@ }, { "competitor": "nape-js", - "referenceMs": 5.480000000000018, - "referenceP95Ms": 5.919999999999845, + "referenceMs": 5.139999999999873, + "referenceP95Ms": 6.019999999999982, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 10.020000000004075, - "competitorP95Ms": 13.440000000002328, + "competitorMs": 9.639999999999418, + "competitorP95Ms": 12.660000000003492, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5469061876245299, - "factor": 1.828467153285409, - "label": "ExoJS leads (1.83x)", + "ratio": 0.533195020746907, + "factor": 1.8754863813228904, + "label": "ExoJS leads (1.88x)", "structural": false }, "mechanism": "identical scene (5501 bodies, 5501 contacts, 0 joints on both arms); the difference is solver cost", @@ -5530,14 +5566,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.379999999999654, - "maxMs": 5.5, - "ratio": 1.0223048327138202 + "minMs": 5.059999999999945, + "maxMs": 5.400000000000091, + "ratio": 1.0671936758893577 }, "competitor": { - "minMs": 9.610000000000582, - "maxMs": 10.029999999998836, - "ratio": 1.0437044745055388 + "minMs": 9.540000000008149, + "maxMs": 10.020000000004075, + "ratio": 1.050314465408335 }, "stable": true, "rungs": [ @@ -5549,19 +5585,19 @@ }, { "competitor": "planck", - "referenceMs": 5.480000000000018, - "referenceP95Ms": 5.919999999999845, + "referenceMs": 5.139999999999873, + "referenceP95Ms": 6.019999999999982, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 29.979999999999563, - "competitorP95Ms": 36.36000000000058, + "competitorMs": 29.5099999999984, + "competitorP95Ms": 36.61999999999534, "competitorGpuMs": null, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.18278852568379247, - "factor": 5.470802919707931, - "label": "ExoJS leads clearly (5.47x)", + "ratio": 0.1741782446628313, + "factor": 5.741245136186601, + "label": "ExoJS leads clearly (5.74x)", "structural": true }, "mechanism": "ExoJS resolves fewer solved contacts (5501 vs 10677); the counters are not semantically identical across arms (see the run's caveats)", @@ -5569,14 +5605,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.379999999999654, - "maxMs": 5.5, - "ratio": 1.0223048327138202 + "minMs": 5.059999999999945, + "maxMs": 5.400000000000091, + "ratio": 1.0671936758893577 }, "competitor": { - "minMs": 29.43000000000029, - "maxMs": 30.4800000000032, - "ratio": 1.0356778797146755 + "minMs": 29.479999999999563, + "maxMs": 30.360000000000582, + "ratio": 1.0298507462686917 }, "stable": true, "rungs": [ @@ -5588,19 +5624,19 @@ }, { "competitor": "rapier", - "referenceMs": 5.480000000000018, - "referenceP95Ms": 5.919999999999845, + "referenceMs": 5.139999999999873, + "referenceP95Ms": 6.019999999999982, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 4.060000000004948, - "competitorP95Ms": 5.740000000005239, + "competitorMs": 4.290000000000873, + "competitorP95Ms": 5.360000000000582, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { - "side": "competitor", - "ratio": 1.3497536945796405, - "factor": 1.3497536945796405, - "label": "competitor leads (1.35x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "ExoJS resolves fewer solved contacts (5501 vs 10045); the counters are not semantically identical across arms (see the run's caveats)", @@ -5608,19 +5644,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.379999999999654, - "maxMs": 5.5, - "ratio": 1.0223048327138202 + "minMs": 5.059999999999945, + "maxMs": 5.400000000000091, + "ratio": 1.0671936758893577 }, "competitor": { - "minMs": 4.060000000004948, - "maxMs": 4.260000000002037, - "ratio": 1.0492610837430656 + "minMs": 4.05000000000291, + "maxMs": 4.310000000004948, + "ratio": 1.0641975308646545 }, - "stable": true, + "stable": false, "rungs": [ - "competitor-leads", - "competitor-leads", + "level", + "level", "competitor-leads" ] } @@ -5637,17 +5673,17 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 5.420000000000073, - "referenceP95Ms": 6.480000000000018, + "referenceMs": 5.440000000000509, + "referenceP95Ms": 6.460000000000036, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.3400000000001455, - "competitorP95Ms": 6.080000000001746, + "competitorMs": 5.240000000001601, + "competitorP95Ms": 5.979999999999563, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.0149812734082257, + "ratio": 1.0381679389310778, "factor": 1, "label": "level", "structural": false @@ -5657,14 +5693,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.3400000000001455, - "maxMs": 5.5600000000004, - "ratio": 1.0411985018727057 + "minMs": 5.380000000000109, + "maxMs": 5.479999999999563, + "ratio": 1.0185873605946938 }, "competitor": { - "minMs": 5.279999999998836, - "maxMs": 5.559999999999491, - "ratio": 1.0530303030304387 + "minMs": 5.220000000001164, + "maxMs": 5.299999999999272, + "ratio": 1.0153256704977185 }, "stable": true, "rungs": [ @@ -5676,17 +5712,17 @@ }, { "competitor": "nape-js", - "referenceMs": 5.420000000000073, - "referenceP95Ms": 6.480000000000018, + "referenceMs": 5.440000000000509, + "referenceP95Ms": 6.460000000000036, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.80000000000291, - "competitorP95Ms": 6.80000000000291, + "competitorMs": 5.860000000000582, + "competitorP95Ms": 6.680000000007567, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9344827586202333, + "ratio": 0.9283276450511893, "factor": 1, "label": "level", "structural": false @@ -5696,14 +5732,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.3400000000001455, - "maxMs": 5.5600000000004, - "ratio": 1.0411985018727057 + "minMs": 5.380000000000109, + "maxMs": 5.479999999999563, + "ratio": 1.0185873605946938 }, "competitor": { - "minMs": 5.779999999998836, - "maxMs": 5.820000000006985, - "ratio": 1.0069204152263247 + "minMs": 5.819999999992433, + "maxMs": 5.880000000004657, + "ratio": 1.010309278352629 }, "stable": true, "rungs": [ @@ -5715,17 +5751,17 @@ }, { "competitor": "planck", - "referenceMs": 5.420000000000073, - "referenceP95Ms": 6.480000000000018, + "referenceMs": 5.440000000000509, + "referenceP95Ms": 6.460000000000036, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.740000000005239, - "competitorP95Ms": 6.459999999999127, + "competitorMs": 5.620000000002619, + "competitorP95Ms": 6.419999999998254, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9442508710792903, + "ratio": 0.9679715302487498, "factor": 1, "label": "level", "structural": false @@ -5735,14 +5771,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.3400000000001455, - "maxMs": 5.5600000000004, - "ratio": 1.0411985018727057 + "minMs": 5.380000000000109, + "maxMs": 5.479999999999563, + "ratio": 1.0185873605946938 }, "competitor": { - "minMs": 5.519999999996799, - "maxMs": 6, - "ratio": 1.0869565217397608 + "minMs": 5.440000000002328, + "maxMs": 5.739999999997963, + "ratio": 1.0551470588227032 }, "stable": true, "rungs": [ @@ -5754,19 +5790,19 @@ }, { "competitor": "rapier", - "referenceMs": 5.420000000000073, - "referenceP95Ms": 6.480000000000018, + "referenceMs": 5.440000000000509, + "referenceP95Ms": 6.460000000000036, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 1.3000000000029104, - "competitorP95Ms": 1.360000000000582, + "competitorMs": 1.320000000006985, + "competitorP95Ms": 1.3800000000046566, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 4.169230769221492, - "factor": 4.169230769221492, - "label": "competitor leads (4.17x)", + "ratio": 4.121212121190699, + "factor": 4.121212121190699, + "label": "competitor leads (4.12x)", "structural": false }, "mechanism": "identical scene (1504 bodies, 4236 contacts, 0 joints on both arms); the difference is solver cost", @@ -5774,14 +5810,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.3400000000001455, - "maxMs": 5.5600000000004, - "ratio": 1.0411985018727057 + "minMs": 5.380000000000109, + "maxMs": 5.479999999999563, + "ratio": 1.0185873605946938 }, "competitor": { - "minMs": 1.2800000000061118, - "maxMs": 1.3000000000029104, - "ratio": 1.0156249999974243 + "minMs": 1.3000000000029104, + "maxMs": 1.360000000000582, + "ratio": 1.0461538461519517 }, "stable": true, "rungs": [ @@ -5804,18 +5840,18 @@ { "competitor": "matter-js", "referenceMs": 4.579999999999927, - "referenceP95Ms": 5.059999999999491, + "referenceP95Ms": 5.020000000000437, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 2.2399999999979627, - "competitorP95Ms": 2.599999999998545, + "competitorMs": 2.2000000000007276, + "competitorP95Ms": 2.359999999996944, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 2.044642857144684, - "factor": 2.044642857144684, - "label": "competitor leads (2.04x)", + "ratio": 2.0818181818174604, + "factor": 2.0818181818174604, + "label": "competitor leads (2.08x)", "structural": false }, "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms); the difference is solver cost", @@ -5824,13 +5860,13 @@ "runs": 3, "reference": { "minMs": 4.539999999999964, - "maxMs": 4.600000000000364, - "ratio": 1.0132158590309253 + "maxMs": 4.619999999999891, + "ratio": 1.0176211453744335 }, "competitor": { - "minMs": 2.1700000000000728, - "maxMs": 2.2999999999992724, - "ratio": 1.0599078341010117 + "minMs": 2.1200000000026193, + "maxMs": 2.220000000001164, + "ratio": 1.04716981132001 }, "stable": true, "rungs": [ @@ -5843,11 +5879,11 @@ { "competitor": "nape-js", "referenceMs": 4.579999999999927, - "referenceP95Ms": 5.059999999999491, + "referenceP95Ms": 5.020000000000437, "referenceGpuMs": null, "referenceOverFrameBudget": false, "competitorMs": 5.019999999989523, - "competitorP95Ms": 6.059999999997672, + "competitorP95Ms": 6.279999999998836, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { @@ -5863,13 +5899,13 @@ "runs": 3, "reference": { "minMs": 4.539999999999964, - "maxMs": 4.600000000000364, - "ratio": 1.0132158590309253 + "maxMs": 4.619999999999891, + "ratio": 1.0176211453744335 }, "competitor": { - "minMs": 4.959999999999127, - "maxMs": 5.150000000001455, - "ratio": 1.0383064516133793 + "minMs": 4.990000000005239, + "maxMs": 5.099999999991269, + "ratio": 1.02204408817353 }, "stable": true, "rungs": [ @@ -5882,18 +5918,18 @@ { "competitor": "planck", "referenceMs": 4.579999999999927, - "referenceP95Ms": 5.059999999999491, + "referenceP95Ms": 5.020000000000437, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 7.860000000000582, - "competitorP95Ms": 10.69999999999709, + "competitorMs": 7.779999999998836, + "competitorP95Ms": 11.040000000000873, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5826972010177593, - "factor": 1.7161572052403289, - "label": "ExoJS leads (1.72x)", + "ratio": 0.5886889460155029, + "factor": 1.6986899563316504, + "label": "ExoJS leads (1.70x)", "structural": false }, "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms); the difference is solver cost", @@ -5902,13 +5938,13 @@ "runs": 3, "reference": { "minMs": 4.539999999999964, - "maxMs": 4.600000000000364, - "ratio": 1.0132158590309253 + "maxMs": 4.619999999999891, + "ratio": 1.0176211453744335 }, "competitor": { - "minMs": 7.769999999996799, - "maxMs": 7.979999999999563, - "ratio": 1.027027027027394 + "minMs": 7.770000000000437, + "maxMs": 7.860000000000582, + "ratio": 1.0115830115830298 }, "stable": true, "rungs": [ @@ -5921,18 +5957,18 @@ { "competitor": "rapier", "referenceMs": 4.579999999999927, - "referenceP95Ms": 5.059999999999491, + "referenceP95Ms": 5.020000000000437, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 1.5800000000017462, - "competitorP95Ms": 1.8399999999965075, + "competitorMs": 1.639999999999418, + "competitorP95Ms": 1.7799999999988358, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 2.89873417721194, - "factor": 2.89873417721194, - "label": "competitor leads (2.90x)", + "ratio": 2.792682926830215, + "factor": 2.792682926830215, + "label": "competitor leads (2.79x)", "structural": false }, "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms); the difference is solver cost", @@ -5941,13 +5977,13 @@ "runs": 3, "reference": { "minMs": 4.539999999999964, - "maxMs": 4.600000000000364, - "ratio": 1.0132158590309253 + "maxMs": 4.619999999999891, + "ratio": 1.0176211453744335 }, "competitor": { - "minMs": 1.5799999999871943, + "minMs": 1.5800000000017462, "maxMs": 1.639999999999418, - "ratio": 1.037974683552348 + "ratio": 1.0379746835427883 }, "stable": true, "rungs": [ @@ -5969,17 +6005,17 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 5.720000000000255, + "referenceMs": 5.780000000000655, "referenceP95Ms": 6.319999999999709, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 6.520000000000437, - "competitorP95Ms": 8.020000000000437, + "competitorMs": 6.470000000001164, + "competitorP95Ms": 8.299999999999272, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.8773006134969128, + "ratio": 0.8933539412673284, "factor": 1, "label": "level", "structural": false @@ -5989,14 +6025,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.640000000001237, - "maxMs": 5.739999999999782, - "ratio": 1.0177304964536389 + "minMs": 5.739999999999782, + "maxMs": 5.800000000001091, + "ratio": 1.0104529616727025 }, "competitor": { - "minMs": 6.440000000000509, - "maxMs": 6.550000000001091, - "ratio": 1.017080745341704 + "minMs": 6.430000000000291, + "maxMs": 6.479999999999563, + "ratio": 1.007776049766605 }, "stable": true, "rungs": [ @@ -6008,19 +6044,19 @@ }, { "competitor": "nape-js", - "referenceMs": 5.720000000000255, + "referenceMs": 5.780000000000655, "referenceP95Ms": 6.319999999999709, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 12.619999999995343, - "competitorP95Ms": 14.059999999997672, + "competitorMs": 12.660000000003492, + "competitorP95Ms": 14.279999999998836, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.453248811410647, - "factor": 2.206293706292794, - "label": "ExoJS leads (2.21x)", + "ratio": 0.45655608214842497, + "factor": 2.1903114186854773, + "label": "ExoJS leads (2.19x)", "structural": false }, "mechanism": "competitor resolves fewer solved contacts (2195 vs 1461); the counters are not semantically identical across arms (see the run's caveats)", @@ -6028,14 +6064,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.640000000001237, - "maxMs": 5.739999999999782, - "ratio": 1.0177304964536389 + "minMs": 5.739999999999782, + "maxMs": 5.800000000001091, + "ratio": 1.0104529616727025 }, "competitor": { - "minMs": 12.539999999993597, - "maxMs": 12.69999999999709, - "ratio": 1.0127591706541925 + "minMs": 12.55000000000291, + "maxMs": 12.720000000001164, + "ratio": 1.0135458167329254 }, "stable": true, "rungs": [ @@ -6047,19 +6083,19 @@ }, { "competitor": "planck", - "referenceMs": 5.720000000000255, + "referenceMs": 5.780000000000655, "referenceP95Ms": 6.319999999999709, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 8.059999999997672, - "competitorP95Ms": 10.339999999996508, + "competitorMs": 7.990000000001601, + "competitorP95Ms": 10.280000000006112, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.7096774193550753, - "factor": 1.4090909090904393, - "label": "ExoJS leads (1.41x)", + "ratio": 0.7234042553190859, + "factor": 1.382352941176591, + "label": "ExoJS leads (1.38x)", "structural": false }, "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms) with 34 vs 37 ray hits per step; the difference is query and solver cost", @@ -6067,14 +6103,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.640000000001237, - "maxMs": 5.739999999999782, - "ratio": 1.0177304964536389 + "minMs": 5.739999999999782, + "maxMs": 5.800000000001091, + "ratio": 1.0104529616727025 }, "competitor": { - "minMs": 7.990000000001601, - "maxMs": 8.100000000002183, - "ratio": 1.013767209011334 + "minMs": 7.9099999999962165, + "maxMs": 8.0099999999984, + "ratio": 1.0126422250318876 }, "stable": true, "rungs": [ @@ -6086,19 +6122,19 @@ }, { "competitor": "rapier", - "referenceMs": 5.720000000000255, + "referenceMs": 5.780000000000655, "referenceP95Ms": 6.319999999999709, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 1.7000000000116415, - "competitorP95Ms": 1.860000000000582, + "competitorMs": 1.679999999993015, + "competitorP95Ms": 1.8599999999860302, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 3.3647058823300497, - "factor": 3.3647058823300497, - "label": "competitor leads (3.36x)", + "ratio": 3.4404761904908847, + "factor": 3.4404761904908847, + "label": "competitor leads (3.44x)", "structural": false }, "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms) with 34 vs 39 ray hits per step; the difference is query and solver cost", @@ -6106,14 +6142,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.640000000001237, - "maxMs": 5.739999999999782, - "ratio": 1.0177304964536389 + "minMs": 5.739999999999782, + "maxMs": 5.800000000001091, + "ratio": 1.0104529616727025 }, "competitor": { - "minMs": 1.6499999999941792, - "maxMs": 1.7200000000011642, - "ratio": 1.0424242424286254 + "minMs": 1.639999999999418, + "maxMs": 1.7000000000116415, + "ratio": 1.036585365861125 }, "stable": true, "rungs": [ @@ -6135,17 +6171,17 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 5.599999999999454, - "referenceP95Ms": 6.399999999999636, + "referenceMs": 5.619999999998981, + "referenceP95Ms": 6.6200000000008, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.709999999999127, - "competitorP95Ms": 6.700000000000728, + "competitorMs": 5.720000000001164, + "competitorP95Ms": 6.6599999999962165, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9807355516638022, + "ratio": 0.9825174825171045, "factor": 1, "label": "level", "structural": false @@ -6155,14 +6191,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.520000000000437, - "maxMs": 5.6899999999996, - "ratio": 1.0307971014491213 + "minMs": 5.5, + "maxMs": 5.639999999999418, + "ratio": 1.0254545454544397 }, "competitor": { - "minMs": 5.639999999999418, - "maxMs": 5.739999999997963, - "ratio": 1.0177304964536444 + "minMs": 5.709999999999127, + "maxMs": 5.819999999999709, + "ratio": 1.019264448336357 }, "stable": true, "rungs": [ @@ -6174,19 +6210,19 @@ }, { "competitor": "nape-js", - "referenceMs": 5.599999999999454, - "referenceP95Ms": 6.399999999999636, + "referenceMs": 5.619999999998981, + "referenceP95Ms": 6.6200000000008, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 3.039999999993597, - "competitorP95Ms": 3.5599999999976717, + "competitorMs": 2.9199999999982538, + "competitorP95Ms": 3.5200000000040745, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.842105263161595, - "factor": 1.842105263161595, - "label": "competitor leads (1.84x)", + "ratio": 1.9246575342473775, + "factor": 1.9246575342473775, + "label": "competitor leads (1.92x)", "structural": false }, "mechanism": "identical scene (1504 bodies, 1415 contacts, 0 joints on both arms); the difference is solver cost", @@ -6194,14 +6230,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.520000000000437, - "maxMs": 5.6899999999996, - "ratio": 1.0307971014491213 + "minMs": 5.5, + "maxMs": 5.639999999999418, + "ratio": 1.0254545454544397 }, "competitor": { - "minMs": 2.820000000006985, - "maxMs": 3.1000000000058208, - "ratio": 1.0992907801411853 + "minMs": 2.720000000001164, + "maxMs": 3.0800000000017462, + "ratio": 1.132352941176628 }, "stable": true, "rungs": [ @@ -6213,17 +6249,17 @@ }, { "competitor": "planck", - "referenceMs": 5.599999999999454, - "referenceP95Ms": 6.399999999999636, + "referenceMs": 5.619999999998981, + "referenceP95Ms": 6.6200000000008, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.430000000000291, - "competitorP95Ms": 7.180000000007567, + "competitorMs": 5.100000000005821, + "competitorP95Ms": 7.380000000004657, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.0313075506444114, + "ratio": 1.101960784312268, "factor": 1, "label": "level", "structural": false @@ -6233,14 +6269,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.520000000000437, - "maxMs": 5.6899999999996, - "ratio": 1.0307971014491213 + "minMs": 5.5, + "maxMs": 5.639999999999418, + "ratio": 1.0254545454544397 }, "competitor": { - "minMs": 5.280000000006112, - "maxMs": 5.619999999995343, - "ratio": 1.0643939393918254 + "minMs": 5.070000000006985, + "maxMs": 5.4799999999959255, + "ratio": 1.0808678500963265 }, "stable": true, "rungs": [ @@ -6252,19 +6288,19 @@ }, { "competitor": "rapier", - "referenceMs": 5.599999999999454, - "referenceP95Ms": 6.399999999999636, + "referenceMs": 5.619999999998981, + "referenceP95Ms": 6.6200000000008, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 0.7599999999947613, - "competitorP95Ms": 0.7799999999988358, + "competitorMs": 0.7799999999988358, + "competitorP95Ms": 0.819999999992433, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 7.368421052681652, - "factor": 7.368421052681652, - "label": "competitor leads clearly (7.37x)", + "ratio": 7.205128205137653, + "factor": 7.205128205137653, + "label": "competitor leads clearly (7.21x)", "structural": true }, "mechanism": "identical scene (1504 bodies, 1415 contacts, 0 joints on both arms); the difference is solver cost", @@ -6272,14 +6308,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.520000000000437, - "maxMs": 5.6899999999996, - "ratio": 1.0307971014491213 + "minMs": 5.5, + "maxMs": 5.639999999999418, + "ratio": 1.0254545454544397 }, "competitor": { "minMs": 0.7599999999947613, - "maxMs": 0.8000000000029104, - "ratio": 1.0526315789584537 + "maxMs": 0.7999999999883585, + "ratio": 1.0526315789393064 }, "stable": true, "rungs": [ @@ -6301,19 +6337,19 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 5.240000000000691, - "referenceP95Ms": 5.459999999999127, + "referenceMs": 5.289999999999964, + "referenceP95Ms": 5.519999999998618, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 4.819999999999709, - "competitorP95Ms": 6.580000000001746, + "competitorMs": 5.169999999998254, + "competitorP95Ms": 7.119999999995343, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.08713692946079, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "competitor resolves fewer solved contacts (7875 vs 0); the counters are not semantically identical across arms (see the run's caveats)", @@ -6321,18 +6357,18 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.980000000000473, - "maxMs": 5.699999999998909, - "ratio": 1.144578313252684 + "minMs": 5.020000000000437, + "maxMs": 5.680000000000291, + "ratio": 1.1314741035856168 }, "competitor": { - "minMs": 4.740000000001601, - "maxMs": 5.879999999997381, - "ratio": 1.2405063291129526 + "minMs": 4.399999999997817, + "maxMs": 6.120000000002619, + "ratio": 1.3909090909103763 }, - "stable": true, + "stable": false, "rungs": [ - "level", + "competitor-leads", "level", "level" ] @@ -6340,12 +6376,12 @@ }, { "competitor": "nape-js", - "referenceMs": 5.240000000000691, - "referenceP95Ms": 5.459999999999127, + "referenceMs": 5.289999999999964, + "referenceP95Ms": 5.519999999998618, "referenceGpuMs": null, "referenceOverFrameBudget": false, "competitorMs": 0, - "competitorP95Ms": 0, + "competitorP95Ms": 0.0020000000004074536, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { @@ -6360,9 +6396,9 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.980000000000473, - "maxMs": 5.699999999998909, - "ratio": 1.144578313252684 + "minMs": 5.020000000000437, + "maxMs": 5.680000000000291, + "ratio": 1.1314741035856168 }, "competitor": { "minMs": 0, @@ -6379,19 +6415,19 @@ }, { "competitor": "planck", - "referenceMs": 5.240000000000691, - "referenceP95Ms": 5.459999999999127, + "referenceMs": 5.289999999999964, + "referenceP95Ms": 5.519999999998618, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 0.39999999999417923, - "competitorP95Ms": 0.40000000000873115, + "competitorMs": 0.37000000000261934, + "competitorP95Ms": 0.3800000000046566, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 13.100000000192358, - "factor": 13.100000000192358, - "label": "competitor leads clearly (13.10x)", + "ratio": 14.297297297195984, + "factor": 14.297297297195984, + "label": "competitor leads clearly (14.30x)", "structural": true }, "mechanism": "competitor resolves fewer solved contacts (7875 vs 0); the counters are not semantically identical across arms (see the run's caveats)", @@ -6399,14 +6435,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.980000000000473, - "maxMs": 5.699999999998909, - "ratio": 1.144578313252684 + "minMs": 5.020000000000437, + "maxMs": 5.680000000000291, + "ratio": 1.1314741035856168 }, "competitor": { - "minMs": 0.36499999999796273, - "maxMs": 0.41000000000349246, - "ratio": 1.1232876712487148 + "minMs": 0.37000000000261934, + "maxMs": 0.37999999999738066, + "ratio": 1.0270270270126771 }, "stable": true, "rungs": [ @@ -6418,19 +6454,19 @@ }, { "competitor": "rapier", - "referenceMs": 5.240000000000691, - "referenceP95Ms": 5.459999999999127, + "referenceMs": 5.289999999999964, + "referenceP95Ms": 5.519999999998618, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 0.19999999999708962, - "competitorP95Ms": 0.22000000000116415, + "competitorMs": 0.20000000001164153, + "competitorP95Ms": 0.23999999999068677, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 26.200000000384716, - "factor": 26.200000000384716, - "label": "competitor leads clearly (26.20x)", + "ratio": 26.449999998460225, + "factor": 26.449999998460225, + "label": "competitor leads clearly (26.45x)", "structural": true }, "mechanism": "competitor resolves fewer solved contacts (7875 vs 1125); the counters are not semantically identical across arms (see the run's caveats)", @@ -6438,14 +6474,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.980000000000473, - "maxMs": 5.699999999998909, - "ratio": 1.144578313252684 + "minMs": 5.020000000000437, + "maxMs": 5.680000000000291, + "ratio": 1.1314741035856168 }, "competitor": { "minMs": 0.19999999999708962, - "maxMs": 0.20000000001164153, - "ratio": 1.0000000000727596 + "maxMs": 0.21999999998661224, + "ratio": 1.0999999999490684 }, "stable": true, "rungs": [ @@ -6467,19 +6503,19 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 3.7400000000016007, - "referenceP95Ms": 4.440000000002328, + "referenceMs": 3.7399999999979627, + "referenceP95Ms": 4.5, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 10.640000000003056, - "competitorP95Ms": 12.339999999996508, + "competitorMs": 10.680000000000291, + "competitorP95Ms": 12.340000000003783, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.3515037593985457, - "factor": 2.8449197860958564, - "label": "ExoJS leads (2.84x)", + "ratio": 0.3501872659174027, + "factor": 2.8556149732636658, + "label": "ExoJS leads (2.86x)", "structural": false }, "mechanism": "identical scene (3004 bodies, 6199 contacts, 0 joints on both arms); the difference is solver cost", @@ -6487,14 +6523,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.719999999997526, - "maxMs": 3.7599999999983993, - "ratio": 1.0107526881722848 + "minMs": 3.7399999999979627, + "maxMs": 3.7400000000016007, + "ratio": 1.0000000000009728 }, "competitor": { "minMs": 10.639999999999418, - "maxMs": 10.680000000000291, - "ratio": 1.003759398496323 + "maxMs": 10.75, + "ratio": 1.0103383458647168 }, "stable": true, "rungs": [ @@ -6506,19 +6542,19 @@ }, { "competitor": "nape-js", - "referenceMs": 3.7400000000016007, - "referenceP95Ms": 4.440000000002328, + "referenceMs": 3.7399999999979627, + "referenceP95Ms": 4.5, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 11.059999999997672, - "competitorP95Ms": 14.099999999991269, + "competitorMs": 11, + "competitorP95Ms": 13.820000000006985, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.33815551537092114, - "factor": 2.9572192513350104, - "label": "ExoJS leads (2.96x)", + "ratio": 0.3399999999998148, + "factor": 2.9411764705898373, + "label": "ExoJS leads (2.94x)", "structural": false }, "mechanism": "identical scene (3004 bodies, 6199 contacts, 0 joints on both arms); the difference is solver cost", @@ -6526,14 +6562,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.719999999997526, - "maxMs": 3.7599999999983993, - "ratio": 1.0107526881722848 + "minMs": 3.7399999999979627, + "maxMs": 3.7400000000016007, + "ratio": 1.0000000000009728 }, "competitor": { - "minMs": 11, - "maxMs": 11.080000000001746, - "ratio": 1.007272727272886 + "minMs": 10.980000000010477, + "maxMs": 11, + "ratio": 1.0018214936238163 }, "stable": true, "rungs": [ @@ -6545,19 +6581,19 @@ }, { "competitor": "planck", - "referenceMs": 3.7400000000016007, - "referenceP95Ms": 4.440000000002328, + "referenceMs": 3.7399999999979627, + "referenceP95Ms": 4.5, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 13.299999999988358, - "competitorP95Ms": 19.679999999993015, + "competitorMs": 13.080000000001746, + "competitorP95Ms": 19.54000000000815, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.2812030075191635, - "factor": 3.556149732615686, - "label": "ExoJS leads (3.56x)", + "ratio": 0.2859327217123443, + "factor": 3.4973262032109282, + "label": "ExoJS leads (3.50x)", "structural": false }, "mechanism": "identical scene (3004 bodies, 6199 contacts, 0 joints on both arms); the difference is solver cost", @@ -6565,14 +6601,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.719999999997526, - "maxMs": 3.7599999999983993, - "ratio": 1.0107526881722848 + "minMs": 3.7399999999979627, + "maxMs": 3.7400000000016007, + "ratio": 1.0000000000009728 }, "competitor": { - "minMs": 13.240000000005239, - "maxMs": 13.330000000001746, - "ratio": 1.0067975830813045 + "minMs": 12.979999999995925, + "maxMs": 13.34000000001106, + "ratio": 1.0277349768886939 }, "stable": true, "rungs": [ @@ -6584,19 +6620,19 @@ }, { "competitor": "rapier", - "referenceMs": 3.7400000000016007, - "referenceP95Ms": 4.440000000002328, + "referenceMs": 3.7399999999979627, + "referenceP95Ms": 4.5, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 2.099999999991269, - "competitorP95Ms": 2.2599999999947613, + "competitorMs": 2.1199999999953434, + "competitorP95Ms": 2.220000000001164, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.7809523809605479, - "factor": 1.7809523809605479, - "label": "competitor leads (1.78x)", + "ratio": 1.7641509433991405, + "factor": 1.7641509433991405, + "label": "competitor leads (1.76x)", "structural": false }, "mechanism": "identical scene (3004 bodies, 6199 contacts, 0 joints on both arms); the difference is solver cost", @@ -6604,14 +6640,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.719999999997526, - "maxMs": 3.7599999999983993, - "ratio": 1.0107526881722848 + "minMs": 3.7399999999979627, + "maxMs": 3.7400000000016007, + "ratio": 1.0000000000009728 }, "competitor": { "minMs": 2.0800000000017462, - "maxMs": 2.139999999999418, - "ratio": 1.0288461538450102 + "maxMs": 2.1600000000034925, + "ratio": 1.0384615384623457 }, "stable": true, "rungs": [ @@ -6628,6 +6664,6 @@ }, "signature": { "algorithm": "sha256", - "value": "d251be16370d0abc9c51f386e8a873c328de964248aaaf83e80546a0ea754888" + "value": "74734d08514f28925d94d9550137e610aa03b9c692c53cf588b06b4e62d677ce" } } From 51dd433e7610a04adc2726c0e797bd9ca8ff6101 Mon Sep 17 00:00:00 2001 From: Dima Giesler Date: Sat, 12 Sep 2026 06:46:44 +0200 Subject: [PATCH 17/28] bench: re-measure the macOS reference run on the current arms --- .../results/m3-max-macos-27-beta-webkit.json | 3941 ++++++++--------- 1 file changed, 1912 insertions(+), 2029 deletions(-) diff --git a/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json b/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json index cfb1cdc71..863e9a37b 100644 --- a/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json +++ b/packages/exojs-bench/results/m3-max-macos-27-beta-webkit.json @@ -12,7 +12,7 @@ "prerelease": true }, "engineVersion": "0.17.0", - "measuredAt": "2026-09-12T03:46:13.242Z", + "measuredAt": "2026-09-12T04:44:36.717Z", "runs": 3 }, "rendering": { @@ -43,7 +43,7 @@ "headless": true, "software": false, "engineVersion": "0.17.0", - "timestamp": "2026-09-12T03:11:55.632Z" + "timestamp": "2026-09-12T04:11:17.999Z" }, { "backend": "webgpu", @@ -70,7 +70,7 @@ "software": false, "slotTier": 16, "engineVersion": "0.17.0", - "timestamp": "2026-09-12T03:19:13.432Z" + "timestamp": "2026-09-12T04:18:01.494Z" } ] }, @@ -100,7 +100,7 @@ "headless": true, "software": false, "engineVersion": "0.17.0", - "timestamp": "2026-09-12T03:24:03.163Z" + "timestamp": "2026-09-12T04:23:07.261Z" }, { "backend": "webgpu", @@ -127,7 +127,7 @@ "software": false, "slotTier": 16, "engineVersion": "0.17.0", - "timestamp": "2026-09-12T03:31:20.194Z" + "timestamp": "2026-09-12T04:29:50.278Z" } ] }, @@ -157,7 +157,7 @@ "headless": true, "software": false, "engineVersion": "0.17.0", - "timestamp": "2026-09-12T03:36:09.267Z" + "timestamp": "2026-09-12T04:34:54.521Z" }, { "backend": "webgpu", @@ -184,7 +184,7 @@ "software": false, "slotTier": 16, "engineVersion": "0.17.0", - "timestamp": "2026-09-12T03:43:31.391Z" + "timestamp": "2026-09-12T04:41:40.647Z" } ] } @@ -226,19 +226,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.20000000000004547, - "referenceP95Ms": 0.42000000000007276, + "referenceMs": 0.2199999999997999, + "referenceP95Ms": 0.40000000000009095, "referenceGpuMs": 16.660000000000082, "referenceOverFrameBudget": false, - "competitorMs": 2.240000000000009, - "competitorP95Ms": 8.180000000000291, + "competitorMs": 2.339999999999975, + "competitorP95Ms": 9.620000000000118, "competitorGpuMs": 16.660000000000082, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.08928571428573423, - "factor": 11.199999999997498, - "label": "ExoJS leads clearly (11.20x)", + "ratio": 0.09401709401700951, + "factor": 10.636363636373197, + "label": "ExoJS leads clearly (10.64x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -247,13 +247,13 @@ "runs": 3, "reference": { "minMs": 0.20000000000004547, - "maxMs": 0.23999999999995225, - "ratio": 1.1999999999994884 + "maxMs": 0.2400000000000091, + "ratio": 1.1999999999997726 }, "competitor": { - "minMs": 2.2199999999999704, - "maxMs": 2.509999999999991, - "ratio": 1.1306306306306415 + "minMs": 2.2800000000000864, + "maxMs": 2.4500000000000455, + "ratio": 1.0745614035087512 }, "stable": true, "rungs": [ @@ -265,19 +265,19 @@ }, { "competitor": "phaser", - "referenceMs": 0.20000000000004547, - "referenceP95Ms": 0.42000000000007276, + "referenceMs": 0.2199999999997999, + "referenceP95Ms": 0.40000000000009095, "referenceGpuMs": 16.660000000000082, "referenceOverFrameBudget": false, - "competitorMs": 1.2700000000000955, - "competitorP95Ms": 1.7200000000000273, - "competitorGpuMs": 16.66000000000031, + "competitorMs": 1.2100000000000364, + "competitorP95Ms": 1.759999999999991, + "competitorGpuMs": 16.679999999999836, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.1574803149606539, - "factor": 6.349999999999033, - "label": "ExoJS leads clearly (6.35x)", + "ratio": 0.181818181818011, + "factor": 5.500000000005167, + "label": "ExoJS leads clearly (5.50x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -286,13 +286,13 @@ "runs": 3, "reference": { "minMs": 0.20000000000004547, - "maxMs": 0.23999999999995225, - "ratio": 1.1999999999994884 + "maxMs": 0.2400000000000091, + "ratio": 1.1999999999997726 }, "competitor": { - "minMs": 1.259999999999991, - "maxMs": 1.2799999999999727, - "ratio": 1.0158730158730016 + "minMs": 1.2000000000001592, + "maxMs": 1.3199999999999363, + "ratio": 1.0999999999998011 }, "stable": true, "rungs": [ @@ -304,13 +304,13 @@ }, { "competitor": "pixi", - "referenceMs": 0.20000000000004547, - "referenceP95Ms": 0.42000000000007276, + "referenceMs": 0.2199999999997999, + "referenceP95Ms": 0.40000000000009095, "referenceGpuMs": 16.660000000000082, "referenceOverFrameBudget": false, - "competitorMs": 0.18000000000017735, - "competitorP95Ms": 0.3999999999998636, - "competitorGpuMs": 16.67999999999995, + "competitorMs": 0.1999999999998181, + "competitorP95Ms": 0.4399999999999409, + "competitorGpuMs": 16.660000000000082, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -325,13 +325,13 @@ "runs": 3, "reference": { "minMs": 0.20000000000004547, - "maxMs": 0.23999999999995225, - "ratio": 1.1999999999994884 + "maxMs": 0.2400000000000091, + "ratio": 1.1999999999997726 }, "competitor": { - "minMs": 0.1799999999998363, - "maxMs": 0.1999999999998181, - "ratio": 1.1111111111111112 + "minMs": 0.18000000000006366, + "maxMs": 0.20000000000004547, + "ratio": 1.1111111111109708 }, "stable": false, "rungs": [ @@ -354,18 +354,18 @@ { "competitor": "excalibur", "referenceMs": 0.13999999999987267, - "referenceP95Ms": 0.25999999999976353, + "referenceP95Ms": 0.24000000000023647, "referenceGpuMs": 16.66000000000031, "referenceOverFrameBudget": false, - "competitorMs": 19.84999999999991, - "competitorP95Ms": 24.079999999999927, - "competitorGpuMs": 20.960000000000036, + "competitorMs": 19.739999999999554, + "competitorP95Ms": 24.0600000000004, + "competitorGpuMs": 20.780000000000655, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.007052896725434424, - "factor": 141.7857142858426, - "label": "ExoJS leads clearly (141.79x)", + "ratio": 0.007092198581553993, + "factor": 141.00000000012506, + "label": "ExoJS leads clearly (141.00x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -374,13 +374,13 @@ "runs": 3, "reference": { "minMs": 0.13999999999987267, - "maxMs": 0.17000000000007276, - "ratio": 1.2142857142873384 + "maxMs": 0.16000000000030923, + "ratio": 1.142857142860391 }, "competitor": { - "minMs": 19.420000000000073, - "maxMs": 19.98999999999978, - "ratio": 1.02935118434602 + "minMs": 19.439999999999827, + "maxMs": 20.24000000000001, + "ratio": 1.0411522633744954 }, "stable": true, "rungs": [ @@ -393,12 +393,12 @@ { "competitor": "phaser", "referenceMs": 0.13999999999987267, - "referenceP95Ms": 0.25999999999976353, + "referenceP95Ms": 0.24000000000023647, "referenceGpuMs": 16.66000000000031, "referenceOverFrameBudget": false, "competitorMs": 3.0800000000001546, - "competitorP95Ms": 9.159999999999854, - "competitorGpuMs": 16.659999999999854, + "competitorP95Ms": 7.2400000000002365, + "competitorGpuMs": 16.66000000000031, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", @@ -413,13 +413,13 @@ "runs": 3, "reference": { "minMs": 0.13999999999987267, - "maxMs": 0.17000000000007276, - "ratio": 1.2142857142873384 + "maxMs": 0.16000000000030923, + "ratio": 1.142857142860391 }, "competitor": { - "minMs": 3.0799999999999272, - "maxMs": 3.080000000000382, - "ratio": 1.0000000000001477 + "minMs": 3.0499999999999545, + "maxMs": 3.100000000000364, + "ratio": 1.0163934426230852 }, "stable": true, "rungs": [ @@ -432,18 +432,18 @@ { "competitor": "pixi", "referenceMs": 0.13999999999987267, - "referenceP95Ms": 0.25999999999976353, + "referenceP95Ms": 0.24000000000023647, "referenceGpuMs": 16.66000000000031, "referenceOverFrameBudget": false, "competitorMs": 0.13999999999987267, - "competitorP95Ms": 0.2600000000002183, - "competitorGpuMs": 16.679999999999836, + "competitorP95Ms": 0.27999999999974534, + "competitorGpuMs": 16.66000000000031, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 1, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -452,17 +452,17 @@ "runs": 3, "reference": { "minMs": 0.13999999999987267, - "maxMs": 0.17000000000007276, - "ratio": 1.2142857142873384 + "maxMs": 0.16000000000030923, + "ratio": 1.142857142860391 }, "competitor": { "minMs": 0.13999999999987267, "maxMs": 0.13999999999987267, "ratio": 1 }, - "stable": false, + "stable": true, "rungs": [ - "competitor-leads", + "level", "level", "level" ] @@ -480,19 +480,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.06000000000040018, - "referenceP95Ms": 0.2599999999993088, - "referenceGpuMs": 16.67999999999938, + "referenceMs": 0.07000000000016371, + "referenceP95Ms": 0.2600000000002183, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 0.04999999999972715, - "competitorP95Ms": 0.2600000000002183, - "competitorGpuMs": 16.660000000000764, + "competitorMs": 0.03999999999996362, + "competitorP95Ms": 0.21999999999934516, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 1.7500000000056843, + "factor": 1.7500000000056843, + "label": "competitor leads (1.75x)", "structural": false }, "mechanism": "competitor issues fewer draw calls (25 vs 1 per frame)", @@ -500,32 +500,32 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.05999999999949068, - "maxMs": 0.06000000000040018, - "ratio": 1.0000000000151583 + "minMs": 0.06000000000040018, + "maxMs": 0.07999999999992724, + "ratio": 1.3333333333232278 }, "competitor": { - "minMs": 0.03999999999996362, - "maxMs": 0.06999999999970896, - "ratio": 1.7499999999943157 + "minMs": 0.020000000000436557, + "maxMs": 0.03999999999996362, + "ratio": 1.9999999999545253 }, - "stable": false, + "stable": true, "rungs": [ "competitor-leads", "competitor-leads", - "level" + "competitor-leads" ] } }, { "competitor": "phaser", - "referenceMs": 0.06000000000040018, - "referenceP95Ms": 0.2599999999993088, - "referenceGpuMs": 16.67999999999938, + "referenceMs": 0.07500000000004547, + "referenceP95Ms": 0.24000000000023647, + "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 16.88000000000011, - "competitorP95Ms": 21.82000000000062, - "competitorGpuMs": 18.05999999999949, + "competitorMs": 17.670000000000073, + "competitorP95Ms": 22.52999999999929, + "competitorGpuMs": 18.840000000000146, "competitorOverFrameBudget": true, "verdict": { "side": "neither", @@ -537,19 +537,20 @@ "mechanism": "competitor issues fewer draw calls (25 vs 7 per frame)", "timer": "limited", "aggregate": { - "runs": 1, + "runs": 2, "reference": { - "minMs": 0.06000000000040018, - "maxMs": 0.06000000000040018, - "ratio": 1 + "minMs": 0.07000000000016371, + "maxMs": 0.07999999999992724, + "ratio": 1.1428571428534307 }, "competitor": { - "minMs": 16.88000000000011, - "maxMs": 16.88000000000011, - "ratio": 1 + "minMs": 17.3100000000004, + "maxMs": 18.029999999999745, + "ratio": 1.0415944540727515 }, "stable": false, "rungs": [ + "exojs-leads-clearly", "exojs-leads-clearly" ] } @@ -566,19 +567,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 1.7200000000002547, - "referenceP95Ms": 2.480000000000473, - "referenceGpuMs": 16.659999999999854, + "referenceMs": 1.7500000000004547, + "referenceP95Ms": 2.4399999999996, + "referenceGpuMs": 16.660000000000764, "referenceOverFrameBudget": false, - "competitorMs": 26.140000000001237, - "competitorP95Ms": 35.37999999999738, - "competitorGpuMs": 27.260000000002037, + "competitorMs": 25.170000000000073, + "competitorP95Ms": 29.780000000002474, + "competitorGpuMs": 26.7400000000016, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.06579954093344198, - "factor": 15.19767441860312, - "label": "ExoJS leads clearly (15.20x)", + "ratio": 0.06952721493843662, + "factor": 14.382857142853446, + "label": "ExoJS leads clearly (14.38x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -586,14 +587,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.5399999999999636, - "maxMs": 1.980000000000473, - "ratio": 1.285714285714623 + "minMs": 1.6399999999998727, + "maxMs": 1.819999999999709, + "ratio": 1.1097560975608842 }, "competitor": { - "minMs": 25.790000000000873, - "maxMs": 26.600000000002183, - "ratio": 1.031407522295513 + "minMs": 25.090000000000146, + "maxMs": 25.48999999999978, + "ratio": 1.0159426066161672 }, "stable": true, "rungs": [ @@ -605,19 +606,19 @@ }, { "competitor": "phaser", - "referenceMs": 1.7200000000002547, - "referenceP95Ms": 2.480000000000473, - "referenceGpuMs": 16.659999999999854, + "referenceMs": 1.7500000000004547, + "referenceP95Ms": 2.4399999999996, + "referenceGpuMs": 16.660000000000764, "referenceOverFrameBudget": false, - "competitorMs": 2.9900000000016007, - "competitorP95Ms": 7.960000000000036, - "competitorGpuMs": 16.660000000000082, + "competitorMs": 3.25, + "competitorP95Ms": 8.420000000000073, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5752508361201786, - "factor": 1.738372093023929, - "label": "ExoJS leads (1.74x)", + "ratio": 0.5384615384616784, + "factor": 1.8571428571423745, + "label": "ExoJS leads (1.86x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -625,14 +626,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.5399999999999636, - "maxMs": 1.980000000000473, - "ratio": 1.285714285714623 + "minMs": 1.6399999999998727, + "maxMs": 1.819999999999709, + "ratio": 1.1097560975608842 }, "competitor": { - "minMs": 2.990000000000009, - "maxMs": 3.259999999999991, - "ratio": 1.0903010033444753 + "minMs": 3.110000000000582, + "maxMs": 3.269999999999982, + "ratio": 1.051446945337418 }, "stable": true, "rungs": [ @@ -644,19 +645,19 @@ }, { "competitor": "pixi", - "referenceMs": 1.7200000000002547, - "referenceP95Ms": 2.480000000000473, - "referenceGpuMs": 16.659999999999854, + "referenceMs": 1.7500000000004547, + "referenceP95Ms": 2.4399999999996, + "referenceGpuMs": 16.660000000000764, "referenceOverFrameBudget": false, "competitorMs": 1.319999999999709, - "competitorP95Ms": 1.9200000000000728, - "competitorGpuMs": 16.659999999999854, + "competitorP95Ms": 1.8800000000001091, + "competitorGpuMs": 16.67999999999938, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.3030303030307833, - "factor": 1.3030303030307833, - "label": "competitor leads (1.30x)", + "ratio": 1.3257575757582125, + "factor": 1.3257575757582125, + "label": "competitor leads (1.33x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -664,14 +665,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.5399999999999636, - "maxMs": 1.980000000000473, - "ratio": 1.285714285714623 + "minMs": 1.6399999999998727, + "maxMs": 1.819999999999709, + "ratio": 1.1097560975608842 }, "competitor": { - "minMs": 1.2599999999997635, - "maxMs": 1.319999999999709, - "ratio": 1.0476190476190133 + "minMs": 1.2300000000000182, + "maxMs": 1.3399999999996908, + "ratio": 1.0894308943086757 }, "stable": true, "rungs": [ @@ -693,19 +694,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 1.6599999999998545, - "referenceP95Ms": 2.2200000000002547, - "referenceGpuMs": 16.68000000000029, + "referenceMs": 1.550000000000182, + "referenceP95Ms": 2.160000000000764, + "referenceGpuMs": 16.67999999999938, "referenceOverFrameBudget": false, - "competitorMs": 3.4200000000000728, - "competitorP95Ms": 6.600000000002183, + "competitorMs": 3.3299999999999272, + "competitorP95Ms": 8.5, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.48538011695901145, - "factor": 2.0602409638556463, - "label": "ExoJS leads (2.06x)", + "ratio": 0.46546546546553025, + "factor": 2.1483870967738947, + "label": "ExoJS leads (2.15x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -713,14 +714,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.5600000000004002, - "maxMs": 1.7600000000002183, - "ratio": 1.1282051282049788 + "minMs": 1.480000000000473, + "maxMs": 1.8599999999996726, + "ratio": 1.256756756756134 }, "competitor": { - "minMs": 3.390000000001237, - "maxMs": 3.4899999999997817, - "ratio": 1.0294985250733062 + "minMs": 3.2599999999983993, + "maxMs": 3.5, + "ratio": 1.0736196319023676 }, "stable": true, "rungs": [ @@ -732,19 +733,19 @@ }, { "competitor": "phaser", - "referenceMs": 1.6599999999998545, - "referenceP95Ms": 2.2200000000002547, - "referenceGpuMs": 16.68000000000029, + "referenceMs": 1.550000000000182, + "referenceP95Ms": 2.160000000000764, + "referenceGpuMs": 16.67999999999938, "referenceOverFrameBudget": false, - "competitorMs": 1.2100000000000364, - "competitorP95Ms": 1.6599999999998545, - "competitorGpuMs": 16.679999999999836, + "competitorMs": 1.0200000000040745, + "competitorP95Ms": 1.4000000000087311, + "competitorGpuMs": 16.66000000000031, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.3719008264461194, - "factor": 1.3719008264461194, - "label": "competitor leads (1.37x)", + "ratio": 1.5196078431313629, + "factor": 1.5196078431313629, + "label": "competitor leads (1.52x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -752,14 +753,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.5600000000004002, - "maxMs": 1.7600000000002183, - "ratio": 1.1282051282049788 + "minMs": 1.480000000000473, + "maxMs": 1.8599999999996726, + "ratio": 1.256756756756134 }, "competitor": { - "minMs": 1.0399999999972351, - "maxMs": 1.240000000000009, - "ratio": 1.1923076923108709 + "minMs": 0.9900000000052387, + "maxMs": 1.2399999999997817, + "ratio": 1.252525252518404 }, "stable": true, "rungs": [ @@ -771,19 +772,19 @@ }, { "competitor": "pixi", - "referenceMs": 1.6599999999998545, - "referenceP95Ms": 2.2200000000002547, - "referenceGpuMs": 16.68000000000029, + "referenceMs": 1.550000000000182, + "referenceP95Ms": 2.160000000000764, + "referenceGpuMs": 16.67999999999938, "referenceOverFrameBudget": false, - "competitorMs": 0.9800000000000182, - "competitorP95Ms": 1.339999999999236, - "competitorGpuMs": 16.660000000000764, + "competitorMs": 1, + "competitorP95Ms": 1.4400000000005093, + "competitorGpuMs": 16.67999999999938, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.6938775510202282, - "factor": 1.6938775510202282, - "label": "competitor leads (1.69x)", + "ratio": 1.550000000000182, + "factor": 1.550000000000182, + "label": "competitor leads (1.55x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -791,14 +792,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.5600000000004002, - "maxMs": 1.7600000000002183, - "ratio": 1.1282051282049788 + "minMs": 1.480000000000473, + "maxMs": 1.8599999999996726, + "ratio": 1.256756756756134 }, "competitor": { - "minMs": 0.9699999999997999, - "maxMs": 0.9900000000002365, - "ratio": 1.0206185567014852 + "minMs": 0.9600000000000364, + "maxMs": 1.0199999999999818, + "ratio": 1.0624999999999407 }, "stable": true, "rungs": [ @@ -820,19 +821,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 4.059999999999491, - "referenceP95Ms": 5.199999999998909, + "referenceMs": 3.8900000000003274, + "referenceP95Ms": 5.420000000000073, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 31.779999999998836, - "competitorP95Ms": 35.45999999999913, - "competitorGpuMs": 32.60000000000218, + "competitorMs": 30.55000000000109, + "competitorP95Ms": 35.55999999999767, + "competitorGpuMs": 31.81999999999971, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.12775330396474635, - "factor": 7.827586206897248, - "label": "ExoJS leads clearly (7.83x)", + "ratio": 0.12733224222586542, + "factor": 7.853470437017614, + "label": "ExoJS leads clearly (7.85x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -840,14 +841,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.019999999998618, - "maxMs": 4.299999999999272, - "ratio": 1.0696517412937192 + "minMs": 3.8800000000010186, + "maxMs": 4.039999999999054, + "ratio": 1.0412371134015448 }, "competitor": { - "minMs": 31.55999999999949, - "maxMs": 32.25, - "ratio": 1.021863117870739 + "minMs": 29.919999999998254, + "maxMs": 32.72999999999956, + "ratio": 1.0939171122995146 }, "stable": true, "rungs": [ @@ -859,19 +860,19 @@ }, { "competitor": "phaser", - "referenceMs": 4.059999999999491, - "referenceP95Ms": 5.199999999998909, + "referenceMs": 3.8900000000003274, + "referenceP95Ms": 5.420000000000073, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 2.930000000000291, - "competitorP95Ms": 7.6599999999998545, - "competitorGpuMs": 16.660000000000764, + "competitorMs": 3.0800000000017462, + "competitorP95Ms": 7.299999999999272, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.3856655290099273, - "factor": 1.3856655290099273, - "label": "competitor leads (1.39x)", + "ratio": 1.2629870129864031, + "factor": 1.2629870129864031, + "label": "competitor leads (1.26x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -879,14 +880,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.019999999998618, - "maxMs": 4.299999999999272, - "ratio": 1.0696517412937192 + "minMs": 3.8800000000010186, + "maxMs": 4.039999999999054, + "ratio": 1.0412371134015448 }, "competitor": { - "minMs": 2.8400000000001455, - "maxMs": 2.9600000000000364, - "ratio": 1.0422535211267199 + "minMs": 2.869999999999891, + "maxMs": 3.2399999999979627, + "ratio": 1.1289198606265107 }, "stable": true, "rungs": [ @@ -898,19 +899,19 @@ }, { "competitor": "pixi", - "referenceMs": 4.059999999999491, - "referenceP95Ms": 5.199999999998909, + "referenceMs": 3.8900000000003274, + "referenceP95Ms": 5.420000000000073, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 2.6700000000000728, - "competitorP95Ms": 11.5, + "competitorMs": 2.6599999999998545, + "competitorP95Ms": 8.959999999999127, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.5205992509360975, - "factor": 1.5205992509360975, - "label": "competitor leads (1.52x)", + "ratio": 1.462406015037797, + "factor": 1.462406015037797, + "label": "competitor leads (1.46x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -918,14 +919,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.019999999998618, - "maxMs": 4.299999999999272, - "ratio": 1.0696517412937192 + "minMs": 3.8800000000010186, + "maxMs": 4.039999999999054, + "ratio": 1.0412371134015448 }, "competitor": { - "minMs": 2.5799999999999272, - "maxMs": 2.839999999999236, - "ratio": 1.1007751937981844 + "minMs": 2.6500000000005457, + "maxMs": 2.6599999999998545, + "ratio": 1.0037735849053988 }, "stable": true, "rungs": [ @@ -945,58 +946,21 @@ "unit": "sprites", "primary": false, "cells": [ - { - "competitor": "phaser", - "referenceMs": 37.57000000000062, - "referenceP95Ms": 43.31999999999971, - "referenceGpuMs": 38.70000000000073, - "referenceOverFrameBudget": true, - "competitorMs": 20.53999999999951, - "competitorP95Ms": 25.23999999999978, - "competitorGpuMs": 21.300000000000182, - "competitorOverFrameBudget": true, - "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", - "structural": false - }, - "mechanism": "competitor issues fewer draw calls (25 vs 7 per frame)", - "timer": "resolved", - "aggregate": { - "runs": 1, - "reference": { - "minMs": 37.57000000000062, - "maxMs": 37.57000000000062, - "ratio": 1 - }, - "competitor": { - "minMs": 20.53999999999951, - "maxMs": 20.53999999999951, - "ratio": 1 - }, - "stable": false, - "rungs": [ - "competitor-leads" - ] - } - }, { "competitor": "pixi", - "referenceMs": 38.100000000000364, - "referenceP95Ms": 44.18000000000029, - "referenceGpuMs": 40.23999999999978, + "referenceMs": 39.42000000000098, + "referenceP95Ms": 41.82000000000153, + "referenceGpuMs": 41.68000000000029, "referenceOverFrameBudget": true, - "competitorMs": 29.01000000000022, - "competitorP95Ms": 32.13999999999942, - "competitorGpuMs": 30.200000000000728, + "competitorMs": 28.31000000000131, + "competitorP95Ms": 31.780000000000655, + "competitorGpuMs": 29.479999999999563, "competitorOverFrameBudget": true, "verdict": { "side": "competitor", - "ratio": 1.3133402275077586, - "factor": 1.3133402275077586, - "label": "competitor leads (1.31x)", + "ratio": 1.3924408336276637, + "factor": 1.3924408336276637, + "label": "competitor leads (1.39x)", "structural": false }, "mechanism": "competitor issues fewer draw calls (25 vs 1 per frame)", @@ -1004,14 +968,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 37.57000000000062, - "maxMs": 39.88999999999942, - "ratio": 1.061751397391503 + "minMs": 37.280000000000655, + "maxMs": 40.44999999999982, + "ratio": 1.0850321888411778 }, "competitor": { - "minMs": 28.31999999999971, - "maxMs": 29.159999999999854, - "ratio": 1.029661016949158 + "minMs": 27.109999999999673, + "maxMs": 28.360000000000582, + "ratio": 1.0461084470675368 }, "stable": true, "rungs": [ @@ -1034,18 +998,18 @@ { "competitor": "excalibur", "referenceMs": 0.5599999999994907, - "referenceP95Ms": 0.7800000000006548, - "referenceGpuMs": 16.659999999999854, + "referenceP95Ms": 0.7799999999988358, + "referenceGpuMs": 16.679999999998472, "referenceOverFrameBudget": false, - "competitorMs": 28.299999999999272, - "competitorP95Ms": 33.599999999998545, - "competitorGpuMs": 29.659999999996217, + "competitorMs": 28.029999999998836, + "competitorP95Ms": 32.81999999999971, + "competitorGpuMs": 29.31999999999971, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.019787985865706893, - "factor": 50.53571428575895, - "label": "ExoJS leads clearly (50.54x)", + "ratio": 0.019978594363164966, + "factor": 50.053571428614866, + "label": "ExoJS leads clearly (50.05x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1053,14 +1017,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5500000000001819, - "maxMs": 0.6199999999989814, - "ratio": 1.1272727272705023 + "minMs": 0.5599999999994907, + "maxMs": 0.5600000000013097, + "ratio": 1.0000000000032483 }, "competitor": { - "minMs": 27.539999999997235, - "maxMs": 28.80999999999767, - "ratio": 1.046114742193194 + "minMs": 27.57999999999811, + "maxMs": 33.38999999999942, + "ratio": 1.2106598984772192 }, "stable": true, "rungs": [ @@ -1073,18 +1037,18 @@ { "competitor": "phaser", "referenceMs": 0.5599999999994907, - "referenceP95Ms": 0.7800000000006548, - "referenceGpuMs": 16.659999999999854, + "referenceP95Ms": 0.7799999999988358, + "referenceGpuMs": 16.679999999998472, "referenceOverFrameBudget": false, - "competitorMs": 3.0600000000000023, - "competitorP95Ms": 10.060000000012224, + "competitorMs": 3.0299999999999727, + "competitorP95Ms": 9.700000000000045, "competitorGpuMs": 16.660000000000082, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.18300653594754585, - "factor": 5.464285714290688, - "label": "ExoJS leads clearly (5.46x)", + "ratio": 0.18481848184801838, + "factor": 5.410714285719158, + "label": "ExoJS leads clearly (5.41x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1092,14 +1056,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5500000000001819, - "maxMs": 0.6199999999989814, - "ratio": 1.1272727272705023 + "minMs": 0.5599999999994907, + "maxMs": 0.5600000000013097, + "ratio": 1.0000000000032483 }, "competitor": { - "minMs": 3.0599999999999454, - "maxMs": 3.109999999993306, - "ratio": 1.0163398692788763 + "minMs": 2.8400000000000887, + "maxMs": 3.150000000000034, + "ratio": 1.109154929577442 }, "stable": true, "rungs": [ @@ -1112,16 +1076,16 @@ { "competitor": "pixi", "referenceMs": 0.5599999999994907, - "referenceP95Ms": 0.7800000000006548, - "referenceGpuMs": 16.659999999999854, + "referenceP95Ms": 0.7799999999988358, + "referenceGpuMs": 16.679999999998472, "referenceOverFrameBudget": false, - "competitorMs": 0.5599999999994907, - "competitorP95Ms": 0.8000000000010914, - "competitorGpuMs": 16.659999999999854, + "competitorMs": 0.5799999999999272, + "competitorP95Ms": 0.819999999999709, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1, + "ratio": 0.9655172413785533, "factor": 1, "label": "level", "structural": false @@ -1131,14 +1095,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5500000000001819, - "maxMs": 0.6199999999989814, - "ratio": 1.1272727272705023 + "minMs": 0.5599999999994907, + "maxMs": 0.5600000000013097, + "ratio": 1.0000000000032483 }, "competitor": { - "minMs": 0.5400000000008731, - "maxMs": 0.5799999999999272, - "ratio": 1.0740740740722028 + "minMs": 0.5600000000004002, + "maxMs": 0.589999999999236, + "ratio": 1.0535714285693114 }, "stable": true, "rungs": [ @@ -1160,19 +1124,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 2.889999999999418, - "referenceP95Ms": 6.240000000001601, + "referenceMs": 2.4899999999997817, + "referenceP95Ms": 6.360000000000582, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 25.400000000001455, - "competitorP95Ms": 31.079999999987194, - "competitorGpuMs": 26.84000000001106, + "competitorMs": 24.619999999995343, + "competitorP95Ms": 28.220000000001164, + "competitorGpuMs": 25.860000000000582, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.11377952755902568, - "factor": 8.788927335642413, - "label": "ExoJS leads clearly (8.79x)", + "ratio": 0.101137286758743, + "factor": 9.88755020080221, + "label": "ExoJS leads clearly (9.89x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1180,14 +1144,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.8400000000001455, - "maxMs": 2.9500000000025466, - "ratio": 1.0387323943670406 + "minMs": 2.4899999999997817, + "maxMs": 2.6199999999989814, + "ratio": 1.0522088353410486 }, "competitor": { - "minMs": 25.360000000000582, - "maxMs": 25.44000000000233, - "ratio": 1.003154574132561 + "minMs": 23.270000000004075, + "maxMs": 25.799999999995634, + "ratio": 1.108723678555699 }, "stable": true, "rungs": [ @@ -1199,19 +1163,19 @@ }, { "competitor": "phaser", - "referenceMs": 2.889999999999418, - "referenceP95Ms": 6.240000000001601, + "referenceMs": 2.4899999999997817, + "referenceP95Ms": 6.360000000000582, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 10.860000000000582, - "competitorP95Ms": 11.380000000001019, + "competitorMs": 10.659999999999854, + "competitorP95Ms": 11.260000000002037, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.2661141804787535, - "factor": 3.757785467128986, - "label": "ExoJS leads (3.76x)", + "ratio": 0.23358348968103337, + "factor": 4.281124497992285, + "label": "ExoJS leads (4.28x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1219,14 +1183,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.8400000000001455, - "maxMs": 2.9500000000025466, - "ratio": 1.0387323943670406 + "minMs": 2.4899999999997817, + "maxMs": 2.6199999999989814, + "ratio": 1.0522088353410486 }, "competitor": { - "minMs": 10.590000000000146, - "maxMs": 10.880000000001019, - "ratio": 1.0273843248348318 + "minMs": 10.579999999999927, + "maxMs": 10.799999999999272, + "ratio": 1.0207939508505999 }, "stable": true, "rungs": [ @@ -1238,19 +1202,19 @@ }, { "competitor": "pixi", - "referenceMs": 2.889999999999418, - "referenceP95Ms": 6.240000000001601, + "referenceMs": 2.4899999999997817, + "referenceP95Ms": 6.360000000000582, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 2.5100000000002183, - "competitorP95Ms": 7.580000000001746, + "competitorMs": 2.4899999999997817, + "competitorP95Ms": 6.979999999999563, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 1, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/4 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -1258,20 +1222,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.8400000000001455, - "maxMs": 2.9500000000025466, - "ratio": 1.0387323943670406 + "minMs": 2.4899999999997817, + "maxMs": 2.6199999999989814, + "ratio": 1.0522088353410486 }, "competitor": { - "minMs": 2.3699999999989814, - "maxMs": 2.6199999999989814, - "ratio": 1.105485232067556 + "minMs": 2.4799999999995634, + "maxMs": 2.5599999999994907, + "ratio": 1.0322580645161055 }, - "stable": false, + "stable": true, "rungs": [ "level", "level", - "competitor-leads" + "level" ] } } @@ -1293,18 +1257,18 @@ { "competitor": "excalibur", "referenceMs": 0.07999999999992724, - "referenceP95Ms": 0.13999999999941792, + "referenceP95Ms": 0.12000000000080036, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 0.6200000000026193, + "competitorMs": 0.6600000000034925, "competitorP95Ms": 0.8799999999973807, - "competitorGpuMs": 16.660000000003492, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.12903225806385366, - "factor": 7.7500000000397895, - "label": "ExoJS leads clearly (7.75x)", + "ratio": 0.12121212121136957, + "factor": 8.250000000051159, + "label": "ExoJS leads clearly (8.25x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1317,9 +1281,9 @@ "ratio": 1 }, "competitor": { - "minMs": 0.6199999999989814, - "maxMs": 0.6599999999998545, - "ratio": 1.0645161290337724 + "minMs": 0.6399999999994179, + "maxMs": 0.680000000000291, + "ratio": 1.062500000001421 }, "stable": true, "rungs": [ @@ -1332,11 +1296,11 @@ { "competitor": "phaser", "referenceMs": 0.07999999999992724, - "referenceP95Ms": 0.13999999999941792, + "referenceP95Ms": 0.12000000000080036, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, "competitorMs": 0.2600000000002183, - "competitorP95Ms": 0.4600000000000364, + "competitorP95Ms": 0.44000000000005457, "competitorGpuMs": 16.679999999999836, "competitorOverFrameBudget": false, "verdict": { @@ -1356,9 +1320,9 @@ "ratio": 1 }, "competitor": { - "minMs": 0.20000000001164153, - "maxMs": 0.27999999999974534, - "ratio": 1.399999999917236 + "minMs": 0.2600000000002183, + "maxMs": 0.2999999999999545, + "ratio": 1.1538461538450102 }, "stable": true, "rungs": [ @@ -1371,18 +1335,18 @@ { "competitor": "pixi", "referenceMs": 0.07999999999992724, - "referenceP95Ms": 0.13999999999941792, + "referenceP95Ms": 0.12000000000080036, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, "competitorMs": 0.1000000000003638, - "competitorP95Ms": 0.21999999999934516, + "competitorP95Ms": 0.2000000000007276, "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.7999999999963621, - "factor": 1.2500000000056843, - "label": "ExoJS leads (1.25x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -1395,15 +1359,15 @@ "ratio": 1 }, "competitor": { - "minMs": 0.1000000000003638, + "minMs": 0.09999999999854481, "maxMs": 0.1000000000003638, - "ratio": 1 + "ratio": 1.00000000001819 }, - "stable": true, + "stable": false, "rungs": [ "exojs-leads", "exojs-leads", - "exojs-leads" + "level" ] } } @@ -1420,18 +1384,18 @@ { "competitor": "excalibur", "referenceMs": 1.4000000000014552, - "referenceP95Ms": 1.819999999999709, + "referenceP95Ms": 1.7400000000016007, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 16.159999999999854, - "competitorP95Ms": 17.919999999998254, - "competitorGpuMs": 26.89999999999418, + "competitorMs": 16.60000000000582, + "competitorP95Ms": 19.18000000000029, + "competitorGpuMs": 27.080000000001746, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.08663366336642746, - "factor": 11.542857142845042, - "label": "ExoJS leads clearly (11.54x)", + "ratio": 0.08433734939764845, + "factor": 11.85714285713469, + "label": "ExoJS leads clearly (11.86x)", "structural": true }, "mechanism": "ExoJS issues fewer buffer uploads (0 vs 313 per frame)", @@ -1439,14 +1403,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.3799999999991996, - "maxMs": 1.4099999999998545, - "ratio": 1.0217391304352696 + "minMs": 1.3700000000008004, + "maxMs": 1.4200000000000728, + "ratio": 1.0364963503644111 }, "competitor": { - "minMs": 15.869999999998981, - "maxMs": 19.55000000000291, - "ratio": 1.2318840579712769 + "minMs": 15.959999999999127, + "maxMs": 19.349999999998545, + "ratio": 1.212406015037569 }, "stable": true, "rungs": [ @@ -1459,18 +1423,18 @@ { "competitor": "phaser", "referenceMs": 1.4000000000014552, - "referenceP95Ms": 1.819999999999709, + "referenceP95Ms": 1.7400000000016007, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 2.8899999999998727, - "competitorP95Ms": 6.819999999999709, + "competitorMs": 3.019999999999527, + "competitorP95Ms": 4.5600000000004, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.48442906574446953, - "factor": 2.0642857142834776, - "label": "ExoJS leads (2.06x)", + "ratio": 0.4635761589409518, + "factor": 2.1571428571402773, + "label": "ExoJS leads (2.16x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 313/5000/0 vs 313/5000/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1478,14 +1442,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.3799999999991996, - "maxMs": 1.4099999999998545, - "ratio": 1.0217391304352696 + "minMs": 1.3700000000008004, + "maxMs": 1.4200000000000728, + "ratio": 1.0364963503644111 }, "competitor": { - "minMs": 2.7800000000002, - "maxMs": 3, - "ratio": 1.0791366906474043 + "minMs": 2.7799999999997453, + "maxMs": 3.0599999999994907, + "ratio": 1.1007194244603493 }, "stable": true, "rungs": [ @@ -1498,18 +1462,18 @@ { "competitor": "pixi", "referenceMs": 1.4000000000014552, - "referenceP95Ms": 1.819999999999709, + "referenceP95Ms": 1.7400000000016007, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 1.180000000000291, - "competitorP95Ms": 2, + "competitorMs": 1.2599999999983993, + "competitorP95Ms": 2.0599999999994907, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 1.1111111111136776, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 313/5000/0 vs 313/5000/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -1517,20 +1481,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.3799999999991996, - "maxMs": 1.4099999999998545, - "ratio": 1.0217391304352696 + "minMs": 1.3700000000008004, + "maxMs": 1.4200000000000728, + "ratio": 1.0364963503644111 }, "competitor": { - "minMs": 1.139999999999418, - "maxMs": 1.4799999999995634, - "ratio": 1.2982456140353678 + "minMs": 1.1499999999996362, + "maxMs": 1.2799999999997453, + "ratio": 1.1130434782610001 }, - "stable": false, + "stable": true, "rungs": [ "level", "level", - "competitor-leads" + "level" ] } } @@ -1546,19 +1510,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.4599999999991269, - "referenceP95Ms": 0.7399999999979627, + "referenceMs": 0.47000000000116415, + "referenceP95Ms": 0.7200000000011642, "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 9.779999999995198, - "competitorP95Ms": 12.940000000002328, + "competitorMs": 9.659999999999854, + "competitorP95Ms": 13.940000000002328, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.04703476482610969, - "factor": 21.260869565247308, - "label": "ExoJS leads clearly (21.26x)", + "ratio": 0.04865424430653947, + "factor": 20.553191489310482, + "label": "ExoJS leads clearly (20.55x)", "structural": true }, "mechanism": "competitor issues fewer draw calls (120 vs 1 per frame)", @@ -1566,14 +1530,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.38000000000101863, - "maxMs": 0.5400000000008731, - "ratio": 1.4210526315774357 + "minMs": 0.4599999999991269, + "maxMs": 0.4900000000016007, + "ratio": 1.0652173913098495 }, "competitor": { - "minMs": 9.590000000000146, - "maxMs": 9.909999999996217, - "ratio": 1.0333680917618422 + "minMs": 9.600000000002183, + "maxMs": 9.860000000000582, + "ratio": 1.0270833333331604 }, "stable": true, "rungs": [ @@ -1585,58 +1549,58 @@ }, { "competitor": "phaser", - "referenceMs": 0.4599999999991269, - "referenceP95Ms": 0.7399999999979627, + "referenceMs": 0.47000000000116415, + "referenceP95Ms": 0.7200000000011642, "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 2.6699999999982538, - "competitorP95Ms": 4.859999999999673, - "competitorGpuMs": 16.660000000003492, + "competitorMs": 2.5399999999999636, + "competitorP95Ms": 4.579999999999927, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", - "structural": false + "side": "exojs", + "ratio": 0.18503937007920113, + "factor": 5.404255319135473, + "label": "ExoJS leads clearly (5.40x)", + "structural": true }, "mechanism": "competitor issues fewer draw calls (120 vs 1 per frame)", "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.38000000000101863, - "maxMs": 0.5400000000008731, - "ratio": 1.4210526315774357 + "minMs": 0.4599999999991269, + "maxMs": 0.4900000000016007, + "ratio": 1.0652173913098495 }, "competitor": { - "minMs": 2.6299999999996544, - "maxMs": 2.6700000000000728, - "ratio": 1.0152091254754463 + "minMs": 2.5399999999999636, + "maxMs": 2.6400000000003274, + "ratio": 1.0393700787403013 }, - "stable": false, + "stable": true, "rungs": [ "exojs-leads-clearly", "exojs-leads-clearly", - "exojs-leads" + "exojs-leads-clearly" ] } }, { "competitor": "pixi", - "referenceMs": 0.4599999999991269, - "referenceP95Ms": 0.7399999999979627, + "referenceMs": 0.47000000000116415, + "referenceP95Ms": 0.7200000000011642, "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, "competitorMs": 0.31999999999970896, - "competitorP95Ms": 0.4400000000023283, - "competitorGpuMs": 16.679999999996653, + "competitorP95Ms": 0.4399999999986903, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 1.4687500000049738, + "factor": 1.4687500000049738, + "label": "competitor leads (1.47x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 120/0/0 vs 158/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -1644,19 +1608,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.38000000000101863, - "maxMs": 0.5400000000008731, - "ratio": 1.4210526315774357 + "minMs": 0.4599999999991269, + "maxMs": 0.4900000000016007, + "ratio": 1.0652173913098495 }, "competitor": { - "minMs": 0.3000000000029104, + "minMs": 0.2999999999992724, "maxMs": 0.31999999999970896, - "ratio": 1.0666666666553486 + "ratio": 1.0666666666682836 }, - "stable": false, + "stable": true, "rungs": [ "competitor-leads", - "level", + "competitor-leads", "competitor-leads" ] } @@ -1680,17 +1644,17 @@ "competitor": "excalibur", "referenceMs": 0.08000000000174623, "referenceP95Ms": 0.12000000000261934, - "referenceGpuMs": 16.679999999996653, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 9.659999999999854, - "competitorP95Ms": 14.37999999999738, + "competitorMs": 10.299999999999272, + "competitorP95Ms": 14.340000000003783, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.008281573499145697, - "factor": 120.74999999736247, - "label": "ExoJS leads clearly (120.75x)", + "ratio": 0.007766990291432222, + "factor": 128.74999999718057, + "label": "ExoJS leads clearly (128.75x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1698,14 +1662,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.08000000000174623, + "minMs": 0.07999999999810825, "maxMs": 0.08000000000174623, - "ratio": 1 + "ratio": 1.0000000000454747 }, "competitor": { - "minMs": 9.650000000001455, - "maxMs": 10.049999999999272, - "ratio": 1.04145077720184 + "minMs": 9.930000000003929, + "maxMs": 10.350000000002183, + "ratio": 1.0422960725073602 }, "stable": true, "rungs": [ @@ -1719,17 +1683,17 @@ "competitor": "phaser", "referenceMs": 0.08000000000174623, "referenceP95Ms": 0.12000000000261934, - "referenceGpuMs": 16.679999999996653, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 2.600000000000364, - "competitorP95Ms": 4.059999999997672, - "competitorGpuMs": 16.68000000000029, + "competitorMs": 2.7599999999997635, + "competitorP95Ms": 4.899999999999636, + "competitorGpuMs": 16.679999999998472, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.03076923076989809, - "factor": 32.49999999929514, - "label": "ExoJS leads clearly (32.50x)", + "ratio": 0.028985507247011988, + "factor": 34.49999999924398, + "label": "ExoJS leads clearly (34.50x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1737,14 +1701,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.08000000000174623, + "minMs": 0.07999999999810825, "maxMs": 0.08000000000174623, - "ratio": 1 + "ratio": 1.0000000000454747 }, "competitor": { - "minMs": 2.4399999999996, - "maxMs": 2.889999999992142, - "ratio": 1.1844262295051704 + "minMs": 2.5799999999999272, + "maxMs": 2.889999999999418, + "ratio": 1.1201550387594958 }, "stable": true, "rungs": [ @@ -1758,17 +1722,17 @@ "competitor": "pixi", "referenceMs": 0.08000000000174623, "referenceP95Ms": 0.12000000000261934, - "referenceGpuMs": 16.679999999996653, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, "competitorMs": 0.09999999999854481, - "competitorP95Ms": 0.18000000000029104, - "competitorGpuMs": 16.68000000000029, + "competitorP95Ms": 0.19999999999708962, + "competitorGpuMs": 16.679999999996653, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.8000000000291039, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -1776,18 +1740,18 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.08000000000174623, + "minMs": 0.07999999999810825, "maxMs": 0.08000000000174623, - "ratio": 1 + "ratio": 1.0000000000454747 }, "competitor": { "minMs": 0.08000000000174623, "maxMs": 0.09999999999854481, "ratio": 1.2499999999545253 }, - "stable": true, + "stable": false, "rungs": [ - "level", + "exojs-leads", "level", "level" ] @@ -1810,19 +1774,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.10000000000218279, - "referenceP95Ms": 0.22000000000116415, - "referenceGpuMs": 16.659999999999854, + "referenceMs": 0.09999999999854481, + "referenceP95Ms": 0.2000000000007276, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 6.619999999995343, - "competitorP95Ms": 9.440000000002328, + "competitorMs": 6.540000000004511, + "competitorP95Ms": 8.700000000004366, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.015105740181609234, - "factor": 66.19999999850843, - "label": "ExoJS leads clearly (66.20x)", + "ratio": 0.015290519877442788, + "factor": 65.40000000099681, + "label": "ExoJS leads clearly (65.40x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1835,9 +1799,9 @@ "ratio": 1.0000000000363798 }, "competitor": { - "minMs": 6.580000000001746, - "maxMs": 6.700000000000728, - "ratio": 1.0182370820667097 + "minMs": 6.389999999999418, + "maxMs": 6.809999999997672, + "ratio": 1.0657276995302492 }, "stable": true, "rungs": [ @@ -1849,19 +1813,19 @@ }, { "competitor": "phaser", - "referenceMs": 0.10000000000218279, - "referenceP95Ms": 0.22000000000116415, - "referenceGpuMs": 16.659999999999854, + "referenceMs": 0.09999999999854481, + "referenceP95Ms": 0.2000000000007276, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 2.970000000001164, - "competitorP95Ms": 4.220000000001164, - "competitorGpuMs": 16.660000000003492, + "competitorMs": 2.899999999999636, + "competitorP95Ms": 4.040000000000873, + "competitorGpuMs": 16.660000000001673, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.03367003367075542, - "factor": 29.699999999363353, - "label": "ExoJS leads clearly (29.70x)", + "ratio": 0.03448275862019219, + "factor": 29.000000000418368, + "label": "ExoJS leads clearly (29.00x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -1874,9 +1838,9 @@ "ratio": 1.0000000000363798 }, "competitor": { - "minMs": 2.8599999999996726, - "maxMs": 3.039999999999054, - "ratio": 1.062937062936854 + "minMs": 2.880000000000109, + "maxMs": 3.0599999999994907, + "ratio": 1.0624999999997828 }, "stable": true, "rungs": [ @@ -1888,19 +1852,19 @@ }, { "competitor": "pixi", - "referenceMs": 0.10000000000218279, - "referenceP95Ms": 0.22000000000116415, - "referenceGpuMs": 16.659999999999854, + "referenceMs": 0.09999999999854481, + "referenceP95Ms": 0.2000000000007276, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.09999999999854481, - "competitorP95Ms": 0.21999999999752617, - "competitorGpuMs": 16.68000000000029, + "competitorMs": 0.10000000000218279, + "competitorP95Ms": 0.2000000000007276, + "competitorGpuMs": 16.679999999996653, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.0000000000363798, - "factor": 1, - "label": "level", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -1913,14 +1877,14 @@ "ratio": 1.0000000000363798 }, "competitor": { - "minMs": 0.09999999999854481, - "maxMs": 0.09999999999854481, - "ratio": 1 + "minMs": 0.08000000000174623, + "maxMs": 0.10000000000218279, + "ratio": 1.25 }, - "stable": true, + "stable": false, "rungs": [ "level", - "level", + "competitor-leads", "level" ] } @@ -1937,19 +1901,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 0.08000000000174623, - "referenceP95Ms": 0.15999999999985448, - "referenceGpuMs": 16.659999999999854, + "referenceMs": 0.09000000000014552, + "referenceP95Ms": 0.18000000000029104, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 33.390000000003056, - "competitorP95Ms": 38.18000000000029, - "competitorGpuMs": 34.63999999999942, + "competitorMs": 33, + "competitorP95Ms": 36.900000000001455, + "competitorGpuMs": 34.62000000000262, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.0023959269242808897, - "factor": 417.3749999909278, - "label": "ExoJS leads clearly (417.37x)", + "ratio": 0.002727272727277137, + "factor": 366.6666666660738, + "label": "ExoJS leads clearly (366.67x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 3/4/3, none of them large enough to account for the gap); the difference is CPU-side", @@ -1957,14 +1921,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.08000000000174623, + "minMs": 0.09000000000014552, "maxMs": 0.09999999999854481, - "ratio": 1.2499999999545253 + "ratio": 1.1111111110931458 }, "competitor": { - "minMs": 32.85000000000218, - "maxMs": 33.43000000000029, - "ratio": 1.0176560121765013 + "minMs": 32.11000000000058, + "maxMs": 33.44999999999709, + "ratio": 1.0417315478043128 }, "stable": true, "rungs": [ @@ -1976,19 +1940,19 @@ }, { "competitor": "phaser", - "referenceMs": 0.08000000000174623, - "referenceP95Ms": 0.15999999999985448, - "referenceGpuMs": 16.659999999999854, + "referenceMs": 0.09000000000014552, + "referenceP95Ms": 0.18000000000029104, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 6.8400000000037835, - "competitorP95Ms": 7.959999999999127, + "competitorMs": 6.469999999999345, + "competitorP95Ms": 8, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.011695906432997366, - "factor": 85.49999999818101, - "label": "ExoJS leads clearly (85.50x)", + "ratio": 0.013910355486886341, + "factor": 71.88888888876538, + "label": "ExoJS leads clearly (71.89x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/0 vs 4/0/4, none of them large enough to account for the gap); the difference is CPU-side", @@ -1996,14 +1960,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.08000000000174623, + "minMs": 0.09000000000014552, "maxMs": 0.09999999999854481, - "ratio": 1.2499999999545253 + "ratio": 1.1111111110931458 }, "competitor": { - "minMs": 6.5, - "maxMs": 6.899999999999636, - "ratio": 1.0615384615384056 + "minMs": 6.440000000000509, + "maxMs": 6.619999999998981, + "ratio": 1.0279503105587668 }, "stable": true, "rungs": [ @@ -2015,17 +1979,17 @@ }, { "competitor": "pixi", - "referenceMs": 0.08000000000174623, - "referenceP95Ms": 0.15999999999985448, - "referenceGpuMs": 16.659999999999854, + "referenceMs": 0.09000000000014552, + "referenceP95Ms": 0.18000000000029104, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, "competitorMs": 0.09999999999854481, "competitorP95Ms": 0.2000000000007276, - "competitorGpuMs": 16.68000000000029, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.8000000000291039, + "ratio": 0.9000000000145519, "factor": 1, "label": "level", "structural": false @@ -2035,14 +1999,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.08000000000174623, + "minMs": 0.09000000000014552, "maxMs": 0.09999999999854481, - "ratio": 1.2499999999545253 + "ratio": 1.1111111110931458 }, "competitor": { "minMs": 0.09999999999854481, - "maxMs": 0.10000000000218279, - "ratio": 1.0000000000363798 + "maxMs": 0.09999999999854481, + "ratio": 1 }, "stable": true, "rungs": [ @@ -2064,19 +2028,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 2.5900000000019645, - "referenceP95Ms": 6.400000000001455, + "referenceMs": 2.5799999999999272, + "referenceP95Ms": 5.259999999998399, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 7.130000000001019, - "competitorP95Ms": 9.660000000003492, + "competitorMs": 6.729999999999563, + "competitorP95Ms": 9.159999999996217, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.3632538569427201, - "factor": 2.7528957528940583, - "label": "ExoJS leads (2.75x)", + "ratio": 0.38335809806836474, + "factor": 2.60852713178285, + "label": "ExoJS leads (2.61x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -2084,14 +2048,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.460000000000946, - "maxMs": 2.640000000001237, - "ratio": 1.0731707317074073 + "minMs": 2.5200000000004366, + "maxMs": 2.7000000000007276, + "ratio": 1.0714285714286746 }, "competitor": { - "minMs": 7.120000000002619, - "maxMs": 7.5900000000001455, - "ratio": 1.0660112359546845 + "minMs": 5.6599999999962165, + "maxMs": 7.030000000002474, + "ratio": 1.2420494699659317 }, "stable": true, "rungs": [ @@ -2103,19 +2067,19 @@ }, { "competitor": "phaser", - "referenceMs": 2.5900000000019645, - "referenceP95Ms": 6.400000000001455, + "referenceMs": 2.5799999999999272, + "referenceP95Ms": 5.259999999998399, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 3.0600000000004, - "competitorP95Ms": 4.960000000006403, - "competitorGpuMs": 16.659999999999854, + "competitorMs": 2.980000000000473, + "competitorP95Ms": 5.239999999999782, + "competitorGpuMs": 16.679999999998472, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 0.8657718120803751, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -2123,18 +2087,18 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.460000000000946, - "maxMs": 2.640000000001237, - "ratio": 1.0731707317074073 + "minMs": 2.5200000000004366, + "maxMs": 2.7000000000007276, + "ratio": 1.0714285714286746 }, "competitor": { - "minMs": 3.030000000000655, - "maxMs": 3.210000000006403, - "ratio": 1.0594059405959435 + "minMs": 2.9799999999995634, + "maxMs": 3.0100000000002183, + "ratio": 1.010067114094181 }, - "stable": false, + "stable": true, "rungs": [ - "exojs-leads", + "level", "level", "level" ] @@ -2142,19 +2106,19 @@ }, { "competitor": "pixi", - "referenceMs": 2.5900000000019645, - "referenceP95Ms": 6.400000000001455, + "referenceMs": 2.5799999999999272, + "referenceP95Ms": 5.259999999998399, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 4.450000000000728, - "competitorP95Ms": 6, + "competitorMs": 4.259999999998399, + "competitorP95Ms": 5.899999999997817, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5820224719104586, - "factor": 1.718146718145696, - "label": "ExoJS leads (1.72x)", + "ratio": 0.6056338028171119, + "factor": 1.6511627906971007, + "label": "ExoJS leads (1.65x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -2162,14 +2126,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.460000000000946, - "maxMs": 2.640000000001237, - "ratio": 1.0731707317074073 + "minMs": 2.5200000000004366, + "maxMs": 2.7000000000007276, + "ratio": 1.0714285714286746 }, "competitor": { - "minMs": 4.3700000000008, - "maxMs": 4.459999999999127, - "ratio": 1.0205949656746705 + "minMs": 4.170000000000073, + "maxMs": 4.479999999999563, + "ratio": 1.0743405275778142 }, "stable": true, "rungs": [ @@ -2191,19 +2155,19 @@ "cells": [ { "competitor": "excalibur", - "referenceMs": 7.4099999999980355, - "referenceP95Ms": 8.840000000000146, + "referenceMs": 7.210000000000946, + "referenceP95Ms": 9.400000000001455, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 34.94999999999709, - "competitorP95Ms": 39.4800000000032, - "competitorGpuMs": 36.419999999998254, + "competitorMs": 32.25999999999476, + "competitorP95Ms": 39.5199999999968, + "competitorGpuMs": 33.76000000000931, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.21201716738193568, - "factor": 4.716599190284259, - "label": "ExoJS leads (4.72x)", + "ratio": 0.22349659020465334, + "factor": 4.474341192786481, + "label": "ExoJS leads (4.47x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 3/4/3, none of them large enough to account for the gap); the difference is CPU-side", @@ -2211,14 +2175,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 7.099999999998545, - "maxMs": 7.469999999999345, - "ratio": 1.0521126760564614 + "minMs": 7.039999999999054, + "maxMs": 7.280000000000655, + "ratio": 1.034090909091141 }, "competitor": { - "minMs": 34.22999999999956, - "maxMs": 36.279999999998836, - "ratio": 1.059888986269334 + "minMs": 28.43000000000393, + "maxMs": 34.93000000000029, + "ratio": 1.2286317270487324 }, "stable": true, "rungs": [ @@ -2230,17 +2194,17 @@ }, { "competitor": "phaser", - "referenceMs": 7.4099999999980355, - "referenceP95Ms": 8.840000000000146, + "referenceMs": 7.210000000000946, + "referenceP95Ms": 9.400000000001455, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 6.9399999999996, - "competitorP95Ms": 8.220000000001164, + "competitorMs": 6.579999999999927, + "competitorP95Ms": 8.1200000000008, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.0677233429392599, + "ratio": 1.0957446808512197, "factor": 1, "label": "level", "structural": false @@ -2250,14 +2214,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 7.099999999998545, - "maxMs": 7.469999999999345, - "ratio": 1.0521126760564614 + "minMs": 7.039999999999054, + "maxMs": 7.280000000000655, + "ratio": 1.034090909091141 }, "competitor": { - "minMs": 6.930000000007567, - "maxMs": 7.050000000000182, - "ratio": 1.0173160173149327 + "minMs": 6.509999999999309, + "maxMs": 6.9099999999998545, + "ratio": 1.0614439324117646 }, "stable": true, "rungs": [ @@ -2269,19 +2233,19 @@ }, { "competitor": "pixi", - "referenceMs": 7.4099999999980355, - "referenceP95Ms": 8.840000000000146, + "referenceMs": 7.210000000000946, + "referenceP95Ms": 9.400000000001455, "referenceGpuMs": 16.659999999999854, "referenceOverFrameBudget": false, - "competitorMs": 18.779999999998836, - "competitorP95Ms": 22.600000000002183, - "competitorGpuMs": 20.860000000000582, + "competitorMs": 18.7400000000016, + "competitorP95Ms": 21.780000000002474, + "competitorGpuMs": 20.279999999998836, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.3945686900957665, - "factor": 2.534412955466102, - "label": "ExoJS leads (2.53x)", + "ratio": 0.384738527214532, + "factor": 2.5991678224686745, + "label": "ExoJS leads (2.60x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/3 vs 1/0/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -2289,14 +2253,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 7.099999999998545, - "maxMs": 7.469999999999345, - "ratio": 1.0521126760564614 + "minMs": 7.039999999999054, + "maxMs": 7.280000000000655, + "ratio": 1.034090909091141 }, "competitor": { - "minMs": 18.770000000002256, - "maxMs": 18.93999999999869, - "ratio": 1.0090570058602246 + "minMs": 18.530000000002474, + "maxMs": 18.920000000000073, + "ratio": 1.0210469508903155 }, "stable": true, "rungs": [ @@ -2323,19 +2287,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.37999999999919964, - "referenceP95Ms": 0.680000000000291, - "referenceGpuMs": 16.659999999999854, + "referenceMs": 0.3999999999978172, + "referenceP95Ms": 0.6999999999970896, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 2.039999999999054, - "competitorP95Ms": 3.959999999999127, - "competitorGpuMs": 16.659999999999854, + "competitorMs": 2.2299999999977445, + "competitorP95Ms": 3.819999999999709, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.1862745098036156, - "factor": 5.3684210526403975, - "label": "ExoJS leads clearly (5.37x)", + "ratio": 0.17937219730861964, + "factor": 5.575000000024784, + "label": "ExoJS leads clearly (5.58x)", "structural": true }, "mechanism": "competitor issues fewer texture binds (9 vs 3 per frame)", @@ -2343,14 +2307,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.36999999999898137, - "maxMs": 0.38000000000101863, - "ratio": 1.0270270270326076 + "minMs": 0.3499999999985448, + "maxMs": 0.41999999999825377, + "ratio": 1.2 }, "competitor": { - "minMs": 2.0100000000002183, - "maxMs": 2.0900000000001455, - "ratio": 1.039800995024835 + "minMs": 2.210000000000946, + "maxMs": 2.25, + "ratio": 1.0180995475108765 }, "stable": true, "rungs": [ @@ -2372,19 +2336,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.3100000000013097, - "referenceP95Ms": 0.5, - "referenceGpuMs": 16.68000000000029, + "referenceMs": 0.3000000000029104, + "referenceP95Ms": 0.5199999999967986, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 2.4099999999998545, - "competitorP95Ms": 4.580000000001746, - "competitorGpuMs": 16.660000000003492, + "competitorMs": 2.3400000000001455, + "competitorP95Ms": 4.639999999999418, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.12863070539474208, - "factor": 7.774193548353782, - "label": "ExoJS leads clearly (7.77x)", + "ratio": 0.12820512820636398, + "factor": 7.7999999999248155, + "label": "ExoJS leads clearly (7.80x)", "structural": true }, "mechanism": "competitor issues fewer texture binds (11 vs 3 per frame)", @@ -2397,9 +2361,9 @@ "ratio": 1.0666666666553486 }, "competitor": { - "minMs": 2.349999999998545, - "maxMs": 2.640000000003056, - "ratio": 1.123404255321145 + "minMs": 2.2399999999997817, + "maxMs": 2.540000000002692, + "ratio": 1.1339285714298837 }, "stable": true, "rungs": [ @@ -2421,19 +2385,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.2999999999992724, + "referenceMs": 0.2800000000061118, "referenceP95Ms": 0.4599999999991269, - "referenceGpuMs": 16.68000000000029, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 2.7600000000020373, - "competitorP95Ms": 4.440000000002328, - "competitorGpuMs": 16.660000000003492, + "competitorMs": 2.5099999999983993, + "competitorP95Ms": 4.540000000000873, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.10869565217356919, - "factor": 9.200000000029103, - "label": "ExoJS leads clearly (9.20x)", + "ratio": 0.11155378486306389, + "factor": 8.964285714084326, + "label": "ExoJS leads clearly (8.96x)", "structural": true }, "mechanism": "competitor issues fewer texture binds (15 vs 7 per frame)", @@ -2441,14 +2405,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.26000000000203727, - "maxMs": 0.31999999999970896, - "ratio": 1.2307692307584674 + "minMs": 0.2800000000024738, + "maxMs": 0.3100000000013097, + "ratio": 1.1071428571377528 }, "competitor": { - "minMs": 2.399999999997817, - "maxMs": 2.959999999999127, - "ratio": 1.2333333333340912 + "minMs": 2.4199999999982538, + "maxMs": 2.529999999998836, + "ratio": 1.0454545454548188 }, "stable": true, "rungs": [ @@ -2470,19 +2434,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.3400000000037835, - "referenceP95Ms": 0.5600000000049477, - "referenceGpuMs": 16.660000000003492, + "referenceMs": 0.31999999999970896, + "referenceP95Ms": 0.5200000000040745, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 0.4599999999991269, - "competitorP95Ms": 0.7600000000020373, + "competitorMs": 0.47000000000116415, + "competitorP95Ms": 0.6999999999970896, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "exojs", + "ratio": 0.6808510638274816, + "factor": 1.4687500000049738, + "label": "ExoJS leads (1.47x)", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (5 vs 8 per frame)", @@ -2490,20 +2454,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.31999999999970896, - "maxMs": 0.3599999999933061, - "ratio": 1.1249999999801048 + "minMs": 0.3099999999976717, + "maxMs": 0.3400000000001455, + "ratio": 1.096774193557094 }, "competitor": { - "minMs": 0.42000000000552973, - "maxMs": 0.5599999999976717, - "ratio": 1.333333333310235 + "minMs": 0.4400000000023283, + "maxMs": 0.47000000000116415, + "ratio": 1.0681818181788116 }, - "stable": false, + "stable": true, "rungs": [ "exojs-leads", "exojs-leads", - "level" + "exojs-leads" ] } } @@ -2519,19 +2483,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.2400000000052387, - "referenceP95Ms": 0.43999999999505235, + "referenceMs": 0.2599999999983993, + "referenceP95Ms": 0.4400000000023283, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.16000000000349246, - "competitorP95Ms": 0.26000000000203727, + "competitorMs": 0.13999999999941792, + "competitorP95Ms": 0.27999999999883585, "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.5, - "factor": 1.5, - "label": "competitor leads (1.50x)", + "ratio": 1.8571428571391448, + "factor": 1.8571428571391448, + "label": "competitor leads (1.86x)", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", @@ -2539,14 +2503,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.23999999999796273, - "maxMs": 0.25, - "ratio": 1.041666666675509 + "minMs": 0.22000000000116415, + "maxMs": 0.26000000000203727, + "ratio": 1.1818181818211884 }, "competitor": { "minMs": 0.13999999999941792, - "maxMs": 0.16000000000349246, - "ratio": 1.1428571428868406 + "maxMs": 0.1599999999962165, + "ratio": 1.1428571428348695 }, "stable": true, "rungs": [ @@ -2558,6 +2522,56 @@ } ] }, + { + "archetype": "fx-blur", + "category": "Render targets", + "count": 720, + "loadId": "720", + "unit": "viewport", + "primary": true, + "label": "1280 x 720", + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.2800000000061118, + "referenceP95Ms": 0.4800000000032014, + "referenceGpuMs": 16.68000000000029, + "referenceOverFrameBudget": false, + "competitorMs": 0.26000000000203727, + "competitorP95Ms": 0.4599999999991269, + "competitorGpuMs": 16.68000000000029, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": 1.0769230769381455, + "factor": 1, + "label": "level", + "structural": false + }, + "mechanism": "competitor issues fewer texture binds (11 vs 3 per frame)", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.27999999999883585, + "maxMs": 0.2800000000061118, + "ratio": 1.0000000000259857 + }, + "competitor": { + "minMs": 0.26000000000203727, + "maxMs": 0.26000000000203727, + "ratio": 1 + }, + "stable": true, + "rungs": [ + "level", + "level", + "level" + ] + } + } + ] + }, { "archetype": "mask-clip-animated", "category": "Render targets", @@ -2568,19 +2582,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.26000000000203727, - "referenceP95Ms": 0.4400000000023283, + "referenceMs": 0.2800000000024738, + "referenceP95Ms": 0.4800000000032014, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.4000000000014552, + "competitorMs": 0.38000000000101863, "competitorP95Ms": 0.5800000000017462, - "competitorGpuMs": 16.68000000000029, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.6500000000027285, - "factor": 1.5384615384550804, - "label": "ExoJS leads (1.54x)", + "ratio": 0.7368421052676928, + "factor": 1.3571428571345046, + "label": "ExoJS leads (1.36x)", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", @@ -2588,14 +2602,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.2400000000016007, - "maxMs": 0.27999999999883585, - "ratio": 1.1666666666540348 + "minMs": 0.27000000000043656, + "maxMs": 0.2999999999956344, + "ratio": 1.1111111110931458 }, "competitor": { - "minMs": 0.3900000000030559, + "minMs": 0.37999999999738066, "maxMs": 0.4000000000014552, - "ratio": 1.0256410256367203 + "ratio": 1.0526315789584537 }, "stable": true, "rungs": [ @@ -2623,12 +2637,12 @@ { "competitor": "pixi", "referenceMs": 0.18000000000029104, - "referenceP95Ms": 1.6000000000021828, - "referenceGpuMs": 16.679999999996653, + "referenceP95Ms": 1.4600000000027649, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, "competitorMs": 0.11000000000058208, "competitorP95Ms": 0.22000000000116415, - "competitorGpuMs": 16.679999999996653, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", @@ -2642,14 +2656,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.17999999999847205, - "maxMs": 0.18000000000029104, - "ratio": 1.0000000000101055 + "minMs": 0.18000000000029104, + "maxMs": 0.2000000000007276, + "ratio": 1.1111111111133567 }, "competitor": { - "minMs": 0.10000000000218279, + "minMs": 0.1000000000003638, "maxMs": 0.11999999999898137, - "ratio": 1.1999999999636202 + "ratio": 1.199999999985448 }, "stable": true, "rungs": [ @@ -2674,54 +2688,15 @@ "unit": "tiles", "primary": false, "cells": [ - { - "competitor": "excalibur", - "referenceMs": 0.18000000000029104, - "referenceP95Ms": 1.1600000000034925, - "referenceGpuMs": 16.660000000003492, - "referenceOverFrameBudget": false, - "competitorMs": 20.810000000004948, - "competitorP95Ms": 25.90000000000873, - "competitorGpuMs": 22, - "competitorOverFrameBudget": true, - "verdict": { - "side": "exojs", - "ratio": 0.008649687650180118, - "factor": 115.61111111095167, - "label": "ExoJS leads clearly (115.61x)", - "structural": true - }, - "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", - "timer": "limited", - "aggregate": { - "runs": 3, - "reference": { - "minMs": 0.18000000000029104, - "maxMs": 0.20000000000436557, - "ratio": 1.1111111111335679 - }, - "competitor": { - "minMs": 20.580000000001746, - "maxMs": 21.060000000004948, - "ratio": 1.0233236151605034 - }, - "stable": true, - "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" - ] - } - }, { "competitor": "phaser", "referenceMs": 0.18000000000029104, - "referenceP95Ms": 1.1600000000034925, + "referenceP95Ms": 1.1999999999970896, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, "competitorMs": 0.2000000000007276, - "competitorP95Ms": 0.42000000000189175, - "competitorGpuMs": 16.659999999999854, + "competitorP95Ms": 0.3999999999978172, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -2736,13 +2711,13 @@ "runs": 3, "reference": { "minMs": 0.18000000000029104, - "maxMs": 0.20000000000436557, - "ratio": 1.1111111111335679 + "maxMs": 0.19999999999708962, + "ratio": 1.1111111110931458 }, "competitor": { - "minMs": 0.1999999999989086, - "maxMs": 0.20999999999912689, - "ratio": 1.0500000000013643 + "minMs": 0.2000000000007276, + "maxMs": 0.22000000000116415, + "ratio": 1.100000000001819 }, "stable": true, "rungs": [ @@ -2755,12 +2730,12 @@ { "competitor": "pixi", "referenceMs": 0.18000000000029104, - "referenceP95Ms": 1.1600000000034925, + "referenceP95Ms": 1.1999999999970896, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, "competitorMs": 0.22000000000116415, "competitorP95Ms": 0.37999999999738066, - "competitorGpuMs": 16.68000000000029, + "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -2775,13 +2750,13 @@ "runs": 3, "reference": { "minMs": 0.18000000000029104, - "maxMs": 0.20000000000436557, - "ratio": 1.1111111111335679 + "maxMs": 0.19999999999708962, + "ratio": 1.1111111110931458 }, "competitor": { - "minMs": 0.20000000000436557, - "maxMs": 0.22000000000116415, - "ratio": 1.0999999999818102 + "minMs": 0.19999999999708962, + "maxMs": 0.23999999999796273, + "ratio": 1.200000000007276 }, "stable": true, "rungs": [ @@ -2801,60 +2776,21 @@ "unit": "tiles", "primary": true, "cells": [ - { - "competitor": "excalibur", - "referenceMs": 0.13999999999941792, - "referenceP95Ms": 0.7799999999988358, - "referenceGpuMs": 16.660000000003492, - "referenceOverFrameBudget": false, - "competitorMs": 180.13999999999942, - "competitorP95Ms": 189.39999999999418, - "competitorGpuMs": 183.11999999999534, - "competitorOverFrameBudget": true, - "verdict": { - "side": "exojs", - "ratio": 0.0007771733096448227, - "factor": 1286.7142857196313, - "label": "ExoJS leads clearly (1286.71x)", - "structural": true - }, - "mechanism": "competitor issues fewer draw calls (30 vs 5 per frame)", - "timer": "limited", - "aggregate": { - "runs": 3, - "reference": { - "minMs": 0.11999999999898137, - "maxMs": 0.18000000000029104, - "ratio": 1.5000000000151583 - }, - "competitor": { - "minMs": 178.79000000000087, - "maxMs": 182.29000000000087, - "ratio": 1.0195760389283517 - }, - "stable": true, - "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" - ] - } - }, { "competitor": "phaser", "referenceMs": 0.13999999999941792, - "referenceP95Ms": 0.7799999999988358, + "referenceP95Ms": 0.819999999999709, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.16000000000349246, - "competitorP95Ms": 0.38000000000101863, - "competitorGpuMs": 16.700000000000728, + "competitorMs": 0.18000000000029104, + "competitorP95Ms": 0.2999999999992724, + "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "exojs", + "ratio": 0.7777777777732865, + "factor": 1.28571428572171, + "label": "ExoJS leads (1.29x)", "structural": false }, "mechanism": "competitor issues fewer draw calls (30 vs 1 per frame)", @@ -2862,38 +2798,38 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.11999999999898137, - "maxMs": 0.18000000000029104, - "ratio": 1.5000000000151583 + "minMs": 0.11999999999534339, + "maxMs": 0.13999999999941792, + "ratio": 1.1666666667070886 }, "competitor": { - "minMs": 0.15999999999985448, - "maxMs": 0.17000000000189175, - "ratio": 1.0625000000127898 + "minMs": 0.1499999999996362, + "maxMs": 0.18000000000029104, + "ratio": 1.2000000000048507 }, - "stable": false, + "stable": true, "rungs": [ - "level", "exojs-leads", - "level" + "exojs-leads", + "exojs-leads" ] } }, { "competitor": "pixi", "referenceMs": 0.13999999999941792, - "referenceP95Ms": 0.7799999999988358, + "referenceP95Ms": 0.819999999999709, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.22000000000116415, + "competitorMs": 0.18000000000029104, "competitorP95Ms": 0.3600000000005821, - "competitorGpuMs": 16.659999999996217, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "exojs", + "ratio": 0.7777777777732865, + "factor": 1.28571428572171, + "label": "ExoJS leads (1.29x)", "structural": false }, "mechanism": "competitor issues fewer draw calls (30 vs 2 per frame)", @@ -2901,20 +2837,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.11999999999898137, - "maxMs": 0.18000000000029104, - "ratio": 1.5000000000151583 + "minMs": 0.11999999999534339, + "maxMs": 0.13999999999941792, + "ratio": 1.1666666667070886 }, "competitor": { "minMs": 0.18000000000029104, - "maxMs": 0.22000000000116415, - "ratio": 1.2222222222267136 + "maxMs": 0.21999999999752617, + "ratio": 1.2222222222065025 }, - "stable": false, + "stable": true, "rungs": [ "exojs-leads", "exojs-leads", - "level" + "exojs-leads" ] } } @@ -2930,19 +2866,19 @@ "cells": [ { "competitor": "phaser", - "referenceMs": 0.750000000003638, + "referenceMs": 0.6699999999982538, "referenceP95Ms": 1.319999999999709, - "referenceGpuMs": 16.659999999996217, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 1.8000000000010914, - "competitorP95Ms": 6, + "competitorMs": 1.9600000000009459, + "competitorP95Ms": 6.520000000000437, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.4166666666684351, - "factor": 2.399999999989814, - "label": "ExoJS leads (2.40x)", + "ratio": 0.34183673469282166, + "factor": 2.925373134337394, + "label": "ExoJS leads (2.93x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/3/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -2950,14 +2886,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.7000000000007276, - "maxMs": 0.8100000000013097, - "ratio": 1.1571428571435254 + "minMs": 0.63999999999578, + "maxMs": 0.6899999999986903, + "ratio": 1.0781250000050626 }, "competitor": { - "minMs": 1.68000000000211, - "maxMs": 2.400000000001455, - "ratio": 1.4285714285705005 + "minMs": 1.3800000000010186, + "maxMs": 2.600000000000364, + "ratio": 1.8840579710133656 }, "stable": true, "rungs": [ @@ -2969,19 +2905,19 @@ }, { "competitor": "pixi", - "referenceMs": 0.750000000003638, + "referenceMs": 0.6699999999982538, "referenceP95Ms": 1.319999999999709, - "referenceGpuMs": 16.659999999996217, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 1.1299999999973807, + "competitorMs": 1.0100000000020373, "competitorP95Ms": 1.9000000000014552, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.6637168141640499, - "factor": 1.5066666666558661, - "label": "ExoJS leads (1.51x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 2/0/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -2989,18 +2925,18 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.7000000000007276, - "maxMs": 0.8100000000013097, - "ratio": 1.1571428571435254 + "minMs": 0.63999999999578, + "maxMs": 0.6899999999986903, + "ratio": 1.0781250000050626 }, "competitor": { - "minMs": 1.110000000000582, - "maxMs": 1.2600000000020373, - "ratio": 1.1351351351363752 + "minMs": 0.7400000000016007, + "maxMs": 1.0600000000013097, + "ratio": 1.4324324324311037 }, - "stable": true, + "stable": false, "rungs": [ - "exojs-leads", + "level", "exojs-leads", "exojs-leads" ] @@ -3021,60 +2957,21 @@ "unit": "particles", "primary": false, "cells": [ - { - "competitor": "excalibur", - "referenceMs": 0.26000000000203727, - "referenceP95Ms": 0.41999999999825377, - "referenceGpuMs": 16.660000000003492, - "referenceOverFrameBudget": false, - "competitorMs": 2.390000000006694, - "competitorP95Ms": 7.419999999998254, - "competitorGpuMs": 16.660000000003492, - "competitorOverFrameBudget": false, - "verdict": { - "side": "exojs", - "ratio": 0.10878661087920881, - "factor": 9.19230769226141, - "label": "ExoJS leads clearly (9.19x)", - "structural": true - }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", - "timer": "resolved", - "aggregate": { - "runs": 3, - "reference": { - "minMs": 0.26000000000203727, - "maxMs": 0.27999999999883585, - "ratio": 1.076923076910161 - }, - "competitor": { - "minMs": 2.389999999992142, - "maxMs": 2.4199999999982538, - "ratio": 1.0125523012578286 - }, - "stable": true, - "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" - ] - } - }, { "competitor": "phaser", "referenceMs": 0.26000000000203727, "referenceP95Ms": 0.41999999999825377, - "referenceGpuMs": 16.660000000003492, + "referenceGpuMs": 16.679999999993015, "referenceOverFrameBudget": false, - "competitorMs": 0.8400000000001455, + "competitorMs": 0.8500000000003638, "competitorP95Ms": 1.0800000000017462, "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.30952380952618125, - "factor": 3.230769230744475, - "label": "ExoJS leads (3.23x)", + "ratio": 0.30588235294344235, + "factor": 3.2692307692065516, + "label": "ExoJS leads (3.27x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3087,9 +2984,9 @@ "ratio": 1.076923076910161 }, "competitor": { - "minMs": 0.8400000000001455, - "maxMs": 0.8799999999901047, - "ratio": 1.047619047607086 + "minMs": 0.819999999999709, + "maxMs": 0.8600000000005821, + "ratio": 1.0487804878059601 }, "stable": true, "rungs": [ @@ -3103,11 +3000,11 @@ "competitor": "pixi", "referenceMs": 0.26000000000203727, "referenceP95Ms": 0.41999999999825377, - "referenceGpuMs": 16.660000000003492, + "referenceGpuMs": 16.679999999993015, "referenceOverFrameBudget": false, "competitorMs": 0.13999999999941792, - "competitorP95Ms": 0.27999999999883585, - "competitorGpuMs": 16.660000000003492, + "competitorP95Ms": 0.2800000000061118, + "competitorGpuMs": 16.68000000000029, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", @@ -3148,60 +3045,21 @@ "unit": "particles", "primary": true, "cells": [ - { - "competitor": "excalibur", - "referenceMs": 0.46000000000276486, - "referenceP95Ms": 0.5800000000017462, - "referenceGpuMs": 16.68000000000029, - "referenceOverFrameBudget": false, - "competitorMs": 24.489999999997963, - "competitorP95Ms": 26.979999999995925, - "competitorGpuMs": 25.460000000006403, - "competitorOverFrameBudget": true, - "verdict": { - "side": "exojs", - "ratio": 0.018783176806974403, - "factor": 53.23913043445818, - "label": "ExoJS leads clearly (53.24x)", - "structural": true - }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", - "timer": "resolved", - "aggregate": { - "runs": 3, - "reference": { - "minMs": 0.4500000000007276, - "maxMs": 0.47999999999956344, - "ratio": 1.066666666663972 - }, - "competitor": { - "minMs": 14.69999999999709, - "maxMs": 25.69000000000233, - "ratio": 1.7476190476195521 - }, - "stable": true, - "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" - ] - } - }, { "competitor": "phaser", - "referenceMs": 0.46000000000276486, + "referenceMs": 0.47999999999592546, "referenceP95Ms": 0.5800000000017462, - "referenceGpuMs": 16.68000000000029, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 2.9800000000032014, - "competitorP95Ms": 3.779999999998836, - "competitorGpuMs": 16.659999999999854, + "competitorMs": 2.680000000000291, + "competitorP95Ms": 3.7599999999983993, + "competitorGpuMs": 16.679999999996653, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.15436241610814452, - "factor": 6.478260869533239, - "label": "ExoJS leads clearly (6.48x)", + "ratio": 0.1791044776104005, + "factor": 5.583333333381335, + "label": "ExoJS leads clearly (5.58x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3209,14 +3067,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.4500000000007276, - "maxMs": 0.47999999999956344, - "ratio": 1.066666666663972 + "minMs": 0.46000000000276486, + "maxMs": 0.47999999999592546, + "ratio": 1.0434782608544357 }, "competitor": { - "minMs": 2.7999999999992724, - "maxMs": 3.2000000000007276, - "ratio": 1.1428571428576997 + "minMs": 2.5500000000010914, + "maxMs": 3.0799999999999272, + "ratio": 1.2078431372543565 }, "stable": true, "rungs": [ @@ -3228,19 +3086,19 @@ }, { "competitor": "pixi", - "referenceMs": 0.46000000000276486, + "referenceMs": 0.47999999999592546, "referenceP95Ms": 0.5800000000017462, - "referenceGpuMs": 16.68000000000029, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.11999999999898137, + "competitorMs": 0.12000000000261934, "competitorP95Ms": 0.2400000000052387, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 3.8333333333889135, - "factor": 3.8333333333889135, - "label": "competitor leads (3.83x)", + "ratio": 3.999999999878734, + "factor": 3.999999999878734, + "label": "competitor leads (4.00x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", @@ -3248,14 +3106,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.4500000000007276, - "maxMs": 0.47999999999956344, - "ratio": 1.066666666663972 + "minMs": 0.46000000000276486, + "maxMs": 0.47999999999592546, + "ratio": 1.0434782608544357 }, "competitor": { - "minMs": 0.11999999999534339, + "minMs": 0.12000000000261934, "maxMs": 0.12000000000261934, - "ratio": 1.000000000060633 + "ratio": 1 }, "stable": true, "rungs": [ @@ -3277,19 +3135,19 @@ "cells": [ { "competitor": "phaser", - "referenceMs": 1.6699999999982538, - "referenceP95Ms": 3.1000000000058208, + "referenceMs": 1.7900000000008731, + "referenceP95Ms": 2.6199999999953434, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 9.510000000002037, - "competitorP95Ms": 10.659999999999854, + "competitorMs": 9.460000000000946, + "competitorP95Ms": 10.020000000000437, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.1756046267085064, - "factor": 5.694610778450288, - "label": "ExoJS leads clearly (5.69x)", + "ratio": 0.1892177589852742, + "factor": 5.284916201115269, + "label": "ExoJS leads clearly (5.28x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 7/0/7, none of them large enough to account for the gap); the difference is CPU-side", @@ -3297,14 +3155,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.5800000000017462, - "maxMs": 1.8799999999973807, - "ratio": 1.1898734177185462 + "minMs": 1.7899999999972351, + "maxMs": 1.860000000000582, + "ratio": 1.0391061452533268 }, "competitor": { - "minMs": 9.449999999998909, - "maxMs": 9.6299999999992, - "ratio": 1.019047619047652 + "minMs": 9.459999999999127, + "maxMs": 9.570000000001528, + "ratio": 1.0116279069769991 }, "stable": true, "rungs": [ @@ -3316,18 +3174,18 @@ }, { "competitor": "pixi", - "referenceMs": 1.6699999999982538, - "referenceP95Ms": 3.1000000000058208, + "referenceMs": 1.7900000000008731, + "referenceP95Ms": 2.6199999999953434, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.13999999999941792, - "competitorP95Ms": 0.27999999999883585, + "competitorMs": 0.1499999999978172, + "competitorP95Ms": 0.2999999999956344, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 11.928571428608551, - "factor": 11.928571428608551, + "ratio": 11.933333333512808, + "factor": 11.933333333512808, "label": "competitor leads clearly (11.93x)", "structural": true }, @@ -3336,14 +3194,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.5800000000017462, - "maxMs": 1.8799999999973807, - "ratio": 1.1898734177185462 + "minMs": 1.7899999999972351, + "maxMs": 1.860000000000582, + "ratio": 1.0391061452533268 }, "competitor": { - "minMs": 0.13999999999941792, - "maxMs": 0.1499999999978172, - "ratio": 1.0714285714174347 + "minMs": 0.11000000000058208, + "maxMs": 0.1500000000014552, + "ratio": 1.3636363636423767 }, "stable": true, "rungs": [ @@ -3352,43 +3210,6 @@ "competitor-leads-clearly" ] } - }, - { - "competitor": "excalibur", - "referenceMs": 1.8799999999973807, - "referenceP95Ms": 3.1599999999962165, - "referenceGpuMs": 16.660000000003492, - "referenceOverFrameBudget": false, - "competitorMs": 191.07000000000698, - "competitorP95Ms": 195.77999999999884, - "competitorGpuMs": 196.6600000000035, - "competitorOverFrameBudget": true, - "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", - "structural": false - }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 5/6/5, none of them large enough to account for the gap); the difference is CPU-side", - "timer": "resolved", - "aggregate": { - "runs": 1, - "reference": { - "minMs": 1.8799999999973807, - "maxMs": 1.8799999999973807, - "ratio": 1 - }, - "competitor": { - "minMs": 191.07000000000698, - "maxMs": 191.07000000000698, - "ratio": 1 - }, - "stable": false, - "rungs": [ - "exojs-leads-clearly" - ] - } } ] }, @@ -3400,60 +3221,21 @@ "unit": "particles", "primary": true, "cells": [ - { - "competitor": "excalibur", - "referenceMs": 1.360000000000582, - "referenceP95Ms": 2.279999999998836, - "referenceGpuMs": 16.660000000003492, - "referenceOverFrameBudget": false, - "competitorMs": 24.959999999999127, - "competitorP95Ms": 27.360000000000582, - "competitorGpuMs": 26.05999999999767, - "competitorOverFrameBudget": true, - "verdict": { - "side": "exojs", - "ratio": 0.05448717948720471, - "factor": 18.352941176462092, - "label": "ExoJS leads clearly (18.35x)", - "structural": true - }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", - "timer": "resolved", - "aggregate": { - "runs": 3, - "reference": { - "minMs": 1.3400000000001455, - "maxMs": 1.6599999999962165, - "ratio": 1.2388059701462957 - }, - "competitor": { - "minMs": 23.999999999992724, - "maxMs": 25.179999999993015, - "ratio": 1.0491666666666937 - }, - "stable": true, - "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" - ] - } - }, { "competitor": "phaser", - "referenceMs": 1.360000000000582, - "referenceP95Ms": 2.279999999998836, + "referenceMs": 1.3000000000029104, + "referenceP95Ms": 2.2399999999979627, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 3.1600000000034925, - "competitorP95Ms": 6.139999999999418, + "competitorMs": 3.2299999999995634, + "competitorP95Ms": 5.780000000002474, "competitorGpuMs": 16.659999999999854, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.43037974683515157, - "factor": 2.3235294117662795, - "label": "ExoJS leads (2.32x)", + "ratio": 0.40247678018671396, + "factor": 2.4846153846094863, + "label": "ExoJS leads (2.48x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3461,14 +3243,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.3400000000001455, - "maxMs": 1.6599999999962165, - "ratio": 1.2388059701462957 + "minMs": 1.2300000000032014, + "maxMs": 1.6500000000014552, + "ratio": 1.3414634146318378 }, "competitor": { - "minMs": 3.1199999999989814, - "maxMs": 3.360000000000582, - "ratio": 1.076923076923615 + "minMs": 3.0600000000013097, + "maxMs": 3.2699999999986176, + "ratio": 1.068627450979483 }, "stable": true, "rungs": [ @@ -3480,19 +3262,19 @@ }, { "competitor": "pixi", - "referenceMs": 1.360000000000582, - "referenceP95Ms": 2.279999999998836, + "referenceMs": 1.3000000000029104, + "referenceP95Ms": 2.2399999999979627, "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 0.9000000000014552, - "competitorP95Ms": 1.139999999999418, + "competitorMs": 1, + "competitorP95Ms": 1.2800000000061118, "competitorGpuMs": 16.660000000003492, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.5111111111093145, - "factor": 1.5111111111093145, - "label": "competitor leads (1.51x)", + "ratio": 1.3000000000029104, + "factor": 1.3000000000029104, + "label": "competitor leads (1.30x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/0/1 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3500,14 +3282,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.3400000000001455, - "maxMs": 1.6599999999962165, - "ratio": 1.2388059701462957 + "minMs": 1.2300000000032014, + "maxMs": 1.6500000000014552, + "ratio": 1.3414634146318378 }, "competitor": { - "minMs": 0.8900000000030559, - "maxMs": 0.9199999999982538, - "ratio": 1.033707865163028 + "minMs": 0.9000000000014552, + "maxMs": 1.0199999999967986, + "ratio": 1.1333333333279438 }, "stable": true, "rungs": [ @@ -3533,35 +3315,35 @@ "primary": true, "cells": [ { - "competitor": "excalibur", - "referenceMs": 0.569999999999709, - "referenceP95Ms": 0.9400000000023283, - "referenceGpuMs": 16.679999999993015, + "competitor": "phaser", + "referenceMs": 0.5600000000049477, + "referenceP95Ms": 0.8799999999973807, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 24.62999999999738, - "competitorP95Ms": 27.920000000012806, - "competitorGpuMs": 25.720000000001164, + "competitorMs": 32.44000000000233, + "competitorP95Ms": 35.44000000000233, + "competitorGpuMs": 33.31999999999971, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.02314250913519162, - "factor": 43.21052631580694, - "label": "ExoJS leads clearly (43.21x)", + "ratio": 0.01726263871778383, + "factor": 57.92857142806378, + "label": "ExoJS leads clearly (57.93x)", "structural": true }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.5400000000008731, - "maxMs": 0.5799999999944703, - "ratio": 1.074074074062097 + "minMs": 0.5600000000013097, + "maxMs": 0.5999999999985448, + "ratio": 1.071428571423467 }, "competitor": { - "minMs": 24.510000000002037, - "maxMs": 25.810000000004948, - "ratio": 1.053039575683509 + "minMs": 32.279999999998836, + "maxMs": 32.61999999999898, + "ratio": 1.010532837670389 }, "stable": true, "rungs": [ @@ -3572,35 +3354,35 @@ } }, { - "competitor": "phaser", - "referenceMs": 0.569999999999709, - "referenceP95Ms": 0.9400000000023283, - "referenceGpuMs": 16.679999999993015, + "competitor": "pixi", + "referenceMs": 0.5600000000049477, + "referenceP95Ms": 0.8799999999973807, + "referenceGpuMs": 16.68000000000029, "referenceOverFrameBudget": false, - "competitorMs": 32.67000000000007, - "competitorP95Ms": 35.43999999999869, - "competitorGpuMs": 33.5199999999968, - "competitorOverFrameBudget": true, + "competitorMs": 16.5, + "competitorP95Ms": 21.5, + "competitorGpuMs": 17.0199999999968, + "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.017447199265372137, - "factor": 57.3157894737136, - "label": "ExoJS leads clearly (57.32x)", + "ratio": 0.0339393939396938, + "factor": 29.46428571402539, + "label": "ExoJS leads clearly (29.46x)", "structural": true }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/1, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.5400000000008731, - "maxMs": 0.5799999999944703, - "ratio": 1.074074074062097 + "minMs": 0.5600000000013097, + "maxMs": 0.5999999999985448, + "ratio": 1.071428571423467 }, "competitor": { - "minMs": 32.51000000000022, - "maxMs": 32.90000000000873, - "ratio": 1.0119963088283146 + "minMs": 16.270000000000437, + "maxMs": 17.31000000000131, + "ratio": 1.063921327596856 }, "stable": true, "rungs": [ @@ -3609,43 +3391,53 @@ "exojs-leads-clearly" ] } - }, + } + ] + }, + { + "archetype": "ui-layout-update", + "category": "Interaction", + "count": 1000, + "loadId": "1k", + "unit": "widgets", + "primary": true, + "cells": [ { "competitor": "pixi", - "referenceMs": 0.569999999999709, - "referenceP95Ms": 0.9400000000023283, - "referenceGpuMs": 16.679999999993015, + "referenceMs": 10.25, + "referenceP95Ms": 10.720000000001164, + "referenceGpuMs": 16.660000000003492, "referenceOverFrameBudget": false, - "competitorMs": 17.169999999998254, - "competitorP95Ms": 21.87999999999738, - "competitorGpuMs": 17.839999999996508, + "competitorMs": 48.460000000002765, + "competitorP95Ms": 52.75999999999476, + "competitorGpuMs": 49.140000000006694, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.03319743739078433, - "factor": 30.122807017556173, - "label": "ExoJS leads clearly (30.12x)", - "structural": true + "ratio": 0.21151465125875804, + "factor": 4.72780487804905, + "label": "ExoJS leads (4.73x)", + "structural": false }, - "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/0/0 vs 1/0/0, none of them large enough to account for the gap); the difference is CPU-side", + "mechanism": "neither arm submits anything for this scene (draw/bind/upload per frame: 0/0/0 vs 0/0/0); the whole difference is the per-frame work itself", "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.5400000000008731, - "maxMs": 0.5799999999944703, - "ratio": 1.074074074062097 + "minMs": 10.220000000001164, + "maxMs": 10.340000000003783, + "ratio": 1.0117416829748147 }, "competitor": { - "minMs": 16.56999999999971, - "maxMs": 17.5, - "ratio": 1.0561255280627826 + "minMs": 47.99999999999636, + "maxMs": 49.04999999999927, + "ratio": 1.0218750000000623 }, "stable": true, "rungs": [ - "exojs-leads-clearly", - "exojs-leads-clearly", - "exojs-leads-clearly" + "exojs-leads", + "exojs-leads", + "exojs-leads" ] } } @@ -3682,10 +3474,6 @@ { "archetype": "mixed-material-atlased", "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" - }, - { - "archetype": "fx-blur", - "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" } ], "webgl1": [] @@ -3711,18 +3499,18 @@ { "competitor": "pixi", "referenceMs": 0.2999999999999545, - "referenceP95Ms": 0.5799999999999272, - "referenceGpuMs": 0.072688, + "referenceP95Ms": 0.5599999999999454, + "referenceGpuMs": 0.072937, "referenceOverFrameBudget": false, - "competitorMs": 0.2999999999999545, - "competitorP95Ms": 0.5800000000000409, - "competitorGpuMs": 0.07325, + "competitorMs": 0.3000000000001819, + "competitorP95Ms": 0.6000000000001364, + "competitorGpuMs": 0.073125, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 0.999999999999242, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -3730,19 +3518,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.2599999999998772, - "maxMs": 0.32000000000010687, - "ratio": 1.230769230770223 + "minMs": 0.2800000000001148, + "maxMs": 0.3000000000000682, + "ratio": 1.0714285714283758 }, "competitor": { - "minMs": 0.27999999999985903, - "maxMs": 0.4600000000000364, - "ratio": 1.6428571428580998 + "minMs": 0.29999999999998295, + "maxMs": 0.31999999999993634, + "ratio": 1.066666666666515 }, - "stable": false, + "stable": true, "rungs": [ "level", - "exojs-leads", + "level", "level" ] } @@ -3759,40 +3547,40 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.20000000000004547, - "referenceP95Ms": 0.36000000000012733, - "referenceGpuMs": 0.2054165, + "referenceMs": 0.20000000000027285, + "referenceP95Ms": 0.38000000000010914, + "referenceGpuMs": 0.206729, "referenceOverFrameBudget": false, "competitorMs": 0.23999999999978172, - "competitorP95Ms": 0.4600000000000364, - "competitorGpuMs": 0.18993749999999998, + "competitorP95Ms": 0.42000000000007276, + "competitorGpuMs": 0.19004149999999997, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 0.8333333333352281, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", - "timer": "limited", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.14000000000032742, - "maxMs": 0.21000000000003638, - "ratio": 1.4999999999967517 + "minMs": 0.1999999999998181, + "maxMs": 0.20000000000027285, + "ratio": 1.0000000000022737 }, "competitor": { - "minMs": 0.19000000000005457, - "maxMs": 0.2800000000002001, - "ratio": 1.4736842105269456 + "minMs": 0.22000000000025466, + "maxMs": 0.23999999999978172, + "ratio": 1.090909090906836 }, - "stable": false, + "stable": true, "rungs": [ "level", - "exojs-leads", - "exojs-leads" + "level", + "level" ] } } @@ -3808,13 +3596,13 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.15000000000009095, - "referenceP95Ms": 0.3000000000001819, - "referenceGpuMs": 0.429021, + "referenceMs": 0.11000000000012733, + "referenceP95Ms": 0.27999999999974534, + "referenceGpuMs": 0.30341700000000005, "referenceOverFrameBudget": false, - "competitorMs": 0.13000000000010914, - "competitorP95Ms": 0.5, - "competitorGpuMs": 0.274937, + "competitorMs": 0.08000000000038199, + "competitorP95Ms": 0.38000000000010914, + "competitorGpuMs": 0.241354, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -3828,19 +3616,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.05999999999994543, - "maxMs": 0.15999999999985448, - "ratio": 2.6666666666666665 + "minMs": 0.06000000000040018, + "maxMs": 0.169999999999618, + "ratio": 2.8333333333080697 }, "competitor": { - "minMs": 0.09000000000014552, - "maxMs": 0.3599999999996726, - "ratio": 3.9999999999898943 + "minMs": 0.07999999999992724, + "maxMs": 0.15999999999985448, + "ratio": 2 }, "stable": false, "rungs": [ - "competitor-leads", "exojs-leads", + "competitor-leads", "exojs-leads" ] } @@ -3857,19 +3645,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 1.8900000000003274, - "referenceP95Ms": 2.4600000000000364, - "referenceGpuMs": 0.2075835, + "referenceMs": 1.9299999999998363, + "referenceP95Ms": 2.399999999999636, + "referenceGpuMs": 0.206667, "referenceOverFrameBudget": false, - "competitorMs": 1.300000000000182, - "competitorP95Ms": 1.8400000000001455, - "competitorGpuMs": 0.1915835, + "competitorMs": 1.2699999999999818, + "competitorP95Ms": 1.9600000000000364, + "competitorGpuMs": 0.192, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.4538461538462022, - "factor": 1.4538461538462022, - "label": "competitor leads (1.45x)", + "ratio": 1.5196850393699717, + "factor": 1.5196850393699717, + "label": "competitor leads (1.52x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -3877,14 +3665,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.8599999999996726, - "maxMs": 1.900000000000091, - "ratio": 1.0215053763443147 + "minMs": 1.7999999999997272, + "maxMs": 2, + "ratio": 1.1111111111112795 }, "competitor": { - "minMs": 1.0899999999996908, - "maxMs": 1.4200000000000728, - "ratio": 1.302752293578418 + "minMs": 1.2399999999997817, + "maxMs": 1.2799999999997453, + "ratio": 1.0322580645161055 }, "stable": true, "rungs": [ @@ -3906,19 +3694,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 1.7199999999993452, - "referenceP95Ms": 2.199999999999818, - "referenceGpuMs": 0.073708, + "referenceMs": 1.6599999999998545, + "referenceP95Ms": 2.240000000000691, + "referenceGpuMs": 0.073459, "referenceOverFrameBudget": false, - "competitorMs": 0.9799999999995634, - "competitorP95Ms": 1.319999999999709, - "competitorGpuMs": 0.0729795, + "competitorMs": 0.9800000000004729, + "competitorP95Ms": 1.3599999999996726, + "competitorGpuMs": 0.0719585, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.7551020408164402, - "factor": 1.7551020408164402, - "label": "competitor leads (1.76x)", + "ratio": 1.6938775510194422, + "factor": 1.6938775510194422, + "label": "competitor leads (1.69x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -3926,14 +3714,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 1.7100000000000364, - "maxMs": 1.7499999999995453, - "ratio": 1.0233918128652093 + "minMs": 1.6400000000003274, + "maxMs": 1.7100000000000364, + "ratio": 1.0426829268290823 }, "competitor": { - "minMs": 0.9399999999995998, - "maxMs": 1.2300000000000182, - "ratio": 1.3085106382984488 + "minMs": 0.9800000000000182, + "maxMs": 0.9900000000002365, + "ratio": 1.0102040816328757 }, "stable": true, "rungs": [ @@ -3955,18 +3743,18 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 4.220000000001164, - "referenceP95Ms": 10.44000000000051, - "referenceGpuMs": 0.211333, + "referenceMs": 4.229999999999563, + "referenceP95Ms": 5.8400000000001455, + "referenceGpuMs": 0.2107915, "referenceOverFrameBudget": false, "competitorMs": 2.7600000000002183, - "competitorP95Ms": 9.219999999999345, - "competitorGpuMs": 0.1934165, + "competitorP95Ms": 10.05999999999949, + "competitorGpuMs": 0.1925625, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.5289855072466776, - "factor": 1.5289855072466776, + "ratio": 1.5326086956518945, + "factor": 1.5326086956518945, "label": "competitor leads (1.53x)", "structural": false }, @@ -3975,14 +3763,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.7399999999997817, - "maxMs": 4.400000000000546, - "ratio": 1.1764705882355087 + "minMs": 3.860000000000582, + "maxMs": 4.230000000000473, + "ratio": 1.09585492227975 }, "competitor": { - "minMs": 2.7100000000000364, - "maxMs": 2.780000000000655, - "ratio": 1.025830258302811 + "minMs": 2.550000000000182, + "maxMs": 2.769999999999527, + "ratio": 1.0862745098036586 }, "stable": true, "rungs": [ @@ -4004,19 +3792,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 38.039999999999964, - "referenceP95Ms": 41.659999999999854, - "referenceGpuMs": 0.970521, + "referenceMs": 38, + "referenceP95Ms": 44.02000000000044, + "referenceGpuMs": 0.976, "referenceOverFrameBudget": true, - "competitorMs": 28.61999999999898, - "competitorP95Ms": 31.81999999999971, - "competitorGpuMs": 0.7028755, + "competitorMs": 27.970000000000255, + "competitorP95Ms": 32, + "competitorGpuMs": 0.6748544999999999, "competitorOverFrameBudget": true, "verdict": { "side": "competitor", - "ratio": 1.329140461215979, - "factor": 1.329140461215979, - "label": "competitor leads (1.33x)", + "ratio": 1.358598498391121, + "factor": 1.358598498391121, + "label": "competitor leads (1.36x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -4024,14 +3812,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 37, - "maxMs": 39.590000000000146, - "ratio": 1.0700000000000038 + "minMs": 37.01000000000022, + "maxMs": 40.13999999999942, + "ratio": 1.0845717373682566 }, "competitor": { - "minMs": 28.220000000000255, - "maxMs": 28.789999999999964, - "ratio": 1.0201984408221014 + "minMs": 27.639999999999418, + "maxMs": 28.100000000000364, + "ratio": 1.0166425470333196 }, "stable": true, "rungs": [ @@ -4053,19 +3841,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.47000000000025466, - "referenceP95Ms": 0.6599999999998545, - "referenceGpuMs": 0.13700050000000003, + "referenceMs": 0.5, + "referenceP95Ms": 0.7199999999993452, + "referenceGpuMs": 0.2287705, "referenceOverFrameBudget": false, - "competitorMs": 0.5, - "competitorP95Ms": 0.7400000000016007, - "competitorGpuMs": 0.1317705, + "competitorMs": 0.5199999999995271, + "competitorP95Ms": 0.7599999999983993, + "competitorGpuMs": 0.130792, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 0.9615384615393361, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/1 vs 1/2/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -4073,19 +3861,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.42000000000007276, - "maxMs": 0.47999999999956344, - "ratio": 1.1428571428559053 + "minMs": 0.4600000000009459, + "maxMs": 0.5599999999994907, + "ratio": 1.2173913043442157 }, "competitor": { - "minMs": 0.4400000000005093, - "maxMs": 0.7399999999997817, - "ratio": 1.681818181815739 + "minMs": 0.5099999999993088, + "maxMs": 0.5399999999990541, + "ratio": 1.058823529411345 }, - "stable": false, + "stable": true, "rungs": [ "level", - "exojs-leads", + "level", "level" ] } @@ -4102,17 +3890,17 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 2.680000000000291, - "referenceP95Ms": 6.479999999999563, - "referenceGpuMs": 0.142021, + "referenceMs": 2.540000000000873, + "referenceP95Ms": 6.919999999998254, + "referenceGpuMs": 0.1434375, "referenceOverFrameBudget": false, - "competitorMs": 2.5799999999981083, - "competitorP95Ms": 7.440000000002328, - "competitorGpuMs": 0.1590205, + "competitorMs": 2.390000000001237, + "competitorP95Ms": 6.979999999999563, + "competitorGpuMs": 0.15862500000000002, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.038759689923355, + "ratio": 1.0627615062759659, "factor": 1, "label": "level", "structural": false @@ -4122,14 +3910,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.5099999999983993, - "maxMs": 2.7300000000013824, - "ratio": 1.0876494023916827 + "minMs": 2.4799999999995634, + "maxMs": 2.680000000000291, + "ratio": 1.0806451612906303 }, "competitor": { - "minMs": 2.539999999999054, - "maxMs": 2.5899999999983265, - "ratio": 1.0196850393697996 + "minMs": 2.3699999999971624, + "maxMs": 2.5, + "ratio": 1.0548523206763685 }, "stable": true, "rungs": [ @@ -4157,18 +3945,18 @@ { "competitor": "pixi", "referenceMs": 0.13999999999941792, - "referenceP95Ms": 0.23999999999978172, - "referenceGpuMs": 0.589167, + "referenceP95Ms": 0.27999999999883585, + "referenceGpuMs": 0.58875, "referenceOverFrameBudget": false, - "competitorMs": 0.2000000000007276, - "competitorP95Ms": 0.3999999999996362, - "competitorGpuMs": 0.6553745, + "competitorMs": 0.18000000000029104, + "competitorP95Ms": 0.3600000000005821, + "competitorGpuMs": 0.634937, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.699999999994543, - "factor": 1.4285714285825653, - "label": "ExoJS leads (1.43x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -4176,19 +3964,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.1000000000003638, - "maxMs": 0.1400000000012369, - "ratio": 1.4000000000072759 + "minMs": 0.13999999999941792, + "maxMs": 0.15999999999985448, + "ratio": 1.1428571428608552 }, "competitor": { - "minMs": 0.15999999999985448, - "maxMs": 0.38000000000101863, - "ratio": 2.3750000000085265 + "minMs": 0.18000000000029104, + "maxMs": 0.1999999999989086, + "ratio": 1.1111111111032512 }, - "stable": true, + "stable": false, "rungs": [ "exojs-leads", - "exojs-leads", + "level", "exojs-leads" ] } @@ -4205,19 +3993,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.570000000001528, - "referenceP95Ms": 1, - "referenceGpuMs": 0.22197899999999998, + "referenceMs": 0.6399999999994179, + "referenceP95Ms": 1.4799999999995634, + "referenceGpuMs": 0.2210625, "referenceOverFrameBudget": false, - "competitorMs": 0.7600000000011278, - "competitorP95Ms": 1.0799999999999272, - "competitorGpuMs": 0.1250205, + "competitorMs": 0.680000000000291, + "competitorP95Ms": 1.0200000000004366, + "competitorGpuMs": 0.1242705, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.7500000000008975, - "factor": 1.3333333333317379, - "label": "ExoJS leads (1.33x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "competitor issues fewer texture binds (626 vs 314 per frame)", @@ -4225,20 +4013,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5400000000008731, - "maxMs": 0.5799999999999272, - "ratio": 1.0740740740722028 + "minMs": 0.5200000000004366, + "maxMs": 0.7999999999992724, + "ratio": 1.5384615384588476 }, "competitor": { - "minMs": 0.7399999999979627, - "maxMs": 0.7999999999992724, - "ratio": 1.0810810810830742 + "minMs": 0.5399999999999636, + "maxMs": 0.7000000000007276, + "ratio": 1.2962962962977311 }, - "stable": true, + "stable": false, "rungs": [ "exojs-leads", - "exojs-leads", - "exojs-leads" + "level", + "level" ] } } @@ -4254,19 +4042,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.42000000000189175, - "referenceP95Ms": 0.5199999999967986, - "referenceGpuMs": 0.11856249999999999, + "referenceMs": 0.4099999999998545, + "referenceP95Ms": 0.5399999999972351, + "referenceGpuMs": 0.11906249999999999, "referenceOverFrameBudget": false, - "competitorMs": 0.5499999999992724, - "competitorP95Ms": 0.7600000000020373, - "competitorGpuMs": 0.119646, + "competitorMs": 0.5599999999976717, + "competitorP95Ms": 0.680000000000291, + "competitorGpuMs": 0.072354, "competitorOverFrameBudget": false, "verdict": { - "side": "exojs", - "ratio": 0.7636363636408134, - "factor": 1.3095238095161787, - "label": "ExoJS leads (1.31x)", + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", "structural": false }, "mechanism": "competitor issues fewer texture binds (240 vs 159 per frame)", @@ -4274,20 +4062,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.41999999999825377, - "maxMs": 0.42000000000189175, - "ratio": 1.000000000008662 + "minMs": 0.37999999999919964, + "maxMs": 0.4399999999986903, + "ratio": 1.1578947368410974 }, "competitor": { - "minMs": 0.5400000000008731, - "maxMs": 0.7200000000011642, - "ratio": 1.3333333333333333 + "minMs": 0.3599999999969441, + "maxMs": 0.5600000000013097, + "ratio": 1.555555555572398 }, - "stable": true, + "stable": false, "rungs": [ "exojs-leads", "exojs-leads", - "exojs-leads" + "level" ] } } @@ -4309,12 +4097,12 @@ { "competitor": "pixi", "referenceMs": 0.11999999999898137, - "referenceP95Ms": 0.2000000000007276, - "referenceGpuMs": 0.12593749999999998, + "referenceP95Ms": 0.15999999999985448, + "referenceGpuMs": 0.125917, "referenceOverFrameBudget": false, "competitorMs": 0.18000000000029104, - "competitorP95Ms": 0.3600000000005821, - "competitorGpuMs": 0.1145, + "competitorP95Ms": 0.31999999999970896, + "competitorGpuMs": 0.11552100000000001, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", @@ -4328,14 +4116,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.11999999999898137, + "minMs": 0.10000000000218279, "maxMs": 0.11999999999898137, - "ratio": 1 + "ratio": 1.1999999999636202 }, "competitor": { - "minMs": 0.15999999999985448, - "maxMs": 0.3999999999978172, - "ratio": 2.4999999999886313 + "minMs": 0.18000000000029104, + "maxMs": 0.18000000000029104, + "ratio": 1 }, "stable": true, "rungs": [ @@ -4363,18 +4151,18 @@ { "competitor": "pixi", "referenceMs": 0.18000000000029104, - "referenceP95Ms": 0.2999999999992724, - "referenceGpuMs": 1.414479, + "referenceP95Ms": 0.2800000000024738, + "referenceGpuMs": 0.6382915, "referenceOverFrameBudget": false, - "competitorMs": 0.18000000000029104, - "competitorP95Ms": 0.31999999999970896, - "competitorGpuMs": 0.2457295, + "competitorMs": 0.2000000000007276, + "competitorP95Ms": 0.3400000000001455, + "competitorGpuMs": 0.245938, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 0.899999999998181, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -4382,19 +4170,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.13999999999941792, - "maxMs": 0.19999999999708962, - "ratio": 1.4285714285565796 + "minMs": 0.18000000000029104, + "maxMs": 0.2000000000007276, + "ratio": 1.1111111111133567 }, "competitor": { - "minMs": 0.17000000000007276, - "maxMs": 0.37999999999738066, - "ratio": 2.235294117630694 + "minMs": 0.2000000000007276, + "maxMs": 0.2000000000007276, + "ratio": 1 }, - "stable": false, + "stable": true, "rungs": [ "level", - "exojs-leads", + "level", "level" ] } @@ -4411,13 +4199,13 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.1500000000014552, - "referenceP95Ms": 0.27999999999883585, - "referenceGpuMs": 3.4957295000000004, + "referenceMs": 0.15999999999985448, + "referenceP95Ms": 0.2800000000024738, + "referenceGpuMs": 3.4241455, "referenceOverFrameBudget": false, "competitorMs": 0.18000000000029104, - "competitorP95Ms": 0.3400000000001455, - "competitorGpuMs": 0.366375, + "competitorP95Ms": 0.37999999999738066, + "competitorGpuMs": 0.36543749999999997, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -4431,19 +4219,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.11000000000058208, - "maxMs": 0.15999999999985448, - "ratio": 1.4545454545364347 + "minMs": 0.1400000000030559, + "maxMs": 0.17000000000007276, + "ratio": 1.2142857142597288 }, "competitor": { - "minMs": 0.13999999999941792, - "maxMs": 0.3400000000001455, - "ratio": 2.4285714285825653 + "minMs": 0.18000000000029104, + "maxMs": 0.19999999999708962, + "ratio": 1.1111111110931458 }, "stable": false, "rungs": [ "level", - "exojs-leads", + "level", "exojs-leads" ] } @@ -4460,19 +4248,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 2.7000000000007276, - "referenceP95Ms": 7.3400000000001455, - "referenceGpuMs": 1.49175, + "referenceMs": 2.8800000000010186, + "referenceP95Ms": 7.140000000003056, + "referenceGpuMs": 1.4768125, "referenceOverFrameBudget": false, - "competitorMs": 4.319999999999709, - "competitorP95Ms": 5.919999999998254, - "competitorGpuMs": 0.29112499999999997, + "competitorMs": 4.309999999999491, + "competitorP95Ms": 5.819999999999709, + "competitorGpuMs": 0.29060450000000004, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.6250000000002105, - "factor": 1.5999999999994612, - "label": "ExoJS leads (1.60x)", + "ratio": 0.6682134570768814, + "factor": 1.4965277777770716, + "label": "ExoJS leads (1.50x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/3 vs 1/2/3, none of them large enough to account for the gap); the difference is CPU-side", @@ -4480,14 +4268,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.6700000000000728, - "maxMs": 3.1300000000010186, - "ratio": 1.172284644195106 + "minMs": 2.75, + "maxMs": 2.9899999999997817, + "ratio": 1.087272727272648 }, "competitor": { - "minMs": 4.249999999998181, - "maxMs": 4.700000000000728, - "ratio": 1.105882352941821 + "minMs": 4.200000000000728, + "maxMs": 4.479999999999563, + "ratio": 1.066666666666378 }, "stable": true, "rungs": [ @@ -4509,19 +4297,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 7.290000000000873, - "referenceP95Ms": 9.399999999997817, - "referenceGpuMs": 3.8783125, + "referenceMs": 7.920000000000073, + "referenceP95Ms": 9.299999999999272, + "referenceGpuMs": 3.80725, "referenceOverFrameBudget": false, - "competitorMs": 19.57999999999811, - "competitorP95Ms": 24.42000000000189, - "competitorGpuMs": 0.800354, + "competitorMs": 19.170000000000073, + "competitorP95Ms": 23.399999999997817, + "competitorGpuMs": 0.45458350000000003, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.3723186925434922, - "factor": 2.6858710562408454, - "label": "ExoJS leads (2.69x)", + "ratio": 0.41314553990610553, + "factor": 2.420454545454532, + "label": "ExoJS leads (2.42x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/3 vs 1/2/3, none of them large enough to account for the gap); the difference is CPU-side", @@ -4529,14 +4317,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 7.240000000001601, - "maxMs": 8.089999999998327, - "ratio": 1.117403314916649 + "minMs": 7.1599999999998545, + "maxMs": 8.079999999999927, + "ratio": 1.1284916201117445 }, "competitor": { - "minMs": 19, - "maxMs": 19.7599999999984, - "ratio": 1.0399999999999157 + "minMs": 18.959999999999127, + "maxMs": 19.289999999999054, + "ratio": 1.0174050632911362 }, "stable": true, "rungs": [ @@ -4563,19 +4351,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.5400000000008731, - "referenceP95Ms": 0.8000000000029104, - "referenceGpuMs": 0.223082, + "referenceMs": 0.5, + "referenceP95Ms": 0.9000000000014552, + "referenceGpuMs": 0.20479199999999997, "referenceOverFrameBudget": false, - "competitorMs": 1.8999999999996362, - "competitorP95Ms": 2.680000000000291, - "competitorGpuMs": 0.2800835, + "competitorMs": 2.0900000000019645, + "competitorP95Ms": 3.900000000001455, + "competitorGpuMs": 0.2813745, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.28421052631630345, - "factor": 3.5185185185121557, - "label": "ExoJS leads (3.52x)", + "ratio": 0.23923444976054067, + "factor": 4.180000000003929, + "label": "ExoJS leads (4.18x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 3/6/3 vs 2/4/2, none of them large enough to account for the gap); the difference is CPU-side", @@ -4583,14 +4371,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.41999999999825377, + "minMs": 0.46000000000276486, "maxMs": 0.5400000000008731, - "ratio": 1.28571428572171 + "ratio": 1.173913043473103 }, "competitor": { - "minMs": 1.8400000000001455, - "maxMs": 2.3799999999991996, - "ratio": 1.2934782608690278 + "minMs": 2.0600000000013097, + "maxMs": 2.349999999998545, + "ratio": 1.1407766990276946 }, "stable": true, "rungs": [ @@ -4612,38 +4400,38 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.4400000000023283, - "referenceP95Ms": 0.680000000000291, - "referenceGpuMs": 0.235958, + "referenceMs": 0.4599999999991269, + "referenceP95Ms": 0.6000000000058208, + "referenceGpuMs": 0.234937, "referenceOverFrameBudget": false, - "competitorMs": 2.1700000000018917, - "competitorP95Ms": 3.779999999998836, - "competitorGpuMs": 0.449771, + "competitorMs": 2.360000000000582, + "competitorP95Ms": 3.4799999999959255, + "competitorGpuMs": 0.451791, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", - "structural": false + "side": "exojs", + "ratio": 0.1949152542368701, + "factor": 5.130434782619699, + "label": "ExoJS leads clearly (5.13x)", + "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 4/8/3 vs 3/6/3, none of them large enough to account for the gap); the difference is CPU-side", "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.3600000000005821, - "maxMs": 0.4500000000007276, - "ratio": 1.25 + "minMs": 0.4399999999986903, + "maxMs": 0.4599999999991269, + "ratio": 1.045454545455673 }, "competitor": { - "minMs": 2, - "maxMs": 2.400000000001455, - "ratio": 1.2000000000007276 + "minMs": 2.1999999999970896, + "maxMs": 2.7400000000016007, + "ratio": 1.2454545454569206 }, - "stable": false, + "stable": true, "rungs": [ - "exojs-leads", + "exojs-leads-clearly", "exojs-leads-clearly", "exojs-leads-clearly" ] @@ -4661,13 +4449,13 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.42000000000189175, - "referenceP95Ms": 0.6200000000026193, - "referenceGpuMs": 0.30972900000000003, + "referenceMs": 0.4400000000023283, + "referenceP95Ms": 0.6599999999962165, + "referenceGpuMs": 0.3148955, "referenceOverFrameBudget": false, - "competitorMs": 2.1699999999982538, - "competitorP95Ms": 3.720000000001164, - "competitorGpuMs": 0.8490215, + "competitorMs": 2.1999999999970896, + "competitorP95Ms": 3.819999999999709, + "competitorGpuMs": 0.7915205, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -4681,20 +4469,20 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.3600000000005821, - "maxMs": 0.4400000000023283, - "ratio": 1.2222222222267136 + "minMs": 0.4400000000023283, + "maxMs": 0.4599999999991269, + "ratio": 1.045454545447029 }, "competitor": { - "minMs": 1.9199999999982538, - "maxMs": 2.9099999999998545, - "ratio": 1.5156250000013027 + "minMs": 2.100000000002183, + "maxMs": 2.4099999999998545, + "ratio": 1.1476190476177854 }, "stable": false, "rungs": [ - "exojs-leads", "exojs-leads-clearly", - "exojs-leads-clearly" + "exojs-leads", + "exojs-leads" ] } } @@ -4710,19 +4498,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.4599999999991269, - "referenceP95Ms": 0.6999999999970896, - "referenceGpuMs": 0.3076675, + "referenceMs": 0.4500000000007276, + "referenceP95Ms": 0.7199999999938882, + "referenceGpuMs": 0.3193745, "referenceOverFrameBudget": false, - "competitorMs": 0.75, - "competitorP95Ms": 1.180000000000291, - "competitorGpuMs": 0.47770799999999997, + "competitorMs": 0.7599999999983993, + "competitorP95Ms": 1.2200000000011642, + "competitorGpuMs": 0.42333350000000003, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.6133333333321692, - "factor": 1.6304347826117902, - "label": "ExoJS leads (1.63x)", + "ratio": 0.5921052631600991, + "factor": 1.6888888888826012, + "label": "ExoJS leads (1.69x)", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (5 vs 8 per frame)", @@ -4730,14 +4518,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.4500000000007276, - "maxMs": 0.5199999999967986, - "ratio": 1.1555555555465729 + "minMs": 0.42000000000189175, + "maxMs": 0.5600000000013097, + "ratio": 1.333333333330446 }, "competitor": { - "minMs": 0.6999999999970896, - "maxMs": 1.3699999999989814, - "ratio": 1.957142857149539 + "minMs": 0.7299999999995634, + "maxMs": 0.9199999999982538, + "ratio": 1.2602739726011014 }, "stable": true, "rungs": [ @@ -4760,12 +4548,12 @@ { "competitor": "pixi", "referenceMs": 0.3600000000005821, - "referenceP95Ms": 0.5200000000040745, - "referenceGpuMs": 0.3171875, + "referenceP95Ms": 0.5999999999985448, + "referenceGpuMs": 0.30210349999999997, "referenceOverFrameBudget": false, - "competitorMs": 0.3000000000029104, - "competitorP95Ms": 0.5199999999967986, - "competitorGpuMs": 0.153042, + "competitorMs": 0.31999999999970896, + "competitorP95Ms": 0.5, + "competitorGpuMs": 0.1531245, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -4779,20 +4567,70 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.3000000000029104, - "maxMs": 0.3899999999994179, - "ratio": 1.2999999999854481 + "minMs": 0.3499999999985448, + "maxMs": 0.3999999999978172, + "ratio": 1.142857142855658 }, "competitor": { - "minMs": 0.2400000000016007, - "maxMs": 0.5200000000004366, - "ratio": 2.166666666654035 + "minMs": 0.2800000000024738, + "maxMs": 0.41999999999825377, + "ratio": 1.499999999980511 }, "stable": false, "rungs": [ + "level", "competitor-leads", - "exojs-leads", - "competitor-leads" + "level" + ] + } + } + ] + }, + { + "archetype": "fx-blur", + "category": "Render targets", + "count": 720, + "loadId": "720", + "unit": "viewport", + "primary": true, + "label": "1280 x 720", + "cells": [ + { + "competitor": "pixi", + "referenceMs": 0.42000000000552973, + "referenceP95Ms": 0.5800000000017462, + "referenceGpuMs": 0.313563, + "referenceOverFrameBudget": false, + "competitorMs": 0.47999999999592546, + "competitorP95Ms": 0.7399999999979627, + "competitorGpuMs": 0.33620700000000003, + "competitorOverFrameBudget": false, + "verdict": { + "side": "neither", + "ratio": null, + "factor": null, + "label": "unstable across runs", + "structural": false + }, + "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 4/8/5 vs 3/6/3, none of them large enough to account for the gap); the difference is CPU-side", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 0.3800000000046566, + "maxMs": 0.43999999999505235, + "ratio": 1.157894736814896 + }, + "competitor": { + "minMs": 0.4599999999991269, + "maxMs": 0.47999999999956344, + "ratio": 1.0434782608705968 + }, + "stable": false, + "rungs": [ + "level", + "level", + "exojs-leads" ] } } @@ -4808,19 +4646,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.3500000000021828, - "referenceP95Ms": 0.5, - "referenceGpuMs": 0.32100049999999997, + "referenceMs": 0.37999999999738066, + "referenceP95Ms": 0.6199999999953434, + "referenceGpuMs": 0.26908299999999996, "referenceOverFrameBudget": false, - "competitorMs": 0.4599999999991269, - "competitorP95Ms": 0.7599999999947613, - "competitorGpuMs": 0.121666, + "competitorMs": 0.42000000000552973, + "competitorP95Ms": 0.7199999999938882, + "competitorGpuMs": 0.0922915, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 0.9047619047437561, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "ExoJS issues fewer draw calls (4 vs 10 per frame)", @@ -4828,19 +4666,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.33999999999650754, - "maxMs": 0.3600000000005821, - "ratio": 1.0588235294243529 + "minMs": 0.36999999999898137, + "maxMs": 0.4000000000014552, + "ratio": 1.0810810810879903 }, "competitor": { - "minMs": 0.3400000000001455, - "maxMs": 0.6300000000010186, - "ratio": 1.8529411764727912 + "minMs": 0.42000000000189175, + "maxMs": 0.4400000000023283, + "ratio": 1.0476190476198726 }, - "stable": false, + "stable": true, "rungs": [ - "exojs-leads", - "exojs-leads", + "level", + "level", "level" ] } @@ -4862,13 +4700,13 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.23999999999978172, - "referenceP95Ms": 1.4600000000027649, - "referenceGpuMs": 0.101021, + "referenceMs": 0.25, + "referenceP95Ms": 1.5600000000013097, + "referenceGpuMs": 0.100583, "referenceOverFrameBudget": false, - "competitorMs": 0.19999999999708962, - "competitorP95Ms": 0.3600000000005821, - "competitorGpuMs": 0.097458, + "competitorMs": 0.2000000000007276, + "competitorP95Ms": 0.3400000000001455, + "competitorGpuMs": 0.097875, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -4878,23 +4716,23 @@ "structural": false }, "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", - "timer": "limited", + "timer": "resolved", "aggregate": { "runs": 3, "reference": { - "minMs": 0.18000000000029104, - "maxMs": 0.2400000000016007, - "ratio": 1.3333333333400703 + "minMs": 0.23999999999978172, + "maxMs": 0.2800000000024738, + "ratio": 1.1666666666780354 }, "competitor": { - "minMs": 0.17000000000189175, - "maxMs": 0.36999999999898137, - "ratio": 2.1764705882050825 + "minMs": 0.2000000000007276, + "maxMs": 0.21999999999752617, + "ratio": 1.0999999999836292 }, "stable": false, "rungs": [ "competitor-leads", - "exojs-leads", + "competitor-leads", "level" ] } @@ -4916,18 +4754,18 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.2599999999983993, - "referenceP95Ms": 1.4799999999959255, - "referenceGpuMs": 0.067771, + "referenceMs": 0.26000000000203727, + "referenceP95Ms": 1.4400000000023283, + "referenceGpuMs": 0.0677085, "referenceOverFrameBudget": false, - "competitorMs": 0.41999999999825377, - "competitorP95Ms": 0.8399999999965075, - "competitorGpuMs": 0.076708, + "competitorMs": 0.42000000000189175, + "competitorP95Ms": 0.7000000000043656, + "competitorGpuMs": 0.079042, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.6190476190463816, - "factor": 1.6153846153878444, + "ratio": 0.6190476190496814, + "factor": 1.6153846153792337, "label": "ExoJS leads (1.62x)", "structural": false }, @@ -4936,14 +4774,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.22000000000116415, - "maxMs": 0.26000000000203727, - "ratio": 1.1818181818211884 + "minMs": 0.26000000000203727, + "maxMs": 0.27999999999883585, + "ratio": 1.076923076910161 }, "competitor": { - "minMs": 0.37999999999738066, - "maxMs": 0.6399999999994179, - "ratio": 1.6842105263258669 + "minMs": 0.4000000000014552, + "maxMs": 0.5300000000024738, + "ratio": 1.3250000000013642 }, "stable": true, "rungs": [ @@ -4965,34 +4803,34 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.20999999999912689, + "referenceMs": 0.18000000000029104, "referenceP95Ms": 0.8400000000037835, - "referenceGpuMs": 0.06789600000000001, + "referenceGpuMs": 0.066458, "referenceOverFrameBudget": false, - "competitorMs": 0.4400000000023283, - "competitorP95Ms": 0.6999999999970896, - "competitorGpuMs": 0.0767505, + "competitorMs": 0.42999999999665306, + "competitorP95Ms": 0.6399999999994179, + "competitorGpuMs": 0.0762915, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.4772727272682174, - "factor": 2.0952380952578937, - "label": "ExoJS leads (2.10x)", + "ratio": 0.4186046511667258, + "factor": 2.388888888866432, + "label": "ExoJS leads (2.39x)", "structural": false }, "mechanism": "competitor issues fewer draw calls (60 vs 2 per frame)", - "timer": "resolved", + "timer": "limited", "aggregate": { "runs": 3, "reference": { - "minMs": 0.2000000000007276, - "maxMs": 0.22999999999956344, - "ratio": 1.1499999999936334 + "minMs": 0.18000000000029104, + "maxMs": 0.26000000000203727, + "ratio": 1.444444444453427 }, "competitor": { - "minMs": 0.31999999999970896, - "maxMs": 0.48999999999796273, - "ratio": 1.5312499999950262 + "minMs": 0.42000000000189175, + "maxMs": 0.4599999999991269, + "ratio": 1.0952380952310832 }, "stable": true, "rungs": [ @@ -5014,19 +4852,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.6899999999986903, - "referenceP95Ms": 1.319999999999709, - "referenceGpuMs": 0.0669585, + "referenceMs": 0.8600000000005821, + "referenceP95Ms": 1.3800000000046566, + "referenceGpuMs": 0.06756200000000001, "referenceOverFrameBudget": false, - "competitorMs": 2.3300000000017462, - "competitorP95Ms": 3.3800000000046566, - "competitorGpuMs": 0.069917, + "competitorMs": 1.8699999999989814, + "competitorP95Ms": 3.1200000000026193, + "competitorGpuMs": 0.06975, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.29613733905500994, - "factor": 3.376811594211839, - "label": "ExoJS leads (3.38x)", + "ratio": 0.459893048128904, + "factor": 2.1744186046485066, + "label": "ExoJS leads (2.17x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 2/4/2 vs 2/4/3, none of them large enough to account for the gap); the difference is CPU-side", @@ -5034,14 +4872,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.6699999999982538, - "maxMs": 0.9099999999998545, - "ratio": 1.3582089552272034 + "minMs": 0.8599999999969441, + "maxMs": 1.0400000000008731, + "ratio": 1.2093023255867077 }, "competitor": { - "minMs": 1.7000000000007276, - "maxMs": 2.390000000003056, - "ratio": 1.4058823529423723 + "minMs": 1.2000000000043656, + "maxMs": 2.2699999999967986, + "ratio": 1.891666666657117 }, "stable": true, "rungs": [ @@ -5068,13 +4906,13 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.2800000000024738, - "referenceP95Ms": 0.4400000000023283, - "referenceGpuMs": 0.07079150000000001, + "referenceMs": 0.27999999999883585, + "referenceP95Ms": 0.41999999999825377, + "referenceGpuMs": 0.070042, "referenceOverFrameBudget": false, - "competitorMs": 0.26000000000203727, - "competitorP95Ms": 0.5, - "competitorGpuMs": 0.0683335, + "competitorMs": 0.27999999999883585, + "competitorP95Ms": 0.4400000000023283, + "competitorGpuMs": 0.06852050000000001, "competitorOverFrameBudget": false, "verdict": { "side": "neither", @@ -5088,19 +4926,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.2599999999947613, - "maxMs": 0.3000000000029104, - "ratio": 1.1538461538805962 + "minMs": 0.27999999999883585, + "maxMs": 0.2999999999956344, + "ratio": 1.0714285714174347 }, "competitor": { - "minMs": 0.22999999999956344, - "maxMs": 0.4400000000023283, - "ratio": 1.9130434782746237 + "minMs": 0.26000000000203727, + "maxMs": 0.3600000000005821, + "ratio": 1.384615384606774 }, "stable": false, "rungs": [ - "level", "exojs-leads", + "level", "level" ] } @@ -5117,19 +4955,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.4599999999991269, - "referenceP95Ms": 0.5799999999944703, - "referenceGpuMs": 0.146333, + "referenceMs": 0.4400000000023283, + "referenceP95Ms": 0.5800000000017462, + "referenceGpuMs": 0.1442495, "referenceOverFrameBudget": false, - "competitorMs": 0.23999999999796273, - "competitorP95Ms": 0.41999999999825377, - "competitorGpuMs": 0.232042, + "competitorMs": 0.25, + "competitorP95Ms": 0.4599999999991269, + "competitorGpuMs": 0.23293750000000002, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 1.7600000000093132, + "factor": 1.7600000000093132, + "label": "competitor leads (1.76x)", "structural": false }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -5138,18 +4976,18 @@ "runs": 3, "reference": { "minMs": 0.43999999999505235, - "maxMs": 0.47999999999592546, - "ratio": 1.0909090909120975 + "maxMs": 0.4599999999991269, + "ratio": 1.045454545464317 }, "competitor": { - "minMs": 0.22000000000116415, - "maxMs": 0.4599999999991269, - "ratio": 2.090909090894058 + "minMs": 0.23999999999796273, + "maxMs": 0.27999999999883585, + "ratio": 1.1666666666717194 }, - "stable": false, + "stable": true, "rungs": [ "competitor-leads", - "level", + "competitor-leads", "competitor-leads" ] } @@ -5166,19 +5004,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 2.109999999996944, - "referenceP95Ms": 2.5199999999967986, - "referenceGpuMs": 0.38845850000000004, + "referenceMs": 2.030000000002474, + "referenceP95Ms": 2.959999999999127, + "referenceGpuMs": 0.623188, "referenceOverFrameBudget": false, - "competitorMs": 0.26000000000203727, - "competitorP95Ms": 0.4000000000014552, - "competitorGpuMs": 0.261021, + "competitorMs": 0.3099999999976717, + "competitorP95Ms": 0.47999999999592546, + "competitorGpuMs": 0.2931875, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 8.115384615309273, - "factor": 8.115384615309273, - "label": "competitor leads clearly (8.12x)", + "ratio": 6.548387096831356, + "factor": 6.548387096831356, + "label": "competitor leads clearly (6.55x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/2 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -5186,14 +5024,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 2.0200000000004366, - "maxMs": 2.179999999996653, - "ratio": 1.079207920790189 + "minMs": 1.9099999999998545, + "maxMs": 2.139999999999418, + "ratio": 1.1204188481673198 }, "competitor": { - "minMs": 0.20999999999912689, - "maxMs": 0.37999999999738066, - "ratio": 1.80952380951886 + "minMs": 0.27999999999883585, + "maxMs": 0.32999999999810825, + "ratio": 1.1785714285695725 }, "stable": true, "rungs": [ @@ -5215,19 +5053,19 @@ "cells": [ { "competitor": "pixi", - "referenceMs": 0.680000000000291, - "referenceP95Ms": 0.8600000000005821, - "referenceGpuMs": 0.22912500000000002, + "referenceMs": 0.6999999999970896, + "referenceP95Ms": 0.8400000000037835, + "referenceGpuMs": 0.229396, "referenceOverFrameBudget": false, - "competitorMs": 0.9200000000018917, - "competitorP95Ms": 1.180000000000291, - "competitorGpuMs": 0.138229, + "competitorMs": 0.9599999999991269, + "competitorP95Ms": 1.2000000000043656, + "competitorGpuMs": 0.1384795, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.7391304347814052, - "factor": 1.3529411764727912, - "label": "ExoJS leads (1.35x)", + "ratio": 0.7291666666642982, + "factor": 1.371428571433026, + "label": "ExoJS leads (1.37x)", "structural": false }, "mechanism": "competitor issues fewer draw calls (90 vs 1 per frame)", @@ -5235,14 +5073,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.6600000000034925, - "maxMs": 0.6999999999970896, - "ratio": 1.0606060605960386 + "minMs": 0.6900000000023283, + "maxMs": 0.7099999999991269, + "ratio": 1.0289855072416392 }, "competitor": { - "minMs": 0.9199999999982538, - "maxMs": 1.0799999999981083, - "ratio": 1.1739130434784328 + "minMs": 0.9399999999986903, + "maxMs": 1.0400000000008731, + "ratio": 1.1063829787258745 }, "stable": true, "rungs": [ @@ -5270,18 +5108,18 @@ { "competitor": "pixi", "referenceMs": 0.6100000000005821, - "referenceP95Ms": 0.8600000000005821, - "referenceGpuMs": 0.30239649999999996, + "referenceP95Ms": 0.9199999999982538, + "referenceGpuMs": 0.175959, "referenceOverFrameBudget": false, - "competitorMs": 16.660000000003492, - "competitorP95Ms": 21, - "competitorGpuMs": 0.294688, - "competitorOverFrameBudget": false, + "competitorMs": 17.63000000000102, + "competitorP95Ms": 22.099999999998545, + "competitorGpuMs": 0.293354, + "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.0366146458583706, - "factor": 27.31147540981573, - "label": "ExoJS leads clearly (27.31x)", + "ratio": 0.03460011344302591, + "factor": 28.90163934423639, + "label": "ExoJS leads clearly (28.90x)", "structural": true }, "mechanism": "structurally equivalent at this scale (draw/bind/upload per frame: 1/2/0 vs 1/2/1, none of them large enough to account for the gap); the difference is CPU-side", @@ -5289,14 +5127,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 0.5, - "maxMs": 0.6200000000026193, - "ratio": 1.2400000000052387 + "minMs": 0.5599999999976717, + "maxMs": 0.6100000000005821, + "ratio": 1.0892857142912826 }, "competitor": { - "minMs": 16.280000000002474, - "maxMs": 17.74000000000524, - "ratio": 1.089680589680746 + "minMs": 16.61999999999898, + "maxMs": 17.68000000000029, + "ratio": 1.0637785800241502 }, "stable": true, "rungs": [ @@ -5307,6 +5145,55 @@ } } ] + }, + { + "archetype": "ui-layout-update", + "category": "Interaction", + "count": 1000, + "loadId": "1k", + "unit": "widgets", + "primary": true, + "cells": [ + { + "competitor": "pixi", + "referenceMs": 10.2599999999984, + "referenceP95Ms": 10.780000000006112, + "referenceGpuMs": 0.0336665, + "referenceOverFrameBudget": false, + "competitorMs": 48.62000000000262, + "competitorP95Ms": 52.599999999998545, + "competitorGpuMs": 0.033708, + "competitorOverFrameBudget": true, + "verdict": { + "side": "exojs", + "ratio": 0.21102426984775496, + "factor": 4.738791423002944, + "label": "ExoJS leads (4.74x)", + "structural": false + }, + "mechanism": "neither arm submits anything for this scene (draw/bind/upload per frame: 0/0/0 vs 0/0/1); the whole difference is the per-frame work itself", + "timer": "resolved", + "aggregate": { + "runs": 3, + "reference": { + "minMs": 10.220000000001164, + "maxMs": 10.400000000001455, + "ratio": 1.017612524461866 + }, + "competitor": { + "minMs": 48.36000000000058, + "maxMs": 48.70000000000073, + "ratio": 1.0070306038048003 + }, + "stable": true, + "rungs": [ + "exojs-leads", + "exojs-leads", + "exojs-leads" + ] + } + } + ] } ] } @@ -5339,10 +5226,6 @@ { "archetype": "mixed-material-atlased", "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" - }, - { - "archetype": "fx-blur", - "reason": "ExoJS-internal structural probe: a competitor arm renders a different scene here, so a wall-clock comparison would not be like for like" } ], "webgl1": [] @@ -5372,7 +5255,7 @@ }, "fixedDelta": 0.016666666666666666, "clock": { - "resolutionMs": 0.01999999999999602, + "resolutionMs": 0.019999999999999574, "crossOriginIsolated": true }, "caveats": [ @@ -5389,7 +5272,7 @@ "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." ], "engineVersion": "0.17.0", - "timestamp": "2026-09-12T03:21:55.258Z" + "timestamp": "2026-09-12T04:20:58.243Z" }, { "browser": "webkit", @@ -5412,7 +5295,7 @@ }, "fixedDelta": 0.016666666666666666, "clock": { - "resolutionMs": 0.01999999999999602, + "resolutionMs": 0.019999999999999574, "crossOriginIsolated": true }, "caveats": [ @@ -5429,7 +5312,7 @@ "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." ], "engineVersion": "0.17.0", - "timestamp": "2026-09-12T03:34:01.941Z" + "timestamp": "2026-09-12T04:32:46.548Z" }, { "browser": "webkit", @@ -5452,7 +5335,7 @@ }, "fixedDelta": 0.016666666666666666, "clock": { - "resolutionMs": 0.01999999999999602, + "resolutionMs": 0.019999999999999574, "crossOriginIsolated": true }, "caveats": [ @@ -5469,7 +5352,7 @@ "nape-js arm (pure-JS peer): default single-step solver with default velocity/position iterations (10/10), sleeping and dynamic AABB broadphase at library defaults. Materials, body layouts, joints and perturbations come from the shared neutral scene descriptor. Contact count = active collision arbiters; ray queries use Space.rayCast with outer-surface semantics. These counters are engine-specific structural proxies, not a claim that every solver performs identical internal work." ], "engineVersion": "0.17.0", - "timestamp": "2026-09-12T03:46:13.242Z" + "timestamp": "2026-09-12T04:44:36.717Z" } ], "libraries": [ @@ -5507,19 +5390,19 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 5.139999999999873, - "referenceP95Ms": 6.019999999999982, + "referenceMs": 5.350000000000136, + "referenceP95Ms": 6.120000000000118, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 11.139999999999418, - "competitorP95Ms": 12.099999999998545, + "competitorMs": 10.290000000000873, + "competitorP95Ms": 12.119999999998981, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.46140035906643995, - "factor": 2.1673151750972166, - "label": "ExoJS leads (2.17x)", + "ratio": 0.5199222546161013, + "factor": 1.9233644859814225, + "label": "ExoJS leads (1.92x)", "structural": false }, "mechanism": "ExoJS resolves fewer solved contacts (5501 vs 12297); the counters are not semantically identical across arms (see the run's caveats)", @@ -5527,14 +5410,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.059999999999945, - "maxMs": 5.400000000000091, - "ratio": 1.0671936758893577 + "minMs": 5.3400000000001455, + "maxMs": 5.460000000000036, + "ratio": 1.0224719101123385 }, "competitor": { - "minMs": 10.380000000001019, - "maxMs": 11.200000000000728, - "ratio": 1.0789980732176907 + "minMs": 10.180000000000291, + "maxMs": 11.140000000001237, + "ratio": 1.094302554027595 }, "stable": true, "rungs": [ @@ -5546,18 +5429,18 @@ }, { "competitor": "nape-js", - "referenceMs": 5.139999999999873, - "referenceP95Ms": 6.019999999999982, + "referenceMs": 5.350000000000136, + "referenceP95Ms": 6.120000000000118, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 9.639999999999418, - "competitorP95Ms": 12.660000000003492, + "competitorMs": 10.080000000001746, + "competitorP95Ms": 12.15999999998894, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.533195020746907, - "factor": 1.8754863813228904, + "ratio": 0.5307539682538899, + "factor": 1.8841121495329884, "label": "ExoJS leads (1.88x)", "structural": false }, @@ -5566,14 +5449,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.059999999999945, - "maxMs": 5.400000000000091, - "ratio": 1.0671936758893577 + "minMs": 5.3400000000001455, + "maxMs": 5.460000000000036, + "ratio": 1.0224719101123385 }, "competitor": { - "minMs": 9.540000000008149, - "maxMs": 10.020000000004075, - "ratio": 1.050314465408335 + "minMs": 10.009999999994761, + "maxMs": 10.220000000001164, + "ratio": 1.0209790209796716 }, "stable": true, "rungs": [ @@ -5585,19 +5468,19 @@ }, { "competitor": "planck", - "referenceMs": 5.139999999999873, - "referenceP95Ms": 6.019999999999982, + "referenceMs": 5.350000000000136, + "referenceP95Ms": 6.120000000000118, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 29.5099999999984, - "competitorP95Ms": 36.61999999999534, + "competitorMs": 29.81999999999971, + "competitorP95Ms": 36.4800000000032, "competitorGpuMs": null, "competitorOverFrameBudget": true, "verdict": { "side": "exojs", - "ratio": 0.1741782446628313, - "factor": 5.741245136186601, - "label": "ExoJS leads clearly (5.74x)", + "ratio": 0.17940979208585475, + "factor": 5.573831775700738, + "label": "ExoJS leads clearly (5.57x)", "structural": true }, "mechanism": "ExoJS resolves fewer solved contacts (5501 vs 10677); the counters are not semantically identical across arms (see the run's caveats)", @@ -5605,14 +5488,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.059999999999945, - "maxMs": 5.400000000000091, - "ratio": 1.0671936758893577 + "minMs": 5.3400000000001455, + "maxMs": 5.460000000000036, + "ratio": 1.0224719101123385 }, "competitor": { - "minMs": 29.479999999999563, - "maxMs": 30.360000000000582, - "ratio": 1.0298507462686917 + "minMs": 29.450000000000728, + "maxMs": 30.470000000004802, + "ratio": 1.0346349745332444 }, "stable": true, "rungs": [ @@ -5624,19 +5507,19 @@ }, { "competitor": "rapier", - "referenceMs": 5.139999999999873, - "referenceP95Ms": 6.019999999999982, + "referenceMs": 5.350000000000136, + "referenceP95Ms": 6.120000000000118, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 4.290000000000873, - "competitorP95Ms": 5.360000000000582, + "competitorMs": 4.080000000001746, + "competitorP95Ms": 5.320000000006985, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { - "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "side": "competitor", + "ratio": 1.3112745098033938, + "factor": 1.3112745098033938, + "label": "competitor leads (1.31x)", "structural": false }, "mechanism": "ExoJS resolves fewer solved contacts (5501 vs 10045); the counters are not semantically identical across arms (see the run's caveats)", @@ -5644,19 +5527,19 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.059999999999945, - "maxMs": 5.400000000000091, - "ratio": 1.0671936758893577 + "minMs": 5.3400000000001455, + "maxMs": 5.460000000000036, + "ratio": 1.0224719101123385 }, "competitor": { - "minMs": 4.05000000000291, - "maxMs": 4.310000000004948, - "ratio": 1.0641975308646545 + "minMs": 4.059999999997672, + "maxMs": 4.0899999999965075, + "ratio": 1.0073891625612938 }, - "stable": false, + "stable": true, "rungs": [ - "level", - "level", + "competitor-leads", + "competitor-leads", "competitor-leads" ] } @@ -5673,17 +5556,17 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 5.440000000000509, - "referenceP95Ms": 6.460000000000036, + "referenceMs": 5.460000000000036, + "referenceP95Ms": 6.5, "referenceGpuMs": null, "referenceOverFrameBudget": false, "competitorMs": 5.240000000001601, - "competitorP95Ms": 5.979999999999563, + "competitorP95Ms": 6.219999999997526, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.0381679389310778, + "ratio": 1.041984732824116, "factor": 1, "label": "level", "structural": false @@ -5693,14 +5576,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.380000000000109, - "maxMs": 5.479999999999563, - "ratio": 1.0185873605946938 + "minMs": 5.4399999999996, + "maxMs": 5.559999999999945, + "ratio": 1.022058823529477 }, "competitor": { - "minMs": 5.220000000001164, - "maxMs": 5.299999999999272, - "ratio": 1.0153256704977185 + "minMs": 5.240000000001601, + "maxMs": 5.400000000001455, + "ratio": 1.0305343511450011 }, "stable": true, "rungs": [ @@ -5712,17 +5595,17 @@ }, { "competitor": "nape-js", - "referenceMs": 5.440000000000509, - "referenceP95Ms": 6.460000000000036, + "referenceMs": 5.460000000000036, + "referenceP95Ms": 6.5, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.860000000000582, - "competitorP95Ms": 6.680000000007567, + "competitorMs": 5.8399999999965075, + "competitorP95Ms": 6.69999999999709, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9283276450511893, + "ratio": 0.9349315068498804, "factor": 1, "label": "level", "structural": false @@ -5732,14 +5615,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.380000000000109, - "maxMs": 5.479999999999563, - "ratio": 1.0185873605946938 + "minMs": 5.4399999999996, + "maxMs": 5.559999999999945, + "ratio": 1.022058823529477 }, "competitor": { "minMs": 5.819999999992433, - "maxMs": 5.880000000004657, - "ratio": 1.010309278352629 + "maxMs": 5.899999999994179, + "ratio": 1.0137457044676719 }, "stable": true, "rungs": [ @@ -5751,17 +5634,17 @@ }, { "competitor": "planck", - "referenceMs": 5.440000000000509, - "referenceP95Ms": 6.460000000000036, + "referenceMs": 5.460000000000036, + "referenceP95Ms": 6.5, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.620000000002619, - "competitorP95Ms": 6.419999999998254, + "competitorMs": 5.819999999999709, + "competitorP95Ms": 6.619999999995343, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9679715302487498, + "ratio": 0.9381443298969604, "factor": 1, "label": "level", "structural": false @@ -5771,14 +5654,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.380000000000109, - "maxMs": 5.479999999999563, - "ratio": 1.0185873605946938 + "minMs": 5.4399999999996, + "maxMs": 5.559999999999945, + "ratio": 1.022058823529477 }, "competitor": { - "minMs": 5.440000000002328, - "maxMs": 5.739999999997963, - "ratio": 1.0551470588227032 + "minMs": 5.580000000001746, + "maxMs": 5.999999999996362, + "ratio": 1.0752688172033127 }, "stable": true, "rungs": [ @@ -5790,8 +5673,8 @@ }, { "competitor": "rapier", - "referenceMs": 5.440000000000509, - "referenceP95Ms": 6.460000000000036, + "referenceMs": 5.460000000000036, + "referenceP95Ms": 6.5, "referenceGpuMs": null, "referenceOverFrameBudget": false, "competitorMs": 1.320000000006985, @@ -5800,9 +5683,9 @@ "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 4.121212121190699, - "factor": 4.121212121190699, - "label": "competitor leads (4.12x)", + "ratio": 4.136363636341776, + "factor": 4.136363636341776, + "label": "competitor leads (4.14x)", "structural": false }, "mechanism": "identical scene (1504 bodies, 4236 contacts, 0 joints on both arms); the difference is solver cost", @@ -5810,14 +5693,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.380000000000109, - "maxMs": 5.479999999999563, - "ratio": 1.0185873605946938 + "minMs": 5.4399999999996, + "maxMs": 5.559999999999945, + "ratio": 1.022058823529477 }, "competitor": { - "minMs": 1.3000000000029104, - "maxMs": 1.360000000000582, - "ratio": 1.0461538461519517 + "minMs": 1.319999999999709, + "maxMs": 1.3399999999965075, + "ratio": 1.015151515149093 }, "stable": true, "rungs": [ @@ -5839,19 +5722,19 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 4.579999999999927, - "referenceP95Ms": 5.020000000000437, + "referenceMs": 4.539999999999964, + "referenceP95Ms": 4.839999999999236, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 2.2000000000007276, - "competitorP95Ms": 2.359999999996944, + "competitorMs": 2.1199999999989814, + "competitorP95Ms": 2.319999999999709, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 2.0818181818174604, - "factor": 2.0818181818174604, - "label": "competitor leads (2.08x)", + "ratio": 2.141509433963276, + "factor": 2.141509433963276, + "label": "competitor leads (2.14x)", "structural": false }, "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms); the difference is solver cost", @@ -5859,14 +5742,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.539999999999964, - "maxMs": 4.619999999999891, - "ratio": 1.0176211453744335 + "minMs": 4.520000000000437, + "maxMs": 4.579999999999927, + "ratio": 1.0132743362830718 }, "competitor": { - "minMs": 2.1200000000026193, - "maxMs": 2.220000000001164, - "ratio": 1.04716981132001 + "minMs": 2.1199999999989814, + "maxMs": 2.2400000000016007, + "ratio": 1.0566037735861684 }, "stable": true, "rungs": [ @@ -5878,17 +5761,17 @@ }, { "competitor": "nape-js", - "referenceMs": 4.579999999999927, - "referenceP95Ms": 5.020000000000437, + "referenceMs": 4.539999999999964, + "referenceP95Ms": 4.839999999999236, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.019999999989523, - "competitorP95Ms": 6.279999999998836, + "competitorMs": 4.899999999994179, + "competitorP95Ms": 6.039999999993597, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9123505976114514, + "ratio": 0.9265306122459912, "factor": 1, "label": "level", "structural": false @@ -5898,14 +5781,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.539999999999964, - "maxMs": 4.619999999999891, - "ratio": 1.0176211453744335 + "minMs": 4.520000000000437, + "maxMs": 4.579999999999927, + "ratio": 1.0132743362830718 }, "competitor": { - "minMs": 4.990000000005239, - "maxMs": 5.099999999991269, - "ratio": 1.02204408817353 + "minMs": 4.879999999997381, + "maxMs": 5.059999999997672, + "ratio": 1.0368852459017188 }, "stable": true, "rungs": [ @@ -5917,19 +5800,19 @@ }, { "competitor": "planck", - "referenceMs": 4.579999999999927, - "referenceP95Ms": 5.020000000000437, + "referenceMs": 4.539999999999964, + "referenceP95Ms": 4.839999999999236, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 7.779999999998836, - "competitorP95Ms": 11.040000000000873, + "competitorMs": 7.770000000000437, + "competitorP95Ms": 10.540000000000873, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.5886889460155029, - "factor": 1.6986899563316504, - "label": "ExoJS leads (1.70x)", + "ratio": 0.5842985842985468, + "factor": 1.711453744493502, + "label": "ExoJS leads (1.71x)", "structural": false }, "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms); the difference is solver cost", @@ -5937,14 +5820,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.539999999999964, - "maxMs": 4.619999999999891, - "ratio": 1.0176211453744335 + "minMs": 4.520000000000437, + "maxMs": 4.579999999999927, + "ratio": 1.0132743362830718 }, "competitor": { - "minMs": 7.770000000000437, - "maxMs": 7.860000000000582, - "ratio": 1.0115830115830298 + "minMs": 7.710000000002765, + "maxMs": 7.959999999999127, + "ratio": 1.0324254215299964 }, "stable": true, "rungs": [ @@ -5956,19 +5839,19 @@ }, { "competitor": "rapier", - "referenceMs": 4.579999999999927, - "referenceP95Ms": 5.020000000000437, + "referenceMs": 4.539999999999964, + "referenceP95Ms": 4.839999999999236, "referenceGpuMs": null, "referenceOverFrameBudget": false, "competitorMs": 1.639999999999418, - "competitorP95Ms": 1.7799999999988358, + "competitorP95Ms": 1.8000000000029104, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 2.792682926830215, - "factor": 2.792682926830215, - "label": "competitor leads (2.79x)", + "ratio": 2.7682926829277896, + "factor": 2.7682926829277896, + "label": "competitor leads (2.77x)", "structural": false }, "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms); the difference is solver cost", @@ -5976,14 +5859,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 4.539999999999964, - "maxMs": 4.619999999999891, - "ratio": 1.0176211453744335 + "minMs": 4.520000000000437, + "maxMs": 4.579999999999927, + "ratio": 1.0132743362830718 }, "competitor": { - "minMs": 1.5800000000017462, - "maxMs": 1.639999999999418, - "ratio": 1.0379746835427883 + "minMs": 1.639999999999418, + "maxMs": 1.6600000000034925, + "ratio": 1.0121951219537084 }, "stable": true, "rungs": [ @@ -6005,17 +5888,17 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 5.780000000000655, - "referenceP95Ms": 6.319999999999709, + "referenceMs": 5.779999999998836, + "referenceP95Ms": 6.260000000000218, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 6.470000000001164, - "competitorP95Ms": 8.299999999999272, + "competitorMs": 6.529999999998836, + "competitorP95Ms": 8.159999999999854, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.8933539412673284, + "ratio": 0.8851454823889535, "factor": 1, "label": "level", "structural": false @@ -6025,14 +5908,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.739999999999782, - "maxMs": 5.800000000001091, - "ratio": 1.0104529616727025 + "minMs": 5.700000000000728, + "maxMs": 5.800000000000182, + "ratio": 1.017543859649025 }, "competitor": { - "minMs": 6.430000000000291, - "maxMs": 6.479999999999563, - "ratio": 1.007776049766605 + "minMs": 6.489999999999782, + "maxMs": 6.579999999999927, + "ratio": 1.0138674884437826 }, "stable": true, "rungs": [ @@ -6044,18 +5927,18 @@ }, { "competitor": "nape-js", - "referenceMs": 5.780000000000655, - "referenceP95Ms": 6.319999999999709, + "referenceMs": 5.779999999998836, + "referenceP95Ms": 6.260000000000218, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 12.660000000003492, - "competitorP95Ms": 14.279999999998836, + "competitorMs": 12.680000000007567, + "competitorP95Ms": 14.280000000013388, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.45655608214842497, - "factor": 2.1903114186854773, + "ratio": 0.4558359621447466, + "factor": 2.1937716262993288, "label": "ExoJS leads (2.19x)", "structural": false }, @@ -6064,14 +5947,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.739999999999782, - "maxMs": 5.800000000001091, - "ratio": 1.0104529616727025 + "minMs": 5.700000000000728, + "maxMs": 5.800000000000182, + "ratio": 1.017543859649025 }, "competitor": { - "minMs": 12.55000000000291, - "maxMs": 12.720000000001164, - "ratio": 1.0135458167329254 + "minMs": 12.660000000003492, + "maxMs": 12.760000000009313, + "ratio": 1.007898894155276 }, "stable": true, "rungs": [ @@ -6083,19 +5966,19 @@ }, { "competitor": "planck", - "referenceMs": 5.780000000000655, - "referenceP95Ms": 6.319999999999709, + "referenceMs": 5.779999999998836, + "referenceP95Ms": 6.260000000000218, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 7.990000000001601, - "competitorP95Ms": 10.280000000006112, + "competitorMs": 8.209999999999127, + "competitorP95Ms": 10.339999999996508, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.7234042553190859, - "factor": 1.382352941176591, - "label": "ExoJS leads (1.38x)", + "ratio": 0.7040194884286785, + "factor": 1.42041522491363, + "label": "ExoJS leads (1.42x)", "structural": false }, "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms) with 34 vs 37 ray hits per step; the difference is query and solver cost", @@ -6103,14 +5986,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.739999999999782, - "maxMs": 5.800000000001091, - "ratio": 1.0104529616727025 + "minMs": 5.700000000000728, + "maxMs": 5.800000000000182, + "ratio": 1.017543859649025 }, "competitor": { - "minMs": 7.9099999999962165, - "maxMs": 8.0099999999984, - "ratio": 1.0126422250318876 + "minMs": 8.139999999999418, + "maxMs": 8.210000000002765, + "ratio": 1.0085995085999204 }, "stable": true, "rungs": [ @@ -6122,19 +6005,19 @@ }, { "competitor": "rapier", - "referenceMs": 5.780000000000655, - "referenceP95Ms": 6.319999999999709, + "referenceMs": 5.779999999998836, + "referenceP95Ms": 6.260000000000218, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 1.679999999993015, - "competitorP95Ms": 1.8599999999860302, + "competitorMs": 1.7200000000011642, + "competitorP95Ms": 1.8399999999965075, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 3.4404761904908847, - "factor": 3.4404761904908847, - "label": "competitor leads (3.44x)", + "ratio": 3.3604651162761185, + "factor": 3.3604651162761185, + "label": "competitor leads (3.36x)", "structural": false }, "mechanism": "identical scene (1783 bodies, 2195 contacts, 0 joints on both arms) with 34 vs 39 ray hits per step; the difference is query and solver cost", @@ -6142,14 +6025,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.739999999999782, - "maxMs": 5.800000000001091, - "ratio": 1.0104529616727025 + "minMs": 5.700000000000728, + "maxMs": 5.800000000000182, + "ratio": 1.017543859649025 }, "competitor": { - "minMs": 1.639999999999418, - "maxMs": 1.7000000000116415, - "ratio": 1.036585365861125 + "minMs": 1.6999999999970896, + "maxMs": 1.7200000000011642, + "ratio": 1.0117647058847699 }, "stable": true, "rungs": [ @@ -6171,17 +6054,17 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 5.619999999998981, - "referenceP95Ms": 6.6200000000008, + "referenceMs": 5.639999999999418, + "referenceP95Ms": 6.819999999999709, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.720000000001164, - "competitorP95Ms": 6.6599999999962165, + "competitorMs": 5.740000000005239, + "competitorP95Ms": 6.759999999994761, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 0.9825174825171045, + "ratio": 0.9825783972115454, "factor": 1, "label": "level", "structural": false @@ -6191,14 +6074,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.5, - "maxMs": 5.639999999999418, - "ratio": 1.0254545454544397 + "minMs": 5.6200000000008, + "maxMs": 5.650000000000546, + "ratio": 1.005338078291769 }, "competitor": { - "minMs": 5.709999999999127, - "maxMs": 5.819999999999709, - "ratio": 1.019264448336357 + "minMs": 5.680000000000291, + "maxMs": 5.880000000001019, + "ratio": 1.03521126760576 }, "stable": true, "rungs": [ @@ -6210,19 +6093,19 @@ }, { "competitor": "nape-js", - "referenceMs": 5.619999999998981, - "referenceP95Ms": 6.6200000000008, + "referenceMs": 5.639999999999418, + "referenceP95Ms": 6.819999999999709, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 2.9199999999982538, - "competitorP95Ms": 3.5200000000040745, + "competitorMs": 3, + "competitorP95Ms": 3.7400000000052387, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.9246575342473775, - "factor": 1.9246575342473775, - "label": "competitor leads (1.92x)", + "ratio": 1.879999999999806, + "factor": 1.879999999999806, + "label": "competitor leads (1.88x)", "structural": false }, "mechanism": "identical scene (1504 bodies, 1415 contacts, 0 joints on both arms); the difference is solver cost", @@ -6230,14 +6113,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.5, - "maxMs": 5.639999999999418, - "ratio": 1.0254545454544397 + "minMs": 5.6200000000008, + "maxMs": 5.650000000000546, + "ratio": 1.005338078291769 }, "competitor": { - "minMs": 2.720000000001164, - "maxMs": 3.0800000000017462, - "ratio": 1.132352941176628 + "minMs": 2.860000000000582, + "maxMs": 3.460000000006403, + "ratio": 1.2097902097922024 }, "stable": true, "rungs": [ @@ -6249,17 +6132,17 @@ }, { "competitor": "planck", - "referenceMs": 5.619999999998981, - "referenceP95Ms": 6.6200000000008, + "referenceMs": 5.639999999999418, + "referenceP95Ms": 6.819999999999709, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.100000000005821, - "competitorP95Ms": 7.380000000004657, + "competitorMs": 5.120000000009895, + "competitorP95Ms": 7.580000000001746, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": 1.101960784312268, + "ratio": 1.1015624999977573, "factor": 1, "label": "level", "structural": false @@ -6269,14 +6152,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.5, - "maxMs": 5.639999999999418, - "ratio": 1.0254545454544397 + "minMs": 5.6200000000008, + "maxMs": 5.650000000000546, + "ratio": 1.005338078291769 }, "competitor": { - "minMs": 5.070000000006985, - "maxMs": 5.4799999999959255, - "ratio": 1.0808678500963265 + "minMs": 5.07999999999447, + "maxMs": 5.180000000000291, + "ratio": 1.019685039371246 }, "stable": true, "rungs": [ @@ -6288,19 +6171,19 @@ }, { "competitor": "rapier", - "referenceMs": 5.619999999998981, - "referenceP95Ms": 6.6200000000008, + "referenceMs": 5.639999999999418, + "referenceP95Ms": 6.819999999999709, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 0.7799999999988358, - "competitorP95Ms": 0.819999999992433, + "competitorMs": 0.7599999999947613, + "competitorP95Ms": 0.7799999999988358, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 7.205128205137653, - "factor": 7.205128205137653, - "label": "competitor leads clearly (7.21x)", + "ratio": 7.421052631629335, + "factor": 7.421052631629335, + "label": "competitor leads clearly (7.42x)", "structural": true }, "mechanism": "identical scene (1504 bodies, 1415 contacts, 0 joints on both arms); the difference is solver cost", @@ -6308,14 +6191,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.5, - "maxMs": 5.639999999999418, - "ratio": 1.0254545454544397 + "minMs": 5.6200000000008, + "maxMs": 5.650000000000546, + "ratio": 1.005338078291769 }, "competitor": { "minMs": 0.7599999999947613, - "maxMs": 0.7999999999883585, - "ratio": 1.0526315789393064 + "maxMs": 0.7599999999947613, + "ratio": 1 }, "stable": true, "rungs": [ @@ -6337,19 +6220,19 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 5.289999999999964, - "referenceP95Ms": 5.519999999998618, + "referenceMs": 4.959999999999127, + "referenceP95Ms": 5.3400000000001455, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 5.169999999998254, - "competitorP95Ms": 7.119999999995343, + "competitorMs": 4.860000000000582, + "competitorP95Ms": 7.519999999996799, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "neither", - "ratio": null, - "factor": null, - "label": "unstable across runs", + "ratio": 1.0205761316869408, + "factor": 1, + "label": "level", "structural": false }, "mechanism": "competitor resolves fewer solved contacts (7875 vs 0); the counters are not semantically identical across arms (see the run's caveats)", @@ -6357,18 +6240,18 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.020000000000437, - "maxMs": 5.680000000000291, - "ratio": 1.1314741035856168 + "minMs": 4.940000000000509, + "maxMs": 5.6200000000008, + "ratio": 1.137651821862393 }, "competitor": { - "minMs": 4.399999999997817, - "maxMs": 6.120000000002619, - "ratio": 1.3909090909103763 + "minMs": 4.779999999998836, + "maxMs": 6.25, + "ratio": 1.3075313807534565 }, - "stable": false, + "stable": true, "rungs": [ - "competitor-leads", + "level", "level", "level" ] @@ -6376,8 +6259,8 @@ }, { "competitor": "nape-js", - "referenceMs": 5.289999999999964, - "referenceP95Ms": 5.519999999998618, + "referenceMs": 4.959999999999127, + "referenceP95Ms": 5.3400000000001455, "referenceGpuMs": null, "referenceOverFrameBudget": false, "competitorMs": 0, @@ -6396,9 +6279,9 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.020000000000437, - "maxMs": 5.680000000000291, - "ratio": 1.1314741035856168 + "minMs": 4.940000000000509, + "maxMs": 5.6200000000008, + "ratio": 1.137651821862393 }, "competitor": { "minMs": 0, @@ -6415,19 +6298,19 @@ }, { "competitor": "planck", - "referenceMs": 5.289999999999964, - "referenceP95Ms": 5.519999999998618, + "referenceMs": 4.959999999999127, + "referenceP95Ms": 5.3400000000001455, "referenceGpuMs": null, "referenceOverFrameBudget": false, "competitorMs": 0.37000000000261934, - "competitorP95Ms": 0.3800000000046566, + "competitorP95Ms": 0.4599999999991269, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 14.297297297195984, - "factor": 14.297297297195984, - "label": "competitor leads clearly (14.30x)", + "ratio": 13.405405405308144, + "factor": 13.405405405308144, + "label": "competitor leads clearly (13.41x)", "structural": true }, "mechanism": "competitor resolves fewer solved contacts (7875 vs 0); the counters are not semantically identical across arms (see the run's caveats)", @@ -6435,14 +6318,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.020000000000437, - "maxMs": 5.680000000000291, - "ratio": 1.1314741035856168 + "minMs": 4.940000000000509, + "maxMs": 5.6200000000008, + "ratio": 1.137651821862393 }, "competitor": { - "minMs": 0.37000000000261934, - "maxMs": 0.37999999999738066, - "ratio": 1.0270270270126771 + "minMs": 0.3699999999953434, + "maxMs": 0.3799999999901047, + "ratio": 1.0270270270132085 }, "stable": true, "rungs": [ @@ -6454,19 +6337,19 @@ }, { "competitor": "rapier", - "referenceMs": 5.289999999999964, - "referenceP95Ms": 5.519999999998618, + "referenceMs": 4.959999999999127, + "referenceP95Ms": 5.3400000000001455, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 0.20000000001164153, - "competitorP95Ms": 0.23999999999068677, + "competitorMs": 0.19999999999708962, + "competitorP95Ms": 0.22000000000116415, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 26.449999998460225, - "factor": 26.449999998460225, - "label": "competitor leads clearly (26.45x)", + "ratio": 24.800000000356523, + "factor": 24.800000000356523, + "label": "competitor leads clearly (24.80x)", "structural": true }, "mechanism": "competitor resolves fewer solved contacts (7875 vs 1125); the counters are not semantically identical across arms (see the run's caveats)", @@ -6474,14 +6357,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 5.020000000000437, - "maxMs": 5.680000000000291, - "ratio": 1.1314741035856168 + "minMs": 4.940000000000509, + "maxMs": 5.6200000000008, + "ratio": 1.137651821862393 }, "competitor": { "minMs": 0.19999999999708962, - "maxMs": 0.21999999998661224, - "ratio": 1.0999999999490684 + "maxMs": 0.19999999999708962, + "ratio": 1 }, "stable": true, "rungs": [ @@ -6503,19 +6386,19 @@ "cells": [ { "competitor": "matter-js", - "referenceMs": 3.7399999999979627, - "referenceP95Ms": 4.5, + "referenceMs": 3.7400000000016007, + "referenceP95Ms": 4.479999999999563, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 10.680000000000291, - "competitorP95Ms": 12.340000000003783, + "competitorMs": 10.760000000002037, + "competitorP95Ms": 12.540000000000873, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.3501872659174027, - "factor": 2.8556149732636658, - "label": "ExoJS leads (2.86x)", + "ratio": 0.3475836431227595, + "factor": 2.8770053475928963, + "label": "ExoJS leads (2.88x)", "structural": false }, "mechanism": "identical scene (3004 bodies, 6199 contacts, 0 joints on both arms); the difference is solver cost", @@ -6523,14 +6406,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.7399999999979627, + "minMs": 3.719999999997526, "maxMs": 3.7400000000016007, - "ratio": 1.0000000000009728 + "ratio": 1.0053763440871204 }, "competitor": { - "minMs": 10.639999999999418, - "maxMs": 10.75, - "ratio": 1.0103383458647168 + "minMs": 10.660000000003492, + "maxMs": 10.909999999999854, + "ratio": 1.0234521575981501 }, "stable": true, "rungs": [ @@ -6542,19 +6425,19 @@ }, { "competitor": "nape-js", - "referenceMs": 3.7399999999979627, - "referenceP95Ms": 4.5, + "referenceMs": 3.7400000000016007, + "referenceP95Ms": 4.479999999999563, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 11, - "competitorP95Ms": 13.820000000006985, + "competitorMs": 11.07999999999447, + "competitorP95Ms": 14.200000000011642, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.3399999999998148, - "factor": 2.9411764705898373, - "label": "ExoJS leads (2.94x)", + "ratio": 0.33754512635410355, + "factor": 2.9625668449170397, + "label": "ExoJS leads (2.96x)", "structural": false }, "mechanism": "identical scene (3004 bodies, 6199 contacts, 0 joints on both arms); the difference is solver cost", @@ -6562,14 +6445,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.7399999999979627, + "minMs": 3.719999999997526, "maxMs": 3.7400000000016007, - "ratio": 1.0000000000009728 + "ratio": 1.0053763440871204 }, "competitor": { - "minMs": 10.980000000010477, - "maxMs": 11, - "ratio": 1.0018214936238163 + "minMs": 11.059999999997672, + "maxMs": 11.19999999999709, + "ratio": 1.0126582278480514 }, "stable": true, "rungs": [ @@ -6581,19 +6464,19 @@ }, { "competitor": "planck", - "referenceMs": 3.7399999999979627, - "referenceP95Ms": 4.5, + "referenceMs": 3.7400000000016007, + "referenceP95Ms": 4.479999999999563, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 13.080000000001746, - "competitorP95Ms": 19.54000000000815, + "competitorMs": 13.220000000001164, + "competitorP95Ms": 19.860000000000582, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "exojs", - "ratio": 0.2859327217123443, - "factor": 3.4973262032109282, - "label": "ExoJS leads (3.50x)", + "ratio": 0.28290468986393885, + "factor": 3.5347593582875683, + "label": "ExoJS leads (3.53x)", "structural": false }, "mechanism": "identical scene (3004 bodies, 6199 contacts, 0 joints on both arms); the difference is solver cost", @@ -6601,14 +6484,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.7399999999979627, + "minMs": 3.719999999997526, "maxMs": 3.7400000000016007, - "ratio": 1.0000000000009728 + "ratio": 1.0053763440871204 }, "competitor": { - "minMs": 12.979999999995925, - "maxMs": 13.34000000001106, - "ratio": 1.0277349768886939 + "minMs": 13.200000000004366, + "maxMs": 13.319999999992433, + "ratio": 1.0090909090900022 }, "stable": true, "rungs": [ @@ -6620,19 +6503,19 @@ }, { "competitor": "rapier", - "referenceMs": 3.7399999999979627, - "referenceP95Ms": 4.5, + "referenceMs": 3.7400000000016007, + "referenceP95Ms": 4.479999999999563, "referenceGpuMs": null, "referenceOverFrameBudget": false, - "competitorMs": 2.1199999999953434, + "competitorMs": 2.099999999991269, "competitorP95Ms": 2.220000000001164, "competitorGpuMs": null, "competitorOverFrameBudget": false, "verdict": { "side": "competitor", - "ratio": 1.7641509433991405, - "factor": 1.7641509433991405, - "label": "competitor leads (1.76x)", + "ratio": 1.7809523809605479, + "factor": 1.7809523809605479, + "label": "competitor leads (1.78x)", "structural": false }, "mechanism": "identical scene (3004 bodies, 6199 contacts, 0 joints on both arms); the difference is solver cost", @@ -6640,14 +6523,14 @@ "aggregate": { "runs": 3, "reference": { - "minMs": 3.7399999999979627, + "minMs": 3.719999999997526, "maxMs": 3.7400000000016007, - "ratio": 1.0000000000009728 + "ratio": 1.0053763440871204 }, "competitor": { "minMs": 2.0800000000017462, - "maxMs": 2.1600000000034925, - "ratio": 1.0384615384623457 + "maxMs": 2.099999999991269, + "ratio": 1.0096153846103393 }, "stable": true, "rungs": [ @@ -6664,6 +6547,6 @@ }, "signature": { "algorithm": "sha256", - "value": "74734d08514f28925d94d9550137e610aa03b9c692c53cf588b06b4e62d677ce" + "value": "fbd97ee33f914a4f55e761c15fd321fa6d41a7d1c57d5618fffdd7495d9c049a" } } From 866883bc2bd86673eacb83f4d9b6cdf3e003d1c8 Mon Sep 17 00:00:00 2001 From: Exoridus Date: Sat, 12 Sep 2026 06:23:50 +0200 Subject: [PATCH 18/28] docs(changelog): render the 0.17.0 section one line per entry --- CHANGELOG.md | 531 +++------------------------------------------------ 1 file changed, 22 insertions(+), 509 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9f9e5b00..bc6a97e8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,529 +11,42 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and ### Changed - **BREAKING: Retain filtered and clipped scopes, coalesce WebGPU row patches, add hitArea and move culling to RenderNode.** ([#681](https://github.com/Exoridus/ExoJS/pull/681)) - Two per-frame costs that scaled with the scene rather than with the - change. - - **A filter or a clip was a retention floor.** A barrier-bearing node - collected through the effect path and never reached the plan builder's - group branch, so it never got the automatic persistent render - representation. Everything below a filter chain or a rect clip was - walked out of the scene graph, transform-derived and material-resolved - on every single frame, however static it was. The barrier's content now - climbs the same ladder a render root does, while the effect itself stays - live in the barrier entry and the effect executor: a changed filter - chain, clip rect or target size still takes effect on the frame it - changes. - - A 5 000-sprite scene under a colour-matrix chain drops from 3.8 ms to - 0.25 ms per frame on WebGL2 and from 2.7 ms to 0.35 ms on WebGPU, with - the per-frame instance re-upload gone (7 -> 1). Three nested rectangle - masks over the same scene drop from 3.9 ms to 2.2 ms (WebGL2) and 3.1 ms - to 1.9 ms (WebGPU). Draw calls are unchanged everywhere. - - **A moving node cost a GPU upload of its own on WebGPU.** The retained - group bundle wrote each patched transform row with its own - `queue.writeBuffer`, so the per-frame upload count followed the number - of moving nodes: 375 moved sprites were 375 calls, against one on - WebGL2, whose row store is a texture whose dirty rect is unioned and - committed once. The bundle now mirrors its rows, marks the blocks a - patch touches and uploads them at the end of the patch pass: tight runs - while the moves cluster, one span once they scatter. Uploads per frame - on `dynamic-heavy` at 5 000 nodes: 369 -> 1. - - Text nodes get the same treatment as sprites: a moved text node no - longer costs a `queue.writeBuffer` of its own, since the retained - node-data store is now block-mirrored and uploaded per dirty region - through the shared `DirtyRowTracker`. A WGSL filter pass writes its - resolution and user uniform blocks only when they actually change, which - makes the per-frame upload count on `filter-chain-1/2/4` flat in chain - depth (11 -> 3 at depth four) instead of growing by two per link. - - The benchmark's structural baseline is re-recorded in the same change: - the immediate arm's filter-chain and mask-clip cells now submit what the - retained arm already did. - - Measured but not changed here: `lifecycle-churn` is not object - construction cost (1.2 us per sprite create/add/destroy) but a full - immediate collect after every structural change, which needs an - incremental structural delta in the retained source; `mask-clip` keeps - ~1.9 ms of entry replay behind nested clips because a recorded fragment - cannot carry a barrier, which needs a splice-contract change; - `scrolling-world` already renders in one draw call (the earlier "120 - draws" read raw totals over 120 frames). - - **Also in this change: `hitArea`, and `cullable`/`cullArea` move to - `RenderNode`.** A bare `SceneNode` is structural and never rendered, and - the cull test is evaluated over render items, so the two culling - properties now live on `RenderNode`; semantics unchanged (read live, - caller-owned rectangle, default `cullable = true`). `RenderNode.hitArea` - accepts a `Rectangle`, `Circle`, `Ellipse` or `Polygon` in the node's - own space (default `null`): when set, `contains()` maps the world point - through the inverse world transform and tests the shape instead of the - bounds, so a round button, a hex cell or a province outline picks - exactly, rotated or not. Picking only; bounds, culling and rendering - ignore it. Widgets inherit it. - - **Breaking changes** - - `cullable` / `cullArea` no longer exist on a bare `SceneNode`; every - renderable class keeps them through `RenderNode`. - - *** - - **BREAKING: Multi-stop gradients, caps and slant variants, decorations, line clamping, case mapping and tab stops.** ([#682](https://github.com/Exoridus/ExoJS/pull/682)) - A sweep across `TextStyle` and `LayoutOptions` that closes the gap - between what the text stack can lay out and what display typography - actually needs, plus two DX fixes. - - **Multi-stop gradients with an angle.** `gradient: { stops, angle }` - replaces `gradientColors` + `gradientAxis`. Up to eight stops, offsets - clamped and sorted on the way in, and an angle in degrees following the - CSS `linear-gradient` convention (0 = to top, 90 = to right, default - 180). The ramp spans the ink box corner to corner, so the first and last - stops land on the box edges at every angle. Evaluated per fragment from - the node's own packed style row, still a `'tint'` change that never - touches the atlas. The stop type is reused from the existing - `GradientStop`. - - **Caps and slant variants.** `fontStyle` gains `'oblique'`; - `fontVariant: 'small-caps'` is new. Both go into the CSS `font` - shorthand the rasterizer hands to Canvas 2D, and both are part of a - glyph atlas's identity, so a small-cap `a` never shares a cache entry - with an ordinary one. - - **Underline and strikethrough.** `underline`, `strikethrough`, - `decorationColor`, `decorationThickness`, `decorationOffset`. Rules are - quads the layout emits per line, so they follow alignment, wrapping and - letter spacing. Their position comes from the font's own ascent, descent - and x-height rather than from a fraction of the font size. A rule takes - the fill, gradient included, unless `decorationColor` overrides it. - `BitmapText` renders no rules: an offline atlas has no opaque block to - sample. - - **Line clamping.** `maxLines` caps the laid-out line count after - wrapping and clips on its own; pair it with `overflow: 'ellipsis'` for a - marker. `ellipsis` configures that marker (`'...'`, `''`, anything). - Under a cap the marker also reaches a line that no wrap could shorten, - such as `maxLines: 1` on a single unbreakable word. - - **Case mapping.** `textTransform: 'none' | 'uppercase' | 'lowercase' | -'capitalize'`, applied at layout time per grapheme cluster and under the - layout locale. The node's `text` is untouched, and a cluster that - changes length under the mapping still traces back to the character it - came from, so a caret lands where the reader clicked. - - **Tab stops.** `tabSize` (default 8, the CSS `tab-size` initial value) - advances a preserved tab to the next stop from the line origin, so - tab-separated values line up as columns. Only reachable under - `whiteSpace: 'pre'`; the collapsing modes turn a tab into one space - before layout, as CSS does. - - **DX.** `AbstractText.update()` is gone; `syncDirty()` is the name. - Stale references to a distance-field package the engine never depended - on are replaced with a description of the in-house rasterizer. Glyph - caches are keyed by one named `FontVariantKey` instead of four - positional strings. - - New guide sections (case, variants, decorations, line clamping, tab - stops) and the example `text-fonts/typographic-styling`. Verified on - both backends in the browser, including new pixel-probing tests that - prove a rule reaches the frame and that `decorationColor` reaches only - the rule. - - **Breaking changes** - - `TextStyleOptions.gradientColors` / `gradientAxis` -> `gradient`; the - `GradientAxis` type is gone. Serialized text styles carry `gradient` - instead of the two old keys. - - `AbstractText.update()` is removed. `syncDirty()` is the name; the - renderer and every extent read already resolve a pending pass on their - own, so most callers need nothing. - - `GlyphAtlasPool.getAtlas` / `getMetrics` / `getShapedMetrics` / - `clearVariant`, and the `GlyphAtlas` / `GlyphMetrics` / - `ShapedTextMetrics` constructors, take one named `FontVariantKey` (`{ -family, fontStyle?, fontWeight?, fontVariant? }`, each optional field - defaulting to `'normal'`) instead of positional strings. `clearVariant` - takes the narrower `FontTypefaceKey`. `ShapedTextSourceOptions` nests - its font fields under `font`. - - `TextPageQuads` gains `decorations`; the packed per-vertex node index - narrows from 24 to 23 bits to make room for the decoration flag. - - *** - -- **`ShaderSource.glsl.vertex` is optional.** A sprite material and a shader - filter never compiled the author's vertex stage (the sprite vertex program is - engine-owned, the filter draws a fullscreen quad), yet the source required a - non-empty string, so callers passed a dummy. `glsl: { fragment }` is now - enough for both; `ShaderSource.glsl.vertex` reads `null` in that case, and a - mesh or particle material without a vertex stage fails with a named error. - -- **The extension seams that survived the `@internal` strip lost their - underscore prefix.** A member an official package (or any extension author) - has to call is API, and now reads like it: `SceneNode._setLocalBounds` is - `setLocalBounds`, `RenderNode._collect` is `collect`, `Material._onDispose` is - `onDispose`, `AudioBus._getInputNode`/`_getOutputNode` are - `getInputNode`/`getOutputNode`, and `Playable._createVoice` is `createVoice` - (with `Sound`, `AudioStream` and `AudioGenerator` following). On the renderer - SDK, `WebGl2Backend._stageViewportUniform` is `stageViewportUniform`, - `_pushTransform` and `_recordRetainedBatch` are `pushTransform` and - `recordRetainedBatch` on both backends, `WebGpuBackend._transformStorageWouldGrow` - and `_textureUploadWouldMutate` are `transformStorageWouldGrow` and - `textureUploadWouldMutate`, the retained replayer contracts - (`_scanRetainedNodeIndexRange`, `_rebaseRetainedNodeIndices`, - `_configureRetainedVao`, `_validateRetainedBatch`, `_replayRetainedBatch`) and - `RetainedGroupBundle`'s `_patchTransformRow`/`_patchTintRow` drop the prefix - the same way. Rename the call sites; there are no compatibility aliases. Every - remaining underscore member is engine-private and no longer reaches the - published `.d.ts` at all. - -- **Punctuation keys are named after their `code` on both input surfaces.** - `Keyboard.Colon`, `Equals`, `Dash`, `QuestionMark`, `Tilde`, `OpenBracket`, - `BackwardSlash`, `ClosedBracket` and `Quotes` are now `Semicolon`, `Equal`, - `Minus`, `Slash`, `Backquote`, `BracketLeft`, `Backslash`, `BracketRight` and - `Quote`, matching the pattern tokens (`keyboard.semicolon`, ...) and - `KeyboardEvent.code`. Channel values, bindings and actions are unchanged. - -- **`ShaderFilter` sources get a `uOrientation` auto-bind, so one shader offsets - along `v` the same way on both backends.** A WebGL2 render texture stores the - effect domain bottom-up and a WebGPU one top-down, which used to make a - directional `v` offset move the image up on one backend and down on the other. - `uOrientation` is `+1` where `v` grows along the domain's y axis and `-1` - where it grows against it (GLSL `uniform float uOrientation`, WGSL - `@group(0) @binding(3) var uOrientation: f32`); multiply the v - component of a directional offset by it. Existing sources that only sample - their own texel need no change. - -- **Every remaining public duration input takes branded `Seconds` instead of a - plain millisecond `number`.** `AudioBus.fadeIn`/`fadeOut`, `Voice.fade`/`stop` - (and `crossFade`'s duration), `InputVoice.record`, `Envelope`'s - `attack`/`decay`/`release`/`totalDuration` (renamed from the `*Ms` fields, - with `trigger`'s `elapsed` following) and its `releaseAt` method (renamed - from `release`, which now names the duration field instead), - `View.shake`'s duration, `AnimatedSprite`'s `frameDuration`/`frameDurations`, - and `PhasedSceneTransition`/`CrossFadeSceneTransition`'s `duration` all match - the `Seconds` unit the rest of the engine already uses. Wrap existing - millisecond literals with `Time.seconds(ms / 1000)` (or write the value - directly in seconds); `Envelope.releaseAt` replaces `envelope.release(...)` - now that `release` names the duration property. - -- **The published `.d.ts` no longer carries `@internal` members.** The - declaration emit now strips them, so consumer autocomplete on `Loader`, - `AssetRef`, `Text`, `Tween`, `AudioBus` and the rest shows the API instead of - the engine's internals, and the published types finally agree with the - published API reference. Types that a public or renderer-SDK signature - genuinely exposes became part of the surface rather than disappearing with - the tag: `CheckableWidget`, `TextEditWidget`, `UIBackgroundNode`, `Ticker`, - `InputVoice`, `ShapeLike`, `PointerChannel`, `GamepadButtonChannel`, - `GamepadAxisChannel`, `CatalogResourceLeaf`, `CatalogValueLeaf`, - `OwnedNetworkHintSource` and `BmFontAdapter` on the root barrel; - `RenderPlanBuilder`, `DrawCommand`, `MaterialKey`, `InstanceDataView`, - `InstanceAttributeBinding`, `ShaderProgram`, `RenderPassCoordinator`, - `RenderPassDescriptor`, `RenderPassLoad`, `StencilAttachmentMode`, the - retained-group payload/replayer types and `WebGpuActiveRenderPass` on - `@codexo/exojs/renderer-sdk`. Going the other way, `SpriteFlags` and - `ViewFlags` - internal dirty-flag bitmasks that were exported by accident - - are gone from the root barrel, `ObservableVector`'s owner constructor is - internal (construct a plain vector with `new ObservableVector()`), and - `onAudioContextReady` is typed as the `Signal<[AudioContext]>` it always - was. - -- **`AnimatedSprite.defineClip` is now `addClip`, `Sound.defineSprite` is now - `addSprite`, and `Spritesheet.addFrame`/`removeFrame` return `this`.** One - verb pair, `add`/`remove`, for every named-registration mutator, and every - one of them chainable. Rename the calls; the `clips`/`sprites` constructor - options and `setClips`/`setSprites` are unchanged. - -- **`Assets.from` rejects a bare path whose suffix no asset type claims, at the - literal.** `'hero.pgn'` used to type-check and hand back `unknown`; it now - fails to compile with a message naming the path and the way out. Paths that - only exist at runtime (`string`, not a literal) are unaffected. - -- **`BurstSpawn`'s `loop: boolean` is replaced by `interval: number`, the - period in seconds between two runs of the schedule.** `loop: true` restarted - the schedule in the same `apply()` call that exhausted it and zeroed the - clock, so a schedule with nothing after its final burst re-fired every frame - and the emitted count followed the frame rate rather than elapsed time. The - period is now declared, the clock wraps by subtracting it so overshoot - carries into the next cycle, and a long frame fires every period it covered. - Replace `loop: true` with `interval: `; a schedule that used a - trailing `{ time: period, count: 0 }` entry to fake a period can drop it. - -- **`TileSet._setDefinitions` is now `setDefinitions`.** The tilemap SDK - contract for extension packages that build tilesets programmatically loses - the leading underscore now that the method is the only cross-package member - in the extension packages that isn't `@internal`; the other 25 - package-private members across `@codexo/exojs-physics` and - `@codexo/exojs-tilemap` are now tagged `@internal` and no longer appear in - the published `.d.ts`. Rename the call. +- **BREAKING: Name the renderer and audio SDK seams, tag the rest @internal.** ([#672](https://github.com/Exoridus/ExoJS/pull/672)) +- **BREAKING: Name punctuation keys after their code on both surfaces.** ([#667](https://github.com/Exoridus/ExoJS/pull/667)) +- **BREAKING: Strip internals from the declaration emit.** ([#668](https://github.com/Exoridus/ExoJS/pull/668)) +- **BREAKING: Take every public duration input in seconds.** ([#666](https://github.com/Exoridus/ExoJS/pull/666)) +- **BREAKING: Unify add/remove mutator verbs and reject unknown asset suffixes at the literal.** ([#661](https://github.com/Exoridus/ExoJS/pull/661)) +- **Tag package-private members @internal.** ([#673](https://github.com/Exoridus/ExoJS/pull/673)) ### Added - **Show examples without guide markers and with one-line imports.** ([#683](https://github.com/Exoridus/ExoJS/pull/683)) - The playground and guide code blocks showed example sources verbatim, - including the `// #region guide:...` markers that exist only for snippet - extraction and the import lists Prettier had wrapped one specifier per - line. The display source now drops the markers (with the blank line they - would leave) and joins each wrapped import back onto one line. Execution - source and files on disk are unchanged, so snippet extraction, - typechecking and formatting keep working on the originals. - - **Add JobScheduler for frame-budgeted generator jobs.** ([#679](https://github.com/Exoridus/ExoJS/pull/679)) - Heavy work (world generation, batch pathfinding, visibility rebuilds) no - longer has to choose between blocking a frame and an `async` update that - resumes in a later microtask. - - - `JobScheduler` advances generator jobs one `yield` at a time inside a - per-frame time budget (default 2 ms; at least one step per update so a - job always progresses), in strict priority order with round-robin inside - a priority. Instantiable with its own budget and order, so a scene can - own one via `scene.systems.add(new JobScheduler())`. - - `Job` handle: frame code polls `status` / `result` / `error`, async - code awaits `done` (created lazily, so an unawaited failure never raises - an unhandled rejection; cancellation rejects with an `AbortError`). - `cancel()` stops the generator at its `yield` and runs `finally` blocks; - `{ scope }` lets a `DestroyScope` own the job only while it runs. - - `app.jobs` is the application-owned instance, ticked in the `update` - phase at the new `SystemOrder.CoreJobs`. - - Guide section "Spreading work over frames" in the Application chapter; - API docs generated; export snapshot updated. - - Worker-backed jobs are deliberately not part of this: the handle is - designed so a `WorkerPool` can hand out the same `Job` later. - - *** - -- **`@codexo/exojs-pathfinding`, the official pathfinding extension.** One - search core - A\* over integer node handles - serving pluggable navigation - spaces. `GridSpace` is a finite window of weighted cells with diagonal - policies, `setCost`/`revision` for runtime edits, brushfire clearance for - agents wider than one cell, and string-pulling smoothing; `WaypointGraph` is a - directed graph whose edges carry a `kind` and a typed payload, which is what a - platformer's jump and fall links need and what a grid cannot express, and - which degrades to plain Dijkstra when its nodes have no positions. - `Pathfinder.findPath`/`findPathBetween` return a `PathResult` whose `status` - distinguishes `found`, `unreachable` and `budget-exceeded` instead of throwing - or returning `null`, and `floodFrom` answers "everything reachable within this - cost". Jump-point search self-enables on a uniform-cost grid and returns the - same optimal path from a fraction of the expanded nodes. Paths are - reproducible across runs and machines, and a search allocates nothing that - scales with the nodes it visits. The package depends on `@codexo/exojs` alone: - a tilemap reaches it through the cost callback `GridSpace.from` takes, not - through a package edge. - -- **Browser-native shaping for bidirectional and contextual text.** - `LayoutOptions.shaping` selects how a glyph's appearance is resolved: - `'auto'` (the default) keeps ordinary content on the shared glyph atlas and - hands text that needs its surroundings - a right-to-left base direction, an - explicit bidi control, or any script outside a proven-safe allow-list - to - the browser's canvas text engine one complete line at a time, which resolves - the bidirectional order and the contextual forms. `'simple'` and `'browser'` - force either path; `Text.shapingMode` reports which one settled. Shaped - lines are rasterized into pages the node owns and released with it, so no - process-wide cache of whole strings accumulates. No runtime dependency is - added: the platform provides the segmentation, the shaping and the raster. - -- **`LayoutOptions.locale`.** The language tag Unicode segmentation runs - under - which clusters count as one character, and where a line may break. - It selects no font and loads nothing. - -- **`GlyphPlacement.sourceStart` / `sourceEnd` and the same pair on - `TextLineMetrics`.** Every laid-out glyph and every laid-out line now carries - the UTF-16 range of the string it came from. Nothing in a string marks a soft - wrap, so this is what maps a caret, a selection or a hit test onto wrapped - text; a glyph that stands for no source character (the ellipsis an overflow - appended) reports an empty range at the point it replaced. - -- **`TextArea` wraps and scrolls.** A line too long for the field breaks at a - word boundary instead of scrolling sideways, and a vertical scrollbar appears - along the right edge once the value outgrows the field and drives the scroll - position. `wrap: false` restores horizontal scrolling for content whose own - line breaks are what matters; `scrollbar: false` and `scrollbarThickness` - control the bar, and `TextArea.verticalScrollbar` exposes it. Caret motion, - `Home`/`End`, `PageUp`/`PageDown`, hit testing and selection rectangles - follow the laid-out lines, so a wrapped line behaves like the two lines it - looks like while the value keeps exactly the line breaks the user typed. - -- **`when` on `SceneInteraction.observe()` and `scope()`.** Interaction - registrations take the same `SceneAvailability` policy the input, tween and - audio facades have. The default stays `'always'`, so a pause menu drawn by - the paused scene keeps receiving pointer events; `'active'` detaches a - registration while the scene is paused and `'paused'` attaches it only then. - -- **`DisplacementFilter`.** Warps the filtered node by a direction read out of a - map texture - heat haze, water refraction, glass, shockwaves. `map`'s red and - green channels are decoded to `[-1, 1]` and scaled by `scale` (one number or - `[x, y]`, logical units, default `20`); `offsetU`/`offsetV` move where the map - is sampled, so animating them scrolls the distortion. The reach is declared - through `getOutputBounds`, and a fragment displaced past the effect domain - comes out transparent rather than smearing the border. - -- **`PhasedSceneTransition.destroyPhaseState(state)`.** The release half of - `createPhaseState()`, called exactly once per session on every exit path - - normal completion, an abort before the commit, or the application being - destroyed mid-transition. Override it when the phase state owns GPU-backed - resources; plain scratch needs no override. A `{ enter, exit }` pair holds - one state per side and each side is released through its own phase's hook. - -- **A guide chapter and a playground example for writing your own scene - transition.** The **Writing your own transition** chapter spells out the - lifecycle contract `SceneTransitionLifecycleError` enforces - the - definition/session split, what the director guarantees per frame, why - `commit()` does not switch the scene in the same call, and what an abort and - a `destroy()` have to leave behind - and builds a complete bar-wipe - transition against it, both as a full `SceneTransition` and as a - `PhasedSceneTransition`. The matching `application-scenes/custom-transition` - example runs that transition between two scenes. No API change. - -- **`DropShadowFilter`.** A soft, offset silhouette of the filtered node drawn - behind it: `offsetX`/`offsetY`, `blur`, `quality`, `color` (alpha is the - shadow opacity) and `shadowOnly` for glows and detached shadows. Composed from - the stock colour-matrix and blur passes, so it runs on both backends and - declares the extra reach it needs through `getOutputBounds`. - -- **`@codexo/exojs-lighting`, forward normal-mapped point lighting for - sprites.** Lighting happens inside the sprite fragment stage, so a lit scene - costs no extra render pass and no extra draw call - sprites sharing one - `LitSpriteMaterial` stay in one batch. `PointLight` is plain mutable - world-space data (`x`, `y`, `radius`, `color`, `intensity`, `height`); - `LightingSystem` collects lights and packs them, together with the active - count and the ambient term, into one `rgba32f` data texture per frame, and - registers on any `SystemRegistry` like every other system. The light list - being a texture rather than a uniform array is what makes the light count a - shader loop bound instead of a compiled-in constant: a material user uniform - is one `vec4` per name, which would have capped a scene at a handful of - lights and needed a recompile to change. `LitSpriteMaterial` samples a - tangent-space normal map next to the albedo and rotates the normal by the - instance's local-to-world basis, so spinning and mirrored sprites keep their - bumps facing the right way. One normal map per material (= per atlas) is the - v1 contract; there is no deferred path yet. Two examples ship with it: - `lighting/normal-mapped-sprites` and `lighting/many-lights`, which walks from - 1 to 48 lights over a normal-mapped floor without leaving a single draw call. - -- **Custom sprite materials receive the fragment's world position and the - instance's local-to-world basis.** `v_worldPosition` / `v_basis` (GLSL) and - `worldPosition` / `basis` on `VertexOutput` (WGSL) let a fragment shade - against world-space lights and rotate a tangent-space normal with the - sprite, which is what a lighting effect needs and what the varyings did not - carry before. The `lighting/normal-mapped-sprites` example lights a batch of - spinning and mirrored sprites through one material and four point lights. - -- **`Scene.animations`, a scene-bound animation facade with the same `when` - policy the tween and audio facades already have.** An `AnimatedSprite` - attached to a scene tree kept advancing through `SceneDirector.pause()` and - through retention, so a pause menu drawn over a "frozen" world still had - moving sprites and a suspended scene kept burning frames. - `this.animations.add(sprite, { when: 'active' })` binds playback to the - scene: frozen while it is paused, frozen while it is retained, stopped when - it ends. `when` takes the same `SceneAvailability` values with the same - `'always'` default, so an untracked sprite behaves exactly as before. - -- **`TrailParticles`, a particle render mode that draws a motion trail behind - every particle.** Where `RibbonParticles` connects the particles of one - system into a single band, this gives each particle its own strip through the - positions it recently occupied, kept in a per-particle ring buffer and drawn - in one non-instanced draw. Positions are recorded on each particle's own - clock (`interval`), so a trail covers the same travel at any frame rate; - `points` sets how far back it reaches, `width` its thickness and `fade` how - its alpha falls off towards the tail. CPU-only, like `RibbonParticles`. +- **Unicode-safe layout, browser-native shaping, and soft-wrapping TextArea.** ([#677](https://github.com/Exoridus/ExoJS/pull/677)) +- **Add the @codexo/exojs-pathfinding extension package.** ([#676](https://github.com/Exoridus/ExoJS/pull/676)) +- **When policy for SceneInteraction, optional GLSL vertex stage, DropShadowFilter guide.** ([#671](https://github.com/Exoridus/ExoJS/pull/671)) +- **Transition authoring kit, and release phase state when a session ends.** ([#670](https://github.com/Exoridus/ExoJS/pull/670)) +- **V-axis orientation uniform for shader filters, and DisplacementFilter.** ([#669](https://github.com/Exoridus/ExoJS/pull/669)) +- **Add the @codexo/exojs-lighting extension package.** ([#664](https://github.com/Exoridus/ExoJS/pull/664)) +- **Expose world position and basis to custom sprite materials.** ([#659](https://github.com/Exoridus/ExoJS/pull/659)) +- **Add DropShadowFilter and fix the vertical mirror in WebGPU shader filter passes.** ([#660](https://github.com/Exoridus/ExoJS/pull/660)) +- **Periodic BurstSpawn and TrailParticles render mode.** ([#656](https://github.com/Exoridus/ExoJS/pull/656)) +- **Frame budgets, GPU frame timing and asset cache inspection.** ([#658](https://github.com/Exoridus/ExoJS/pull/658)) +- **DPR watcher, hit-ordered scroll wheel, pointer lock, animated widget backgrounds.** ([#657](https://github.com/Exoridus/ExoJS/pull/657)) ### Fixed - **Raise the contact push-out cap to its pixel-scale value and sleep settled piles.** ([#680](https://github.com/Exoridus/ExoJS/pull/680)) - The soft-constraint push-out was capped at 4 px/s. Box2D-v3's analogue - is 3 m/s, which at the pixel scales ExoJS targets is 60-300 px/s, so the - cap stood an order of magnitude below the speed it was meant to express. - A gravity-driven pile rearranges faster than such a push-out can work: - overlap accumulated instead of resolving, and 1000 dynamic circles - settled at 4.7 px mean penetration across 4939 touching pairs where the - geometry has about 2800. The cap is now 60 px/s, the low end of the - honest conversion band and the point where measurement shows the scene - stops fighting it. - - The sleep gate compounded the problem: it reset both bodies' timers - while a contact's penetration exceeded the sleep tolerance, but a loaded - contact rests at the depth its own soft-constraint deflection holds it - at, which in a pile is several times a lone body's, so islands never - slept and a fully settled scene kept being solved in full. The gate now - reads push-out progress instead of depth: a contact deeper than the - tolerance blocks sleep only while the last step actually moved its - overlap, or while the point has no history at all, so geometry appearing - inside a sleeping body still reopens the decision. - - Numbers (median ms/step, touching pairs): `many-dynamic` 1000 bodies - 35.5 ms / 4875 pairs -> 16.1 ms / 2766; 4000 bodies 235.9 ms / 34 423 -> - 99.1 ms / 10 894. A settling pile now sleeps completely at 2.4 ms/step - instead of never at 5.2. `box-stack` 4000: 16.5 -> 9.7 ms with identical - contact counts, which is the sleep gate alone. Three tests whose - literals pinned the old cap were retuned deliberately and one was added: - a contact resting deeper than the tolerance must still sleep once its - push-out has stalled. - - Bench package, same change set: - - planck.js joins the physics matrix as a second pure-JS peer - (`lengthUnitsPerMeter = 30`; its default of 1 double-counts contacts in - pixel coordinates), Rapier is labelled as the WASM reference rather than - a peer, and `--engine` filters the physics domain. Contact counts now - agree across exojs, matter-js, planck and rapier, which they previously - did not. - - New `composite` rendering archetype: scene into an offscreen render - texture, blur sweeps, additive composite over the direct draw. ExoJS - through the public `RenderPipeline`, Pixi hand-rolled with - `RenderTexture` and filters. Structural baseline re-recorded for the two - new cells; no other counter moved. - - *** - -- **Text no longer splits a grapheme cluster.** Layout counted code points, so - a combining sequence, an emoji with a skin-tone modifier, a ZWJ sequence and - a regional-indicator flag were each placed as several glyphs, could be broken - in half by `breakWords` or `maxWidth`, and could be truncated to a dangling - mark or a lone regional indicator by `overflow: 'ellipsis'`. The unit of - layout is now the grapheme cluster throughout - placement, wrapping, - truncation and the caret granularity of the editing widgets - resolved - through `Intl.Segmenter`. Where a browser does not provide it, clusters - degrade to code points and word boundaries to blank runs; no polyfill ships. - -- **Word wrapping is locale-aware rather than a split on spaces.** Text in a - script written without inter-word spaces used to overflow as one unbreakable - token; it now wraps at its own word boundaries. A run of blanks is one break - candidate, and a run that stays inside a line is preserved verbatim. - -- **`FadeSceneTransition` no longer leaks a `QuadGeometry` per navigation.** - Its per-session phase state allocates one, and nothing released it, so every - faded scene change left a backend vertex/index buffer pair behind for the - lifetime of the context. It now releases the quad through the new - `destroyPhaseState` hook. - -- **WebGPU shader filters no longer mirror their input vertically.** The - fullscreen pass sampled v = 0 at the bottom of the quad, which is texel row - 0 on WebGL2 but the last row on WebGPU, so every `ShaderFilter` pass on - WebGPU wrote its input upside down. Invisible while the effect domain equals - the content bounds; wrong for a filter with asymmetric reach or one that - samples away from its own texel. - -- **`Application.destroy()` now aborts the lifecycle signal of a navigation - still inside `load()`.** Teardown already waited for an in-flight navigation - to settle, but nothing told the incoming scene to stop: a `load()` awaiting - `Scene.lifecycleSignal` never resolved, so the wait ran out the full - five-second grace period and the backend went down while the scene was still - preparing. The signal is aborted at the same point the navigation's - generation is invalidated, so a cooperative `load()` returns immediately and - the incoming scope's teardown completes before anything it depends on is - released. +- **Let the package policy accept Core's sideEffects allowlist.** ([#665](https://github.com/Exoridus/ExoJS/pull/665)) +- **Typed renderer bindings, buffered SceneAudio fades, Color.fromCss, LDtk enum fields.** ([#655](https://github.com/Exoridus/ExoJS/pull/655)) +- **Pause-aware scene animations and abortable in-flight navigation.** ([#654](https://github.com/Exoridus/ExoJS/pull/654)) ### Documentation - **Bring the README up to the 0.17 surface.** ([#684](https://github.com/Exoridus/ExoJS/pull/684)) - Package table and install list gain lighting, pathfinding and - tilemap-physics, each package linked to its directory. The feature list - covers custom sprite materials, the lighting package, the Unicode text - stack with its new style properties, exact picking with `hitArea`, the - frame-budgeted job scheduler, the transition authoring kit and the - pathfinding package. The quickstart compiles again against the current - API (`Seconds` instead of the removed `Time` type, a mounted canvas, a - colour that still exists; verified with tsc against `src/`), and the - roadmap names the 0.18 themes instead of items that have shipped. - - *** +- **Show the brand mark and the companion.** ([#663](https://github.com/Exoridus/ExoJS/pull/663)) +- **Add the which-call-when table to the loading guide.** ([#662](https://github.com/Exoridus/ExoJS/pull/662)) ## [0.16.2] - 2026-09-03 From 93599251297d85a6b155328d58312a8a8986b333 Mon Sep 17 00:00:00 2001 From: Exoridus Date: Sat, 12 Sep 2026 06:27:25 +0200 Subject: [PATCH 19/28] docs(changelog): render the 0.16.x sections one line per entry --- CHANGELOG.md | 2634 +++++--------------------------------------------- 1 file changed, 249 insertions(+), 2385 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc6a97e8c..a6bef6c92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,2498 +52,362 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and ### Added -- **`Spritesheet.removeFrame(name)` and `Stack.removeItem(item)`.** Both - mirror their existing `add`-side methods, completing the add/remove pair - every other mutator on these classes already has. +- **`Spritesheet.removeFrame(name)` and `Stack.removeItem(item)`.** ### Fixed -- **Two class doc comments no longer erase their class from the published - declarations under `stripInternal`.** `SceneNode` and `Loader` mentioned - `@internal` in prose, which TypeScript reads as a real tag on the class; the - wording changed. The 22 backend, scene-graph, material, audio, asset-type and - tile-set members that official extension packages reach through the SDK entry - points are now documented as that SDK contract instead of being tagged - `@internal`. The flag itself stays off: 41 internal types still appear in - public signatures and must be made public first. -- **A material drawn into a multi-attachment target now gets a dev-build - warning when its fragment shader under-declares outputs.** The guard that - refuses a drawable without a material never checked whether a material's - own shader actually writes every attachment; a shader with fewer declared - outputs than the target's attachment count silently left the extra - attachments at their previous contents on WebGL2 (WebGPU already refuses - pipeline creation for this). `ShaderSource.countFragmentOutputs` reflects - the declared `@location`/`layout(location = n) out` count from the active - backend's language and warns once per shader/attachment-count pairing when - it falls short. +- **Two class doc comments no longer erase their class from the published declarations under `stripInternal`.** +- **A material drawn into a multi-attachment target now gets a dev-build warning when its fragment shader under-declares outputs.** - **`RenderTexturePool` keys pooled textures by format as well as size.** - `acquire()` matched on `width x height` alone, so a pool holding an - `Rgba8` entry could hand it back for a request in a different format. - Latent today (every caller acquires the default format), but silent - the moment an HDR intermediate requests `Rgba16F`/`Rgba32F`. `acquire()` - now takes an optional `format` parameter (defaulting to `Rgba8`) and - matches on it too. -- **`SharedAbort` dropped its unused multi-holder API.** `retain()`, `holders` - and `aborted` had no caller anywhere in the tree - cancellation is actually - decided by the claim refcount elsewhere - and the class documented an - N-holder contract that was never wired up. Removed, along with the doc - paragraph describing it. +- **`SharedAbort` dropped its unused multi-holder API.** - **A press that hits no interactive node now clears keyboard focus.** - Nothing blurred the focused node on a click or tap that resolved to empty - canvas, so an open `Dropdown` stayed open and a `TextInput` kept its caret - and DOM transport after the user clicked away from it. -- **An infinitely repeating `Tween` releases its target once that target is - destroyed.** A tween had no link to its target's lifetime, so - `repeat(-1)` kept interpolating and writing to a destroyed `SceneNode` - forever, pinning it in memory. `Tween.update` now stops itself (and is - released from `TweenSystem`) the frame after its target reports - `destroyed === true`. +- **An infinitely repeating `Tween` releases its target once that target is destroyed.** - **The fixed-timestep spiral-of-death guard now scales with `fixedTimeStep`.** - The catch-up cap was a constant 5 steps regardless of the configured step - size, so a step smaller than the default silently ran the simulation slower - than wall time once the frame rate dropped enough to hit the clamp - with no - warning. The cap is now derived from the existing frame-delta clamp and the - configured step, so a smaller step gets proportionally more catch-up steps. -- **A `Sprite` whose frame was set before its texture finished loading gets the - right UVs.** Texture coordinates are the frame divided by the texture's - dimensions, so a frame chosen against a still-loading handle - what - `Spritesheet` does on an atlas that has not arrived - was computed against - 0x0 and never recomputed: the sprite sampled a single texel, and a retained - product recorded around it kept the non-finite coordinates for the life of - the root. The sprite now recomputes its coordinates and announces the change - when the payload lands. -- **Tile layers on WebGL2 sample their own tileset again when a sprite is drawn - between them.** `@codexo/exojs-tilemap`'s WebGL2 chunk renderer skipped its - texture bind and blend call whenever its private memo matched, but the memo - outlived the batch it described - and a sprite drawn between two tile layers - binds its own texture to the very unit the tile shader samples. The layer - after it drew the sprite's pixels, with the sprite's blend mode. The WebGPU - chunk renderer was never affected, so the backends visibly disagreed. -- **`Text` and `BitmapText` honour `blendMode` on both backends.** The setter - is public on every drawable and already broke the render batch, but neither - text renderer applied it: WebGPU baked `Normal` into its pipeline, and WebGL2 - drew with whatever blend state the previously flushed renderer had left, so - the same run could composite differently from frame to frame. A text batch - now breaks on a blend change and draws with the mode it declares. -- **Sprites and meshes on WebGL2 draw with the blend mode they declare, even - when another renderer type is interleaved.** The WebGL2 blend state is one - global the backend owns, but the sprite and mesh renderers kept a private - copy and skipped the backend call whenever a batch declared the mode that - copy already held. A `Graphics` or nine-slice drawn in between had changed - the real state in the meantime, so the next batch composited with a foreign - blend mode - visibly disagreeing with WebGPU, which resolves blend per - pipeline. Each batch now establishes its blend mode at its own draw call. -- **Nine-slice sprites on WebGL2 pick up a texture whose payload changed under - a stable identity, and keep their own blend mode.** The renderer bound the - batch texture only when the identity differed from the last one it had seen, - and the bind is what carries the upload - so a skin repainted through - `Texture.setSource()` / `updateSource()`, a canvas or `ImageBitmap` texture - refreshed per frame, and a `RenderTexture` re-rendered into went on drawing - the pixels of their first upload forever. The same memo skipped the live - check that rejects a destroyed texture, and its blend counterpart let another - renderer's blend mode through. Texture and blend state are now established at - the batch's own draw call. -- **Particle systems on WebGL2 pick up a texture whose payload arrives after - the first draw.** `WebGl2ParticleRenderer` bound the system's texture only - when its identity changed, which for a single-system scene meant exactly - once - while the handle from `loader.get(...)` was still empty. The image - landed a few frames later and never reached the GPU, so the system simulated - and drew its quads against blank pixels for the rest of its life. The same - memo could hold a stale blend mode after another renderer changed it. Both - are now offered to the backend on every system, which already collapses a - redundant bind and is the only holder of the live GL state. -- **A payload of one to three bytes is sniffed as `text/plain` instead of - throwing.** The MP4 magic-byte check read a 32-bit box size without checking - that the buffer holds one, so a truncated or near-empty response surfaced as - `Offset is outside the bounds of the DataView` wrapped in a load failure, - rather than as the documented fallback. -- **`WebStorageStore.set()` rejects on a full quota instead of throwing - synchronously.** Web Storage is synchronous, so a `QuotaExceededError` - escaped the returned promise and never reached the `.catch()` that is the - only handler a write has - unlike the serialization failure two lines above - it, which did reject. +- **A `Sprite` whose frame was set before its texture finished loading gets the right UVs.** +- **Tile layers on WebGL2 sample their own tileset again when a sprite is drawn between them.** +- **`Text` and `BitmapText` honour `blendMode` on both backends.** +- **Sprites and meshes on WebGL2 draw with the blend mode they declare, even when another renderer type is interleaved.** +- **Nine-slice sprites on WebGL2 pick up a texture whose payload changed under a stable identity, and keep their own blend mode.** +- **Particle systems on WebGL2 pick up a texture whose payload arrives after the first draw.** +- **A payload of one to three bytes is sniffed as `text/plain` instead of throwing.** +- **`WebStorageStore.set()` rejects on a full quota instead of throwing synchronously.** - **A blocked IndexedDB open no longer leaks the connection it later gets.** - The `blocked` event rejected the open, but the request still completed once - the blocking connection went away, handing over an `IDBDatabase` nobody was - waiting for and nothing closed - which then blocked every future upgrade in - its turn. A connection arriving after a rejected open is closed. - **A container that packs one source twice is rejected instead of unpacked.** - Both entries resolved to a single asset identity, so the second payload - replaced the first - and for a texture, video or music entry the replaced one - owned a GPU upload or a media element that no owner could release. -- **Two sources that differ only in a `|` are two assets again.** Resource and - source keys joined their fields with an unescaped separator, so a URL - carrying an unencoded `|` in its query could compose the very key another - request composes from a source plus a discriminator - and the two then shared - one residency entry, one fetch and one claim set. The fields are now escaped - the way persisted cache record keys already were. Because the source key is - part of a persisted record's identity, the default cache layout version is - raised: records written by an earlier version are no longer found and are - acquired again rather than read under the old spelling. -- **`awaitBackground()` settles for every caller, and for a queue an eviction - emptied.** The residency kept a single resolver slot, so a second concurrent - call overwrote the first and that promise never settled - a loading screen - and a scene preloader awaiting one drain deadlocked one of them, with no - error. Dropping a queued entry because its last claim went away (a scene - teardown, a cancelled load) also left the queue counted as unfinished - forever, so a lone awaiting caller hung and `onProgress` stayed below its - total. Both now settle. -- **A `get()` or `load()` reaching a source while its container is unpacking - joins the unpack.** An unpack registered no in-flight identity, so for the - whole window between parsing the index and storing a payload the key looked - unknown to the loader and a concurrent acquisition of the same source started - a second, competing fetch whose payload overwrote the container's. Which one - a consumer saw was timing-dependent, and the loser - a texture upload, a - media element - was never released. +- **Two sources that differ only in a `|` are two assets again.** +- **`awaitBackground()` settles for every caller, and for a queue an eviction emptied.** +- **A `get()` or `load()` reaching a source while its container is unpacking joins the unpack.** - **A container that fails while unpacking releases what it already claimed.** - `Loader.loadContainer` claims every entry up front and only then builds them, - but a failure rejected without ever handing the caller the scope holding - those claims, so every entry of a failed container stayed resident for the - loader's lifetime with no owner able to free it. -- **A destroyed `LoaderScope` refuses to claim.** `get`, `load` and - `loadContainer` still registered claims after `destroy()`, and since destroy - is idempotent by contract there was no way to release them: an async - continuation that outlived a scene teardown pinned its assets for the - application's lifetime, and `Loader.inspect()` listed a destroyed scope as an - owner. They now throw instead. -- **Releasing a font or an image frees what it owns.** Neither factory - implemented the per-resource teardown its resources need: a released font - stayed registered on `document.fonts` (so CSS and Canvas went on resolving - its family) and pinned for the loader's lifetime, and a released image never - closed the `ImageBitmap` it had decoded. Only a bitmap the engine decoded - itself is closed. -- **An asset unloaded mid-fetch frees the resource its factory had already - built.** The store was skipped, as it must be, but the finished resource - - which for a video or a music stream owns a media element - was handed back - undisposed and stayed alive until the loader was torn down. -- **A failed or cancelled `music`/`video` load cleans up after itself.** The - element was registered with the factory before the source was attached, and a - failure left it there with its object URL unrevoked - and since no resource - was built, nothing would ever release it. A repeatedly retried blob-backed - load accumulated one element and one live blob per attempt. -- **A `fetchOptions.signal` set on the loader aborts asset loads too.** Every - asset load replaced it with the loader's own cancellation signal, so an - application-wide abort signal worked for `.exoa` containers and silently did - nothing for every other asset. The two are composed now: either one aborts - the request, and neither disables the other. -- **Options a later `get()` loses are diagnosed in development.** `Loader.get` - documents that the same source yields the same instance and that conflicting - options on a later call are ignored with a one-time dev warning - but no such - warning existed, so a second `get('x.png', { textureOptions })` dropped its - sampler request with nothing to diagnose it. The warning now exists (once per - source, stripped in production). Options that take part in asset identity are - unaffected: they resolve to their own instance and lose nothing. -- **A `stop()` made while `start()` is still loading is no longer overwritten - when the startup run settles.** The startup continuation promoted the state - to `Running` unconditionally, so a `stop()` that landed inside the loading - window - where the frame loop is live but the state is still `Loading` - was - undone the moment `start()` resolved. The application then advertised - `Running` for a halted loop, and because both `start()` and `stop()` - early-return on that state, the instance was permanently unusable. The - promotion now happens only if the loop the run started is still the live one. -- **A destroyed `Application` stays destroyed.** A `start()` that failed while - `destroy()` was running reset the state to `Stopped` from its own catch, and - because that continuation resumes after the teardown chain has finished, - `Stopped` was the last value written. `state` then lied about a destroyed - instance and `start()` accepted it, reinitializing an already-destroyed - backend and restarting the frame loop over released subsystems. `Destroying` - and `Destroyed` are now terminal - the only transition out of them is - `Destroying` to `Destroyed` - so a late startup failure cannot resurrect the - application and `start()` rejects as documented. -- **`Application.destroy()` waits for a scene navigation that is still in - flight.** `destroy()` documents that `scenes` is fully disposed before the - Loader, rendering context, audio system and backend are released, but a - `change()`/`restore()` (or the initial `start(Target)` navigation) still - inside `load()` was tracked by nothing the disposal awaited: aborting it only - invalidated its generation, so the incoming scene went on to run `init()`, - `unload()` and `destroy()` after teardown had finished, against subsystems - that no longer existed. The disposal now awaits the run it just aborted, the - same way it already awaits a preload. A scene whose `load()` never settles is - bounded by `destroy()`'s existing teardown grace period. +- **A destroyed `LoaderScope` refuses to claim.** +- **Releasing a font or an image frees what it owns.** +- **An asset unloaded mid-fetch frees the resource its factory had already built.** +- **A failed or cancelled `music`/`video` load cleans up after itself.** +- **A `fetchOptions.signal` set on the loader aborts asset loads too.** +- **Options a later `get()` loses are diagnosed in development.** +- **A `stop()` made while `start()` is still loading is no longer overwritten when the startup run settles.** +- **A destroyed `Application` stays destroyed.** +- **`Application.destroy()` waits for a scene navigation that is still in flight.** - **A destroyed scene graph is no longer pinned by the changed-record index.** - The process-wide dirty index recycled a generation by resetting its logical - length only, leaving every entry above it in place - and a `SceneNode` links - to its parent while a `Container` links to its children, so one retired entry - kept a whole destroyed scene alive for the life of the process, long after - the index itself reported those marks as outside its window. A recycled - generation now drops its references, and a node releases its entry when it is - destroyed, which is what leaves nothing pinned once `Application.destroy()` - has stopped advancing the index. - **A system removed and added back inside the same frame stays registered.** - `remove()` marks a registration inactive and queues the structural delete for - the frame boundary, but left it in the registry - so the `add()` that - followed hit the duplicate-registration no-op, and the queued removal then - deleted the system at the end of the frame with no error anywhere. The - pattern is what a system re-registering itself to change its order or phase - does, and it silently lost the system. `add()` now cancels a pending removal - and re-registers with the options that call asks for. -- **`SystemRegistry.remove()` reports the truth for a buffered add.** Removing - a system added earlier in the same frame - one that never became a - registration, and for which `has()` answers `false` - returned `true`, - contradicting the method's own "true if it was registered". The add is still - cancelled; the return value now agrees with `has()`. -- **Loader claims are released last on both scene teardown paths.** A scope - torn down after a failed `load()`/`init()` released its loader claims before - calling `scene.destroy()`, while the ordinary teardown - whose documentation - names "release loader claims last" as the normative order - released them - after it. A `Scene.destroy()` override reaching for `this.loader` therefore - saw a live claim scope or a destroyed one depending on whether activation had - succeeded, and a claim taken on the failed path was never released. Both - paths now release last. -- **A manual `Application.update()` no longer forks the frame loop.** The - public tick rescheduled the next animation frame unconditionally, so calling - it while the loop was live - from `onFrame`, from an external fixed-rate - host, from a test harness that also lets the loop run - started a second - chain alongside the first and silently doubled the frame rate. Scheduling now - belongs to the loop's own callback: `update()` runs exactly one frame. -- **`SceneNode.destroy()` and `RenderNode.destroy()` are idempotent.** Only - `Container` guarded re-entry, while the two layers beneath it documented - releasing state a second pass must not touch again - so a double `destroy()` - on a leaf node took its transform, bounds and flags through a second - teardown, and on a `RenderNode` re-ran the filter and signal release as well. - A second call is now a no-op at every layer. -- **Audio objects created while the context is locked again are set up on the - next unlock.** `onAudioContextReady` latched after its very first dispatch, - so a bus, listener, stream voice, worklet or effect constructed while the - context sat suspended - after an iOS audio-session interruption or a bfcache - restore - subscribed to a signal that could never fire and stayed silent for - the rest of the session. The signal now dispatches once per run of the - context, to whoever is subscribed at that moment. -- **An effect that never finishes its setup no longer silences the bus it is - attached to.** `AudioBus` rebuilt its effect chain by disconnecting the bus - input first and only then reading each effect's nodes, so an effect still - unready on the retry pass threw with the graph half torn down - inside a - microtask, where no caller could see it - and the bus stayed cut from its pan - stage for good. The chain is now resolved before anything is disconnected, - and an unready effect is bypassed with one diagnostic naming it. +- **`SystemRegistry.remove()` reports the truth for a buffered add.** +- **Loader claims are released last on both scene teardown paths.** +- **A manual `Application.update()` no longer forks the frame loop.** +- **`SceneNode.destroy()` and `RenderNode.destroy()` are idempotent.** +- **Audio objects created while the context is locked again are set up on the next unlock.** +- **An effect that never finishes its setup no longer silences the bus it is attached to.** - **A worklet effect whose module fails to load degrades to a passthrough.** - `WorkletEffect.ready` rejected with nobody attached, so a blocked - `addModule` - a Content-Security-Policy forbidding `blob:` worker sources is - the realistic case - surfaced as an unhandled rejection. The failure is - logged instead and `ready` resolves; the effect keeps passing dry signal. -- **A rejected `AudioContext.resume()` is logged instead of surfacing as an - unhandled rejection.** The autoplay-unlock gesture handler attached no - rejection handler, so a context the browser refused to resume produced a bare - rejection with no hint of where it came from. -- **Audio zones only colour spatial voices.** `SpatialZones` documented that it - reconciles the live set of spatial voices, but `AudioSystem` handed it every - voice - so a reverb zone opened a send on UI blips and music beds that have no - position in the world at all. A voice that stops being positional now has its - zone sends closed instead of keeping them open untouched. -- **`AudioZone.height` is documented as the half-band it is.** The option and - the field described it two different ways; `height: 100` covers `z` from - `-100` to `100`, which both now say. -- **A per-call `repeat` on `AnimatedSprite.play()` no longer leaks into the next - clip.** The override was written on every `play()` and never cleared, so - `play('attack', { repeat: 2 })` followed by `play('idle')` stopped the - indefinitely-looping idle clip after two cycles. An override now belongs to - the playback run that set it. -- **A single-frame `AnimatedSprite` clip completes.** `update()` returned before - any timing ran for a clip with one frame - the shape an Aseprite frame tag - covering a single frame exports - so `onComplete` never fired, the sprite - stayed `playing`, and its `AnimationSystem` registration was never released. - The frame is now held for its own duration and then takes the normal - completion path. -- **`TweenSystem.clear()` and `destroy()` stop the tickers they drop.** Tweens - were stopped but registered tickers were only dropped, so a `TweenSequencer` - kept reporting `Active` after an application teardown with nothing left to - advance it. +- **A rejected `AudioContext.resume()` is logged instead of surfacing as an unhandled rejection.** +- **Audio zones only colour spatial voices.** +- **`AudioZone.height` is documented as the half-band it is.** +- **A per-call `repeat` on `AnimatedSprite.play()` no longer leaks into the next clip.** +- **A single-frame `AnimatedSprite` clip completes.** +- **`TweenSystem.clear()` and `destroy()` stop the tickers they drop.** - **A `TweenSequencer` delay stage carries its overshoot into the next stage.** - The time past a `wait()` stage's own duration was dropped, so a chain of - waits drifted by up to one frame per stage and a repeated sequence - accumulated the error - `Tween.update` already carries the same remainder - into its next repeat cycle. - **A rebuilt particle GPU state no longer delivers garbage death contexts.** - `@codexo/exojs-particles` kept the queue of deaths still awaiting a readback - when the GPU state was torn down - after a device loss, a backend change or a - transition back to CPU modules - so the next state staged that many records - out of a freshly zeroed device buffer and every death module saw a - zero-valued context, firing `SpawnOnDeath` sub-emitters at the origin. -- **`RateSpawn` recovers from a negative rate sample.** A distribution that can - return a value below zero drove the accumulator down without bound, and the - emitter never spawned again. The sampled rate is clamped at zero. +- **`RateSpawn` recovers from a negative rate sample.** - **An out-of-range `ParticleWriter.frame` no longer wraps onto a valid frame.** - The frame channel is a `Uint16Array`, so `-1` became `65535` and `70000` - became `4464` - both of which can be a frame the system declares, instead of - the documented frame-0 fallback. Development builds throw with the offending - index; production clamps. -- **A particle system is no longer culled by the size of one particle.** A - `ParticleSystem`'s local bounds cover one texture frame at its local origin - - a single pixel for the default white texture - while its particles travel - arbitrarily far from there, so the viewport check removed the whole cloud as - soon as the emitter's origin scrolled out of view. Systems opt out of culling - by default; `cullArea` is the documented way back in for a system whose reach - is known. +- **A particle system is no longer culled by the size of one particle.** - **The WebGPU compute path samples the texture frame the CPU path does.** - `@codexo/exojs-particles` baked a particle system's frame UVs into a uniform - block once, when the GPU state was built, and assumed the whole texture - whenever no atlas was declared - so `system.textureFrame` was ignored - outright, and a texture swapped in later left the UVs divided by the previous - texture's dimensions. The same scene drew a sub-rect on WebGL2 and the whole - atlas on WebGPU. Both setters now re-bake the block. -- **Two update modules of the same class are reported by name instead of - breaking WebGPU only.** A module's WGSL `key` names one struct and one member - of the composite compute shader's uniform block, so a second module under the - same key made the codegen declare each twice - invalid WGSL that failed - pipeline creation at the first `update()`, while WebGL2 ran the same scene. - `@codexo/exojs-particles` now throws `ParticleModuleKeyCollisionError` naming - both modules and the key, before the device is asked to build anything. +- **Two update modules of the same class are reported by name instead of breaking WebGPU only.** - **A focused text field no longer blanks the keyboard and wheel pipeline.** - Focusing a field hands host focus to the platform's own text transport, which - blurs the canvas - and keyboard and wheel input were gated on the canvas - element holding focus alone. Every editing key the widget owns (caret motion, - Home/End, selection extension, `Escape`, `Ctrl+A`, `Ctrl+Z`/`Y`, `Enter` in a - single-line field) and all wheel scrolling were dead for as long as any field - was focused, and the held keys of a running game were released. The gate now - treats an engine-owned transport holding host focus as the application - holding it, and closes again when focus really leaves - including to a - foreign element of the embedding page. Leaving a field hands host focus back - to the surface rather than dropping it on the document. A transport that - reports an edit for a keystroke the widget also handles (Backspace, Delete, - and `Enter` in a multi-line field) now applies it once instead of twice. - `app.input.canvasFocused` and `onCanvasFocusChange` mean "this application - holds keyboard focus"; a press on a surface the host refuses focus for - reports the change instead of flipping the flag silently. -- **Text input works again in browsers that ship `EditContext`.** The backend - attached its context with `EditContext.attachToElement()`, a draft method no - browser implements, and hung it on a `