Skip to content

Commit f505e37

Browse files
authored
Merge pull request #170 from eqrion/zlib-compress
Zlib compress all wasm files and decompress them during prefetch
2 parents d863626 + 37e3dce commit f505e37

File tree

16 files changed

+201
-84079
lines changed

16 files changed

+201
-84079
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ jobs:
3939
- name: Install Node Packages
4040
run: npm ci
4141

42+
- name: Decompress compressed files
43+
run: npm run decompress
44+
4245
- name: Cache jsvu Binaries
4346
uses: actions/cache@v4
4447
with:

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@ node_modules
88

99
# v8.log is generated by the d8-shell if profiling is enabled
1010
v8.log
11+
12+
# Decompressed files go here
13+
RexBench/FlightPlanner/waypoints.js
14+
SeaMonster/inspector-json-payload.js
15+
wasm/argon2/build/argon2.wasm

JetStreamDriver.js

Lines changed: 133 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,17 @@ const defaultIterationCount = 120;
3131
const defaultWorstCaseCount = 4;
3232

3333
if (!JetStreamParams.prefetchResources)
34-
console.warn("Disabling resource prefetching!");
34+
console.warn("Disabling resource prefetching! All compressed files must have been decompressed using `npm run decompress`");
35+
36+
if (!isInBrowser && JetStreamParams.prefetchResources) {
37+
// Use the wasm compiled zlib as a polyfill when decompression stream is
38+
// not available in JS shells.
39+
load("./wasm/zlib/shell.js");
40+
41+
// Load a polyfill for TextEncoder/TextDecoder in shells. Used when
42+
// decompressing a prefetched resource and converting it to text.
43+
load("./polyfills/fast-text-encoding/1.0.3/text.js");
44+
}
3545

3646
// Used for the promise representing the current benchmark run.
3747
this.currentResolve = null;
@@ -138,6 +148,20 @@ function uiFriendlyDuration(time) {
138148
return `${time.toFixed(3)} ms`;
139149
}
140150

