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 Mar 22, 2024
1 parent 759ee85 commit 2d911be
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 3 deletions.
8 changes: 7 additions & 1 deletion src/api/environment.cc
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
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
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
30 changes: 30 additions & 0 deletions test/parallel/test-snapshot-reproducible.js
@@ -0,0 +1,30 @@
'use strict';

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

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();
assert.deepStrictEqual(buf1, buf2);

Check failure on line 30 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]: Expected values to be strictly deep-equal: + actual - expected ... Lines skipped Buffer(3859397) [Uint8Array] [ 25, ... 0, 0, + 107, - 55, 185, + 41, + 250, - 33, - 104, 225, 122, ... 5, 172, + 97, + 43, + 29, - 1, - 50, - 28, 127, 0, ... 64, 6, + 79, + 85, + 100, - 156, - 40, - 101, 5, 0, ... 1, 172, + 97, + 43, + 29, - 1, - 50, - 28, 127, 0, ... 64, 8, + 79, + 85, + 100, ... - 156, - 40, - 101, ... at Object.<anonymous> (/home/runner/work/node/node/test/parallel/test-snapshot-reproducible.js:30:8) at Module._compile (node:internal/modules/cjs/loader:1421:14) at Module._extensions..js (node:internal/modules/cjs/loader:1499:10) at Module.load (node:internal/modules/cjs/loader:1232:32) at Module._load (node:internal/modules/cjs/loader:1048:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:187:14) at node:internal/main/run_main_module:28:49 { generatedMessage: true, code: 'ERR_ASSERTION', actual: Buffer(3859397) [Uint8Array] [ 25, 218, 67, 1, 0, 10, 0, 0, 0, 0, 0, 0, 0, 50, 50, 46, 48, 46, 48, 45, 112, 114, 101, 3, 0, 0, 0, 0, 0, 0, 0, 120, 54, 52, 5, 0, 0, 0, 0, 0, 0, 0, 108, 105, 110, 117, 120, 207, 241, 99, 132, 0, 0, 0, 0, 12, 152, 22, 0, 4, 0, 0, 0, 1, 0, 0, 0, 107, 185, 41, 250, 225, 122, 16, 185, 49, 49, 46, 57, 46, 49, 54, 57, 46, 55, 45, 110, 111, 100, 101, 46, 49, 53, 0, 0, 0, 0, 0, 0, 0, ... 3859297 more items ], expected: Buffer(3859397) [Uint8Array] [ 25, 218, 67, 1, 0, 10, 0, 0, 0, 0, 0, 0, 0, 50, 50, 46, 48, 46, 48, 45, 112, 114, 101, 3, 0, 0, 0, 0, 0, 0, 0, 120, 54, 52, 5, 0, 0, 0, 0, 0, 0, 0, 108, 105, 110, 117, 120, 207, 241, 99, 132, 0, 0, 0, 0, 12, 152, 22, 0, 4, 0, 0, 0, 1, 0, 0, 0, 55, 185, 33, 104, 225, 122, 16, 185, 49, 49, 46, 57, 46, 49, 54, 57, 46, 55, 45, 110, 111, 100, 101, 46, 49, 53, 0, 0, 0, 0, 0, 0, 0, ... 3859297 more items ], operator: 'deepStrictEqual' } Node.js v22.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

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

View workflow job for this annotation

GitHub Actions / test-ubsan

--- stderr --- node:assert:126 throw new AssertionError(obj); ^ AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + actual - expected ... Lines skipped Buffer(3859397) [Uint8Array] [ 25, ... 0, 0, + 59, - 149, 181, + 203, + 86, - 244, - 41, 225, 122, ... 96, 5, + 27, + 52, + 94, + 238, - 59, - 11, - 71, - 248, 127, 0, ... 192, 1, + 27, + 52, + 94, + 238, - 59, - 11, - 71, - 248, 127, 0, ... 96, 2, + 28, + 52, + 94, + 238, - 60, - 11, - 71, - 248, 127, 0, ... 64, 7, + 206, ... - 207, ... at Object.<anonymous> (/home/runner/work/node/node/test/parallel/test-snapshot-reproducible.js:30:8) at Module._compile (node:internal/modules/cjs/loader:1421:14) at Module._extensions..js (node:internal/modules/cjs/loader:1499:10) at Module.load (node:internal/modules/cjs/loader:1232:32) at Module._load (node:internal/modules/cjs/loader:1048:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:187:14) at node:internal/main/run_main_module:28:49 { generatedMessage: true, code: 'ERR_ASSERTION', actual: Buffer(3859397) [Uint8Array] [ 25, 218, 67, 1, 0, 10, 0, 0, 0, 0, 0, 0, 0, 50, 50, 46, 48, 46, 48, 45, 112, 114, 101, 3, 0, 0, 0, 0, 0, 0, 0, 120, 54, 52, 5, 0, 0, 0, 0, 0, 0, 0, 108, 105, 110, 117, 120, 207, 241, 99, 132, 0, 0, 0, 0, 12, 152, 22, 0, 4, 0, 0, 0, 1, 0, 0, 0, 59, 181, 203, 86, 225, 122, 16, 185, 49, 49, 46, 57, 46, 49, 54, 57, 46, 55, 45, 110, 111, 100, 101, 46, 49, 53, 0, 0, 0, 0, 0, 0, 0, ... 3859297 more items ], expected: Buffer(3859397) [Uint8Array] [ 25, 218, 67, 1, 0, 10, 0, 0, 0, 0, 0, 0, 0, 50, 50, 46, 48, 46, 48, 45, 112, 114, 101, 3, 0, 0, 0, 0, 0, 0, 0, 120, 54, 52, 5, 0, 0, 0, 0, 0, 0, 0, 108, 105, 110, 117, 120, 207, 241, 99, 132, 0, 0, 0, 0, 12, 152, 22, 0, 4, 0, 0, 0, 1, 0, 0, 0, 149, 181, 244, 41, 225, 122, 16, 185, 49, 49, 46, 57, 46, 49, 54, 57, 46, 55, 45, 110, 111, 100, 101, 46, 49, 53, 0, 0, 0, 0, 0, 0, 0, ... 3859297 more items ], operator: 'deepStrictEqual' } Node.js v22.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

0 comments on commit 2d911be

Please sign in to comment.