Skip to content

New Workload: Babylon class-heavy workload #143

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions JetStreamDriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -2266,6 +2266,60 @@ let BENCHMARKS = [
deterministicRandom: true,
tags: ["Default", "Wasm"],
}),
new AsyncBenchmark({
name: "babylonjs-startup-es5",
files: [
"./babylonjs/benchmark/startup.js",
],
preload: {
BUNDLE_BLOB: "./babylonjs/dist/bundle.es5.min.js",
},
tags: ["Default", "startup", "class", "es5"],
iterations: 10,
}),
new AsyncBenchmark({
name: "babylonjs-startup-es6",
files: [
"./babylonjs/benchmark/startup.js",
],
preload: {
BUNDLE_BLOB: "./babylonjs/dist/bundle.es6.min.js",
},
tags: ["Default", "startup", "class", "es6"],
iterations: 10,
}),
new AsyncBenchmark({
name: "babylonjs-scene-es5",
files: [
// Use non-minified sources for easier profiling:
// "./babylonjs/dist/bundle.es5.js",
"./babylonjs/dist/bundle.es5.min.js",
"./babylonjs/benchmark/scene.js",
],
preload: {
PARTICLES_BLOB: "./babylonjs/data/particles.json",
PIRATE_FORT_BLOB: "./babylonjs/data/pirateFort.glb",
CANNON_BLOB: "./babylonjs/data/cannon.glb",
},
tags: ["Default", "scene", "es6"],
iterations: 5,
}),
new AsyncBenchmark({
name: "babylonjs-scene-es6",
files: [
// Use non-minified sources for easier profiling:
// "./babylonjs/dist/bundle.es6.js",
"./babylonjs/dist/bundle.es6.min.js",
"./babylonjs/benchmark/scene.js",
],
preload: {
PARTICLES_BLOB: "./babylonjs/data/particles.json",
PIRATE_FORT_BLOB: "./babylonjs/data/pirateFort.glb",
CANNON_BLOB: "./babylonjs/data/cannon.glb",
},
tags: ["Default", "scene", "es6"],
iterations: 5,
}),
// WorkerTests
new AsyncBenchmark({
name: "bomb-workers",
Expand Down
34 changes: 34 additions & 0 deletions babylonjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Babylon.js Benchmarks for JetStream

This project contains two benchmarks for testing the performance of the Babylon.js 3D engine.

## Build Instructions

```bash
# install required node packages.
npm ci
# build the workload, output is ./dist
npm run build
```

## Workloads

There are two distinct workloads in this benchmark suite:

### 1. Startup Workload

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.

To run this benchmark in node for testing:
```bash
npm run test:startup
```

### 2. Scene Workload

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.

To run this benchmark in node for testing:
```bash
npm run test:scene
```
25 changes: 25 additions & 0 deletions babylonjs/benchmark/scene-node.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { runComplexScene } from "../src/babylon-js-benchmark.mjs";
import { promises as fs } from "fs";
import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const fortPath = path.resolve(__dirname, "../data/pirateFort.glb");
const cannonPath = path.resolve(__dirname, "../data/cannon.glb");
const particlePath = path.resolve(__dirname, "../data/particles.json");

async function main() {
try {
const fortBuffer = await fs.readFile(fortPath);
const cannonBuffer = await fs.readFile(cannonPath);
const particleData = JSON.parse(await fs.readFile(particlePath, "utf-8"))
const {classNames, cameraRotationLength} = await runComplexScene(fortBuffer, cannonBuffer, particleData, 1000);
} catch(e) {
console.error(e);
console.error(e.stack);
}
}

main();
50 changes: 50 additions & 0 deletions babylonjs/benchmark/scene.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// console.log = () => {};

// JetStream benchmark.
class Benchmark {
iterationCount = 0;
lastResult = {};
preloaded = {
fortData: null,
cannonData: null,
particlesJson: null,
};

constructor(iterationCount) {
this.iterationCount = iterationCount;
}

async init() {
const [fort, cannon, particles] = await Promise.all([
JetStream.getBinary(JetStream.preload.PIRATE_FORT_BLOB),
JetStream.getBinary(JetStream.preload.CANNON_BLOB),
JetStream.getString(JetStream.preload.PARTICLES_BLOB),
]);
this.preloaded.fortData = fort;
this.preloaded.cannonData = cannon;
this.preloaded.particlesJson = JSON.parse(particles);
}

async runIteration() {
const {classNames, cameraRotationLength} = await BabylonJSBenchmark.runComplexScene(
this.preloaded.fortData,
this.preloaded.cannonData,
this.preloaded.particlesJson,
100
);
this.lastResult = {
classNames,
cameraRotationLength
};
}

validate() {
this.expect("this.lastResult.classNames.length", this.lastResult.classNames.length, 2135);
this.expect("this.lastResult.cameraRotationLength", this.lastResult.cameraRotationLength, 0);
}

expect(name, value, expected) {
if (value != expected)
throw new Error(`Expected ${name} to be ${expected}, but got ${value}`);
}
}
3 changes: 3 additions & 0 deletions babylonjs/benchmark/startup-node.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { runTest } from "../src/babylon-js-benchmark.mjs";

console.log(runTest());
91 changes: 91 additions & 0 deletions babylonjs/benchmark/startup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@


function quickHash(str) {
let hash = 5381;
let i = str.length;
while (i > 0) {
hash = (hash * 33) ^ (str.charCodeAt(i) | 0);
i-= 919;
}
return hash | 0;
}

// console.log = () => {};

const CACHE_BUST_COMMENT = "/*ThouShaltNotCache*/";
const CACHE_BUST_COMMENT_RE = new RegExp(`\n${RegExp.escape(CACHE_BUST_COMMENT)}\n`, "g");

// JetStream benchmark.
class Benchmark {
// How many times (separate iterations) should we reuse the source code.
// Use 0 to skip.
CODE_REUSE_COUNT = 2
iterationCount = 0;
iteration = 0;
sourceCode;
sourceHash = 0
iterationSourceCodes = [];
lastResult = {};

constructor(iterationCount) {
this.iterationCount = iterationCount
}

async init() {
this.sourceCode = await JetStream.getString(JetStream.preload.BUNDLE_BLOB);
console.assert("Cache Comment Count", [21230, 21238].includes(this.sourceCode.match(CACHE_BUST_COMMENT_RE).length));
for (let i = 0; i < this.iterationCount; i++)
this.iterationSourceCodes[i] = this.prepareCode(i);
}


prepareCode(iteration) {
if (!this.CODE_REUSE_COUNT)
return this.sourceCode;
// Alter the code per iteration to prevent caching.
const cacheId = Math.floor(iteration / this.CODE_REUSE_COUNT);
const previousSourceCode = this.iterationSourceCodes[cacheId];
if (previousSourceCode)
return previousSourceCode
const sourceCode = this.sourceCode.replaceAll(CACHE_BUST_COMMENT_RE, `/*${cacheId}*/`);
// Ensure efficient string representation.
this.sourceHash = quickHash(sourceCode);
return sourceCode;
}

runIteration() {
let sourceCode = this.iterationSourceCodes[this.iteration];
if (!sourceCode)
throw new Error(`Could not find source for iteration ${this.iteration}`);
// Module in sourceCode it assigned to the ClassStartupTest variable.
let BabylonJSBenchmark;

let initStart = performance.now();
const res = eval(sourceCode);
const runStart = performance.now();

const {classNames, cameraRotationLength} = BabylonJSBenchmark.runTest(30);
this.lastResult = {
classNames,
cameraRotationLength,
};
const end = performance.now();
const loadTime = runStart - initStart;
const runTime = end - runStart;
// For local debugging:
// print(`Iteration ${this.iteration}:`);
// print(` Load time: ${loadTime.toFixed(2)}ms`);
// print(` Render time: ${runTime.toFixed(2)}ms`);
this.iteration++;
}

validate() {
this.expect("this.lastResult.classNames.length", this.lastResult.classNames.length, 2135);
this.expect("this.lastResult.cameraRotationLength", Math.round(this.lastResult.cameraRotationLength * 1000), 464);
}

expect(name, value, expected) {
if (value != expected)
throw new Error(`Expected ${name} to be ${expected}, but got ${value}`);
}
}
30 changes: 30 additions & 0 deletions babylonjs/build/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const SNIPPET_URL = 'https://snippet.babylonjs.com/LCBQ5Y/6';
const RAW_SNIPPET_PATH = path.resolve(__dirname, '../data/snippets.LCBQ5Y.raw.json');
const PARTICLES_PATH = path.resolve(__dirname, '../data/particles.json');

async function main() {
const response = await fetch(SNIPPET_URL);
if (!response.ok) {
throw new Error(`Failed to download file: ${response.status} ${response.statusText}`);
}
const snippetJson = await response.json();

console.log(`Saving raw snippet: ${RAW_SNIPPET_PATH}`);
fs.writeFileSync(RAW_SNIPPET_PATH, JSON.stringify(snippetJson));

const jsonPayload = JSON.parse(snippetJson.jsonPayload);
const particleSystem = JSON.parse(jsonPayload.particleSystem);

console.log(`Saving particles: ${PARTICLES_PATH}:`);
fs.writeFileSync(PARTICLES_PATH, JSON.stringify(particleSystem, null, 2));
}

main();
29 changes: 29 additions & 0 deletions babylonjs/build/cache-buster-comment-plugin.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Babel plugin that adds CACHE_BUST_COMMENT to every function body.
const CACHE_BUST_COMMENT = "ThouShaltNotCache";


module.exports = function({ types: t }) {
return {
visitor: {
Function(path) {
const bodyPath = path.get("body");
// Handle arrow functions: () => "value"
// Convert them to block statements: () => { return "value"; }
if (!bodyPath.isBlockStatement()) {
const newBody = t.blockStatement([t.returnStatement(bodyPath.node)]);
path.set("body", newBody);
}

// Handle empty function bodies: function foo() {}
// Add an empty statement so we have a first node to attach the comment to.
if (path.get("body.body").length === 0) {
path.get("body").pushContainer("body", t.emptyStatement());
}

const firstNode = path.node.body.body[0];
t.addComment(firstNode, "leading", CACHE_BUST_COMMENT);

}
},
};
};
Loading
Loading