Skip to content

Commit

Permalink
bootstrap: make snapshot reproducible
Browse files Browse the repository at this point in the history
This patch uses the new V8 API to {de}serialize context slots for
snapshot in order to make the snapshot reproducible. Also
added a test for the reproducibility of snapshots.
  • Loading branch information
joyeecheung committed May 20, 2024
1 parent 1efb342 commit f03a7ba
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 3 deletions.
8 changes: 7 additions & 1 deletion src/api/environment.cc
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,13 @@ Environment* CreateEnvironment(
if (use_snapshot) {
context = Context::FromSnapshot(isolate,
SnapshotData::kNodeMainContextIndex,
{DeserializeNodeInternalFields, env})
v8::DeserializeInternalFieldsCallback(
DeserializeNodeInternalFields, env),
nullptr,
MaybeLocal<Value>(),
nullptr,
v8::DeserializeContextDataCallback(
DeserializeNodeContextData, env))
.ToLocalChecked();

CHECK(!context.IsEmpty());
Expand Down
56 changes: 54 additions & 2 deletions src/node_snapshotable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1155,8 +1155,11 @@ ExitCode SnapshotBuilder::CreateSnapshot(SnapshotData* out,
CHECK_EQ(index, SnapshotData::kNodeVMContextIndex);
index = creator->AddContext(base_context);
CHECK_EQ(index, SnapshotData::kNodeBaseContextIndex);
index = creator->AddContext(main_context,
{SerializeNodeContextInternalFields, env});
index = creator->AddContext(
main_context,
v8::SerializeInternalFieldsCallback(SerializeNodeContextInternalFields,
env),
v8::SerializeContextDataCallback(SerializeNodeContextData, env));
CHECK_EQ(index, SnapshotData::kNodeMainContextIndex);
}

Expand Down Expand Up @@ -1255,6 +1258,17 @@ std::string SnapshotableObject::GetTypeName() const {
}
}

void DeserializeNodeContextData(Local<Context> holder,
int index,
StartupData payload,
void* callback_data) {
DCHECK(index == ContextEmbedderIndex::kEnvironment ||
index == ContextEmbedderIndex::kRealm ||
index == ContextEmbedderIndex::kContextTag);
// This is a no-op for now. We will reset all the pointers in
// Environment::AssignToContext() via the realm constructor.
}

void DeserializeNodeInternalFields(Local<Object> holder,
int index,
StartupData payload,
Expand Down Expand Up @@ -1320,6 +1334,44 @@ void DeserializeNodeInternalFields(Local<Object> holder,
}
}

StartupData SerializeNodeContextData(Local<Context> holder,
int index,
void* callback_data) {
DCHECK(index == ContextEmbedderIndex::kEnvironment ||
index == ContextEmbedderIndex::kContextifyContext ||
index == ContextEmbedderIndex::kRealm ||
index == ContextEmbedderIndex::kContextTag);
void* data = holder->GetAlignedPointerFromEmbedderData(index);
per_process::Debug(DebugCategory::MKSNAPSHOT,
"Serialize context data, index=%d, holder=%p, ptr=%p\n",
static_cast<int>(index),
*holder,
data);
// Serialization of contextify context is not yet supported.
if (index == ContextEmbedderIndex::kContextifyContext) {
DCHECK_NULL(data);
return {nullptr, 0};
}

// We need to use use new[] because V8 calls delete[] on the returned data.
int size = sizeof(ContextEmbedderIndex);
char* result = new char[size];
ContextEmbedderIndex* index_data =
reinterpret_cast<ContextEmbedderIndex*>(result);
*index_data = static_cast<ContextEmbedderIndex>(index);

// For now we just reset all of them in Environment::AssignToContext()
switch (index) {
case ContextEmbedderIndex::kEnvironment:
case ContextEmbedderIndex::kContextifyContext:
case ContextEmbedderIndex::kRealm:
case ContextEmbedderIndex::kContextTag:
return StartupData{result, size};
default:
UNREACHABLE();
}
}

