From ac136728581f5008e58c0798a9eeb97c6b836265 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Mon, 15 Apr 2024 16:05:01 +0200 Subject: [PATCH] feat: enable optionally enabling/disabling blob compression (#58) While blobs such as startup snapshots (40MB in the case of mongosh) can be quite large and compressing them saves binary size, decompressing them also costs non-trivial startup time. We make compression optional in this commit so that we can turn it off in mongosh. --- bin/boxednode.js | 3 ++- src/helpers.ts | 34 ++++++++++++++++++++++---------- src/index.ts | 11 ++++++++--- test/index.ts | 51 +++++++++++++++++++++++++----------------------- 4 files changed, 61 insertions(+), 38 deletions(-) diff --git a/bin/boxednode.js b/bin/boxednode.js index b309214..7f7804c 100755 --- a/bin/boxednode.js +++ b/bin/boxednode.js @@ -54,7 +54,8 @@ const argv = require('yargs') namespace: argv.N, useLegacyDefaultUvLoop: argv.useLegacyDefaultUvLoop, useCodeCache: argv.H, - useNodeSnapshot: argv.S + useNodeSnapshot: argv.S, + compressBlobs: argv.Z }); } catch (err) { console.error(err); diff --git a/src/helpers.ts b/src/helpers.ts index 2fddc6c..cee4fc5 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -107,6 +107,21 @@ export function createCppJsStringDefinition (fnName: string, source: string): st `; } +export async function createUncompressedBlobDefinition (fnName: string, source: Uint8Array): Promise { + return ` + static const uint8_t ${fnName}_source_[] = { + ${Uint8Array.prototype.toString.call(source) || '0'} + }; + + std::vector ${fnName}Vector() { + return std::vector( + reinterpret_cast(&${fnName}_source_[0]), + reinterpret_cast(&${fnName}_source_[${source.length}])); + } + + ${blobTypedArrayAccessors(fnName, source.length)}`; +} + export async function createCompressedBlobDefinition (fnName: string, source: Uint8Array): Promise { const compressed = await promisify(zlib.brotliCompress)(source, { params: { @@ -133,13 +148,6 @@ export async function createCompressedBlobDefinition (fnName: string, source: Ui assert(decoded_size == ${source.length}); } - std::string ${fnName}() { - ${source.length === 0 ? 'return {};' : ` - std::string dst(${source.length}, 0); - ${fnName}_Read(&dst[0]); - return dst;`} - } - std::vector ${fnName}Vector() { ${source.length === 0 ? 'return {};' : ` std::vector dst(${source.length}); @@ -147,19 +155,25 @@ export async function createCompressedBlobDefinition (fnName: string, source: Ui return dst;`} } + ${blobTypedArrayAccessors(fnName, source.length)} + `; +} + +function blobTypedArrayAccessors (fnName: string, sourceLength: number): string { + return ` std::shared_ptr ${fnName}BackingStore() { - std::string* str = new std::string(std::move(${fnName}())); + std::vector* str = new std::vector(std::move(${fnName}Vector())); return v8::SharedArrayBuffer::NewBackingStore( &str->front(), str->size(), [](void*, size_t, void* deleter_data) { - delete static_cast(deleter_data); + delete static_cast*>(deleter_data); }, static_cast(str)); } v8::Local ${fnName}Buffer(v8::Isolate* isolate) { - ${source.length === 0 ? ` + ${sourceLength === 0 ? ` auto array_buffer = v8::SharedArrayBuffer::New(isolate, 0); ` : ` auto array_buffer = v8::SharedArrayBuffer::New(isolate, ${fnName}BackingStore()); diff --git a/src/index.ts b/src/index.ts index cfad964..2a9b379 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ import { promisify } from 'util'; import { promises as fs, createReadStream, createWriteStream } from 'fs'; import { AddonConfig, loadGYPConfig, storeGYPConfig, modifyAddonGyp } from './native-addons'; import { ExecutableMetadata, generateRCFile } from './executable-metadata'; -import { spawnBuildCommand, ProcessEnv, pipeline, createCppJsStringDefinition, createCompressedBlobDefinition } from './helpers'; +import { spawnBuildCommand, ProcessEnv, pipeline, createCppJsStringDefinition, createCompressedBlobDefinition, createUncompressedBlobDefinition } from './helpers'; import { Readable } from 'stream'; import nv from '@pkgjs/nv'; import { fileURLToPath, URL } from 'url'; @@ -275,6 +275,7 @@ type CompilationOptions = { useLegacyDefaultUvLoop?: boolean; useCodeCache?: boolean, useNodeSnapshot?: boolean, + compressBlobs?: boolean, nodeSnapshotConfigFlags?: string[], // e.g. 'WithoutCodeCache' executableMetadata?: ExecutableMetadata, preCompileHook?: (nodeSourceTree: string, options: CompilationOptions) => void | Promise @@ -387,6 +388,10 @@ async function compileJSFileAsBinaryImpl (options: CompilationOptions, logger: L logger.stepCompleted(); } + const createBlobDefinition = options.compressBlobs + ? createCompressedBlobDefinition + : createUncompressedBlobDefinition; + async function writeMainFileAndCompile ({ codeCacheBlob = new Uint8Array(0), codeCacheMode = 'ignore', @@ -409,8 +414,8 @@ async function compileJSFileAsBinaryImpl (options: CompilationOptions, logger: L registerFunctions.map((fn) => `${fn},`).join('')); mainSource = mainSource.replace(/\bREPLACE_WITH_MAIN_SCRIPT_SOURCE_GETTER\b/g, createCppJsStringDefinition('GetBoxednodeMainScriptSource', snapshotMode !== 'consume' ? jsMainSource : '') + '\n' + - await createCompressedBlobDefinition('GetBoxednodeCodeCache', codeCacheBlob) + '\n' + - await createCompressedBlobDefinition('GetBoxednodeSnapshotBlob', snapshotBlob)); + await createBlobDefinition('GetBoxednodeCodeCache', codeCacheBlob) + '\n' + + await createBlobDefinition('GetBoxednodeSnapshotBlob', snapshotBlob)); mainSource = mainSource.replace(/\bBOXEDNODE_CODE_CACHE_MODE\b/g, JSON.stringify(codeCacheMode)); if (options.useLegacyDefaultUvLoop) { diff --git a/test/index.ts b/test/index.ts index 725af8c..723db64 100644 --- a/test/index.ts +++ b/test/index.ts @@ -214,30 +214,33 @@ describe('basic functionality', () => { } }); - it('works with snapshot support', async function () { - this.timeout(2 * 60 * 60 * 1000); // 2 hours - await compileJSFileAsBinary({ - nodeVersionRange: '^21.6.2', - sourceFile: path.resolve(__dirname, 'resources/snapshot-echo-args.js'), - targetFile: path.resolve(__dirname, `resources/snapshot-echo-args${exeSuffix}`), - useNodeSnapshot: true, - nodeSnapshotConfigFlags: ['WithoutCodeCache'], - // the nightly path name is too long for Windows... - tmpdir: process.platform === 'win32' ? path.join(os.tmpdir(), 'bn') : undefined - }); + for (const compressBlobs of [false, true]) { + it(`works with snapshot support (compressBlobs = ${compressBlobs})`, async function () { + this.timeout(2 * 60 * 60 * 1000); // 2 hours + await compileJSFileAsBinary({ + nodeVersionRange: '^21.6.2', + sourceFile: path.resolve(__dirname, 'resources/snapshot-echo-args.js'), + targetFile: path.resolve(__dirname, `resources/snapshot-echo-args${exeSuffix}`), + useNodeSnapshot: true, + compressBlobs, + nodeSnapshotConfigFlags: ['WithoutCodeCache'], + // the nightly path name is too long for Windows... + tmpdir: process.platform === 'win32' ? path.join(os.tmpdir(), 'bn') : undefined + }); - { - const { stdout } = await execFile( - path.resolve(__dirname, `resources/snapshot-echo-args${exeSuffix}`), ['a', 'b', 'c'], - { encoding: 'utf8' }); - const { currentArgv, originalArgv, timingData } = JSON.parse(stdout); - assert(currentArgv[0].includes('snapshot-echo-args')); - assert(currentArgv[1].includes('snapshot-echo-args')); - assert.deepStrictEqual(currentArgv.slice(2), ['a', 'b', 'c']); - assert.strictEqual(originalArgv.length, 2); // [execPath, execPath] - assert.strictEqual(timingData[0][0], 'Node.js Instance'); - assert.strictEqual(timingData[0][1], 'Process initialization'); - } - }); + { + const { stdout } = await execFile( + path.resolve(__dirname, `resources/snapshot-echo-args${exeSuffix}`), ['a', 'b', 'c'], + { encoding: 'utf8' }); + const { currentArgv, originalArgv, timingData } = JSON.parse(stdout); + assert(currentArgv[0].includes('snapshot-echo-args')); + assert(currentArgv[1].includes('snapshot-echo-args')); + assert.deepStrictEqual(currentArgv.slice(2), ['a', 'b', 'c']); + assert.strictEqual(originalArgv.length, 2); // [execPath, execPath] + assert.strictEqual(timingData[0][0], 'Node.js Instance'); + assert.strictEqual(timingData[0][1], 'Process initialization'); + } + }); + } }); });