From 100d58420fecd235f6bbe803773b9b5605b6d41d Mon Sep 17 00:00:00 2001 From: Daijiro Wachi Date: Fri, 24 Apr 2026 02:15:37 +0900 Subject: [PATCH] lib: avoid quadratic shift() in startup snapshot callback --- benchmark/v8/startup-snapshot-callbacks.js | 58 ++++++++++++++++++++++ lib/internal/v8/startup_snapshot.js | 15 +++--- 2 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 benchmark/v8/startup-snapshot-callbacks.js diff --git a/benchmark/v8/startup-snapshot-callbacks.js b/benchmark/v8/startup-snapshot-callbacks.js new file mode 100644 index 00000000000000..7ff245e29758df --- /dev/null +++ b/benchmark/v8/startup-snapshot-callbacks.js @@ -0,0 +1,58 @@ +'use strict'; + +// Benchmarks startup time when deserialize callbacks are registered in a +// startup snapshot, measuring real binary execution with --snapshot-blob. + +const common = require('../common.js'); +const { spawnSync } = require('child_process'); +const { mkdtempSync, writeFileSync, rmSync } = require('fs'); +const { tmpdir } = require('os'); +const path = require('path'); + +const bench = common.createBenchmark(main, { + count: [10, 100, 1000, 10000], + n: [30], +}); + +function buildSnapshot(snapshotBlob, fixtureScript, count) { + writeFileSync(fixtureScript, ` +'use strict'; +const v8 = require('node:v8'); +for (let i = 0; i < ${count}; i++) { + v8.startupSnapshot.addDeserializeCallback(() => {}); +} +v8.startupSnapshot.setDeserializeMainFunction(() => {}); +`); + const result = spawnSync(process.execPath, [ + '--snapshot-blob', snapshotBlob, + '--build-snapshot', fixtureScript, + ]); + if (result.status !== 0) { + throw new Error(`Failed to build snapshot:\n${result.stderr}`); + } +} + +function main({ n, count }) { + const dir = mkdtempSync(path.join(tmpdir(), 'node-bench-snapshot-')); + const snapshotBlob = path.join(dir, 'snapshot.blob'); + const fixtureScript = path.join(dir, 'build.js'); + + try { + buildSnapshot(snapshotBlob, fixtureScript, count); + + const warmup = 3; + let finished = -warmup; + + while (finished < n) { + const child = spawnSync(process.execPath, ['--snapshot-blob', snapshotBlob]); + if (child.status !== 0) { + throw new Error(`Snapshot run failed:\n${child.stderr}`); + } + finished++; + if (finished === 0) bench.start(); + if (finished === n) bench.end(n); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} diff --git a/lib/internal/v8/startup_snapshot.js b/lib/internal/v8/startup_snapshot.js index af5fb07925dc15..a7f9ebcf779bd6 100644 --- a/lib/internal/v8/startup_snapshot.js +++ b/lib/internal/v8/startup_snapshot.js @@ -37,10 +37,11 @@ function throwIfBuildingSnapshot(reason) { const deserializeCallbacks = []; let deserializeCallbackIsSet = false; function runDeserializeCallbacks() { - while (deserializeCallbacks.length > 0) { - const { 0: callback, 1: data } = deserializeCallbacks.shift(); + for (let i = 0; i < deserializeCallbacks.length; i++) { + const { 0: callback, 1: data } = deserializeCallbacks[i]; callback(data); } + deserializeCallbacks.length = 0; } function addDeserializeCallback(callback, data) { @@ -60,14 +61,16 @@ function addDeserializeCallback(callback, data) { const serializeCallbacks = []; const afterUserSerializeCallbacks = []; // Callbacks to run after user callbacks function runSerializeCallbacks() { - while (serializeCallbacks.length > 0) { - const { 0: callback, 1: data } = serializeCallbacks.shift(); + for (let i = 0; i < serializeCallbacks.length; i++) { + const { 0: callback, 1: data } = serializeCallbacks[i]; callback(data); } - while (afterUserSerializeCallbacks.length > 0) { - const { 0: callback, 1: data } = afterUserSerializeCallbacks.shift(); + serializeCallbacks.length = 0; + for (let i = 0; i < afterUserSerializeCallbacks.length; i++) { + const { 0: callback, 1: data } = afterUserSerializeCallbacks[i]; callback(data); } + afterUserSerializeCallbacks.length = 0; } function addSerializeCallback(callback, data) {