Skip to content

Commit 5055ed2

Browse files
committed
Decompress .z files in prefetch
1 parent c9c0d98 commit 5055ed2

File tree

2 files changed

+168
-10
lines changed

2 files changed

+168
-10
lines changed

JetStreamDriver.js

Lines changed: 125 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ if (typeof(URLSearchParams) !== "undefined") {
8181
globalThis.prefetchResources = getBoolParam(urlParameters, "prefetchResources");
8282
}
8383

84+
if (!isInBrowser) {
85+
load("wasm-zlib.js");
86+
}
87+
8488
if (!globalThis.prefetchResources)
8589
console.warn("Disabling resource prefetching!");
8690

@@ -189,6 +193,47 @@ function uiFriendlyDuration(time) {
189193
return `${time.toFixed(3)} ms`;
190194
}
191195

196+
// Files can be zlib compressed to reduce the download size. We don't use http
197+
// compression because we support running from the shell and don't want to
198+
// require a complicated server setup.
199+
//
200+
// zlib was chosen because we already have it in tree for the wasm-zlib test.
201+
function isCompressed(name) {
202+
return name.endsWith(".z");
203+
}
204+
205+
// Fallback for shell environments without TextDecoder. This only handles valid
206+
// UTF-8, invalid buffers will lead to unexpected results.
207+
function decodeUTF8(int8Array) {
208+
let result = '';
209+
let i = 0;
210+
while (i < int8Array.length) {
211+
let byte1 = int8Array[i++];
212+
if (byte1 < 0x80) {
213+
// 1-byte sequence (ASCII)
214+
result += String.fromCharCode(byte1);
215+
} else if ((byte1 & 0xE0) === 0xC0) {
216+
// 2-byte sequence
217+
let byte2 = int8Array[i++];
218+
result += String.fromCharCode(((byte1 & 0x1F) << 6) | (byte2 & 0x3F));
219+
} else if ((byte1 & 0xF0) === 0xE0) {
220+
// 3-byte sequence
221+
let byte2 = int8Array[i++];
222+
let byte3 = int8Array[i++];
223+
result += String.fromCharCode(((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F));
224+
} else if ((byte1 & 0xF8) === 0xF0) {
225+
// 4-byte sequence (needs surrogate pairs)
226+
let byte2 = int8Array[i++];
227+
let byte3 = int8Array[i++];
228+
let byte4 = int8Array[i++];
229+
let codePoint = ((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F);
230+
codePoint -= 0x10000;
231+
result += String.fromCharCode(0xD800 + (codePoint >> 10), 0xDC00 + (codePoint & 0x3FF));
232+
}
233+
}
234+
return result;
235+
}
236+
192237
// TODO: Cleanup / remove / merge. This is only used for caching loads in the
193238
// non-browser setting. In the browser we use exclusively `loadCache`,
194239
// `loadBlob`, `doLoadBlob`, `prefetchResourcesForBrowser` etc., see below.
@@ -201,14 +246,25 @@ class ShellFileLoader {
201246
// share common code.
202247
load(url) {
203248
console.assert(!isInBrowser);
204-
if (!globalThis.prefetchResources)
249+
250+
// If we aren't supposed to prefetch this and don't need to decompress it,
251+
// then return code snippet that will load the url on-demand.
252+
let compressed = isCompressed(url);
253+
if (!compressed && !globalThis.prefetchResources)
205254
return `load("${url}");`
206255

207256
if (this.requests.has(url)) {
208257
return this.requests.get(url);
209258
}
210259

211-
const contents = readFile(url);
260+
let contents;
261+
if (isCompressed(url)) {
262+
let bytes = new Int8Array(read(url, "binary"));
263+
bytes = zlib.decompress(bytes);
264+
contents = decodeUTF8(bytes);
265+
} else {
266+
contents = readFile(url);
267+
}
212268
this.requests.set(url, contents);
213269
return contents;
214270
}
@@ -260,10 +316,14 @@ class Driver {
260316
performance.mark("update-ui");
261317
benchmark.updateUIAfterRun();
262318

263-
if (isInBrowser && globalThis.prefetchResources) {
319+
if (isInBrowser) {
264320
const cache = JetStream.blobDataCache;
265321
for (const file of benchmark.files) {
266322
const blobData = cache[file];
323+
// If we didn't prefetch this resource, then no need to free it
324+
if (!blobData.blob) {
325+
continue
326+
}
267327
blobData.refCount--;
268328
if (!blobData.refCount)
269329
cache[file] = undefined;
@@ -415,6 +475,7 @@ class Driver {
415475

416476
async prefetchResources() {
417477
if (!isInBrowser) {
478+
await zlib.initialize();
418479
for (const benchmark of this.benchmarks)
419480
benchmark.prefetchResourcesForShell();
420481
return;
@@ -629,6 +690,11 @@ class Scripts {
629690
}
630691

631692
class ShellScripts extends Scripts {
693+
constructor() {
694+
super();
695+
this.blobs = [];
696+
}
697+
632698
run() {
633699
let globalObject;
634700
let realm;
@@ -655,13 +721,23 @@ class ShellScripts extends Scripts {
655721
currentReject
656722
};
657723

724+
// Store shellBlobs on JetStreamBlobs so that getBinary can find them.
725+
globalObject.JetStreamBlobs = {};
726+
for (const [name, value] of this.blobs) {
727+
globalObject.JetStreamBlobs[name] = value;
728+
}
729+
658730
globalObject.performance ??= performance;
659731
for (const script of this.scripts)
660732
globalObject.loadString(script);
661733

662734
return isD8 ? realm : globalObject;
663735
}
664736

737+
addBlobs(blobs) {
738+
this.blobs.push(...blobs);
739+
}
740+
665741
add(text) {
666742
this.scripts.push(text);
667743
}
@@ -694,7 +770,6 @@ class BrowserScripts extends Scripts {
694770
return magicFrame;
695771
}
696772

697-
698773
add(text) {
699774
this.scripts.push(`<script>${text}</script>`);
700775
}
@@ -714,6 +789,7 @@ class Benchmark {
714789
this.allowUtf16 = !!plan.allowUtf16;
715790
this.scripts = null;
716791
this.preloads = null;
792+
this.shellBlobs = null;
717793
this.results = [];
718794
this._state = BenchmarkState.READY;
719795
}
@@ -820,10 +896,14 @@ class Benchmark {
820896
if (!!this.plan.exposeBrowserTest)
821897
scripts.addBrowserTest();
822898

899+
if (this.shellBlobs) {
900+
scripts.addBlobs(this.shellBlobs);
901+
}
823902
if (this.plan.preload) {
824903
let preloadCode = "";
825-
for (let [ variableName, blobURLOrPath ] of this.preloads)
904+
for (let [ variableName, blobURLOrPath ] of this.preloads) {
826905
preloadCode += `JetStream.preload.${variableName} = "${blobURLOrPath}";\n`;
906+
}
827907
scripts.add(preloadCode);
828908
}
829909

@@ -838,7 +918,7 @@ class Benchmark {
838918
} else {
839919
const cache = JetStream.blobDataCache;
840920
for (const file of this.plan.files) {
841-
scripts.addWithURL(globalThis.prefetchResources ? cache[file].blobURL : file);
921+
scripts.addWithURL(cache[file].blobURL);
842922
}
843923
}
844924

@@ -889,10 +969,15 @@ class Benchmark {
889969

890970
async doLoadBlob(resource) {
891971
const blobData = JetStream.blobDataCache[resource];
892-
if (!globalThis.prefetchResources) {
972+
973+
// If we aren't supposed to prefetch this and don't need to decompress it,
974+
// then set the blobURL to just be the resource URL.
975+
const compressed = isCompressed(resource);
976+
if (!compressed && !globalThis.prefetchResources) {
893977
blobData.blobURL = resource;
894978
return blobData;
895979
}
980+
896981
let response;
897982
let tries = 3;
898983
while (tries--) {
@@ -908,7 +993,15 @@ class Benchmark {
908993
continue;
909994
throw new Error("Fetch failed");
910995
}
911-
const blob = await response.blob();
996+
997+
// If we need to decompress this, then run it through a decompression
998+
// stream.
999+
if (compressed) {
1000+
const stream = response.body.pipeThrough(new DecompressionStream('deflate'))
1001+
response = new Response(stream);
1002+
}
1003+
1004+
let blob = await response.blob();
9121005
blobData.blob = blob;
9131006
blobData.blobURL = URL.createObjectURL(blob);
9141007
return blobData;
@@ -1044,7 +1137,26 @@ class Benchmark {
10441137
this.scripts = this.plan.files.map(file => shellFileLoader.load(file));
10451138

10461139
console.assert(this.preloads === null, "This initialization should be called only once.");
1047-
this.preloads = Object.entries(this.plan.preload ?? {});
1140+
this.preloads = [];
1141+
this.shellBlobs = [];
1142+
for (let name of Object.getOwnPropertyNames(this.plan.preload)) {
1143+
let file = this.plan.preload[name];
1144+
1145+
const compressed = isCompressed(file);
1146+
if (compressed || globalThis.prefetchResources) {
1147+
let bytes = new Int8Array(read(file, "binary"));
1148+
if (compressed) {
1149+
bytes = zlib.decompress(bytes);
1150+
}
1151+
// Add a `blob` prefix to the file name so that `getBinary`
1152+
// knows to look for a blob in JetStreamBlobs (setup by
1153+
// ShellScripts).
1154+
file = `blob://${name}`;
1155+
this.shellBlobs.push([file, bytes]);
1156+
}
1157+
1158+
this.preloads.push([name, file]);
1159+
}
10481160
}
10491161

10501162
scoreIdentifiers() {
@@ -1161,7 +1273,7 @@ class GroupedBenchmark extends Benchmark {
11611273
await benchmark.prefetchResourcesForBrowser();
11621274
}
11631275

1164-
async retryPrefetchResourcesForBrowser() {
1276+
async retryjForBrowser() {
11651277
for (const benchmark of this.benchmarks)
11661278
await benchmark.retryPrefetchResourcesForBrowser();
11671279
}
@@ -1296,6 +1408,9 @@ class AsyncBenchmark extends DefaultBenchmark {
12961408
} else {
12971409
str += `
12981410
JetStream.getBinary = async function(path) {
1411+
if (path.startsWith("blob://")) {
1412+
return JetStreamBlobs[path];
1413+
}
12991414
return new Int8Array(read(path, "binary"));
13001415
};
13011416

wasm-zlib.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// zlib-based utility for use in shells where CompressionStream and
2+
// DecompressionStream are not available.
3+
4+
function module() {
5+
'use strict';
6+
7+
let zlibPromise = null;
8+
let zlibModule = null;
9+
10+
async function initialize() {
11+
if (zlibPromise) {
12+
zlibModule = await zlibPromise;
13+
return zlibModule;
14+
}
15+
load('wasm/zlib/build/zlib.js');
16+
zlibPromise = setupModule({
17+
wasmBinary: new Int8Array(read('wasm/zlib/build/zlib.wasm', "binary")),
18+
});
19+
zlibModule = await zlibPromise;
20+
return zlibModule;
21+
}
22+
23+
function decompress(bytes) {
24+
zlibModule.FS.writeFile('in', bytes);
25+
const inputzStr = zlibModule.stringToNewUTF8('in');
26+
const inputzoutStr = zlibModule.stringToNewUTF8('out');
27+
if (zlibModule._decompressFile(inputzStr, inputzoutStr) !== 0) {
28+
throw new Error();
29+
}
30+
const output = zlibModule.FS.readFile('out');
31+
zlibModule._free(inputzStr);
32+
zlibModule._free(inputzoutStr);
33+
return output;
34+
}
35+
36+
return {
37+
initialize: initialize,
38+
decompress: decompress,
39+
};
40+
}
41+
42+
globalThis.zlib = module();
43+

0 commit comments

Comments
 (0)