Skip to content

Commit bf0a6cd

Browse files
authored
New Workload: JSDOM + d3 (#124)
Startup focused d3 + jsdom workload. - Evals unique sources per iteration (custom babel plugin which injects comments into each function) - Measures [d3](https://d3js.org/) demo including parsing csv - Uses bundled [jsdom](https://github.com/jsdom/jsdom) as mock library
1 parent 7df59a4 commit bf0a6cd

20 files changed

+276440
-0
lines changed

JetStreamDriver.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2254,6 +2254,23 @@ let BENCHMARKS = [
22542254
],
22552255
tags: ["default", "js", "Proxy"],
22562256
}),
2257+
new AsyncBenchmark({
2258+
name: "jsdom-d3-startup",
2259+
files: [
2260+
"./startup-helper/StartupBenchmark.js",
2261+
"./jsdom-d3-startup/benchmark.js",
2262+
],
2263+
preload: {
2264+
// Unminified sources for profiling.
2265+
// BUNDLE: "./jsdom-d3-startup/dist/bundle.js",
2266+
BUNDLE: "./jsdom-d3-startup/dist/bundle.min.js",
2267+
US_DATA: "./jsdom-d3-startup/data/counties-albers-10m.json",
2268+
AIRPORTS: "./jsdom-d3-startup/data/airports.csv",
2269+
},
2270+
tags: ["d3", "startup", "jsdom"],
2271+
iterations: 15,
2272+
worstCaseCount: 2,
2273+
}),
22572274
new AsyncBenchmark({
22582275
name: "web-ssr",
22592276
files: [

jsdom-d3-startup/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# JSDOM D3 Startup Benchmark
2+
3+
The benchmark reads airport and US geography data, then uses D3 to create a Voronoi diagram of the airports overlaid on a map of the US.
4+
It uses jsdom to simulate a browser environment for D3 to render to an SVG element.
5+
6+
## JetStream integration
7+
- We use a custom `./build/build/cache-buster-comment-plugin.cjs` which injects a known comment into every function in the bundle
8+
- The JetStream benchmark replaces these comments with a unique string per iteration
9+
- Each benchmark iteration includes parse and top-level eval time
10+
11+
## Setup
12+
```bash
13+
# Install node deps from package-lock.json
14+
npm ci --include=dev;
15+
# Bundle sources to dist/*.
16+
npm run build
17+
# Use build:dev for non-minified sources.
18+
npm run build:dev
19+
```
20+
21+
# Testing
22+
```bash
23+
# Run the basic node benchmark implementation for development.
24+
npm run test
25+
```
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { performance } from 'perf_hooks';
2+
import fs from 'fs';
3+
import * as d3 from 'd3';
4+
import { runTest } from './src/test.mjs';
5+
6+
async function main() {
7+
const usData = JSON.parse(fs.readFileSync('./data/counties-albers-10m.json', 'utf-8'));
8+
const airportsData = fs.readFileSync('./data/airports.csv', 'utf-8');
9+
10+
const startTime = performance.now();
11+
12+
const svg = await runTest(airportsData, usData);
13+
14+
const endTime = performance.now();
15+
16+
// console.log(svg); // The SVG output
17+
console.log(`Execution time: ${endTime - startTime} ms`);
18+
}
19+
20+
main();

jsdom-d3-startup/benchmark.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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+
// Load D3 and data loading utilities for d8
27+
28+
const EXPECTED_LAST_RESULT_LENGTH = 691366;
29+
const EXPECTED_LAST_RESULT_HASH = 144487595;
30+
31+
globalThis.clearTimeout = () => { };
32+
33+
class Benchmark extends StartupBenchmark {
34+
lastResult;
35+
totalHash = 0xdeadbeef;
36+
currentIteration = 0;
37+
38+
constructor({iterationCount}) {
39+
super({
40+
iterationCount,
41+
expectedCacheCommentCount: 10028,
42+
sourceCodeReuseCount: 2,
43+
});
44+
}
45+
46+
async init() {
47+
await super.init();
48+
this.airportsCsvString = (await JetStream.getString(JetStream.preload.AIRPORTS));
49+
console.assert(this.airportsCsvString.length == 145493, `Expected this.airportsCsvString.length to be 141490 but got ${this.airportsCsvString.length}`);
50+
this.usDataJsonString = await JetStream.getString(JetStream.preload.US_DATA);
51+
console.assert(this.usDataJsonString.length == 2880996, `Expected this.usData.length to be 2880996 but got ${this.usDataJsonString.length}`);
52+
this.usData = JSON.parse(this.usDataJsonString);
53+
}
54+
55+
runIteration() {
56+
let iterationSourceCode = this.iterationSourceCodes[this.currentIteration];
57+
if (!iterationSourceCode)
58+
throw new Error(`Could not find source for iteration ${this.currentIteration}`);
59+
// Module in sourceCode it assigned to the ReactRenderTest variable.
60+
let D3Test;
61+
eval(iterationSourceCode);
62+
const html = D3Test.runTest(this.airportsCsvString, this.usData);
63+
const htmlHash = this.quickHash(html);
64+
this.lastResult = { html, htmlHash };
65+
this.totalHash ^= this.lastResult.htmlHash;
66+
this.currentIteration++;
67+
}
68+
69+
validate() {
70+
if (this.lastResult.html.length != EXPECTED_LAST_RESULT_LENGTH)
71+
throw new Error(`Expected this.lastResult.html.length to be ${EXPECTED_LAST_RESULT_LENGTH} but got ${this.lastResult.length}`);
72+
if (this.lastResult.htmlHash != EXPECTED_LAST_RESULT_HASH)
73+
throw new Error(`Expected this.lastResult.htmlHash to be ${EXPECTED_LAST_RESULT_HASH} but got ${this.lastResult.htmlHash}`);
74+
}
75+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Babel plugin that adds CACHE_BUST_COMMENT to every function body.
2+
const CACHE_BUST_COMMENT = "ThouShaltNotCache";
3+
4+
5+
module.exports = function({ types: t }) {
6+
return {
7+
visitor: {
8+
Function(path) {
9+
const bodyPath = path.get("body");
10+
// Handle arrow functions: () => "value"
11+
// Convert them to block statements: () => { return "value"; }
12+
if (!bodyPath.isBlockStatement()) {
13+
const newBody = t.blockStatement([t.returnStatement(bodyPath.node)]);
14+
path.set("body", newBody);
15+
}
16+
17+
// Handle empty function bodies: function foo() {}
18+
// Add an empty statement so we have a first node to attach the comment to.
19+
if (path.get("body.body").length === 0) {
20+
path.get("body").pushContainer("body", t.emptyStatement());
21+
}
22+
23+
const firstNode = path.node.body.body[0];
24+
t.addComment(firstNode, "leading", CACHE_BUST_COMMENT);
25+
26+
}
27+
},
28+
};
29+
};

0 commit comments

Comments
 (0)