@supermachine/core 0.5.5 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/examples/07-npm-workspace-fast.mjs +115 -0
- package/examples/README.md +1 -0
- package/index.d.ts +53 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -216,6 +216,44 @@ The `"opaque"` default exists because of how `symlink(2)` works: the target is o
|
|
|
216
216
|
|
|
217
217
|
A runnable example is in [`examples/06-mount-workspace.mjs`](./examples/06-mount-workspace.mjs).
|
|
218
218
|
|
|
219
|
+
#### Performance: don't put dependency caches on the mount
|
|
220
|
+
|
|
221
|
+
A mount is great for source code (host-editable, DAX-fast reads). It is **the wrong place** for `node_modules`, `__pycache__`, `target/`, `.next/`, or anything else that gets thousands of small files written by a build/install step.
|
|
222
|
+
|
|
223
|
+
Why: every guest file operation on a virtio-fs mount becomes a FUSE message round-trip over vsock — open, write, close, fsync each cost ~µs of in-guest overhead plus ~µs of host-side handling. For one big file (a tarball, an image), that overhead is invisible. For an `npm install` that creates ~10–30 k tiny files, you're paying milliseconds × tens of thousands of round-trips. We've measured the same `npm install` at **3–6 minutes** on a virtio-fs mount vs **10–30 seconds** on a virtio-blk volume backing `node_modules`. Same workload, same data, same VM — the difference is purely FUSE round-trip cost × small-file count.
|
|
224
|
+
|
|
225
|
+
Block devices (`virtio-blk`, exposed via `volumes`) bypass FUSE entirely — the guest's own filesystem driver writes to a backing file on the host with no per-syscall protocol overhead. Same speed as a "normal" disk inside the guest.
|
|
226
|
+
|
|
227
|
+
### Pattern 6: npm / pnpm / cargo workflow (mount for source, volume for cache)
|
|
228
|
+
|
|
229
|
+
The shape that gets you both live host edits and fast installs:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
const image = await Image.build({
|
|
233
|
+
ref: "node:20-alpine",
|
|
234
|
+
// Source tree — live host edits, DAX reads
|
|
235
|
+
mounts: [{ hostPath: "/Users/me/my-app", guestTag: "src", symlinks: "opaque" }],
|
|
236
|
+
// Dependency cache — fast writes, persists between bakes via the snapshot
|
|
237
|
+
volumes: [
|
|
238
|
+
{ name: "node_modules", sizeMib: 1024, mountPoint: "/work/node_modules" },
|
|
239
|
+
],
|
|
240
|
+
// One-time install at bake. Once snapshotted, every restore has
|
|
241
|
+
// node_modules already populated — no install on the hot path.
|
|
242
|
+
warmup: async (vm) => {
|
|
243
|
+
await vm.exec({ argv: ["mount", "-t", "virtiofs", "src", "/work"] });
|
|
244
|
+
await vm.exec({ argv: ["cp", "-r", "/work/package*.json", "/work/node_modules/.."] });
|
|
245
|
+
await vm.exec({ argv: ["sh", "-c", "cd /work && npm install"], timeoutMs: 600_000 });
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Three layers, each playing to their strengths:
|
|
251
|
+
- **`mounts`** for source: read-only-ish on the hot path, DAX zero-copy, host edits show up immediately (kqueue invalidation).
|
|
252
|
+
- **`volumes`** for write-heavy caches: native-speed writes, no FUSE overhead, persists across restores.
|
|
253
|
+
- **`warmup`** + bake to do the install once: every restore reuses the baked node_modules; cold install is amortized across all future runs of this snapshot.
|
|
254
|
+
|
|
255
|
+
A runnable example is in [`examples/07-npm-workspace-fast.mjs`](./examples/07-npm-workspace-fast.mjs).
|
|
256
|
+
|
|
219
257
|
---
|
|
220
258
|
|
|
221
259
|
## Full API
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// 07-npm-workspace-fast.mjs — the integrator-recommended pattern
|
|
2
|
+
// for "edit on host, run in VM, install dependencies once."
|
|
3
|
+
//
|
|
4
|
+
// Three layers, each playing to its strength:
|
|
5
|
+
//
|
|
6
|
+
// 1. virtio-fs MOUNT for the source tree — host edits are live
|
|
7
|
+
// in the guest (DAX-fast reads, kqueue-driven invalidation),
|
|
8
|
+
// `symlinks: 'opaque'` so npm workspace symlinks work without
|
|
9
|
+
// letting the host follow guest-supplied targets.
|
|
10
|
+
//
|
|
11
|
+
// 2. virtio-blk VOLUME for `node_modules` — block I/O is
|
|
12
|
+
// ~native disk speed; bypasses the FUSE per-file round-trip
|
|
13
|
+
// cost that makes `npm install` 3-6 min on a virtio-fs mount.
|
|
14
|
+
// Same install on a volume: ~10-30 s.
|
|
15
|
+
//
|
|
16
|
+
// 3. WARMUP runs `npm install` once at bake time. The result
|
|
17
|
+
// (node_modules populated on the volume) persists across
|
|
18
|
+
// restores — every subsequent restore has dependencies ready,
|
|
19
|
+
// no install on the hot path.
|
|
20
|
+
//
|
|
21
|
+
// Run:
|
|
22
|
+
// node examples/07-npm-workspace-fast.mjs
|
|
23
|
+
|
|
24
|
+
import { Image } from "@supermachine/core";
|
|
25
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
26
|
+
import { tmpdir } from "node:os";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
|
|
29
|
+
// 1. Build a host-side project that mimics a tiny npm workspace.
|
|
30
|
+
const project = mkdtempSync(join(tmpdir(), "sm-fast-"));
|
|
31
|
+
mkdirSync(project, { recursive: true });
|
|
32
|
+
writeFileSync(
|
|
33
|
+
join(project, "package.json"),
|
|
34
|
+
JSON.stringify(
|
|
35
|
+
{
|
|
36
|
+
name: "fast-demo",
|
|
37
|
+
version: "0.0.0",
|
|
38
|
+
dependencies: { "lodash.chunk": "^4.2.0" },
|
|
39
|
+
},
|
|
40
|
+
null,
|
|
41
|
+
2,
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// 2. Volume backing file — sparse, 1 GiB cap. Sits alongside the
|
|
46
|
+
// project for the demo; in production you'd put it under
|
|
47
|
+
// ~/.local/supermachine/volumes/ or wherever your tenancy boundary
|
|
48
|
+
// lives.
|
|
49
|
+
const volumePath = join(project, "node_modules.img");
|
|
50
|
+
|
|
51
|
+
// 3. Bake with a warmup that runs `npm install` once on the volume.
|
|
52
|
+
// The snapshot captures the *post-install* state; every restore
|
|
53
|
+
// has node_modules ready instantly.
|
|
54
|
+
const t0 = Date.now();
|
|
55
|
+
const img = await Image.build({
|
|
56
|
+
ref: "node:20-alpine",
|
|
57
|
+
name: "npm-workspace-fast-demo",
|
|
58
|
+
memoryMib: 512,
|
|
59
|
+
cmd: ["/bin/sh", "-c", "sleep infinity"],
|
|
60
|
+
mounts: [
|
|
61
|
+
{ hostPath: project, guestTag: "src", symlinks: "opaque" },
|
|
62
|
+
],
|
|
63
|
+
volumes: [
|
|
64
|
+
{ hostPath: volumePath, guestPath: "/work/node_modules", sizeMib: 1024 },
|
|
65
|
+
],
|
|
66
|
+
warmup: async (vm) => {
|
|
67
|
+
// Mount source RO-ish + format and mount the node_modules volume.
|
|
68
|
+
await vm.exec({
|
|
69
|
+
argv: [
|
|
70
|
+
"sh",
|
|
71
|
+
"-c",
|
|
72
|
+
[
|
|
73
|
+
"mkdir -p /src /work/node_modules",
|
|
74
|
+
"mount -t virtiofs src /src",
|
|
75
|
+
// The volume comes up as the next /dev/vdN; on first bake
|
|
76
|
+
// it's blank, format ext4. Idempotent: existing fs is left
|
|
77
|
+
// alone by mke2fs -F? No — use `blkid` to skip if already
|
|
78
|
+
// formatted. For a demo, the bake-time guest always sees
|
|
79
|
+
// an empty volume.
|
|
80
|
+
"mkfs.ext4 -F -q /dev/vdb",
|
|
81
|
+
"mount /dev/vdb /work/node_modules",
|
|
82
|
+
// Copy package*.json out of the read-mount into the
|
|
83
|
+
// volume-backed dir so `npm install` writes locally.
|
|
84
|
+
"cp /src/package.json /work/package.json",
|
|
85
|
+
"cd /work && npm install --silent --no-fund --no-audit",
|
|
86
|
+
].join(" && "),
|
|
87
|
+
],
|
|
88
|
+
timeoutMs: 120_000,
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
warmupTag: "v1",
|
|
92
|
+
});
|
|
93
|
+
console.log(`[fast] bake total: ${Date.now() - t0} ms`);
|
|
94
|
+
|
|
95
|
+
// 4. Run a workload: every restore lands with node_modules already
|
|
96
|
+
// populated. Re-mount mounts and continue.
|
|
97
|
+
const pool = await img.pool({ min: 1, max: 1, restoreOnRelease: false });
|
|
98
|
+
const vm = await pool.acquire();
|
|
99
|
+
const r = await vm.exec({
|
|
100
|
+
argv: [
|
|
101
|
+
"sh",
|
|
102
|
+
"-c",
|
|
103
|
+
[
|
|
104
|
+
"mount -t virtiofs src /src 2>/dev/null || true",
|
|
105
|
+
"mount /dev/vdb /work/node_modules 2>/dev/null || true",
|
|
106
|
+
"cd /work && node -e \"console.log(require('lodash.chunk')([1,2,3,4,5], 2))\"",
|
|
107
|
+
].join("; "),
|
|
108
|
+
],
|
|
109
|
+
timeoutMs: 10_000,
|
|
110
|
+
});
|
|
111
|
+
console.log(r.stdout.toString());
|
|
112
|
+
|
|
113
|
+
await vm.release();
|
|
114
|
+
await pool.shutdown();
|
|
115
|
+
rmSync(project, { recursive: true, force: true });
|
package/examples/README.md
CHANGED
|
@@ -12,6 +12,7 @@ with the package installed.
|
|
|
12
12
|
| `04-expose-tcp.mjs` | `listenerRequired: true` bakes, `vm.exposeTcp()`, `TcpForwarder` |
|
|
13
13
|
| `05-spawn-streaming.mjs` | `vm.spawn()` → `ExecChild` (writeStdin / readStdout / wait) |
|
|
14
14
|
| `06-mount-workspace.mjs` | `mounts` + `symlinks` policy: live host dir into guest, npm-workspace symlink pattern |
|
|
15
|
+
| `07-npm-workspace-fast.mjs` | The recommended npm/pnpm/cargo pattern: virtio-fs mount for source + virtio-blk volume for node_modules + warmup-bake for one-time install |
|
|
15
16
|
|
|
16
17
|
## Requirements
|
|
17
18
|
|
package/index.d.ts
CHANGED
|
@@ -50,6 +50,40 @@ export interface MountSpec {
|
|
|
50
50
|
symlinks?: string
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* A writable virtio-blk volume backed by a host file. Use for
|
|
55
|
+
* dependency caches (`node_modules`, `target/`, `__pycache__`,
|
|
56
|
+
* `.next/`) and other write-heavy paths — block I/O is ~native
|
|
57
|
+
* disk speed, no FUSE round-trip per file. Contrast with
|
|
58
|
+
* [`MountSpec`] which exposes a host directory via virtio-fs
|
|
59
|
+
* (right for source trees you want live-editable from the host,
|
|
60
|
+
* wrong for thousands-of-small-files write workloads).
|
|
61
|
+
*
|
|
62
|
+
* The host file is created sparse at `hostPath` if missing
|
|
63
|
+
* (actual disk usage tracks what the guest writes). Snapshots
|
|
64
|
+
* capture the mapping, not the volume contents — same backing
|
|
65
|
+
* file is reused across all restores of this snapshot.
|
|
66
|
+
*/
|
|
67
|
+
export interface VolumeSpec {
|
|
68
|
+
/**
|
|
69
|
+
* Host file backing this volume. Created sparse at `sizeMib`
|
|
70
|
+
* MiB if missing. Persists across restores; delete it to
|
|
71
|
+
* reset state.
|
|
72
|
+
*/
|
|
73
|
+
hostPath: string
|
|
74
|
+
/**
|
|
75
|
+
* Mount point inside the guest, e.g. `"/var/lib/postgres"`
|
|
76
|
+
* or `"/work/node_modules"`. The guest needs to actually
|
|
77
|
+
* mount it (init can do this, or your warmup callback).
|
|
78
|
+
*/
|
|
79
|
+
guestPath: string
|
|
80
|
+
/**
|
|
81
|
+
* Size cap in MiB. Default 1024 (1 GiB). Sparse — actual
|
|
82
|
+
* disk usage matches what the guest writes.
|
|
83
|
+
*/
|
|
84
|
+
sizeMib?: number
|
|
85
|
+
}
|
|
86
|
+
|
|
53
87
|
/**
|
|
54
88
|
* Options for [`Image.build`]. Mirrors `OciImageBuilder` in the
|
|
55
89
|
* Rust crate. All fields optional except `ref` (the OCI image
|
|
@@ -114,6 +148,25 @@ export interface BuildOptions {
|
|
|
114
148
|
* re-bakes.
|
|
115
149
|
*/
|
|
116
150
|
mounts?: Array<MountSpec>
|
|
151
|
+
/**
|
|
152
|
+
* Writable virtio-blk volumes. Each entry maps a sparse host
|
|
153
|
+
* file to a guest mount point. Use for dependency caches
|
|
154
|
+
* (`node_modules`, `target/`) — block I/O is ~native disk
|
|
155
|
+
* speed and bypasses the FUSE round-trip cost that makes
|
|
156
|
+
* `mounts` slow for thousands-of-small-files workloads.
|
|
157
|
+
* Volume bytes persist across restores; only the mapping is
|
|
158
|
+
* captured in the snapshot.
|
|
159
|
+
*/
|
|
160
|
+
volumes?: Array<VolumeSpec>
|
|
161
|
+
/**
|
|
162
|
+
* Extra kernel cmdline tokens, appended after supermachine's
|
|
163
|
+
* own defaults (earlycon, console, tsi_hijack, etc.). Power-
|
|
164
|
+
* user / testing escape hatch — e.g. `["panic=1"]` to force
|
|
165
|
+
* an immediate panic on warning, or `["init=/nope"]` to
|
|
166
|
+
* deliberately trigger an early-init kernel panic in a test.
|
|
167
|
+
* Folded into the bake's input hash so changing this re-bakes.
|
|
168
|
+
*/
|
|
169
|
+
extraKernelArgs?: Array<string>
|
|
117
170
|
/**
|
|
118
171
|
* Override where to fetch the image bytes. Default behavior
|
|
119
172
|
* (when omitted) treats `ref` as a registry reference and pulls
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supermachine/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Run any OCI/Docker image as a hardware-isolated microVM. Node/Bun/Deno binding for the supermachine Rust crate.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"arm64"
|
|
23
23
|
],
|
|
24
24
|
"optionalDependencies": {
|
|
25
|
-
"@supermachine/core-darwin-arm64": "0.
|
|
25
|
+
"@supermachine/core-darwin-arm64": "0.7.3"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build:rust": "TYPE_DEF_TMP_PATH=$(pwd)/scripts/.napi_type_defs.tmp cargo build --release -p supermachine-napi && cargo build --release --bin supermachine-worker -p supermachine",
|