StartupData SerializeNodeContextInternalFields(Local<Object> holder,
int index,
void* callback_data) {
Expand Down
7 changes: 7 additions & 0 deletions src/node_snapshotable.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,17 @@ class SnapshotableObject : public BaseObject {
v8::StartupData SerializeNodeContextInternalFields(v8::Local<v8::Object> holder,
int index,
void* env);
v8::StartupData SerializeNodeContextData(v8::Local<v8::Context> holder,
int index,
void* env);
void DeserializeNodeInternalFields(v8::Local<v8::Object> holder,
int index,
v8::StartupData payload,
void* env);
void DeserializeNodeContextData(v8::Local<v8::Context> holder,
int index,
v8::StartupData payload,
void* env);
void SerializeSnapshotableObjects(Realm* realm,
v8::SnapshotCreator* creator,
RealmSerializeInfo* info);
Expand Down
53 changes: 53 additions & 0 deletions test/parallel/test-snapshot-reproducible.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict';

require('../common');
const { spawnSyncAndExitWithoutError } = require('../common/child_process');
const tmpdir = require('../common/tmpdir');
const fs = require('fs');
const assert = require('assert');
const fixtures = require('../common/fixtures');

Check failure on line 8 in test/parallel/test-snapshot-reproducible.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

'fixtures' is assigned a value but never used

function generateSnapshot() {
tmpdir.refresh();

spawnSyncAndExitWithoutError(
process.execPath,
[
'--random_seed=42',
'--predictable',
'--build-snapshot',
'node:generate_default_snapshot',
],
{
cwd: tmpdir.path
}
);
const blobPath = tmpdir.resolve('snapshot.blob');
return fs.readFileSync(blobPath);
}

const buf1 = generateSnapshot();
const buf2 = generateSnapshot();
const diff = [];
let offset = 0;
const step = 16;
do {
const length = Math.min(buf1.length - offset, step);
const slice1 = buf1.slice(offset, offset + length).toString('hex');
const slice2 = buf2.slice(offset, offset + length).toString('hex');
if (slice1 != slice2) {

Check failure on line 38 in test/parallel/test-snapshot-reproducible.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected '!==' and instead saw '!='
diff.push({offset, slice1, slice2});

Check failure on line 39 in test/parallel/test-snapshot-reproducible.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

A space is required after '{'

Check failure on line 39 in test/parallel/test-snapshot-reproducible.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

A space is required before '}'
}
offset += length;
} while (offset < buf1.length);

assert.strictEqual(offset, buf1.length);
if (offset < buf2.length) {
const length = Math.min(buf2.length - offset, step);
const slice2 = buf2.slice(offset, offset + length).toString('hex');
diff.push({offset, slice1: '', slice2});

Check failure on line 48 in test/parallel/test-snapshot-reproducible.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

A space is required after '{'

Check failure on line 48 in test/parallel/test-snapshot-reproducible.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

A space is required before '}'
offset += length;
} while (offset < buf2.length);

assert.deepStrictEqual(diff, [], 'Built-in snapshot should not change in different builds.');

Check failure on line 52 in test/parallel/test-snapshot-reproducible.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Do not use a literal for the third argument of assert.deepStrictEqual()

Check failure on line 52 in test/parallel/test-snapshot-reproducible.js

View workflow job for this annotation

GitHub Actions / test-linux

--- stderr --- node:assert:126 throw new AssertionError(obj); ^ AssertionError [ERR_ASSERTION]: Built-in snapshot should not change in different builds. at Object.<anonymous> (/home/runner/work/node/node/test/parallel/test-snapshot-reproducible.js:52:8) at Module._compile (node:internal/modules/cjs/loader:1434:14) at Module._extensions..js (node:internal/modules/cjs/loader:1518:10) at Module.load (node:internal/modules/cjs/loader:1249:32) at Module._load (node:internal/modules/cjs/loader:1065:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:158:12) at node:internal/main/run_main_module:30:49 { generatedMessage: false, code: 'ERR_ASSERTION', actual: [ { offset: 64, slice1: '000000701bb020821f137731322e342e', slice2: '000000a21d43d8821f137731322e342e' }, { offset: 1500160, slice1: '6005ac61e0c87f000018000000000000', slice2: '6005ac416cdb7f000018000000000000' }, { offset: 1500192, slice1: '00dd0b04400631545605000000100000', slice2: '00dd0b044006984f5c05000000100000' }, { offset: 1500224, slice1: 'c0013154560500000030000000000000', slice2: 'c001984f5c0500000030000000000000' }, { offset: 1500272, slice1: '0000392a0008080000392a0440083154', slice2: '0000392a0008080000392a044008984f' }, { offset: 1500288, slice1: '56050000001000000000000000001d4b', slice2: '5c050000001000000000000000001d4b' }, { offset: 1500336, slice1: '01650460003154560500000018000000', slice2: '0165046000984f5c0500000018000000' }, { offset: 1500368, slice1: '080200001165046002ac61e0c87f0000', slice2: '080200001165046002ac416cdb7f0000' }, { offset: 1500400, slice1: '00196500080400001965044004797515', slice2: '0019650008040000196504400403c4a5' }, { offset: 1500416, slice1: '60550000100000000000000000216500', slice2: 'c1550000100000000000000000216500' }, { offset: 1500432, slice1: '08070000216504400779751560550000', slice2: '08070000216504400703c4a5c1550000' } ], expected: [], operator: 'deepStrictEqual' } Node.js v23.0.0-pre Command: out/Release/node --test-reporter=spec --test-reporter-destination=stdout --test-reporter=./tools/github_reporter/index.js --test-reporter-destination=stdout /home/runner/work/node/node/test/parallel/test-snapshot-reproducible.js --- TIMEOUT ---
assert.strictEqual(buf1.length, buf2.length);

0 comments on commit f03a7ba

Please sign in to comment.