From 811b43c215beedf705508c8c3e32355aaf957939 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Wed, 3 May 2023 15:14:56 +0200 Subject: [PATCH] doc,test: update the v8.startupSnapshot doc and test the example The API is now available to user-land run-time snapshots. So update the example. This also makes the intention of the examples a bit clearer and test it in our test suite. PR-URL: https://github.com/nodejs/node/pull/47468 Reviewed-By: Chengzhong Wu --- doc/api/v8.md | 85 +++++++++++------- .../snapshot/v8-startup-snapshot-api.js | 86 ++++++++++++------- test/parallel/test-snapshot-api.js | 20 ++++- 3 files changed, 124 insertions(+), 67 deletions(-) diff --git a/doc/api/v8.md b/doc/api/v8.md index 9ce500bf19c71d..399cb1e82adf12 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -921,15 +921,12 @@ added: > Stability: 1 - Experimental The `v8.startupSnapshot` interface can be used to add serialization and -deserialization hooks for custom startup snapshots. Currently the startup -snapshots can only be built into the Node.js binary from source. +deserialization hooks for custom startup snapshots. ```console -$ cd /path/to/node -$ ./configure --node-snapshot-main=entry.js -$ make node -# This binary contains the result of the execution of entry.js -$ out/Release/node +$ node --snapshot-blob snapshot.blob --build-snapshot entry.js +# This launches a process with the snapshot +$ node --snapshot-blob snapshot.blob ``` In the example above, `entry.js` can use methods from the `v8.startupSnapshot` @@ -946,42 +943,66 @@ const zlib = require('node:zlib'); const path = require('node:path'); const assert = require('node:assert'); -const { - isBuildingSnapshot, - addSerializeCallback, - addDeserializeCallback, - setDeserializeMainFunction, -} = require('node:v8').startupSnapshot; +const v8 = require('node:v8'); -const filePath = path.resolve(__dirname, '../x1024.txt'); -const storage = {}; +class BookShelf { + storage = new Map(); -assert(isBuildingSnapshot()); + // Reading a series of files from directory and store them into storage. + constructor(directory, books) { + for (const book of books) { + this.storage.set(book, fs.readFileSync(path.join(directory, book))); + } + } -addSerializeCallback(({ filePath }) => { - storage[filePath] = zlib.gzipSync(fs.readFileSync(filePath)); -}, { filePath }); + static compressAll(shelf) { + for (const [ book, content ] of shelf.storage) { + shelf.storage.set(book, zlib.gzipSync(content)); + } + } -addDeserializeCallback(({ filePath }) => { - storage[filePath] = zlib.gunzipSync(storage[filePath]); -}, { filePath }); + static decompressAll(shelf) { + for (const [ book, content ] of shelf.storage) { + shelf.storage.set(book, zlib.gunzipSync(content)); + } + } +} -setDeserializeMainFunction(({ filePath }) => { - console.log(storage[filePath].toString()); -}, { filePath }); +// __dirname here is where the snapshot script is placed +// during snapshot building time. +const shelf = new BookShelf(__dirname, [ + 'book1.en_US.txt', + 'book1.es_ES.txt', + 'book2.zh_CN.txt', +]); + +assert(v8.startupSnapshot.isBuildingSnapshot()); +// On snapshot serialization, compress the books to reduce size. +v8.startupSnapshot.addSerializeCallback(BookShelf.compressAll, shelf); +// On snapshot deserialization, decompress the books. +v8.startupSnapshot.addDeserializeCallback(BookShelf.decompressAll, shelf); +v8.startupSnapshot.setDeserializeMainFunction((shelf) => { + // process.env and process.argv are refreshed during snapshot + // deserialization. + const lang = process.env.BOOK_LANG || 'en_US'; + const book = process.argv[1]; + const name = `${book}.${lang}.txt`; + console.log(shelf.storage.get(name)); +}, shelf); ``` -The resulted binary will simply print the data deserialized from the snapshot -during start up: +The resulted binary will get print the data deserialized from the snapshot +during start up, using the refreshed `process.env` and `process.argv` of +the launched process: ```console -$ out/Release/node -# Prints content of ./test/fixtures/x1024.txt +$ BOOK_LANG=es_ES node --snapshot-blob snapshot.blob book1 +# Prints content of book1.es_ES.txt deserialized from the snapshot. ``` -Currently the API is only available to a Node.js instance launched from the -default snapshot, that is, the application deserialized from a user-land -snapshot cannot use these APIs again. +Currently the application deserialized from a user-land snapshot cannot +be snapshotted again, so these APIs are only available to applications +that are not deserialized from a user-land snapshot. ### `v8.startupSnapshot.addSerializeCallback(callback[, data])` diff --git a/test/fixtures/snapshot/v8-startup-snapshot-api.js b/test/fixtures/snapshot/v8-startup-snapshot-api.js index 9eccc655990572..0d32cd14c500e0 100644 --- a/test/fixtures/snapshot/v8-startup-snapshot-api.js +++ b/test/fixtures/snapshot/v8-startup-snapshot-api.js @@ -1,36 +1,60 @@ 'use strict'; -const fs = require('fs'); -const zlib = require('zlib'); -const path = require('path'); -const assert = require('assert'); - -const { - isBuildingSnapshot, - addSerializeCallback, - addDeserializeCallback, - setDeserializeMainFunction -} = require('v8').startupSnapshot; - -const filePath = path.resolve(__dirname, '../x1024.txt'); -const storage = {}; - -assert(isBuildingSnapshot()); - -addSerializeCallback(({ filePath }) => { - console.error('serializing', filePath); - storage[filePath] = zlib.gzipSync(fs.readFileSync(filePath)); -}, { filePath }); - -addDeserializeCallback(({ filePath }) => { - console.error('deserializing', filePath); - storage[filePath] = zlib.gunzipSync(storage[filePath]); -}, { filePath }); - -setDeserializeMainFunction(({ filePath }) => { - console.log(storage[filePath].toString()); -}, { filePath }); -assert.throws(() => setDeserializeMainFunction(() => { +const fs = require('node:fs'); +const zlib = require('node:zlib'); +const path = require('node:path'); +const assert = require('node:assert'); + +const v8 = require('node:v8'); + +class BookShelf { + storage = new Map(); + + // Reading a series of files from directory and store them into storage. + constructor(directory, books) { + for (const book of books) { + this.storage.set(book, fs.readFileSync(path.join(directory, book))); + }; + } + + static compressAll(shelf) { + for (const [ book, content ] of shelf.storage) { + shelf.storage.set(book, zlib.gzipSync(content)); + } + } + + static decompressAll(shelf) { + for (const [ book, content ] of shelf.storage) { + shelf.storage.set(book, zlib.gunzipSync(content)); + } + } +} + +// __dirname here is where the snapshot script is placed +// during snapshot building time. +const shelf = new BookShelf(__dirname, [ + 'book1.en_US.txt', + 'book1.es_ES.txt', + 'book2.zh_CN.txt', +]); + +assert(v8.startupSnapshot.isBuildingSnapshot()); + +// On snapshot serialization, compress the books to reduce size. +v8.startupSnapshot.addSerializeCallback(BookShelf.compressAll, shelf); +// On snapshot deserialization, decompress the books. +v8.startupSnapshot.addDeserializeCallback(BookShelf.decompressAll, shelf); +v8.startupSnapshot.setDeserializeMainFunction((shelf) => { + // process.env and process.argv are refreshed during snapshot + // deserialization. + const lang = process.env.BOOK_LANG || 'en_US'; + const book = process.argv[1]; + const name = `${book}.${lang}.txt`; + console.error('Reading', name); + console.log(shelf.storage.get(name).toString()); +}, shelf); + +assert.throws(() => v8.startupSnapshot.setDeserializeMainFunction(() => { assert.fail('unreachable duplicated main function'); }), { code: 'ERR_DUPLICATE_STARTUP_SNAPSHOT_MAIN_FUNCTION', diff --git a/test/parallel/test-snapshot-api.js b/test/parallel/test-snapshot-api.js index 49f764d2cb43bd..b66a2b2a12c9ee 100644 --- a/test/parallel/test-snapshot-api.js +++ b/test/parallel/test-snapshot-api.js @@ -1,6 +1,6 @@ 'use strict'; -// This tests snapshot JS API +// This tests snapshot JS API using the example in the docs. require('../common'); const assert = require('assert'); @@ -20,11 +20,20 @@ tmpdir.refresh(); const blobPath = path.join(tmpdir.path, 'snapshot.blob'); const entry = fixtures.path('snapshot', 'v8-startup-snapshot-api.js'); { + for (const book of [ + 'book1.en_US.txt', + 'book1.es_ES.txt', + 'book2.zh_CN.txt', + ]) { + const content = `This is ${book}`; + fs.writeFileSync(path.join(tmpdir.path, book), content, 'utf8'); + } + fs.copyFileSync(entry, path.join(tmpdir.path, 'entry.js')); const child = spawnSync(process.execPath, [ '--snapshot-blob', blobPath, '--build-snapshot', - entry, + 'entry.js', ], { cwd: tmpdir.path }); @@ -41,15 +50,18 @@ const entry = fixtures.path('snapshot', 'v8-startup-snapshot-api.js'); const child = spawnSync(process.execPath, [ '--snapshot-blob', blobPath, + 'book1', ], { cwd: tmpdir.path, env: { ...process.env, + BOOK_LANG: 'en_US', } }); const stdout = child.stdout.toString().trim(); - const file = fs.readFileSync(fixtures.path('x1024.txt'), 'utf8'); - assert.strictEqual(stdout, file); + const stderr = child.stderr.toString().trim(); + assert.strictEqual(stderr, 'Reading book1.en_US.txt'); + assert.strictEqual(stdout, 'This is book1.en_US.txt'); assert.strictEqual(child.status, 0); }