diff --git a/res/gz-worker.js b/res/gz-worker.js new file mode 100644 index 0000000000..07c891e660 --- /dev/null +++ b/res/gz-worker.js @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +async function readableStreamToBuffer(stream) { + const reader = stream.getReader(); + const chunks = []; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (value) { + chunks.push(value); + } + } + } finally { + reader.releaseLock(); + } + + // Calculate total length and combine chunks + const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + for (const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.length; + } + + return result; +} + +onmessage = async (e) => { + let data = e.data; + if (data.kind === 'compress') { + // Create a gzip compression stream + const compressionStream = new CompressionStream('gzip'); + + // Write the data to the compression stream + const writer = compressionStream.writable.getWriter(); + writer.write(data.arrayData); + writer.close(); + + // Read the compressed data back into a buffer + let result = await readableStreamToBuffer(compressionStream.readable); + postMessage(result, [result.buffer]); + } else if (data.kind === 'decompress') { + // Create a gzip compression stream + const decompressionStream = new DecompressionStream('gzip'); + + // Write the data to the compression stream + const writer = decompressionStream.writable.getWriter(); + writer.write(data.arrayData); + writer.close(); + + // Read the compressed data back into a buffer + let result = await readableStreamToBuffer(decompressionStream.readable); + postMessage(result, [result.buffer]); + } else { + throw new Error('unknown message'); + } +}; diff --git a/src/test/fixtures/node-worker.ts b/src/test/fixtures/node-worker.ts index f54a5e956c..a4cd6aabdc 100644 --- a/src/test/fixtures/node-worker.ts +++ b/src/test/fixtures/node-worker.ts @@ -21,6 +21,8 @@ function getWorkerScript(file: string): string { }, postMessage: parentPort.postMessage.bind(parentPort), onmessage: function () {}, + DecompressionStream, + CompressionStream, }; vm.runInNewContext(scriptContent, sandbox, { filename: "${file}" }); diff --git a/src/test/setup.ts b/src/test/setup.ts index 4e4120e9d6..cccc5d2f36 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -24,6 +24,10 @@ if (process.env.TZ !== 'UTC') { fetchMock.mockGlobal(); (global as any).fetchMock = fetchMock; +// Mock the effects of the file-loader which our Webpack config defines +// for JS files under res: The "default export" is the path to the file. +jest.mock('firefox-profiler-res/gz-worker.js', () => './res/gz-worker.js'); + // Install a Worker class which is similar to the DOM Worker class. (global as any).Worker = NodeWorker; diff --git a/src/utils/gz.ts b/src/utils/gz.ts index ca24a04625..3f5bedd5f4 100644 --- a/src/utils/gz.ts +++ b/src/utils/gz.ts @@ -2,68 +2,45 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -async function readableStreamToBuffer( - stream: ReadableStream> -): Promise> { - const reader = stream.getReader(); - const chunks: Uint8Array[] = []; - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - if (value) { - chunks.push(value); - } - } - } finally { - reader.releaseLock(); - } - - // Calculate total length and combine chunks - const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0); - const result = new Uint8Array(totalLength); - let offset = 0; - for (const chunk of chunks) { - result.set(chunk, offset); - offset += chunk.length; - } +import gzWorkerPath from 'firefox-profiler-res/gz-worker.js'; - return result; +function runGzWorker( + kind: 'compress' | 'decompress', + arrayData: Uint8Array +): Promise> { + return new Promise((resolve, reject) => { + // On-demand spawn the worker. If this is too slow we can look into keeping + // a pool of workers around. + const worker = new Worker(gzWorkerPath); + + worker.onmessage = (e) => { + resolve(e.data as Uint8Array); + worker.terminate(); + }; + + worker.onerror = (e) => { + reject(e.error); + worker.terminate(); + }; + + worker.postMessage({ kind, arrayData }, [arrayData.buffer]); + }); } +// This will transfer `data` if it is an array buffer. export async function compress( data: string | Uint8Array ): Promise> { // Encode the data if it's a string const arrayData = typeof data === 'string' ? new TextEncoder().encode(data) : data; - - // Create a gzip compression stream - const compressionStream = new CompressionStream('gzip'); - - // Write the data to the compression stream - const writer = compressionStream.writable.getWriter(); - writer.write(arrayData); - writer.close(); - - // Read the compressed data back into a buffer - return readableStreamToBuffer(compressionStream.readable); + return runGzWorker('compress', arrayData); } export async function decompress( data: Uint8Array ): Promise> { - // Create a gzip compression stream - const decompressionStream = new DecompressionStream('gzip'); - - // Write the data to the compression stream - const writer = decompressionStream.writable.getWriter(); - writer.write(data); - writer.close(); - - // Read the compressed data back into a buffer - return readableStreamToBuffer(decompressionStream.readable); + return runGzWorker('decompress', data); } export function isGzip(data: Uint8Array): boolean { diff --git a/webpack.config.js b/webpack.config.js index 70757ec8aa..cb7c15bdc4 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -99,6 +99,7 @@ const config = { patterns: [ 'res/_headers', 'res/_redirects', + 'res/gz-worker.js', 'res/contribute.json', 'res/robots.txt', 'res/service-worker-compat.js',