151+
// Files can be zlib compressed to reduce the size of the JetStream source code.
152+
// We don't use http compression because we support running from the shell and
153+
// don't want to require a complicated server setup.
154+
//
155+
// zlib was chosen because we already have it in tree for the wasm-zlib test.
156+
function isCompressed(name) {
157+
return name.endsWith(".z");
158+
}
159+
160+
function uncompressedName(name) {
161+
console.assert(isCompressed(name));
162+
return name.slice(0, -2);
163+
}
164+
141165
// TODO: Cleanup / remove / merge. This is only used for caching loads in the
142166
// non-browser setting. In the browser we use exclusively `loadCache`,
143167
// `loadBlob`, `doLoadBlob`, `prefetchResourcesForBrowser` etc., see below.
@@ -150,14 +174,28 @@ class ShellFileLoader {
150174
// share common code.
151175
load(url) {
152176
console.assert(!isInBrowser);
177+
178+
let compressed = isCompressed(url);
179+
if (compressed && !JetStreamParams.prefetchResources) {
180+
url = uncompressedName(url);
181+
}
182+
183+
// If we aren't supposed to prefetch this then return code snippet that will load the url on-demand.
153184
if (!JetStreamParams.prefetchResources)
154185
return `load("${url}");`
155186

156187
if (this.requests.has(url)) {
157188
return this.requests.get(url);
158189
}
159190

160-
const contents = readFile(url);
191+
let contents;
192+
if (compressed) {
193+
const compressedBytes = new Int8Array(read(url, "binary"));
194+
const decompressedBytes = zlib.decompress(compressedBytes);
195+
contents = new TextDecoder().decode(decompressedBytes);
196+
} else {
197+
contents = readFile(url);
198+
}
161199
this.requests.set(url, contents);
162200
return contents;
163201
}
@@ -209,10 +247,14 @@ class Driver {
209247
performance.mark("update-ui");
210248
benchmark.updateUIAfterRun();
211249

212-
if (isInBrowser && JetStreamParams.prefetchResources) {
250+
if (isInBrowser) {
213251
const cache = JetStream.blobDataCache;
214252
for (const file of benchmark.files) {
215253
const blobData = cache[file];
254+
// If we didn't prefetch this resource, then no need to free it
255+
if (!blobData.blob) {
256+
continue
257+
}
216258
blobData.refCount--;
217259
if (!blobData.refCount)
218260
cache[file] = undefined;
@@ -338,6 +380,9 @@ class Driver {
338380

339381
async prefetchResources() {
340382
if (!isInBrowser) {
383+
if (JetStreamParams.prefetchResources) {
384+
await zlib.initialize();
385+
}
341386
for (const benchmark of this.benchmarks)
342387
benchmark.prefetchResourcesForShell();
343388
return;
@@ -553,6 +598,11 @@ class Scripts {
553598
}
554599

555600
class ShellScripts extends Scripts {
601+
constructor() {
602+
super();
603+
this.prefetchedResources = Object.create(null);;
604+
}
605+
556606
run() {
557607
let globalObject;
558608
let realm;
@@ -579,13 +629,32 @@ class ShellScripts extends Scripts {
579629
currentReject
580630
};
581631

632+
// Pass the prefetched resources to the benchmark global.
633+
if (JetStreamParams.prefetchResources) {
634+
// Pass the 'TextDecoder' polyfill into the benchmark global. Don't
635+
// use 'TextDecoder' as that will get picked up in the kotlin test
636+
// without full support.
637+
globalObject.ShellTextDecoder = TextDecoder;
638+
// Store shellPrefetchedResources on ShellPrefetchedResources so that
639+
// getBinary and getString can find them.
640+
globalObject.ShellPrefetchedResources = this.prefetchedResources;
641+
} else {
642+
console.assert(Object.values(this.prefetchedResources).length === 0, "Unexpected prefetched resources");
643+
}
644+
582645
globalObject.performance ??= performance;
583646
for (const script of this.scripts)
584647
globalObject.loadString(script);
585648

586649
return isD8 ? realm : globalObject;
587650
}
588651

652+
addPrefetchedResources(prefetchedResources) {
653+
for (let [file, bytes] of Object.entries(prefetchedResources)) {
654+
this.prefetchedResources[file] = bytes;
655+
}
656+
}
657+
589658
add(text) {
590659
this.scripts.push(text);
591660
}
@@ -618,7 +687,6 @@ class BrowserScripts extends Scripts {
618687
return magicFrame;
619688
}
620689

621-
622690
add(text) {
623691
this.scripts.push(`<script>${text}</script>`);
624692
}
@@ -638,6 +706,7 @@ class Benchmark {
638706
this.allowUtf16 = !!plan.allowUtf16;
639707
this.scripts = null;
640708
this.preloads = null;
709+
this.shellPrefetchedResources = null;
641710
this.results = [];
642711
this._state = BenchmarkState.READY;
643712
}
@@ -771,6 +840,9 @@ class Benchmark {
771840
if (!!this.plan.exposeBrowserTest)
772841
scripts.addBrowserTest();
773842

843+
if (this.shellPrefetchedResources) {
844+
scripts.addPrefetchedResources(this.shellPrefetchedResources);
845+
}
774846
if (this.plan.preload) {
775847
let preloadCode = "";
776848
for (let [ variableName, blobURLOrPath ] of this.preloads)
@@ -789,7 +861,7 @@ class Benchmark {
789861
} else {
790862
const cache = JetStream.blobDataCache;
791863
for (const file of this.plan.files) {
792-
scripts.addWithURL(JetStreamParams.prefetchResources ? cache[file].blobURL : file);
864+
scripts.addWithURL(cache[file].blobURL);
793865
}
794866
}
795867

@@ -840,10 +912,19 @@ class Benchmark {
840912

841913
async doLoadBlob(resource) {
842914
const blobData = JetStream.blobDataCache[resource];
915+
916+
const compressed = isCompressed(resource);
917+
if (compressed && !JetStreamParams.prefetchResources) {
918+
resource = uncompressedName(resource);
919+
}
920+
921+
// If we aren't supposed to prefetch this then set the blobURL to just
922+
// be the resource URL.
843923
if (!JetStreamParams.prefetchResources) {
844924
blobData.blobURL = resource;
845925
return blobData;
846926
}
927+
847928
let response;
848929
let tries = 3;
849930
while (tries--) {
@@ -859,7 +940,15 @@ class Benchmark {
859940
continue;
860941
throw new Error("Fetch failed");
861942
}
862-
const blob = await response.blob();
943+
944+
// If we need to decompress this, then run it through a decompression
945+
// stream.
946+
if (compressed) {
947+
const stream = response.body.pipeThrough(new DecompressionStream("deflate"))
948+
response = new Response(stream);
949+
}
950+
951+
let blob = await response.blob();
863952
blobData.blob = blob;
864953
blobData.blobURL = URL.createObjectURL(blob);
865954
return blobData;
@@ -995,7 +1084,27 @@ class Benchmark {
9951084
this.scripts = this.plan.files.map(file => shellFileLoader.load(file));
9961085

9971086
console.assert(this.preloads === null, "This initialization should be called only once.");
998-
this.preloads = Object.entries(this.plan.preload ?? {});
1087+
this.preloads = [];
1088+
this.shellPrefetchedResources = Object.create(null);
1089+
if (!this.plan.preload) {
1090+
return;
1091+
}
1092+
for (let [name, file] of Object.entries(this.plan.preload)) {
1093+
const compressed = isCompressed(file);
1094+
if (compressed && !JetStreamParams.prefetchResources) {
1095+
file = uncompressedName(file);
1096+
}
1097+
1098+
if (JetStreamParams.prefetchResources) {
1099+
let bytes = new Int8Array(read(file, "binary"));
1100+
if (compressed) {
1101+
bytes = zlib.decompress(bytes);
1102+
}
1103+
this.shellPrefetchedResources[file] = bytes;
1104+
}
1105+
1106+
this.preloads.push([name, file]);
1107+
}
9991108
}
10001109

10011110
scoreIdentifiers() {
@@ -1268,15 +1377,23 @@ class AsyncBenchmark extends DefaultBenchmark {
12681377
} else {
12691378
str += `
12701379
JetStream.getBinary = async function(path) {
1380+
if ("ShellPrefetchedResources" in globalThis) {
1381+
return ShellPrefetchedResources[path];
1382+
}
12711383
return new Int8Array(read(path, "binary"));
12721384
};
12731385
12741386
JetStream.getString = async function(path) {
1387+
if ("ShellPrefetchedResources" in globalThis) {
1388+
return new ShellTextDecoder().decode(ShellPrefetchedResources[path]);
1389+
}
12751390
return read(path);
12761391
};
12771392
12781393
JetStream.dynamicImport = async function(path) {
12791394
try {
1395+
// TODO: this skips the prefetched resources, but I'm
1396+
// not sure of a way around that.
12801397
return await import(path);
12811398
} catch (e) {
12821399
// In shells, relative imports require different paths, so try with and
@@ -1493,7 +1610,11 @@ class WasmLegacyBenchmark extends Benchmark {
14931610
`;
14941611
} else {
14951612
str += `
1496-
Module[key] = new Int8Array(read(path, "binary"));
1613+
if (ShellPrefetchedResources) {
1614+
Module[key] = ShellPrefetchedResources[path];
1615+
} else {
1616+
Module[key] = new Int8Array(read(path, "binary"));
1617+
}
14971618
if (andThen == doRun) {
14981619
globalObject.read = (...args) => {
14991620
console.log("should not be inside read: ", ...args);
@@ -1803,7 +1924,7 @@ let BENCHMARKS = [
18031924
name: "FlightPlanner",
18041925
files: [
18051926
"./RexBench/FlightPlanner/airways.js",
1806-
"./RexBench/FlightPlanner/waypoints.js",
1927+
"./RexBench/FlightPlanner/waypoints.js.z",
18071928
"./RexBench/FlightPlanner/flight_planner.js",
18081929
"./RexBench/FlightPlanner/expectations.js",
18091930
"./RexBench/FlightPlanner/benchmark.js",
@@ -1914,7 +2035,7 @@ let BENCHMARKS = [
19142035
new DefaultBenchmark({
19152036
name: "json-stringify-inspector",
19162037
files: [
1917-
"./SeaMonster/inspector-json-payload.js",
2038+
"./SeaMonster/inspector-json-payload.js.z",
19182039
"./SeaMonster/json-stringify-inspector.js",
19192040
],
19202041
iterations: 20,
@@ -1924,7 +2045,7 @@ let BENCHMARKS = [
19242045
new DefaultBenchmark({
19252046
name: "json-parse-inspector",
19262047
files: [
1927-
"./SeaMonster/inspector-json-payload.js",
2048+
"./SeaMonster/inspector-json-payload.js.z",
19282049
"./SeaMonster/json-parse-inspector.js",
19292050
],
19302051
iterations: 20,
@@ -2331,7 +2452,7 @@ let BENCHMARKS = [
23312452
"./wasm/argon2/benchmark.js",
23322453
],
23332454
preload: {
2334-
wasmBinary: "./wasm/argon2/build/argon2.wasm",
2455+
wasmBinary: "./wasm/argon2/build/argon2.wasm.z",
23352456
},
23362457
iterations: 30,
23372458
worstCaseCount: 3,

0 commit comments

Comments
 (0)