Skip to content

Commit 6e112ce

Browse files
authored
New Workload: Babylon class-heavy workload (#143)
Applying the same cache-busting pattern, I've created a [Babylon.js](https://github.com/BabylonJS/Babylon.js)-based startup workload. - Startup Version (parse - first-eval focused): - Eval a new string on every second iteration (configurable) - Load all class names from the babylon module - Set up a basic scene and run a few frames (configurable) - Scene Version (JS execution focused): - Does not measure startup - Sets up complex scene (parsing .glb blobs) - Renders and animates 100 frames There is an ES6 and ES5 version of the same workload and they both heavily stress parse+top-level setup code with many classes.
1 parent a4cb123 commit 6e112ce

22 files changed

+111108
-0
lines changed

JetStreamDriver.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2603,6 +2603,68 @@ let BENCHMARKS = [
26032603
allowUtf16: true,
26042604
tags: ["default", "Wasm"],
26052605
}),
2606+
new AsyncBenchmark({
2607+
name: "babylonjs-startup-es5",
2608+
files: [
2609+
"./utils/StartupBenchmark.js",
2610+
"./babylonjs/benchmark/startup.js",
2611+
],
2612+
preload: {
2613+
BUNDLE: "./babylonjs/dist/bundle.es5.min.js",
2614+
},
2615+
arguments: {
2616+
expectedCacheCommentCount: 23988,
2617+
},
2618+
tags: ["startup", "js", "class", "es5", "babylonjs"],
2619+
iterations: 10,
2620+
}),
2621+
new AsyncBenchmark({
2622+
name: "babylonjs-startup-es6",
2623+
files: [
2624+
"./utils/StartupBenchmark.js",
2625+
"./babylonjs/benchmark/startup.js",
2626+
],
2627+
preload: {
2628+
BUNDLE: "./babylonjs/dist/bundle.es6.min.js",
2629+
},
2630+
arguments: {
2631+
expectedCacheCommentCount: 21222,
2632+
},
2633+
tags: ["Default", "js", "startup", "class", "es6", "babylonjs"],
2634+
iterations: 10,
2635+
}),
2636+
new AsyncBenchmark({
2637+
name: "babylonjs-scene-es5",
2638+
files: [
2639+
// Use non-minified sources for easier profiling:
2640+
// "./babylonjs/dist/bundle.es5.js",
2641+
"./babylonjs/dist/bundle.es5.min.js",
2642+
"./babylonjs/benchmark/scene.js",
2643+
],
2644+
preload: {
2645+
PARTICLES_BLOB: "./babylonjs/data/particles.json",
2646+
PIRATE_FORT_BLOB: "./babylonjs/data/pirateFort.glb",
2647+
CANNON_BLOB: "./babylonjs/data/cannon.glb",
2648+
},
2649+
tags: ["scene", "js", "es5", "babylonjs"],
2650+
iterations: 5,
2651+
}),
2652+
new AsyncBenchmark({
2653+
name: "babylonjs-scene-es6",
2654+
files: [
2655+
// Use non-minified sources for easier profiling:
2656+
// "./babylonjs/dist/bundle.es6.js",
2657+
"./babylonjs/dist/bundle.es6.min.js",
2658+
"./babylonjs/benchmark/scene.js",
2659+
],
2660+
preload: {
2661+
PARTICLES_BLOB: "./babylonjs/data/particles.json",
2662+
PIRATE_FORT_BLOB: "./babylonjs/data/pirateFort.glb",
2663+
CANNON_BLOB: "./babylonjs/data/cannon.glb",
2664+
},
2665+
tags: ["Default", "js", "scene", "es6", "babylonjs"],
2666+
iterations: 5,
2667+
}),
26062668
// WorkerTests
26072669
new AsyncBenchmark({
26082670
name: "bomb-workers",

babylonjs/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Babylon.js Benchmarks for JetStream
2+
3+
This project contains two benchmarks for testing the performance of the Babylon.js 3D engine.
4+
5+
## Build Instructions
6+
7+
```bash
8+
# install required node packages.
9+
npm ci
10+
# build the workload, output is ./dist
11+
npm run build
12+
```
13+
14+
## Workloads
15+
16+
There are two distinct workloads in this benchmark suite:
17+
18+
### 1. Startup Workload
19+
20+
This benchmark measures the time it takes for the Babylon.js engine to initialize. It evaluates a large, bundled source file and measures the time to parse the code and execute a simple test. This workload is primarily focused on parse and startup time.
21+
22+
To run this benchmark in node for testing:
23+
```bash
24+
npm run test:startup
25+
```
26+
27+
### 2. Scene Workload
28+
29+
This benchmark measures the rendering performance of a complex 3D scene. It loads 3D models (`.glb` files), animations, and particle systems, and then renders the scene for a number of frames.
30+
31+
To run this benchmark in node for testing:
32+
```bash
33+
npm run test:scene
34+
```

babylonjs/benchmark/scene-node.mjs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { runComplexScene } from "../src/babylon-js-benchmark.mjs";
2+
import { promises as fs } from "fs";
3+
import path from "path";
4+
import { fileURLToPath } from "url";
5+
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = path.dirname(__filename);
8+
9+
const fortPath = path.resolve(__dirname, "../data/pirateFort.glb");
10+
const cannonPath = path.resolve(__dirname, "../data/cannon.glb");
11+
const particlePath = path.resolve(__dirname, "../data/particles.json");
12+
13+
async function main() {
14+
try {
15+
const fortBuffer = await fs.readFile(fortPath);
16+
const cannonBuffer = await fs.readFile(cannonPath);
17+
const particleData = JSON.parse(await fs.readFile(particlePath, "utf-8"))
18+
const {classNames, cameraRotationLength} = await runComplexScene(fortBuffer, cannonBuffer, particleData, 1000);
19+
} catch(e) {
20+
console.error(e);
21+
console.error(e.stack);
22+
}
23+
}
24+
25+
main();

babylonjs/benchmark/scene.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// console.log = () => {};
2+
3+
globalThis.setTimeout = (callback, timeout) => callback();
4+
globalThis.requestAnimationFrame = (callback) => callback();
5+
6+
// JetStream benchmark.
7+
class Benchmark {
8+
iterationCount = 0;
9+
preloaded = {
10+
fortData: null,
11+
cannonData: null,
12+
particlesJson: null,
13+
};
14+
15+
constructor(iterationCount) {
16+
this.iterationCount = iterationCount;
17+
}
18+
19+
async init() {
20+
const [fort, cannon, particles] = await Promise.all([
21+
JetStream.getBinary(JetStream.preload.PIRATE_FORT_BLOB),
22+
JetStream.getBinary(JetStream.preload.CANNON_BLOB),
23+
JetStream.getString(JetStream.preload.PARTICLES_BLOB),
24+
]);
25+
this.preloaded.fortData = fort;
26+
this.preloaded.cannonData = cannon;
27+
this.preloaded.particlesJson = JSON.parse(particles);
28+
}
29+
30+
async runIteration() {
31+
const {classNames, cameraRotationLength} = await BabylonJSBenchmark.runComplexScene(
32+
this.preloaded.fortData,
33+
this.preloaded.cannonData,
34+
this.preloaded.particlesJson,
35+
100
36+
);
37+
const lastResult = {
38+
classNames,
39+
cameraRotationLength
40+
};
41+
this.validateIteration(lastResult);
42+
}
43+
44+
validateIteration(lastResult) {
45+
this.expect("this.lastResult.classNames.length", lastResult.classNames.length, 2135);
46+
this.expect("this.lastResult.cameraRotationLength", lastResult.cameraRotationLength, 0);
47+
}
48+
49+
expect(name, value, expected) {
50+
if (value != expected)
51+
throw new Error(`Expected ${name} to be ${expected}, but got ${value}`);
52+
}
53+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { runTest } from "../src/babylon-js-benchmark.mjs";
2+
3+
console.log(runTest());

babylonjs/benchmark/startup.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright (C) 2025 Apple Inc. All rights reserved.
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions
6+
* are met:
7+
* 1. Redistributions of source code must retain the above copyright
8+
* notice, this list of conditions and the following disclaimer.
9+
* 2. Redistributions in binary form must reproduce the above copyright
10+
* notice, this list of conditions and the following disclaimer in the
11+
* documentation and/or other materials provided with the distribution.
12+
*
13+
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14+
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16+
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
17+
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18+
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19+
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20+
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21+
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24+
*/
25+
26+
// console.log = () => {};
27+
28+
class Benchmark extends StartupBenchmark {
29+
iteration = 0;
30+
31+
constructor({iterationCount, expectedCacheCommentCount}) {
32+
super({
33+
iterationCount,
34+
codeReuseCount: 2,
35+
expectedCacheCommentCount,
36+
});
37+
}
38+
39+
runIteration() {
40+
const sourceCode = this.iterationSourceCodes[this.iteration];
41+
if (!sourceCode)
42+
throw new Error(`Could not find source for iteration ${this.iteration}`);
43+
// Module in sourceCode it assigned to the ClassStartupTest variable.
44+
let BabylonJSBenchmark;
45+
46+
// let initStart = performance.now();
47+
eval(sourceCode);
48+
// const runStart = performance.now();
49+
50+
const { classNames, cameraRotationLength } = BabylonJSBenchmark.runTest(30);
51+
const lastResult = {
52+
classNames,
53+
cameraRotationLength,
54+
};
55+
this.validateIteration(lastResult)
56+
// const end = performance.now();
57+
// const loadTime = runStart - initStart;
58+
// const runTime = end - runStart;
59+
// For local debugging:
60+
// print(`Iteration ${this.iteration}:`);
61+
// print(` Load time: ${loadTime.toFixed(2)}ms`);
62+
// print(` Render time: ${runTime.toFixed(2)}ms`);
63+
this.iteration++;
64+
}
65+
66+
validateIteration(lastResult) {
67+
this.expect(
68+
"this.lastResult.classNames.length",
69+
lastResult.classNames.length,
70+
2135
71+
);
72+
this.expect(
73+
"this.lastResult.cameraRotationLength",
74+
Math.round(lastResult.cameraRotationLength * 1000),
75+
464
76+
);
77+
}
78+
79+
expect(name, value, expected) {
80+
if (value != expected)
81+
throw new Error(`Expected ${name} to be ${expected}, but got ${value}`);
82+
}
83+
}

babylonjs/build/build.mjs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
2+
import fs from 'fs';
3+
import path from 'path';
4+
import { fileURLToPath } from 'url';
5+
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = path.dirname(__filename);
8+
9+
const SNIPPET_URL = 'https://snippet.babylonjs.com/LCBQ5Y/6';
10+
const RAW_SNIPPET_PATH = path.resolve(__dirname, '../data/snippets.LCBQ5Y.raw.json');
11+
const PARTICLES_PATH = path.resolve(__dirname, '../data/particles.json');
12+
13+
async function main() {
14+
const response = await fetch(SNIPPET_URL);
15+
if (!response.ok) {
16+
throw new Error(`Failed to download file: ${response.status} ${response.statusText}`);
17+
}
18+
const snippetJson = await response.json();
19+
20+
console.log(`Saving raw snippet: ${RAW_SNIPPET_PATH}`);
21+
fs.writeFileSync(RAW_SNIPPET_PATH, JSON.stringify(snippetJson));
22+
23+
const jsonPayload = JSON.parse(snippetJson.jsonPayload);
24+
const particleSystem = JSON.parse(jsonPayload.particleSystem);
25+
26+
console.log(`Saving particles: ${PARTICLES_PATH}:`);
27+
fs.writeFileSync(PARTICLES_PATH, JSON.stringify(particleSystem, null, 2));
28+
}
29+
30+
main();

0 commit comments

Comments
 (0)