Skip to content

Commit

Permalink
feat: enable optionally enabling/disabling blob compression (#58)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
addaleax authored Apr 15, 2024
1 parent 6d28f9f commit ac13672
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 38 deletions.
3 changes: 2 additions & 1 deletion bin/boxednode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
34 changes: 24 additions & 10 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,21 @@ export function createCppJsStringDefinition (fnName: string, source: string): st
`;
}

export async function createUncompressedBlobDefinition (fnName: string, source: Uint8Array): Promise<string> {
return `
static const uint8_t ${fnName}_source_[] = {
${Uint8Array.prototype.toString.call(source) || '0'}
};
std::vector<char> ${fnName}Vector() {
return std::vector<char>(
reinterpret_cast<const char*>(&${fnName}_source_[0]),
reinterpret_cast<const char*>(&${fnName}_source_[${source.length}]));
}
${blobTypedArrayAccessors(fnName, source.length)}`;
}

export async function createCompressedBlobDefinition (fnName: string, source: Uint8Array): Promise<string> {
const compressed = await promisify(zlib.brotliCompress)(source, {
params: {
Expand All @@ -133,33 +148,32 @@ 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<char> ${fnName}Vector() {
${source.length === 0 ? 'return {};' : `
std::vector<char> dst(${source.length});
${fnName}_Read(&dst[0]);
return dst;`}
}
${blobTypedArrayAccessors(fnName, source.length)}
`;
}

function blobTypedArrayAccessors (fnName: string, sourceLength: number): string {
return `
std::shared_ptr<v8::BackingStore> ${fnName}BackingStore() {
std::string* str = new std::string(std::move(${fnName}()));
std::vector<char>* str = new std::vector<char>(std::move(${fnName}Vector()));
return v8::SharedArrayBuffer::NewBackingStore(
&str->front(),
str->size(),
[](void*, size_t, void* deleter_data) {
delete static_cast<std::string*>(deleter_data);
delete static_cast<std::vector<char>*>(deleter_data);
},
static_cast<void*>(str));
}
v8::Local<v8::Uint8Array> ${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());
Expand Down
11 changes: 8 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<void>
Expand Down Expand Up @@ -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',
Expand All @@ -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) {
Expand Down
51 changes: 27 additions & 24 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
});
}
});
});

0 comments on commit ac13672

Please sign in to comment.