From 61cdcb4fc3ccabaa74da79734408ac468f48b816 Mon Sep 17 00:00:00 2001 From: Ryan Hunt Date: Thu, 4 Sep 2025 12:55:28 -0500 Subject: [PATCH 1/3] Move compression and decompression to a worker --- res/gz-worker.js | 60 ++++++++++++++++++++++++++ src/test/fixtures/node-worker.ts | 2 + src/test/setup.ts | 4 ++ src/utils/gz.ts | 72 ++++++++++---------------------- webpack.config.js | 1 + 5 files changed, 90 insertions(+), 49 deletions(-) create mode 100644 res/gz-worker.js diff --git a/res/gz-worker.js b/res/gz-worker.js new file mode 100644 index 0000000000..bd2e89f634 --- /dev/null +++ b/res/gz-worker.js @@ -0,0 +1,60 @@ +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..4e73107661 100644 --- a/src/utils/gz.ts +++ b/src/utils/gz.ts @@ -2,68 +2,42 @@ * 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; - } - - return result; +import gzWorkerPath from 'firefox-profiler-res/gz-worker.js'; + +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', From 67b509e1c6529453d42881cf51c5013b1515b41c Mon Sep 17 00:00:00 2001 From: Ryan Hunt Date: Thu, 4 Sep 2025 13:22:29 -0500 Subject: [PATCH 2/3] Fix lints --- res/gz-worker.js | 10 ++++------ src/utils/gz.ts | 11 +++++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/res/gz-worker.js b/res/gz-worker.js index bd2e89f634..a7c194ce46 100644 --- a/res/gz-worker.js +++ b/res/gz-worker.js @@ -1,6 +1,4 @@ -async function readableStreamToBuffer( - stream -) { +async function readableStreamToBuffer(stream) { const reader = stream.getReader(); const chunks = []; @@ -30,7 +28,7 @@ async function readableStreamToBuffer( onmessage = async (e) => { let data = e.data; - if (data.kind === "compress") { + if (data.kind === 'compress') { // Create a gzip compression stream const compressionStream = new CompressionStream('gzip'); @@ -42,7 +40,7 @@ onmessage = async (e) => { // Read the compressed data back into a buffer let result = await readableStreamToBuffer(compressionStream.readable); postMessage(result, [result.buffer]); - } else if (data.kind === "decompress") { + } else if (data.kind === 'decompress') { // Create a gzip compression stream const decompressionStream = new DecompressionStream('gzip'); @@ -55,6 +53,6 @@ onmessage = async (e) => { let result = await readableStreamToBuffer(decompressionStream.readable); postMessage(result, [result.buffer]); } else { - throw new Error("unknown message"); + throw new Error('unknown message'); } }; diff --git a/src/utils/gz.ts b/src/utils/gz.ts index 4e73107661..3f5bedd5f4 100644 --- a/src/utils/gz.ts +++ b/src/utils/gz.ts @@ -4,7 +4,10 @@ import gzWorkerPath from 'firefox-profiler-res/gz-worker.js'; -function runGzWorker(kind: "compress" | "decompress", arrayData: Uint8Array): Promise> { +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. @@ -20,7 +23,7 @@ function runGzWorker(kind: "compress" | "decompress", arrayData: Uint8Array ): Promise> { - return runGzWorker("decompress", data); + return runGzWorker('decompress', data); } export function isGzip(data: Uint8Array): boolean { From b282779fe5221d9458eaf1f948a0f5df79a9832d Mon Sep 17 00:00:00 2001 From: Ryan Hunt Date: Thu, 4 Sep 2025 13:22:47 -0500 Subject: [PATCH 3/3] Add license --- res/gz-worker.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/gz-worker.js b/res/gz-worker.js index a7c194ce46..07c891e660 100644 --- a/res/gz-worker.js +++ b/res/gz-worker.js @@ -1,3 +1,7 @@ +/* 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 = [];