sea: add vfsArchive to serve the assets from a ZIP archive · nodejs/node@a1bfb6f · GitHub
Skip to content

Commit a1bfb6f

Browse files
mcollinaaduh95
authored andcommitted
sea: add vfsArchive to serve the assets from a ZIP archive
When "vfsArchive" names a ZIP archive in the SEA configuration (with "useVfs": true), --build-sea embeds the archive verbatim as one reserved asset, and at runtime the SEA virtual file system mounts the existing ZipProvider over a zero-copy view of the embedded archive instead of the SEAProvider. The main script is injected into the in-memory archive index as a stored entry, so the mounted tree looks the same as with "assets". The archive can be produced with any ZIP tool or with the ZIP support in node:zlib; no ZIP serialization logic is added to the build side. This trades asset read speed (entries are inflated when opened) for a substantially smaller executable when the assets are compressible. Also make FindSingleExecutableResource() return the deserialized SeaResource by reference instead of by value: the copy included the whole assets map, and getAsset() calls it once per asset read, so every fs operation served by the SEA virtual file system paid a cost linear in the number of bundled assets - with 8192 assets, reading each of them took seconds instead of milliseconds. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65810 Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent 50bc27f commit a1bfb6f

17 files changed

Lines changed: 389 additions & 18 deletions

‎doc/api/single-executable-applications.md‎

Lines changed: 37 additions & 0 deletions

‎doc/api/vfs.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,11 @@ is loaded from inside the mount through the ESM loader, and
518518
`"useVfs"` cannot be used together with `"useSnapshot"` or `"useCodeCache"`.
519519
The SEA configuration parser will error if either combination is detected.
520520

521+
Instead of listing individual `"assets"`, the SEA configuration can point
522+
`"vfsArchive"` at a prebuilt ZIP archive; the mount is then backed by a
523+
[`ZipProvider`][] over the embedded archive, and each file is inflated when
524+
it is read. See [Serving the assets from a ZIP archive][] for details.
525+
521526
See the [Single Executable Application][] documentation for more information
522527
on creating SEA builds with assets.
523528

@@ -693,6 +698,7 @@ fields use synthetic but stable values:
693698
[CommonJS resolution algorithm]: modules.md#all-together
694699
[ES modules resolution algorithm]: esm.md#resolution-algorithm
695700
[Explicit Resource Management]: https://github.com/tc39/proposal-explicit-resource-management
701+
[Serving the assets from a ZIP archive]: single-executable-applications.md#serving-the-assets-from-a-zip-archive-with-vfsarchive
696702
[Single Executable Application]: single-executable-applications.md
697703
[`--import`]: cli.md#--importmodule
698704
[`--require`]: cli.md#-r---require-module

‎lib/internal/vfs/sea.js‎

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
'use strict';
22

