Skip to content

Commit 2d98269

Browse files
authored
Configurable resource prefetching (#70)
This addresses issue #58 and is useful for local profiling. If the prefetchResources is disabled, we now no longer preload them eagerly, but rather use the live URL and either script-inject it or use it directly with workers. This means we can easily get the original source positions for profiling. - `--no-prefetch` command ilne support - `&prefetchResources=0` URLParam support - Improve test coverage and add --no-prefetch tests - Check that test shell output for failures to catch JSC errors (it does not report exit != 0 on rejected promises) Drive-by-fix: - Move FileLoader out of the IIFE - Check that JetStreamDriver has at least one active benchmark - Add assertions for valid scores - Fix benchmarks selection with tags
1 parent a912b9c commit 2d98269

File tree

6 files changed

+117
-50
lines changed

6 files changed

+117
-50
lines changed

JetStreamDriver.js

Lines changed: 64 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,21 @@ globalThis.dumpJSONResults ??= false;
4040
globalThis.testList ??= undefined;
4141
globalThis.startDelay ??= undefined;
4242
globalThis.shouldReport ??= false;
43+
globalThis.prefetchResources ??= true;
4344

4445
function getIntParam(urlParams, key) {
45-
if (!urlParams.has(key))
46-
return undefined
4746
const rawValue = urlParams.get(key);
4847
const value = parseInt(rawValue);
4948
if (value <= 0)
5049
throw new Error(`Expected positive value for ${key}, but got ${rawValue}`);
5150
return value;
5251
}
5352

53+
function getBoolParam(urlParams, key) {
54+
const rawValue = urlParams.get(key).toLowerCase()
55+
return !(rawValue === "false" || rawValue === "0")
56+
}
57+
5458
function getTestListParam(urlParams, key) {
5559
if (globalThis.testList?.length)
5660
throw new Error(`Overriding previous testList=${globalThis.testList.join()} with ${key} url-parameter.`);
@@ -73,8 +77,13 @@ if (typeof(URLSearchParams) !== "undefined") {
7377
globalThis.testIterationCount = getIntParam(urlParameters, "iterationCount");
7478
if (urlParameters.has("worstCaseCount"))
7579
globalThis.testWorstCaseCount = getIntParam(urlParameters, "worstCaseCount");
80+
if (urlParameters.has("prefetchResources"))
81+
globalThis.prefetchResources = getBoolParam(urlParameters, "prefetchResources");
7682
}
7783

84+
if (!globalThis.prefetchResources)
85+
console.warn("Disabling resource prefetching!");
86+
7887
// Used for the promise representing the current benchmark run.
7988
this.currentResolve = null;
8089
this.currentReject = null;
@@ -88,7 +97,7 @@ function displayCategoryScores() {
8897

8998
let summaryElement = document.getElementById("result-summary");
9099
for (let [category, scores] of categoryScores)
91-
summaryElement.innerHTML += `<p> ${category}: ${uiFriendlyScore(geomean(scores))}</p>`
100+
summaryElement.innerHTML += `<p> ${category}: ${uiFriendlyScore(geomeanScore(scores))}</p>`
92101

93102
categoryScores = null;
94103
}
@@ -138,12 +147,15 @@ function mean(values) {
138147
return sum / values.length;
139148
}
140149

141-
function geomean(values) {
150+
function geomeanScore(values) {
142151
assert(values instanceof Array);
143152
let product = 1;
144153
for (let x of values)
145154
product *= x;
146-
return product ** (1 / values.length);
155+
const score = product ** (1 / values.length);
156+
// Allow 0 for uninitialized subScores().
157+
assert(score >= 0, `Got invalid score: ${score}`)
158+
return score;
147159
}
148160

149161
function toScore(timeValue) {
@@ -180,28 +192,29 @@ function uiFriendlyDuration(time) {
180192
// TODO: Cleanup / remove / merge. This is only used for caching loads in the
181193
// non-browser setting. In the browser we use exclusively `loadCache`,
182194
// `loadBlob`, `doLoadBlob`, `prefetchResourcesForBrowser` etc., see below.
183-
const fileLoader = (function() {
184-
class Loader {
185-
constructor() {
186-
this.requests = new Map;
187-
}
188-
189-
// Cache / memoize previously read files, because some workloads
190-
// share common code.
191-
load(url) {
192-
assert(!isInBrowser);
195+
class ShellFileLoader {
196+
constructor() {
197+
this.requests = new Map;
198+
}
193199

194-
if (this.requests.has(url)) {
195-
return this.requests.get(url);
196-
}
200+
// Cache / memoize previously read files, because some workloads
201+
// share common code.
202+
load(url) {
203+
assert(!isInBrowser);
204+
if (!globalThis.prefetchResources)
205+
return `load("${url}");`
197206

198-
const contents = readFile(url);
199-
this.requests.set(url, contents);
200-
return contents;
207+
if (this.requests.has(url)) {
208+
return this.requests.get(url);
201209
}
210+
211+
const contents = readFile(url);
212+
this.requests.set(url, contents);
213+
return contents;
202214
}
203-
return new Loader;
204-
})();
215+
};
216+
217+
const shellFileLoader = new ShellFileLoader();
205218

206219
class Driver {
207220
constructor(benchmarks) {
@@ -211,6 +224,7 @@ class Driver {
211224
// Make benchmark list unique and sort it.
212225
this.benchmarks = Array.from(new Set(benchmarks));
213226
this.benchmarks.sort((a, b) => a.plan.name.toLowerCase() < b.plan.name.toLowerCase() ? 1 : -1);
227+
assert(this.benchmarks.length, "No benchmarks selected");
214228
// TODO: Cleanup / remove / merge `blobDataCache` and `loadCache` vs.
215229
// the global `fileLoader` cache.
216230
this.blobDataCache = { };
@@ -248,7 +262,7 @@ class Driver {
248262
performance.mark("update-ui");
249263
benchmark.updateUIAfterRun();
250264

251-
if (isInBrowser) {
265+
if (isInBrowser && globalThis.prefetchResources) {
252266
const cache = JetStream.blobDataCache;
253267
for (const file of benchmark.plan.files) {
254268
const blobData = cache[file];
@@ -270,8 +284,11 @@ class Driver {
270284
}
271285

272286
const allScores = [];
273-
for (const benchmark of this.benchmarks)
274-
allScores.push(benchmark.score);
287+
for (const benchmark of this.benchmarks) {
288+
const score = benchmark.score;
289+
assert(score > 0, `Invalid ${benchmark.name} score: ${score}`);
290+
allScores.push(score);
291+
}
275292

276293
categoryScores = new Map;
277294
for (const benchmark of this.benchmarks) {
@@ -282,23 +299,27 @@ class Driver {
282299
for (const benchmark of this.benchmarks) {
283300
for (let [category, value] of Object.entries(benchmark.subScores())) {
284301
const arr = categoryScores.get(category);
302+
assert(value > 0, `Invalid ${benchmark.name} ${category} score: ${value}`);
285303
arr.push(value);
286304
}
287305
}
288306

307+
const totalScore = geomeanScore(allScores);
308+
assert(totalScore > 0, `Invalid total score: ${totalScore}`);
309+
289310
if (isInBrowser) {
290-
summaryElement.classList.add('done');
291-
summaryElement.innerHTML = `<div class="score">${uiFriendlyScore(geomean(allScores))}</div><label>Score</label>`;
311+
summaryElement.classList.add("done");
312+
summaryElement.innerHTML = `<div class="score">${uiFriendlyScore(totalScore)}</div><label>Score</label>`;
292313
summaryElement.onclick = displayCategoryScores;
293314
if (showScoreDetails)
294315
displayCategoryScores();
295-
statusElement.innerHTML = '';
316+
statusElement.innerHTML = "";
296317
} else if (!dumpJSONResults) {
297318
console.log("\n");
298319
for (let [category, scores] of categoryScores)
299-
console.log(`${category}: ${uiFriendlyScore(geomean(scores))}`);
320+
console.log(`${category}: ${uiFriendlyScore(geomeanScore(scores))}`);
300321

301-
console.log("\nTotal Score: ", uiFriendlyScore(geomean(allScores)), "\n");
322+
console.log("\nTotal Score: ", uiFriendlyScore(totalScore), "\n");
302323
}
303324

304325
this.reportScoreToRunBenchmarkRunner();
@@ -727,7 +748,7 @@ class Benchmark {
727748

728749
get score() {
729750
const subScores = Object.values(this.subScores());
730-
return geomean(subScores);
751+
return geomeanScore(subScores);
731752
}
732753

733754
subScores() {
@@ -788,8 +809,9 @@ class Benchmark {
788809
scripts.add(text);
789810
} else {
790811
const cache = JetStream.blobDataCache;
791-
for (const file of this.plan.files)
792-
scripts.addWithURL(cache[file].blobURL);
812+
for (const file of this.plan.files) {
813+
scripts.addWithURL(globalThis.prefetchResources ? cache[file].blobURL : file);
814+
}
793815
}
794816

795817
const promise = new Promise((resolve, reject) => {
@@ -838,6 +860,11 @@ class Benchmark {
838860
}
839861

840862
async doLoadBlob(resource) {
863+
const blobData = JetStream.blobDataCache[resource];
864+
if (!globalThis.prefetchResources) {
865+
blobData.blobURL = resource;
866+
return blobData;
867+
}
841868
let response;
842869
let tries = 3;
843870
while (tries--) {
@@ -854,7 +881,6 @@ class Benchmark {
854881
throw new Error("Fetch failed");
855882
}
856883
const blob = await response.blob();
857-
const blobData = JetStream.blobDataCache[resource];
858884
blobData.blob = blob;
859885
blobData.blobURL = URL.createObjectURL(blob);
860886
return blobData;
@@ -987,7 +1013,7 @@ class Benchmark {
9871013
assert(!isInBrowser);
9881014

9891015
assert(this.scripts === null, "This initialization should be called only once.");
990-
this.scripts = this.plan.files.map(file => fileLoader.load(file));
1016+
this.scripts = this.plan.files.map(file => shellFileLoader.load(file));
9911017

9921018
assert(this.preloads === null, "This initialization should be called only once.");
9931019
this.preloads = Object.entries(this.plan.preload ?? {});
@@ -2417,8 +2443,7 @@ for (const benchmark of BENCHMARKS) {
24172443
}
24182444

24192445

2420-
function processTestList(testList)
2421-
{
2446+
function processTestList(testList) {
24222447
let benchmarkNames = [];
24232448
let benchmarks = [];
24242449

@@ -2430,7 +2455,7 @@ function processTestList(testList)
24302455
for (let name of benchmarkNames) {
24312456
name = name.toLowerCase();
24322457
if (benchmarksByTag.has(name))
2433-
benchmarks.concat(findBenchmarksByTag(name));
2458+
benchmarks = benchmarks.concat(findBenchmarksByTag(name));
24342459
else
24352460
benchmarks.push(findBenchmarkByName(name));
24362461
}

cli.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ if (typeof runMode !== "undefined" && runMode == "RAMification")
5656
globalThis.RAMification = true;
5757
if ("--ramification" in cliFlags)
5858
globalThis.RAMification = true;
59+
if ("--no-prefetch" in cliFlags)
60+
globalThis.prefetchResources = false;
5961
if (cliArgs.length)
6062
globalThis.testList = cliArgs;
6163

@@ -78,9 +80,11 @@ if ("--help" in cliFlags) {
7880
console.log("");
7981

8082
console.log("Options:");
81-
console.log(" --iteration-count: Set the default iteration count.");
82-
console.log(" --worst-case-count: Set the default worst-case count");
83+
console.log(" --iteration-count: Set the default iteration count.");
84+
console.log(" --worst-case-count: Set the default worst-case count");
8385
console.log(" --dump-json-results: Print summary json to the console.");
86+
console.log(" --ramification: Enable ramification support. See RAMification.py for more details.");
87+
console.log(" --no-prefetch: Do not prefetch resources. Will add network overhead to measurements!");
8488
console.log("");
8589

8690
console.log("Available tags:");

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"test:firefox": "node tests/run.mjs --browser firefox",
2424
"test:safari": "node tests/run.mjs --browser safari",
2525
"test:edge": "node tests/run.mjs --browser edge",
26+
"test:shell": "npm run test:v8 && npm run test:jsc && npm run test:spidermonkey",
2627
"test:v8": "node tests/run-shell.mjs --shell v8",
2728
"test:jsc": "node tests/run-shell.mjs --shell jsc",
2829
"test:spidermonkey": "node tests/run-shell.mjs --shell spidermonkey"

shell-config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const isInBrowser = false;
2727
console = {
2828
log: globalThis?.console?.log ?? print,
2929
error: globalThis?.console?.error ?? print,
30+
warn: globalThis?.console?.warn ?? print,
3031
}
3132

3233
const isD8 = typeof Realm !== "undefined";

tests/run-shell.mjs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#! /usr/bin/env node
22

33
import commandLineArgs from "command-line-args";
4-
import { spawnSync } from "child_process";
4+
import { spawn } from "child_process";
55
import { fileURLToPath } from "url";
66
import { styleText } from "node:util";
77
import * as path from "path";
@@ -59,7 +59,7 @@ const SPAWN_OPTIONS = {
5959
stdio: ["inherit", "inherit", "inherit"]
6060
};
6161

62-
function sh(binary, ...args) {
62+
async function sh(binary, ...args) {
6363
const cmd = `${binary} ${args.join(" ")}`;
6464
if (GITHUB_ACTIONS_OUTPUT) {
6565
core.startGroup(binary);
@@ -68,22 +68,48 @@ function sh(binary, ...args) {
6868
console.log(styleText("blue", cmd));
6969
}
7070
try {
71-
const result = spawnSync(binary, args, SPAWN_OPTIONS);
71+
const result = await spawnCaptureStdout(binary, args, SPAWN_OPTIONS);
7272
if (result.status || result.error) {
7373
logError(result.error);
7474
throw new Error(`Shell CMD failed: ${binary} ${args.join(" ")}`);
7575
}
76+
return result;
7677
} finally {
7778
if (GITHUB_ACTIONS_OUTPUT)
7879
core.endGroup();
7980
}
8081
}
8182

83+
async function spawnCaptureStdout(binary, args) {
84+
const childProcess = spawn(binary, args);
85+
childProcess.stdout.pipe(process.stdout);
86+
return new Promise((resolve, reject) => {
87+
childProcess.stdoutString = "";
88+
childProcess.stdio[1].on("data", (data) => {
89+
childProcess.stdoutString += data.toString();
90+
});
91+
childProcess.on('close', (code) => {
92+
if (code === 0) {
93+
resolve(childProcess);
94+
} else {
95+
// Reject the Promise with an Error on failure
96+
const error = new Error(`Command failed with exit code ${code}: ${binary} ${args.join(" ")}`);
97+
error.process = childProcess;
98+
error.stdout = childProcess.stdoutString;
99+
error.exitCode = code;
100+
reject(error);
101+
}
102+
});
103+
childProcess.on('error', reject);
104+
})
105+
}
106+
82107
async function runTests() {
83108
const shellBinary = await logGroup(`Installing JavaScript Shell: ${SHELL_NAME}`, testSetup);
84109
let success = true;
85110
success &&= await runTest("Run UnitTests", () => sh(shellBinary, UNIT_TEST_PATH));
86111
success &&= await runCLITest("Run Single Suite", shellBinary, "proxy-mobx");
112+
success &&= await runCLITest("Run Tag No Prefetch", shellBinary, "proxy", "--no-prefetch");
87113
success &&= await runCLITest("Run Disabled Suite", shellBinary, "disabled");
88114
success &&= await runCLITest("Run Default Suite", shellBinary);
89115
if (!success)
@@ -111,8 +137,8 @@ function jsvuOSName() {
111137

112138
const DEFAULT_JSC_LOCATION = "/System/Library/Frameworks/JavaScriptCore.framework/Versions/Current/Helpers/jsc"
113139

114-
function testSetup() {
115-
sh("jsvu", `--engines=${SHELL_NAME}`, `--os=${jsvuOSName()}`);
140+
async function testSetup() {
141+
await sh("jsvu", `--engines=${SHELL_NAME}`, `--os=${jsvuOSName()}`);
116142
let shellBinary = path.join(os.homedir(), ".jsvu/bin", SHELL_NAME);
117143
if (!fs.existsSync(shellBinary) && SHELL_NAME == "javascriptcore")
118144
shellBinary = DEFAULT_JSC_LOCATION;
@@ -123,7 +149,13 @@ function testSetup() {
123149
}
124150

125151
function runCLITest(name, shellBinary, ...args) {
126-
return runTest(name, () => sh(shellBinary, ...convertCliArgs(CLI_PATH, ...args)));
152+
return runTest(name, () => runShell(shellBinary, ...convertCliArgs(CLI_PATH, ...args)));
153+
}
154+
155+
async function runShell(shellBinary, ...args) {
156+
const result = await sh(shellBinary, ...args);
157+
if (result.stdoutString.includes("JetStream3 failed"))
158+
throw new Error("test failed")
127159
}
128160

129161
setImmediate(runTests);

0 commit comments

Comments
 (0)