Skip to content

Commit ac13672

Browse files
authored
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.
1 parent 6d28f9f commit ac13672

File tree

4 files changed

+61
-38
lines changed

4 files changed

+61
-38
lines changed

bin/boxednode.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ const argv = require('yargs')
5454
namespace: argv.N,
5555
useLegacyDefaultUvLoop: argv.useLegacyDefaultUvLoop,
5656
useCodeCache: argv.H,
57-
useNodeSnapshot: argv.S
57+
useNodeSnapshot: argv.S,
58+
compressBlobs: argv.Z
5859
});
5960
} catch (err) {
6061
console.error(err);

src/helpers.ts

+24-10
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,21 @@ export function createCppJsStringDefinition (fnName: string, source: string): st
107107
`;
108108
}
109109

110+
export async function createUncompressedBlobDefinition (fnName: string, source: Uint8Array): Promise<string> {
111+
return `
112+
static const uint8_t ${fnName}_source_[] = {
113+
${Uint8Array.prototype.toString.call(source) || '0'}
114+
};
115+
116+
std::vector<char> ${fnName}Vector() {
117+
return std::vector<char>(
118+
reinterpret_cast<const char*>(&${fnName}_source_[0]),
119+
reinterpret_cast<const char*>(&${fnName}_source_[${source.length}]));
120+
}
121+
122+
${blobTypedArrayAccessors(fnName, source.length)}`;
123+
}
124+
110125
export async function createCompressedBlobDefinition (fnName: string, source: Uint8Array): Promise<string> {
111126
const compressed = await promisify(zlib.brotliCompress)(source, {
112127
params: {
@@ -133,33 +148,32 @@ export async function createCompressedBlobDefinition (fnName: string, source: Ui
133148
assert(decoded_size == ${source.length});
134149
}
135150
136-
std::string ${fnName}() {
137-
${source.length === 0 ? 'return {};' : `
138-
std::string dst(${source.length}, 0);
139-
${fnName}_Read(&dst[0]);
140-
return dst;`}
141-
}
142-
143151
std::vector<char> ${fnName}Vector() {
144152
${source.length === 0 ? 'return {};' : `
145153
std::vector<char> dst(${source.length});
146154
${fnName}_Read(&dst[0]);
147155
return dst;`}
148156
}
149157
158+
${blobTypedArrayAccessors(fnName, source.length)}
159+
`;
160+
}
161+
162+
function blobTypedArrayAccessors (fnName: string, sourceLength: number): string {
163+
return `
150164
std::shared_ptr<v8::BackingStore> ${fnName}BackingStore() {
151-
std::string* str = new std::string(std::move(${fnName}()));
165+
std::vector<char>* str = new std::vector<char>(std::move(${fnName}Vector()));
152166
return v8::SharedArrayBuffer::NewBackingStore(
153167
&str->front(),
154168
str->size(),
155169
[](void*, size_t, void* deleter_data) {
156-
delete static_cast<std::string*>(deleter_data);
170+
delete static_cast<std::vector<char>*>(deleter_data);
157171
},
158172
static_cast<void*>(str));
159173
}
160174
161175
v8::Local<v8::Uint8Array> ${fnName}Buffer(v8::Isolate* isolate) {
162-
${source.length === 0 ? `
176+
${sourceLength === 0 ? `
163177
auto array_buffer = v8::SharedArrayBuffer::New(isolate, 0);
164178
` : `
165179
auto array_buffer = v8::SharedArrayBuffer::New(isolate, ${fnName}BackingStore());

src/index.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { promisify } from 'util';
1111
import { promises as fs, createReadStream, createWriteStream } from 'fs';
1212
import { AddonConfig, loadGYPConfig, storeGYPConfig, modifyAddonGyp } from './native-addons';
1313
import { ExecutableMetadata, generateRCFile } from './executable-metadata';
14-
import { spawnBuildCommand, ProcessEnv, pipeline, createCppJsStringDefinition, createCompressedBlobDefinition } from './helpers';
14+
import { spawnBuildCommand, ProcessEnv, pipeline, createCppJsStringDefinition, createCompressedBlobDefinition, createUncompressedBlobDefinition } from './helpers';
1515
import { Readable } from 'stream';
1616
import nv from '@pkgjs/nv';
1717
import { fileURLToPath, URL } from 'url';
@@ -275,6 +275,7 @@ type CompilationOptions = {
275275
useLegacyDefaultUvLoop?: boolean;
276276
useCodeCache?: boolean,
277277
useNodeSnapshot?: boolean,
278+
compressBlobs?: boolean,
278279
nodeSnapshotConfigFlags?: string[], // e.g. 'WithoutCodeCache'
279280
executableMetadata?: ExecutableMetadata,
280281
preCompileHook?: (nodeSourceTree: string, options: CompilationOptions) => void | Promise<void>
@@ -387,6 +388,10 @@ async function compileJSFileAsBinaryImpl (options: CompilationOptions, logger: L
387388
logger.stepCompleted();
388389
}
389390

391+
const createBlobDefinition = options.compressBlobs
392+
? createCompressedBlobDefinition
393+
: createUncompressedBlobDefinition;
394+
390395
async function writeMainFileAndCompile ({
391396
codeCacheBlob = new Uint8Array(0),
392397
codeCacheMode = 'ignore',
@@ -409,8 +414,8 @@ async function compileJSFileAsBinaryImpl (options: CompilationOptions, logger: L
409414
registerFunctions.map((fn) => `${fn},`).join(''));
410415
mainSource = mainSource.replace(/\bREPLACE_WITH_MAIN_SCRIPT_SOURCE_GETTER\b/g,
411416
createCppJsStringDefinition('GetBoxednodeMainScriptSource', snapshotMode !== 'consume' ? jsMainSource : '') + '\n' +
412-
await createCompressedBlobDefinition('GetBoxednodeCodeCache', codeCacheBlob) + '\n' +
413-
await createCompressedBlobDefinition('GetBoxednodeSnapshotBlob', snapshotBlob));
417+
await createBlobDefinition('GetBoxednodeCodeCache', codeCacheBlob) + '\n' +
418+
await createBlobDefinition('GetBoxednodeSnapshotBlob', snapshotBlob));
414419
mainSource = mainSource.replace(/\bBOXEDNODE_CODE_CACHE_MODE\b/g,
415420
JSON.stringify(codeCacheMode));
416421
if (options.useLegacyDefaultUvLoop) {

test/index.ts

+27-24
Original file line numberDiff line numberDiff line change
@@ -214,30 +214,33 @@ describe('basic functionality', () => {
214214
}
215215
});
216216

217-
it('works with snapshot support', async function () {
218-
this.timeout(2 * 60 * 60 * 1000); // 2 hours
219-
await compileJSFileAsBinary({
220-
nodeVersionRange: '^21.6.2',
221-
sourceFile: path.resolve(__dirname, 'resources/snapshot-echo-args.js'),
222-
targetFile: path.resolve(__dirname, `resources/snapshot-echo-args${exeSuffix}`),
223-
useNodeSnapshot: true,
224-
nodeSnapshotConfigFlags: ['WithoutCodeCache'],
225-
// the nightly path name is too long for Windows...
226-
tmpdir: process.platform === 'win32' ? path.join(os.tmpdir(), 'bn') : undefined
227-
});
217+
for (const compressBlobs of [false, true]) {
218+
it(`works with snapshot support (compressBlobs = ${compressBlobs})`, async function () {
219+
this.timeout(2 * 60 * 60 * 1000); // 2 hours
220+
await compileJSFileAsBinary({
221+
nodeVersionRange: '^21.6.2',
222+
sourceFile: path.resolve(__dirname, 'resources/snapshot-echo-args.js'),
223+
targetFile: path.resolve(__dirname, `resources/snapshot-echo-args${exeSuffix}`),
224+
useNodeSnapshot: true,
225+
compressBlobs,
226+
nodeSnapshotConfigFlags: ['WithoutCodeCache'],
227+
// the nightly path name is too long for Windows...
228+
tmpdir: process.platform === 'win32' ? path.join(os.tmpdir(), 'bn') : undefined
229+
});
228230

229-
{
230-
const { stdout } = await execFile(
231-
path.resolve(__dirname, `resources/snapshot-echo-args${exeSuffix}`), ['a', 'b', 'c'],
232-
{ encoding: 'utf8' });
233-
const { currentArgv, originalArgv, timingData } = JSON.parse(stdout);
234-
assert(currentArgv[0].includes('snapshot-echo-args'));
235-
assert(currentArgv[1].includes('snapshot-echo-args'));
236-
assert.deepStrictEqual(currentArgv.slice(2), ['a', 'b', 'c']);
237-
assert.strictEqual(originalArgv.length, 2); // [execPath, execPath]
238-
assert.strictEqual(timingData[0][0], 'Node.js Instance');
239-
assert.strictEqual(timingData[0][1], 'Process initialization');
240-
}
241-
});
231+
{
232+
const { stdout } = await execFile(
233+
path.resolve(__dirname, `resources/snapshot-echo-args${exeSuffix}`), ['a', 'b', 'c'],
234+
{ encoding: 'utf8' });
235+
const { currentArgv, originalArgv, timingData } = JSON.parse(stdout);
236+
assert(currentArgv[0].includes('snapshot-echo-args'));
237+
assert(currentArgv[1].includes('snapshot-echo-args'));
238+
assert.deepStrictEqual(currentArgv.slice(2), ['a', 'b', 'c']);
239+
assert.strictEqual(originalArgv.length, 2); // [execPath, execPath]
240+
assert.strictEqual(timingData[0][0], 'Node.js Instance');
241+
assert.strictEqual(timingData[0][1], 'Process initialization');
242+
}
243+
});
244+
}
242245
});
243246
});

0 commit comments

Comments
 (0)