3-
const { isSea, isVfsEnabled } = internalBinding('sea');
3+
const {
4+
ObjectKeys,
5+
} = primordials;
6+
7+
const {
8+
isSea,
9+
isVfsEnabled,
10+
isVfsArchiveEnabled,
11+
getAsset,
12+
} = internalBinding('sea');
413
const { kEmptyObject } = require('internal/util');
514
const {
615
codes: {
@@ -38,9 +47,14 @@ function initSeaVfs(options = kEmptyObject) {
3847
}
3948

4049
const { VirtualFileSystem } = require('internal/vfs/file_system');
41-
const { SEAProvider } = require('internal/vfs/providers/sea');
4250

43-
const provider = new SEAProvider({ extraFiles: options.extraFiles });
51+
let provider;
52+
if (isVfsArchiveEnabled()) {
53+
provider = createZipProvider(options.extraFiles);
54+
} else {
55+
const { SEAProvider } = require('internal/vfs/providers/sea');
56+
provider = new SEAProvider({ extraFiles: options.extraFiles });
57+
}
4458
// The SEA warning already covers the feature; don't emit the
4559
// VirtualFileSystem experimental warning for the implicit SEA mount.
4660
const vfs = new VirtualFileSystem(provider, {
@@ -51,6 +65,43 @@ function initSeaVfs(options = kEmptyObject) {
5165
return vfs;
5266
}
5367

68+
// The reserved asset key under which --build-sea stores the ZIP archive
69+
// named by "vfsArchive". Must match kVfsArchiveAssetName in src/node_sea.cc.
70+
const kVfsArchiveAssetName = 'node:sea:vfs.zip';
71+
72+
/**
73+
* Creates a ZipProvider over the ZIP archive embedded by `"vfsArchive"`.
74+
* The archive bytes are used in place (a zero-copy view over the SEA
75+
* blob); entries are inflated on demand when they are opened.
76+
* The extra files (the SEA main script) are added to the in-memory archive
77+
* index as stored entries, leaving the embedded bytes untouched.
78+
* @param {Record<string, string|Buffer>} [extraFiles] Additional files to
79+
* serve alongside the assets (used for the SEA main script)
80+
* @returns {ZipProvider}
81+
*/
82+
function createZipProvider(extraFiles) {
83+
const { Buffer } = require('buffer');
84+
const { ZipBuffer } = require('internal/zip');
85+
const { ZipProvider } = require('internal/vfs/providers/ziparchive');
86+
87+
// getAsset returns a zero-copy ArrayBuffer over the (possibly read-only)
88+
// SEA blob; ZipBuffer only reads from it, and decompressed contents are
89+
// fresh buffers, so the view can be used without copying the archive.
90+
const archive = getAsset(kVfsArchiveAssetName);
91+
const zip = new ZipBuffer(Buffer.from(archive));
92+
93+
if (extraFiles !== undefined) {
94+
const names = ObjectKeys(extraFiles);
95+
for (let i = 0; i < names.length; i++) {
96+
const content = extraFiles[names[i]];
97+
const data = typeof content === 'string' ? Buffer.from(content) : content;
98+
zip.addSync(names[i], data, { __proto__: null, method: 'store' });
99+
}
100+
}
101+
102+
return new ZipProvider(zip);
103+
}
104+
54105
/* c8 ignore stop */
55106

56107
module.exports = {

‎src/module_wrap.cc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) {
367367
// For embedder ESM in a SEA, use the bundled code cache if available.
368368
if (id_symbol == realm->isolate_data()->embedder_module_hdo() &&
369369
sea::IsSingleExecutable()) {
370-
sea::SeaResource sea = sea::FindSingleExecutableResource();
370+
const sea::SeaResource& sea = sea::FindSingleExecutableResource();
371371
if (sea.use_code_cache()) {
372372
std::string_view data = sea.code_cache.value();
373373
user_cached_data = new ScriptCompiler::CachedData(

‎src/node.cc‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ MaybeLocal<Value> StartExecution(Environment* env,
330330
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
331331
// Snapshot in SEA is only loaded for the main thread.
332332
if (sea::IsSingleExecutable() && env->is_main_thread()) {
333-
sea::SeaResource sea = sea::FindSingleExecutableResource();
333+
const sea::SeaResource& sea = sea::FindSingleExecutableResource();
334334
// The SEA preparation blob building process should already enforce this,
335335
// this check is just here to guard against the unlikely case where
336336
// the SEA preparation blob has been manually modified by someone.
@@ -986,7 +986,7 @@ static ExitCode InitializeNodeWithArgsInternal(
986986
!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv);
987987
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
988988
if (sea::IsSingleExecutable()) {
989-
sea::SeaResource sea_resource = sea::FindSingleExecutableResource();
989+
const sea::SeaResource& sea_resource = sea::FindSingleExecutableResource();
990990
if (sea_resource.exec_argv_extension != sea::SeaExecArgvExtension::kEnv) {
991991
should_parse_node_options = false;
992992
}
@@ -1603,7 +1603,7 @@ bool LoadSnapshotData(const SnapshotData** snapshot_data_ptr) {
16031603
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
16041604
if (sea::IsSingleExecutable()) {
16051605
is_sea = true;
1606-
sea::SeaResource sea = sea::FindSingleExecutableResource();
1606+
const sea::SeaResource& sea = sea::FindSingleExecutableResource();
16071607
if (sea.use_snapshot()) {
16081608
std::unique_ptr<SnapshotData> read_data =
16091609
std::make_unique<SnapshotData>();

‎src/node_contextify.cc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1768,7 +1768,7 @@ static void CompileFunctionForCJSLoader(
17681768
ScriptCompiler::CachedData* cached_data = nullptr;
17691769
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
17701770
if (is_sea_main) {
1771-
sea::SeaResource sea = sea::FindSingleExecutableResource();
1771+
const sea::SeaResource& sea = sea::FindSingleExecutableResource();
17721772
// Use the "main" field in SEA config for the filename.
17731773
Local<Value> filename_from_sea;
17741774
if (!ToV8Value(context, sea.code_path).ToLocal(&filename_from_sea)) {

‎src/node_sea.cc‎

Lines changed: 75 additions & 9 deletions

0 commit comments

Comments
 (0)