diff --git a/JetStreamDriver.js b/JetStreamDriver.js index b5a7d114..8c40afa5 100644 --- a/JetStreamDriver.js +++ b/JetStreamDriver.js @@ -135,90 +135,45 @@ function shellFriendlyScore(time) { } -// Files can be zlib compressed to reduce the size of the JetStream source code. -// We don't use http compression because we support running from the shell and -// don't want to require a complicated server setup. -// -// zlib was chosen because we already have it in tree for the wasm-zlib test. -function isCompressed(name) { - return name.endsWith(".z"); -} - -function uncompressedName(name) { - console.assert(isCompressed(name)); - return name.slice(0, -2); -} - -// TODO: Cleanup / remove / merge. This is only used for caching loads in the -// non-browser setting. In the browser we use exclusively `loadCache`, -// `loadBlob`, `doLoadBlob`, `prefetchResourcesForBrowser` etc., see below. -class ShellFileLoader { - constructor() { - this.requests = new Map; - } - - // Cache / memoize previously read files, because some workloads - // share common code. - load(url) { - console.assert(!isInBrowser); - - let compressed = isCompressed(url); - if (compressed && !JetStreamParams.prefetchResources) { - url = uncompressedName(url); - } - - // If we aren't supposed to prefetch this then return code snippet that will load the url on-demand. - if (!JetStreamParams.prefetchResources) - return `load("${url}");` - - if (this.requests.has(url)) { - return this.requests.get(url); - } - - let contents; - if (compressed) { - const compressedBytes = new Int8Array(read(url, "binary")); - const decompressedBytes = zlib.decompress(compressedBytes); - contents = new TextDecoder().decode(decompressedBytes); - } else { - contents = readFile(url); - } - this.requests.set(url, contents); - return contents; - } -}; - - -const RETRY_COUNT = 3; -const RETRY_DELAY_MS = 500; - -class BrowserFileLoader { +class FileLoader { constructor() { this._blobDataCache = { __proto__ : null }; this.counter = { __proto__: null, loadedResources: 0, totalResources: 0, - } + }; } - getBlobURL(file) { - const blobURL = this._blobDataCache[file].blobURL; - if (!blobURL) { - throw new Error(`Missing blob data for ${file}`); - } - return blobURL; - } _updateCounter() { ++this.counter.loadedResources; - JetStream.updateCounterUI(); + } + + async initialize() { + throw new Error("Subclasses need to implement this"); + } + + getBlobData(file) { + const blobData = this._blobDataCache[file]; + if (!blobData) { + throw new Error(`Missing blob data for ${file}`); + } + if (!blobData.resource) { + throw new Error(`Incomplete blob data for ${file}`); + } + return blobData; } async prefetchResourcePreload(name, resource) { const blobData = await this.prefetchResourceFile(resource); - if (!globalThis.allIsGood) + if (!globalThis.allIsGood) { return; - return { name: name, resource: resource, blobURLOrPath: blobData.blobURL }; + } + // Return blobData copy with the preload name. + return { + ...blobData, + name, + }; } async prefetchResourceFile(resource) { @@ -226,35 +181,111 @@ class BrowserFileLoader { let blobDataOrPromise = this._blobDataCache[resource]; if (!blobDataOrPromise) { const newBlobData = { - resource: resource, + resource, blob: null, blobURL: null, refCount: 0 }; - blobDataOrPromise = this._loadBlob(newBlobData); + blobDataOrPromise = this._fetchBlobData(newBlobData); // Temporarily cache the loading promise. this._blobDataCache[resource] = blobDataOrPromise; } const blobData = await blobDataOrPromise; + if (!blobData) { + throw new Error(`Missing blobData for ${resource}`); + } // Replace the potential promise in the cache. this._blobDataCache[resource] = blobData; blobData.refCount++; - if (globalThis.allIsGood) + if (globalThis.allIsGood) { this._updateCounter(); + } return blobData; } - async _loadBlob(blobData) { - let resource = blobData.resource; - const compressed = isCompressed(resource); - if (compressed && !JetStreamParams.prefetchResources) { - resource = uncompressedName(resource); + // Files can be zlib compressed to reduce the size of the JetStream source code. + // We don't use http compression because we support running from the shell and + // don't want to require a complicated server setup. + // + // zlib was chosen because we already have it in tree for the wasm-zlib test. + _isCompressed(name) { + return name.endsWith(".z"); + } + + _decompressedName(name) { + return name.slice(0, -2); + } + + _decompressedResourceName(resource) { + const isCompressed = this._isCompressed(resource); + if (isCompressed) { + return [isCompressed, this._decompressedName(resource)]; } + return [isCompressed, resource]; + } + + async _fetchBlobData(blobData) { + throw new Error("Subclasses need to implement this"); + } + +} + +// TODO: Cleanup / remove / merge. This is only used for caching loads in the +// non-browser setting. In the browser we use exclusively `loadCache`, +// `loadBlob`, `doLoadBlob`, `prefetchResourcesForBrowser` etc., see below. +class ShellFileLoader extends FileLoader { + async initialize() { + console.assert(!isInBrowser); + if (JetStreamParams.prefetchResources) { + await zlib.initialize(); + } + } + + async _fetchBlobData(blobData) { + const originalResource = blobData.resource; + const [isCompressed, decompressedResource] = this._decompressedResourceName(originalResource); + blobData.blobURL = decompressedResource; + + if (!JetStreamParams.prefetchResources) { + // If we aren't supposed to prefetch this, then handle this later + // in ShellScripts.addFile. Assumes that the files have been + // locally decompressed. + return blobData; + } + + let contents = new Int8Array(read(originalResource, "binary")); + if (isCompressed) { + contents = zlib.decompress(contents); + } + blobData.blob = contents; + + return blobData;; + } +}; + + +const RETRY_COUNT = 3; +const RETRY_DELAY_MS = 500; + +class BrowserFileLoader extends FileLoader { + + async initialize() { + console.assert(isInBrowser); + } + + _updateCounter() { + super._updateCounter(); + JetStream.updateCounterUI(); + } + + async _fetchBlobData(blobData) { + const originalResource = blobData.resource; + const [isCompressed, decompressedResource] = this._decompressedResourceName(blobData.resource); // If we aren't supposed to prefetch this then set the blobURL to just // be the resource URL. if (!JetStreamParams.prefetchResources) { - blobData.blobURL = resource; + blobData.blobURL = decompressedResource; return blobData; } @@ -263,7 +294,7 @@ class BrowserFileLoader { while (tries--) { let hasError = false; try { - response = await fetch(resource, { cache: "no-store" }); + response = await fetch(originalResource, { cache: "no-store" }); } catch (e) { hasError = true; } @@ -271,7 +302,7 @@ class BrowserFileLoader { break; if (tries) { await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS)); - console.warning(`Request failed, retrying: ${resource}`); + console.warn(`Request failed, retrying: ${originalResource}`); continue; } globalThis.allIsGood = false; @@ -280,8 +311,8 @@ class BrowserFileLoader { // If we need to decompress this, then run it through a decompression // stream. - if (compressed) { - const stream = response.body.pipeThrough(new DecompressionStream("deflate")) + if (isCompressed) { + const stream = response.body.pipeThrough(new DecompressionStream("deflate")); response = new Response(stream); } @@ -296,19 +327,19 @@ class BrowserFileLoader { const blobData = this._blobDataCache[file]; // If we didn't prefetch this resource, then no need to free it if (!blobData.blob) { - continue + continue; } blobData.refCount--; if (!blobData.refCount) { this._blobDataCache[file] = undefined; - console.log("DELETING", file); } } } } -const browserFileLoader = new BrowserFileLoader(); -const shellFileLoader = new ShellFileLoader(); +const fileLoader = isInBrowser ? + new BrowserFileLoader() : + new ShellFileLoader(); class Driver { constructor(benchmarks) { @@ -498,29 +529,18 @@ class Driver { } async prefetchResources() { - if (isInBrowser) { - await this.prefetchResourcesForBrowser(); - } else { - await this.prefetchResourcesForShell(); - } - } + await fileLoader.initialize(); - async prefetchResourcesForShell() { - if (JetStreamParams.prefetchResources) { - await zlib.initialize(); + const promises = this.benchmarks.map( + benchmark => benchmark.prefetchResources()); + await Promise.all(promises); + + if (isInBrowser) { + this.updateUIAfterResourceLoading() } - for (const benchmark of this.benchmarks) - benchmark.prefetchResourcesForShell(); } - async prefetchResourcesForBrowser() { - // TODO: Cleanup the browser path of the preloading below and in - // `prefetchResourcesForBrowser`. - const promises = []; - for (const benchmark of this.benchmarks) - promises.push(benchmark.prefetchResourcesForBrowser()); - await Promise.all(promises); - + updateUIAfterResourceLoading() { const statusElement = document.getElementById("status"); statusElement.classList.remove('loading'); statusElement.innerHTML = `Start Test`; @@ -532,7 +552,7 @@ class Driver { } updateCounterUI() { - const counter = browserFileLoader.counter; + const counter = fileLoader.counter; const statusElement = document.getElementById("status-text"); statusElement.innerText = `Loading ${counter.loadedResources} of ${counter.totalResources} ...`; @@ -647,15 +667,22 @@ const BenchmarkState = Object.freeze({ class Scripts { constructor(preloads) { this.scripts = []; + this._preparePreloads(preloads); + } + + static get prerunCode() { + throw new Error("Subclasses need to implement this"); + } + _preparePreloads(preloads) { let preloadsCode = ""; let resourcesCode = ""; - for (let { name, resource, blobURLOrPath } of preloads) { + for (let { name, resource, blobURL} of preloads) { console.assert(name?.length > 0, "Invalid preload name."); console.assert(resource?.length > 0, "Invalid preload resource."); - console.assert(blobURLOrPath?.length > 0, "Invalid preload data."); - preloadsCode += `${JSON.stringify(name)}: "${blobURLOrPath}",\n`; - resourcesCode += `${JSON.stringify(resource)}: "${blobURLOrPath}",\n`; + console.assert(blobURL?.length > 0, "Invalid preload data."); + preloadsCode += `${JSON.stringify(name)}: "${blobURL}",\n`; + resourcesCode += `${JSON.stringify(resource)}: "${blobURL}",\n`; } // Expose a globalThis.JetStream object to the workload. We use // a proxy to prevent prototype access and throw on unknown properties. @@ -693,7 +720,7 @@ class Scripts { throw new Error("Subclasses need to implement this"); } - addWithURL(url) { + addFile(blobData) { throw new Error("addWithURL not supported"); } @@ -733,7 +760,47 @@ class Scripts { class ShellScripts extends Scripts { constructor(preloads) { super(preloads); - this.prefetchedResources = Object.create(null);; + this.shellPrefetchedResources = Object.create(null); + this._processPrefetchedPreloads(preloads); + } + + _processPrefetchedPreloads(preloads) { + if (JetStreamParams.prefetchResources) { + for (const {resource, blobURL, blob} of preloads) { + this.shellPrefetchedResources[blobURL] = blob; + } + } + } + + static get prerunCode() { + return ` + JetStream.getBinary = async function(path) { + if ("ShellPrefetchedResources" in globalThis) { + const data = ShellPrefetchedResources[path]; + return data; + } + return new Int8Array(read(path, "binary")); + }; + + JetStream.getString = async function(path) { + if ("ShellPrefetchedResources" in globalThis) { + return new ShellTextDecoder().decode(ShellPrefetchedResources[path]); + } + return read(path); + }; + + JetStream.dynamicImport = async function(path) { + try { + // TODO: this skips the prefetched resources, but I'm + // not sure of a way around that. + return await import(path); + } catch (e) { + // In shells, relative imports require different paths, so try with and + // without the "./" prefix (e.g., JSC requires it). + return await import(path.slice("./".length)) + } + }; + `; } run() { @@ -770,9 +837,9 @@ class ShellScripts extends Scripts { globalObject.ShellTextDecoder = TextDecoder; // Store shellPrefetchedResources on ShellPrefetchedResources so that // getBinary and getString can find them. - globalObject.ShellPrefetchedResources = this.prefetchedResources; + globalObject.ShellPrefetchedResources = this.shellPrefetchedResources; } else { - console.assert(Object.values(this.prefetchedResources).length === 0, "Unexpected prefetched resources"); + console.assert(Object.values(this.shellPrefetchedResources).length === 0, "Unexpected prefetched resources"); } globalObject.performance ??= performance; @@ -782,10 +849,9 @@ class ShellScripts extends Scripts { return isD8 ? realm : globalObject; } - addPrefetchedResources(prefetchedResources) { - for (let [file, bytes] of Object.entries(prefetchedResources)) { - this.prefetchedResources[file] = bytes; - } + + addFile(blobData) { + this.add(`load("${blobData.blobURL}");`); } add(text) { @@ -794,10 +860,6 @@ class ShellScripts extends Scripts { } this.scripts.push(text); } - - addWithURL(url) { - console.assert(false, "Should not reach here in CLI"); - } } class BrowserScripts extends Scripts { @@ -806,6 +868,24 @@ class BrowserScripts extends Scripts { this.add("window.onerror = top.currentReject;"); } + static get prerunCode() { + return ` + JetStream.getBinary = async function(blobURL) { + const response = await fetch(blobURL); + return new Int8Array(await response.arrayBuffer()); + }; + + JetStream.getString = async function(blobURL) { + const response = await fetch(blobURL); + return response.text(); + }; + + JetStream.dynamicImport = async function(blobURL) { + return await import(blobURL); + }; + `; + } + run() { const string = this.scripts.join("\n"); const magic = document.getElementById("magic"); @@ -823,6 +903,10 @@ class BrowserScripts extends Scripts { return magicFrame; } + addFile(blobData) { + this.addWithURL(blobData.blobURL); + } + add(text) { if (!text) { throw new Error("Missing script source"); @@ -863,11 +947,9 @@ class Benchmark { this.allowUtf16 = !!allowUtf16; // Resource handling: - this._scripts = null; this._files = files; this._preloadEntries = Object.entries(preload); this._preloadBlobData = []; - this._shellPrefetchedResources = null; } // Use getter so it can be overridden in subclasses (GroupedBenchmark). @@ -1066,22 +1148,12 @@ class Benchmark { if (this._exposeBrowserTest) scripts.addBrowserTest(); - if (this._shellPrefetchedResources) { - scripts.addPrefetchedResources(this._shellPrefetchedResources); - } - const prerunCode = this.prerunCode; if (prerunCode) scripts.add(prerunCode); - if (!isInBrowser) { - console.assert(this._scripts && this._scripts.length === this.files.length); - for (const text of this._scripts) - scripts.add(text); - } else { - for (const file of this.files) { - scripts.addWithURL(browserFileLoader.getBlobURL(file)); - } + for (const file of this.files) { + scripts.addFile(fileLoader.getBlobData(file)); } const promise = new Promise((resolve, reject) => { @@ -1129,10 +1201,9 @@ class Benchmark { Realm.dispose(magicFrame); } - - async prefetchResourcesForBrowser() { - console.assert(isInBrowser); - const promises = this.files.map((file) => browserFileLoader.prefetchResourceFile(file)); + async prefetchResources() { + const promises = this.files.map( + (file) => fileLoader.prefetchResourceFile(file)); for (const [name, resource] of this.preloadEntries) { promises.push(this.prefetchResourcePreload(name, resource)); } @@ -1140,37 +1211,10 @@ class Benchmark { } async prefetchResourcePreload(name, resource) { - const preloadData = await browserFileLoader.prefetchResourcePreload(name, resource); + const preloadData = await fileLoader.prefetchResourcePreload(name, resource); this._preloadBlobData.push(preloadData); } - prefetchResourcesForShell() { - // FIXME: move to ShellFileLoader. - console.assert(!isInBrowser); - - console.assert(this._scripts === null, "This initialization should be called only once."); - this._scripts = this.files.map(file => shellFileLoader.load(file)); - - console.assert(this._preloadBlobData.length === 0, "This initialization should be called only once."); - this._shellPrefetchedResources = Object.create(null); - for (let [name, resource] of this.preloadEntries) { - const compressed = isCompressed(resource); - if (compressed && !JetStreamParams.prefetchResources) { - resource = uncompressedName(resource); - } - - if (JetStreamParams.prefetchResources) { - let bytes = new Int8Array(read(resource, "binary")); - if (compressed) { - bytes = zlib.decompress(bytes); - } - this._shellPrefetchedResources[resource] = bytes; - } - - this._preloadBlobData.push({ name, resource, blobURLOrPath: resource }); - } - } - allScoreIdentifiers() { const ids = Object.keys(this.allScores()).map(name => this.scoreIdentifier(name)); return ids; @@ -1286,9 +1330,9 @@ class Benchmark { tearDown() { if (isInBrowser) { - browserFileLoader.free(this.files); + fileLoader.free(this.files); const preloadFiles = this.preloadEntries.map(([_, file]) => file); - browserFileLoader.free(preloadFiles); + fileLoader.free(preloadFiles); } } }; @@ -1305,16 +1349,11 @@ class GroupedBenchmark extends Benchmark { this.benchmarks = benchmarks; } - async prefetchResourcesForBrowser() { + async prefetchResources() { for (const benchmark of this.benchmarks) - await benchmark.prefetchResourcesForBrowser(); + await benchmark.prefetchResources(); } - prefetchResourcesForShell() { - for (const benchmark of this.benchmarks) - benchmark.prefetchResourcesForShell(); - } - renderHTML() { let text = super.renderHTML(); if (JetStreamParams.groupDetails) { @@ -1484,51 +1523,9 @@ class AsyncBenchmark extends DefaultBenchmark { // FIXME: It would be nice if these were available to any benchmark not just async ones but since these functions // are async they would only work in a context where the benchmark is async anyway. Long term, we should do away // with this class and make all benchmarks async. - if (isInBrowser) { - str += ` - JetStream.getBinary = async function(blobURL) { - const response = await fetch(blobURL); - return new Int8Array(await response.arrayBuffer()); - }; - - JetStream.getString = async function(blobURL) { - const response = await fetch(blobURL); - return response.text(); - }; - - JetStream.dynamicImport = async function(blobURL) { - return await import(blobURL); - }; - `; - } else { - str += ` - JetStream.getBinary = async function(path) { - if ("ShellPrefetchedResources" in globalThis) { - return ShellPrefetchedResources[path]; - } - return new Int8Array(read(path, "binary")); - }; - - JetStream.getString = async function(path) { - if ("ShellPrefetchedResources" in globalThis) { - return new ShellTextDecoder().decode(ShellPrefetchedResources[path]); - } - return read(path); - }; - - JetStream.dynamicImport = async function(path) { - try { - // TODO: this skips the prefetched resources, but I'm - // not sure of a way around that. - return await import(path); - } catch (e) { - // In shells, relative imports require different paths, so try with and - // without the "./" prefix (e.g., JSC requires it). - return await import(path.slice("./".length)) - } - }; - `; - } + str += isInBrowser ? + BrowserScripts.prerunCode : + ShellScripts.prerunCode; return str; } diff --git a/utils/shell-config.js b/utils/shell-config.js index 59d7895c..5a611da5 100644 --- a/utils/shell-config.js +++ b/utils/shell-config.js @@ -23,6 +23,8 @@ * THE POSSIBILITY OF SUCH DAMAGE. */ +globalThis.allIsGood = true; + const isInBrowser = false; if (typeof console == "undefined") console = {};