@wenathlan/saddle 2.0.151 → 2.0.153
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 +3 -3
- package/biome.jsonc +12 -5
- package/dist/App.d.ts +5 -0
- package/dist/App.js +52 -0
- package/dist/Sol/Sol.d.ts +3 -0
- package/dist/Sol/Sol.js +46 -0
- package/dist/Sol/agentbrowser/agentbrowser.d.ts +9 -0
- package/dist/Sol/agentbrowser/agentbrowser.js +121 -0
- package/dist/Sol/architecture/architecture.d.ts +9 -0
- package/dist/Sol/architecture/architecture.js +93 -0
- package/dist/Sol/compute/compute.d.ts +9 -0
- package/dist/Sol/compute/compute.js +22 -0
- package/dist/Sol/console/console.d.ts +15 -0
- package/dist/Sol/console/console.js +292 -0
- package/dist/Sol/dashboard/dashboard.d.ts +10 -0
- package/dist/Sol/dashboard/dashboard.js +181 -0
- package/dist/Sol/docs/docs.d.ts +9 -0
- package/dist/Sol/docs/docs.js +28 -0
- package/dist/Sol/home/home.d.ts +35 -0
- package/dist/Sol/home/home.js +239 -0
- package/dist/Sol/integrations/integrations.d.ts +9 -0
- package/dist/Sol/integrations/integrations.js +83 -0
- package/dist/Sol/intro/intro.d.ts +7 -0
- package/dist/Sol/intro/intro.js +102 -0
- package/dist/Sol/login/login.d.ts +16 -0
- package/dist/Sol/login/login.js +49 -0
- package/dist/Sol/notfound/notfound.d.ts +9 -0
- package/dist/Sol/notfound/notfound.js +15 -0
- package/dist/Sol/onboarding/onboarding.d.ts +25 -0
- package/dist/Sol/onboarding/onboarding.js +60 -0
- package/dist/Sol/playground/playground.d.ts +9 -0
- package/dist/Sol/playground/playground.js +127 -0
- package/dist/Sol/register/register.d.ts +16 -0
- package/dist/Sol/register/register.js +193 -0
- package/dist/Sol/shell/Shell.d.ts +124 -0
- package/dist/Sol/shell/Shell.js +335 -0
- package/dist/Sol/wingets/ComputeBootladder.d.ts +6 -0
- package/dist/Sol/wingets/ComputeBootladder.js +71 -0
- package/dist/Sol/wingets/ComputeModalitycatalog.d.ts +7 -0
- package/dist/Sol/wingets/ComputeModalitycatalog.js +71 -0
- package/dist/Sol/wingets/ConsoleApibadge.d.ts +7 -0
- package/dist/Sol/wingets/ConsoleApibadge.js +5 -0
- package/dist/Sol/wingets/ConsoleEdgelaw.d.ts +6 -0
- package/dist/Sol/wingets/ConsoleEdgelaw.js +36 -0
- package/dist/Sol/wingets/ConsoleEventstimeline.d.ts +19 -0
- package/dist/Sol/wingets/ConsoleEventstimeline.js +12 -0
- package/dist/Sol/wingets/ConsoleSnapshotcontrols.d.ts +8 -0
- package/dist/Sol/wingets/ConsoleSnapshotcontrols.js +8 -0
- package/dist/Sol/wingets/ConsoleSpecpanel.d.ts +21 -0
- package/dist/Sol/wingets/ConsoleSpecpanel.js +75 -0
- package/dist/Sol/wingets/ConsoleTerminal.d.ts +22 -0
- package/dist/Sol/wingets/ConsoleTerminal.js +77 -0
- package/dist/Sol/wingets/DashboardAccountpanel.d.ts +18 -0
- package/dist/Sol/wingets/DashboardAccountpanel.js +65 -0
- package/dist/Sol/wingets/DashboardAdminpanel.d.ts +6 -0
- package/dist/Sol/wingets/DashboardAdminpanel.js +90 -0
- package/dist/Sol/wingets/DashboardAuditlog.d.ts +8 -0
- package/dist/Sol/wingets/DashboardAuditlog.js +50 -0
- package/dist/Sol/wingets/DashboardCreatesandboxform.d.ts +10 -0
- package/dist/Sol/wingets/DashboardCreatesandboxform.js +71 -0
- package/dist/Sol/wingets/DashboardEventstimeline.d.ts +8 -0
- package/dist/Sol/wingets/DashboardEventstimeline.js +83 -0
- package/dist/Sol/wingets/DashboardGlobalsandboxestable.d.ts +8 -0
- package/dist/Sol/wingets/DashboardGlobalsandboxestable.js +57 -0
- package/dist/Sol/wingets/DashboardMeshnodestable.d.ts +8 -0
- package/dist/Sol/wingets/DashboardMeshnodestable.js +103 -0
- package/dist/Sol/wingets/DashboardSandboxlist.d.ts +10 -0
- package/dist/Sol/wingets/DashboardSandboxlist.js +163 -0
- package/dist/Sol/wingets/DashboardUserstable.d.ts +8 -0
- package/dist/Sol/wingets/DashboardUserstable.js +78 -0
- package/dist/Sol/wingets/HomeEntrygate.d.ts +23 -0
- package/dist/Sol/wingets/HomeEntrygate.js +73 -0
- package/dist/Sol/wingets/HomeMetricstrip.d.ts +9 -0
- package/dist/Sol/wingets/HomeMetricstrip.js +4 -0
- package/dist/Sol/wingets/HomeRuntimediagram.d.ts +1 -0
- package/dist/Sol/wingets/HomeRuntimediagram.js +22 -0
- package/dist/Sol/wingets/LoginForm.d.ts +19 -0
- package/dist/Sol/wingets/LoginForm.js +141 -0
- package/dist/Sol/wingets/RegisterPasswordconfirm.d.ts +13 -0
- package/dist/Sol/wingets/RegisterPasswordconfirm.js +16 -0
- package/dist/Sol/wingets/RegisterPasswordmeter.d.ts +19 -0
- package/dist/Sol/wingets/RegisterPasswordmeter.js +65 -0
- package/dist/Sol/wingets/RegisterUsernamerules.d.ts +11 -0
- package/dist/Sol/wingets/RegisterUsernamerules.js +16 -0
- package/dist/Sol/wingets/index.d.ts +39 -0
- package/dist/Sol/wingets/index.js +39 -0
- package/dist/alternatives.d.ts +508 -0
- package/dist/alternatives.js +950 -0
- package/dist/api.d.ts +154 -0
- package/dist/api.js +174 -0
- package/dist/auth.d.ts +268 -0
- package/dist/auth.js +476 -0
- package/dist/automation.d.ts +425 -0
- package/dist/automation.js +566 -0
- package/dist/browser.d.ts +561 -0
- package/dist/browser.js +1058 -0
- package/dist/capacitor.config.d.ts +45 -0
- package/dist/capacitor.config.js +48 -0
- package/dist/catalog.d.ts +43 -0
- package/dist/catalog.js +44 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +168 -0
- package/dist/communication.d.ts +181 -0
- package/dist/communication.js +376 -0
- package/dist/compute.d.ts +1160 -0
- package/dist/compute.js +2272 -0
- package/dist/dashboardlib.d.ts +58 -0
- package/dist/dashboardlib.js +161 -0
- package/dist/db.d.ts +569 -0
- package/dist/db.js +1080 -0
- package/dist/distribution.d.ts +528 -0
- package/dist/distribution.js +810 -0
- package/dist/drizzle.config.d.ts +20 -0
- package/dist/drizzle.config.js +18 -0
- package/dist/edgelandes.d.ts +70 -0
- package/dist/edgelandes.host.d.ts +26 -0
- package/dist/edgelandes.host.js +1089 -0
- package/dist/edgelandes.inproc.d.ts +27 -0
- package/dist/edgelandes.inproc.js +952 -0
- package/dist/edgelandes.js +1133 -0
- package/dist/edgelandes.ninety.d.ts +31 -0
- package/dist/edgelandes.ninety.js +174 -0
- package/dist/edgelandes.virtual.d.ts +25 -0
- package/dist/edgelandes.virtual.js +629 -0
- package/dist/edgeplane.d.ts +114 -0
- package/dist/edgeplane.js +305 -0
- package/dist/examplelocaljob.d.ts +1 -0
- package/{docs/example-localjob.ts → dist/examplelocaljob.js} +0 -1
- package/dist/execution.d.ts +529 -0
- package/dist/execution.js +760 -0
- package/dist/family.d.ts +16 -0
- package/dist/family.js +42 -0
- package/dist/familyurl.d.ts +21 -0
- package/dist/familyurl.js +24 -0
- package/dist/format.d.ts +18 -0
- package/dist/format.js +59 -0
- package/dist/foundation.d.ts +172 -0
- package/dist/foundation.js +312 -0
- package/dist/index.d.ts +735 -0
- package/dist/index.js +1020 -0
- package/dist/integration.d.ts +235 -0
- package/dist/integration.js +228 -0
- package/dist/intelligence.d.ts +93 -0
- package/dist/intelligence.js +144 -0
- package/dist/isolation.d.ts +128 -0
- package/dist/isolation.js +95 -0
- package/dist/labels.d.ts +665 -0
- package/dist/labels.js +751 -0
- package/dist/localauth.d.ts +127 -0
- package/dist/localauth.js +413 -0
- package/dist/media.d.ts +343 -0
- package/dist/media.js +985 -0
- package/dist/mesh.d.ts +177 -0
- package/dist/mesh.js +468 -0
- package/dist/modalityregistry.d.ts +109 -0
- package/dist/modalityregistry.js +1386 -0
- package/dist/modes.d.ts +358 -0
- package/dist/modes.js +133 -0
- package/dist/onboardingstate.d.ts +79 -0
- package/dist/onboardingstate.js +282 -0
- package/dist/orchestrator.d.ts +1338 -0
- package/dist/orchestrator.js +4310 -0
- package/dist/packager/targetcli.js +4 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +14 -0
- package/dist/performance.d.ts +415 -0
- package/dist/performance.js +826 -0
- package/dist/quantum.d.ts +809 -0
- package/dist/quantum.js +2072 -0
- package/dist/render.d.ts +380 -0
- package/dist/render.js +1072 -0
- package/dist/runall.d.ts +2 -0
- package/dist/runall.js +39 -0
- package/dist/runtimeprobe.d.ts +4 -0
- package/dist/runtimeprobe.js +8 -0
- package/dist/sandbox.d.ts +355 -0
- package/dist/sandbox.js +1721 -0
- package/dist/sandboxenvprofile.d.ts +29 -0
- package/dist/sandboxenvprofile.js +89 -0
- package/dist/sandboxprofile.d.ts +137 -0
- package/dist/sandboxprofile.js +241 -0
- package/dist/sandboxqueue.d.ts +108 -0
- package/dist/sandboxqueue.js +201 -0
- package/dist/sandboxquota.d.ts +121 -0
- package/dist/sandboxquota.js +236 -0
- package/dist/scheduler.d.ts +595 -0
- package/dist/scheduler.js +1258 -0
- package/dist/scriptlanes.d.ts +182 -0
- package/dist/scriptlanes.js +660 -0
- package/dist/scripts.buildengine.d.ts +1 -0
- package/dist/scripts.buildengine.js +39 -0
- package/dist/scripts.securitygate.d.ts +1 -0
- package/dist/scripts.securitygate.js +68 -0
- package/dist/scripts.validateflatnative.d.ts +1 -0
- package/dist/scripts.validateflatnative.js +72 -0
- package/dist/security.d.ts +500 -0
- package/dist/security.js +1169 -0
- package/dist/seedcatalog.d.ts +13 -0
- package/dist/seedcatalog.js +46 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +53 -0
- package/dist/speccatalog.d.ts +31 -0
- package/dist/speccatalog.js +163 -0
- package/dist/tiers.d.ts +1001 -0
- package/dist/tiers.js +2169 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +5 -0
- package/dist/vaulthandoff.d.ts +141 -0
- package/dist/vaulthandoff.js +444 -0
- package/dist/virtual.d.ts +640 -0
- package/dist/virtual.js +1284 -0
- package/dist/virtualcpu.d.ts +315 -0
- package/dist/virtualcpu.js +1180 -0
- package/dist/virtualgpu.d.ts +344 -0
- package/dist/virtualgpu.js +1000 -0
- package/dist/virtualization.d.ts +606 -0
- package/dist/virtualization.js +1186 -0
- package/dist/virtualmemory.d.ts +817 -0
- package/dist/virtualmemory.js +1795 -0
- package/dist/vite.config.d.ts +3 -0
- package/dist/vite.config.js +43 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +25 -0
- package/dist/webserver.d.ts +166 -0
- package/dist/webserver.js +2226 -0
- package/dist/workloadlaw.d.ts +133 -0
- package/dist/workloadlaw.js +193 -0
- package/docs/artifactavailability.md +2 -2
- package/docs/{containerplatforms-2.1.0.md → containerplatforms.2.1.0.md} +1 -1
- package/docs/{documentmap-1.8.12.md → documentmap.1.8.12.md} +1 -1
- package/docs/ninety.modalities.md +108 -0
- package/docs/planning.ninety.md +391 -0
- package/docs/{releasenotes-1.8.13.md → releasenotes.1.8.13.md} +2 -2
- package/docs/{releasenotes-1.8.14.md → releasenotes.1.8.14.md} +1 -1
- package/docs/{repo-synthesis-1.8.16.md → repo.synthesis.1.8.16.md} +1 -1
- package/docs/{todo-1.8.15.md → todo.1.8.15.md} +1 -1
- package/docs/{todo-1.8.16.md → todo.1.8.16.md} +5 -5
- package/docs/todo.md +2 -2
- package/docs/workloadlaw.md +104 -0
- package/index.ts +1 -1
- package/manifest.json +1 -1
- package/modalityregistry.ts +1371 -34
- package/orchestrator.ts +1 -1
- package/package.json +78 -33
- package/docs/CONVERSA.txt +0 -1523
- package/docs/architecture-1.8.19.png +0 -0
- package/docs/assets/architecture.svg +0 -45
- package/docs/assets/saddle-browser-trace.webp +0 -0
- package/docs/assets/saddle-hero-bridge.webp +0 -0
- package/docs/assets/saddle-mark.webp +0 -0
- package/docs/assets/saddle-runtime-map.webp +0 -0
- package/docs/assets/saddlemark.svg +0 -13
- package/docs/saddle.archive.1.8.17.tar.gz.gpg +0 -0
- /package/docs/{architecture-1.8.19.md → architecture.1.8.19.md} +0 -0
- /package/docs/{architecture-1.8.19.mmd → architecture.1.8.19.mmd} +0 -0
- /package/docs/{artifactresearch-1.8.9.md → artifactresearch.1.8.9.md} +0 -0
- /package/docs/{batches-1816-1820.md → batches.1816.to.1820.md} +0 -0
- /package/docs/{brancharchive-2026-08-18.md → brancharchive.2026.08.18.md} +0 -0
- /package/docs/{containerplatforms-1.8.17.md → containerplatforms.1.8.17.md} +0 -0
- /package/docs/{e2ugh-engine.md → e2ugh.engine.md} +0 -0
- /package/docs/{flatbuildresearch-1.8.11.md → flatbuildresearch.1.8.11.md} +0 -0
- /package/docs/{licenseaudit-1.8.12.md → licenseaudit.1.8.12.md} +0 -0
- /package/docs/{mobileconversionresearch-1.8.10.md → mobileconversionresearch.1.8.10.md} +0 -0
- /package/docs/{native-wrappers.md → native.wrappers.md} +0 -0
- /package/docs/{nativeidentityresearch-1.8.11.md → nativeidentityresearch.1.8.11.md} +0 -0
- /package/docs/{releasenotes-1.8.12.md → releasenotes.1.8.12.md} +0 -0
- /package/docs/{releasenotes-1.8.15.md → releasenotes.1.8.15.md} +0 -0
- /package/docs/{releasenotes-1.8.16.md → releasenotes.1.8.16.md} +0 -0
- /package/docs/{releasenotes-1.8.17.md → releasenotes.1.8.17.md} +0 -0
- /package/docs/{releasenotes-1.8.18.md → releasenotes.1.8.18.md} +0 -0
- /package/docs/{releasenotes-1.8.19.md → releasenotes.1.8.19.md} +0 -0
- /package/docs/{reorganization-1.8.8.md → reorganization.1.8.8.md} +0 -0
- /package/docs/{reorganization-1.8.9.md → reorganization.1.8.9.md} +0 -0
- /package/docs/{repo-analysis-1.8.21.json → repo.analysis.1.8.21.json} +0 -0
- /package/docs/{repo-candidates-1.8.21.jsonl → repo.candidates.1.8.21.jsonl} +0 -0
- /package/docs/{repo-research-1.8.16.md → repo.research.1.8.16.md} +0 -0
- /package/docs/{repo-research-1.8.21.md → repo.research.1.8.21.md} +0 -0
- /package/docs/{repo-selected-1.8.21.json → repo.selected.1.8.21.json} +0 -0
- /package/docs/{repo-synthesis-1.8.21.md → repo.synthesis.1.8.21.md} +0 -0
- /package/docs/{research-1.8.18-isolation.md → research.1.8.18.isolation.md} +0 -0
- /package/docs/{research-1.8.19-virtual-browser.md → research.1.8.19.virtual.browser.md} +0 -0
- /package/docs/{scanningresearch-1.8.11.md → scanningresearch.1.8.11.md} +0 -0
- /package/docs/{securityaudit-1.8.7.md → securityaudit.1.8.7.md} +0 -0
- /package/docs/{securityreferences-1.8.11.md → securityreferences.1.8.11.md} +0 -0
- /package/docs/{securityresearch-1.8.11.md → securityresearch.1.8.11.md} +0 -0
- /package/docs/{signingresearch-1.8.12.md → signingresearch.1.8.12.md} +0 -0
- /package/docs/{tauri-glib-risk-1.8.11.md → tauri.glib.risk.1.8.11.md} +0 -0
- /package/docs/{todo-1.8.18.md → todo.1.8.18.md} +0 -0
- /package/docs/{todo-1.8.19.md → todo.1.8.19.md} +0 -0
- /package/docs/{toolchainresearch-1.8.9.md → toolchainresearch.1.8.9.md} +0 -0
- /package/docs/{web-duplicatecleanup.md → web.duplicatecleanup.md} +0 -0
- /package/docs/{web-ideas.md → web.ideas.md} +0 -0
- /package/docs/{workflowimprovements-2026-08-19.md → workflowimprovements.2026.08.19.md} +0 -0
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,1721 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sandbox.ts — browser-pure port of the saddle virtual hardware engine.
|
|
3
|
+
*
|
|
4
|
+
* this module is a lightweight rewrite of the engine generators
|
|
5
|
+
* (virtualcpu.ts, virtualmemory.ts, virtualgpu.ts and render.ts) that runs
|
|
6
|
+
* unchanged in the browser and in node: it imports nothing, touches no
|
|
7
|
+
* node:* builtin and no dom api at module scope, and produces the exact
|
|
8
|
+
* same procfs payloads, the 89-character nvidia-smi summary table, the
|
|
9
|
+
* mesa software-stack summaries (rusticl opencl 3.1, lavapipe vulkan 1.4,
|
|
10
|
+
* llvmpipe opengl 4.6) and the boot dmesg sequence the native engine emits.
|
|
11
|
+
*
|
|
12
|
+
* contexts (22): cpudata, gpudata, migprofiles, vendortables, topology,
|
|
13
|
+
* cpuinfo, lscpu, pagealign, meminfo, freeh, smiformat, smitable, smilist,
|
|
14
|
+
* clinfo, vulkaninfo, glxinfo, mesaenv, bootsequence, sandboxstate,
|
|
15
|
+
* virtualfs, dispatcher, commands.
|
|
16
|
+
*
|
|
17
|
+
* rules: lowercase identifiers, english jsdoc in third person, no emoji,
|
|
18
|
+
* zero dependencies, and deterministic fallbacks when crypto randomness
|
|
19
|
+
* is unavailable.
|
|
20
|
+
*/
|
|
21
|
+
/* ------------------------------------------------------------------ */
|
|
22
|
+
/* context: shared helpers */
|
|
23
|
+
/* ------------------------------------------------------------------ */
|
|
24
|
+
/** inclusive pseudo-random integer between min and max; deterministic
|
|
25
|
+
* fallback when crypto randomness is unavailable. */
|
|
26
|
+
function rnd(min, max) {
|
|
27
|
+
try {
|
|
28
|
+
const span = max - min + 1;
|
|
29
|
+
if (typeof globalThis.crypto?.getRandomValues === 'function') {
|
|
30
|
+
const buf = new Uint32Array(1);
|
|
31
|
+
globalThis.crypto.getRandomValues(buf);
|
|
32
|
+
return min + (buf[0] % span);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* falls through to math.random */
|
|
37
|
+
}
|
|
38
|
+
return min + Math.floor(Math.random() * (max - min + 1));
|
|
39
|
+
}
|
|
40
|
+
/** right-pad helper that never exceeds the target width. */
|
|
41
|
+
function pad(text, width) {
|
|
42
|
+
return text.length >= width ? text.slice(0, width) : text + ' '.repeat(width - text.length);
|
|
43
|
+
}
|
|
44
|
+
/** left-pad helper for right-aligned columns. */
|
|
45
|
+
function padstart(text, width) {
|
|
46
|
+
return text.length >= width
|
|
47
|
+
? text.slice(text.length - width)
|
|
48
|
+
: ' '.repeat(width - text.length) + text;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* formats one byte count with binary units (512 B, 4 KiB, 16 MiB, 1.5 GiB);
|
|
52
|
+
* whole numbers drop the decimal so the default quota reads "16 MiB".
|
|
53
|
+
*
|
|
54
|
+
* @param {number} bytes the byte count.
|
|
55
|
+
* @returns {string} the human readable size.
|
|
56
|
+
*/
|
|
57
|
+
function humanbytes(bytes) {
|
|
58
|
+
const value = Math.max(0, Math.round(bytes));
|
|
59
|
+
const unit = (size, suffix) => {
|
|
60
|
+
const scaled = value / size;
|
|
61
|
+
const rounded = Math.round(scaled * 10) / 10;
|
|
62
|
+
return `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)} ${suffix}`;
|
|
63
|
+
};
|
|
64
|
+
if (value >= 1024 * 1024 * 1024) {
|
|
65
|
+
return unit(1024 * 1024 * 1024, 'GiB');
|
|
66
|
+
}
|
|
67
|
+
if (value >= 1024 * 1024) {
|
|
68
|
+
return unit(1024 * 1024, 'MiB');
|
|
69
|
+
}
|
|
70
|
+
if (value >= 1024) {
|
|
71
|
+
return unit(1024, 'KiB');
|
|
72
|
+
}
|
|
73
|
+
return `${value} B`;
|
|
74
|
+
}
|
|
75
|
+
/** lowercases a model string for case-insensitive lookups. */
|
|
76
|
+
function normalizemodel(model) {
|
|
77
|
+
return String(model ?? '').trim().toLowerCase();
|
|
78
|
+
}
|
|
79
|
+
/** generates a random uuid-shaped hex token with a prefix. */
|
|
80
|
+
function token(prefix) {
|
|
81
|
+
const raw = typeof globalThis.crypto?.randomUUID === 'function'
|
|
82
|
+
? globalThis.crypto.randomUUID().replace(/-/g, '')
|
|
83
|
+
: Array.from({ length: 32 }, () => '0123456789abcdef'[rnd(0, 15)]).join('');
|
|
84
|
+
return `${prefix}${raw.slice(0, 8)}-${raw.slice(8, 12)}-${raw.slice(12, 16)}-${raw.slice(16, 20)}-${raw.slice(20, 32)}`;
|
|
85
|
+
}
|
|
86
|
+
/* ------------------------------------------------------------------ */
|
|
87
|
+
/* context: cpudata — the processor bank (specs verified 2026-08-22) */
|
|
88
|
+
/* ------------------------------------------------------------------ */
|
|
89
|
+
/**
|
|
90
|
+
* cpudata carries the six engine-bank processors (amd epyc 9965, ryzen 9
|
|
91
|
+
* 9950x3d, threadripper pro 9995wx, threadripper 7980x, core ultra 9
|
|
92
|
+
* 285k, epyc 9955) plus the two web showcase additions intel xeon 6980p
|
|
93
|
+
* granite rapids and apple m3 ultra. every field mirrors the vendor pages
|
|
94
|
+
* and the techpowerup database as pinned by best_virtual_processors.
|
|
95
|
+
*/
|
|
96
|
+
export const cpudata = [
|
|
97
|
+
{
|
|
98
|
+
model: 'AMD EPYC 9965',
|
|
99
|
+
displayname: 'AMD EPYC 9965 192-Core Processor',
|
|
100
|
+
vendor: 'amd',
|
|
101
|
+
arch: 'x86_64',
|
|
102
|
+
cores: 192,
|
|
103
|
+
threads: 384,
|
|
104
|
+
baseclockmhz: 2250,
|
|
105
|
+
boostclockmhz: 3700,
|
|
106
|
+
allcoreclockmhz: 3350,
|
|
107
|
+
l3mb: 384,
|
|
108
|
+
l2mb: 192,
|
|
109
|
+
socket: 'SP5 (LGA6096)',
|
|
110
|
+
tdpwatts: 500,
|
|
111
|
+
pcie: 'PCIe 5.0 x128',
|
|
112
|
+
maxmemorygb: 6144,
|
|
113
|
+
memorytype: 'DDR5-6400 ECC RDIMM',
|
|
114
|
+
memorychannels: 12,
|
|
115
|
+
microarch: 'Zen 5c (Turin)',
|
|
116
|
+
cpufamily: 25,
|
|
117
|
+
cpumodel: 17,
|
|
118
|
+
stepping: 2,
|
|
119
|
+
physicalline: '52 bits physical',
|
|
120
|
+
virtualline: '57 bits virtual',
|
|
121
|
+
launch: '2024-10-10',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
model: 'AMD Ryzen 9 9950X3D',
|
|
125
|
+
displayname: 'AMD Ryzen 9 9950X3D 16-Core Processor',
|
|
126
|
+
vendor: 'amd',
|
|
127
|
+
arch: 'x86_64',
|
|
128
|
+
cores: 16,
|
|
129
|
+
threads: 32,
|
|
130
|
+
baseclockmhz: 4300,
|
|
131
|
+
boostclockmhz: 5700,
|
|
132
|
+
allcoreclockmhz: 5400,
|
|
133
|
+
l3mb: 128,
|
|
134
|
+
l2mb: 16,
|
|
135
|
+
socket: 'AM5',
|
|
136
|
+
tdpwatts: 170,
|
|
137
|
+
pcie: 'PCIe 5.0 x24',
|
|
138
|
+
maxmemorygb: 192,
|
|
139
|
+
memorytype: 'DDR5-5600',
|
|
140
|
+
memorychannels: 2,
|
|
141
|
+
microarch: 'Zen 5 X3D (3D V-Cache on one CCD)',
|
|
142
|
+
cpufamily: 25,
|
|
143
|
+
cpumodel: 17,
|
|
144
|
+
stepping: 2,
|
|
145
|
+
physicalline: '48 bits physical',
|
|
146
|
+
virtualline: '57 bits virtual',
|
|
147
|
+
launch: '2025-03-12',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
model: 'AMD Threadripper PRO 9995WX',
|
|
151
|
+
displayname: 'AMD Ryzen Threadripper PRO 9995WX 96-Core Processor',
|
|
152
|
+
vendor: 'amd',
|
|
153
|
+
arch: 'x86_64',
|
|
154
|
+
cores: 96,
|
|
155
|
+
threads: 192,
|
|
156
|
+
baseclockmhz: 2500,
|
|
157
|
+
boostclockmhz: 5400,
|
|
158
|
+
allcoreclockmhz: 4100,
|
|
159
|
+
l3mb: 384,
|
|
160
|
+
l2mb: 96,
|
|
161
|
+
socket: 'sTR5 (LGA4844)',
|
|
162
|
+
tdpwatts: 350,
|
|
163
|
+
pcie: 'PCIe 5.0 x128',
|
|
164
|
+
maxmemorygb: 2048,
|
|
165
|
+
memorytype: 'DDR5-6400 ECC RDIMM',
|
|
166
|
+
memorychannels: 8,
|
|
167
|
+
microarch: 'Zen 5 (Shimada Peak, WRX90)',
|
|
168
|
+
cpufamily: 25,
|
|
169
|
+
cpumodel: 17,
|
|
170
|
+
stepping: 2,
|
|
171
|
+
physicalline: '52 bits physical',
|
|
172
|
+
virtualline: '57 bits virtual',
|
|
173
|
+
launch: '2025-07',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
model: 'AMD Threadripper 7980X',
|
|
177
|
+
displayname: 'AMD Ryzen Threadripper 7980X 64-Core Processor',
|
|
178
|
+
vendor: 'amd',
|
|
179
|
+
arch: 'x86_64',
|
|
180
|
+
cores: 64,
|
|
181
|
+
threads: 128,
|
|
182
|
+
baseclockmhz: 3200,
|
|
183
|
+
boostclockmhz: 5100,
|
|
184
|
+
allcoreclockmhz: 4400,
|
|
185
|
+
l3mb: 256,
|
|
186
|
+
l2mb: 64,
|
|
187
|
+
socket: 'sTR5',
|
|
188
|
+
tdpwatts: 350,
|
|
189
|
+
pcie: 'PCIe 5.0 x48',
|
|
190
|
+
maxmemorygb: 1024,
|
|
191
|
+
memorytype: 'DDR5-6400',
|
|
192
|
+
memorychannels: 4,
|
|
193
|
+
microarch: 'Zen 5 (TRX50)',
|
|
194
|
+
cpufamily: 25,
|
|
195
|
+
cpumodel: 17,
|
|
196
|
+
stepping: 2,
|
|
197
|
+
physicalline: '52 bits physical',
|
|
198
|
+
virtualline: '57 bits virtual',
|
|
199
|
+
launch: '2023-10',
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
model: 'Intel Core Ultra 9 285K',
|
|
203
|
+
displayname: 'Intel(R) Core(TM) Ultra 9 285K',
|
|
204
|
+
vendor: 'intel',
|
|
205
|
+
arch: 'x86_64',
|
|
206
|
+
cores: 24,
|
|
207
|
+
threads: 24,
|
|
208
|
+
baseclockmhz: 3700,
|
|
209
|
+
boostclockmhz: 5700,
|
|
210
|
+
allcoreclockmhz: 5400,
|
|
211
|
+
l3mb: 36,
|
|
212
|
+
l2mb: 36,
|
|
213
|
+
socket: 'LGA1851',
|
|
214
|
+
tdpwatts: 125,
|
|
215
|
+
pcie: 'PCIe 5.0 x20 + PCIe 4.0 x4',
|
|
216
|
+
maxmemorygb: 192,
|
|
217
|
+
memorytype: 'DDR5-6400',
|
|
218
|
+
memorychannels: 2,
|
|
219
|
+
microarch: 'Arrow Lake (8P + 16E, no hyper-threading)',
|
|
220
|
+
cpufamily: 6,
|
|
221
|
+
cpumodel: 191,
|
|
222
|
+
stepping: 2,
|
|
223
|
+
physicalline: '46 bits physical',
|
|
224
|
+
virtualline: '57 bits virtual',
|
|
225
|
+
launch: '2024-10-24',
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
model: 'AMD EPYC 9955',
|
|
229
|
+
displayname: 'AMD EPYC 9955 128-Core Processor',
|
|
230
|
+
vendor: 'amd',
|
|
231
|
+
arch: 'x86_64',
|
|
232
|
+
cores: 128,
|
|
233
|
+
threads: 256,
|
|
234
|
+
baseclockmhz: 2600,
|
|
235
|
+
boostclockmhz: 3750,
|
|
236
|
+
allcoreclockmhz: 3450,
|
|
237
|
+
l3mb: 384,
|
|
238
|
+
l2mb: 128,
|
|
239
|
+
socket: 'SP5',
|
|
240
|
+
tdpwatts: 400,
|
|
241
|
+
pcie: 'PCIe 5.0 x128',
|
|
242
|
+
maxmemorygb: 6144,
|
|
243
|
+
memorytype: 'DDR5-6400 ECC RDIMM',
|
|
244
|
+
memorychannels: 12,
|
|
245
|
+
microarch: 'Zen 5c (Turin dense)',
|
|
246
|
+
cpufamily: 25,
|
|
247
|
+
cpumodel: 17,
|
|
248
|
+
stepping: 2,
|
|
249
|
+
physicalline: '52 bits physical',
|
|
250
|
+
virtualline: '57 bits virtual',
|
|
251
|
+
launch: '2024-10-10',
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
model: 'Intel Xeon 6980P',
|
|
255
|
+
displayname: 'Intel(R) Xeon(R) Platinum 6980P',
|
|
256
|
+
vendor: 'intel',
|
|
257
|
+
arch: 'x86_64',
|
|
258
|
+
cores: 128,
|
|
259
|
+
threads: 256,
|
|
260
|
+
baseclockmhz: 2200,
|
|
261
|
+
boostclockmhz: 3900,
|
|
262
|
+
allcoreclockmhz: 3300,
|
|
263
|
+
l3mb: 504,
|
|
264
|
+
l2mb: 128,
|
|
265
|
+
socket: 'LGA4710',
|
|
266
|
+
tdpwatts: 350,
|
|
267
|
+
pcie: 'PCIe 5.0 x128 + CXL 2.0 x16',
|
|
268
|
+
maxmemorygb: 6144,
|
|
269
|
+
memorytype: 'DDR5-6400 / MRDIMM-8800',
|
|
270
|
+
memorychannels: 12,
|
|
271
|
+
microarch: 'Granite Rapids (Intel 3)',
|
|
272
|
+
cpufamily: 6,
|
|
273
|
+
cpumodel: 173,
|
|
274
|
+
stepping: 2,
|
|
275
|
+
physicalline: '46 bits physical',
|
|
276
|
+
virtualline: '57 bits virtual',
|
|
277
|
+
launch: '2025-02',
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
model: 'Apple M3 Ultra',
|
|
281
|
+
displayname: 'Apple M3 Ultra (24P + 8E, 32-core CPU)',
|
|
282
|
+
vendor: 'apple',
|
|
283
|
+
arch: 'arm64',
|
|
284
|
+
cores: 32,
|
|
285
|
+
threads: 32,
|
|
286
|
+
baseclockmhz: 1000,
|
|
287
|
+
boostclockmhz: 4050,
|
|
288
|
+
allcoreclockmhz: 3400,
|
|
289
|
+
l3mb: 96,
|
|
290
|
+
l2mb: 96,
|
|
291
|
+
socket: 'soldered (UltraFusion interconnect)',
|
|
292
|
+
tdpwatts: 140,
|
|
293
|
+
pcie: 'PCIe 4.0 x8 + Thunderbolt 5 x2',
|
|
294
|
+
maxmemorygb: 512,
|
|
295
|
+
memorytype: 'LPDDR5 unified 819 GB/s',
|
|
296
|
+
memorychannels: 16,
|
|
297
|
+
microarch: 'Everest (3nm, two M3 Max dies fused)',
|
|
298
|
+
cpufamily: 0,
|
|
299
|
+
cpumodel: 0,
|
|
300
|
+
stepping: 0,
|
|
301
|
+
physicalline: '44 bits physical',
|
|
302
|
+
virtualline: '48 bits virtual',
|
|
303
|
+
launch: '2025-03',
|
|
304
|
+
},
|
|
305
|
+
];
|
|
306
|
+
/** resolves a processor spec by model name, case-insensitive; returns
|
|
307
|
+
* undefined for unknown models so callers can shape friendly errors. */
|
|
308
|
+
export function getcpu(model) {
|
|
309
|
+
const needle = normalizemodel(model);
|
|
310
|
+
return cpudata.find((spec) => normalizemodel(spec.model) === needle);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* resolves a catalog entry by its slug id (epyc-9965) as served by the
|
|
314
|
+
* /api/v1/specs/cpus endpoint; returns undefined for unknown ids.
|
|
315
|
+
* @param {string} id the catalog id.
|
|
316
|
+
* @returns {object | undefined} the matching cpu entry.
|
|
317
|
+
*/
|
|
318
|
+
export function getcpubyid(id) {
|
|
319
|
+
const key = String(id).toLowerCase().replace(/\s+/g, '-');
|
|
320
|
+
const strip = (name) => name
|
|
321
|
+
.toLowerCase()
|
|
322
|
+
.replace(/\s+/g, '-')
|
|
323
|
+
.replace(/^(amd|intel|apple|nvidia)-/, '');
|
|
324
|
+
return cpudata.find((entry) => {
|
|
325
|
+
const slug = entry.model.toLowerCase().replace(/\s+/g, '-');
|
|
326
|
+
return slug === key || slug.endsWith(key) || strip(entry.model) === strip(key);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
/* ------------------------------------------------------------------ */
|
|
330
|
+
/* context: vendortables — flag sets and identity strings */
|
|
331
|
+
/* ------------------------------------------------------------------ */
|
|
332
|
+
/** zen 5 flag set exactly as generatevirtualcpuinfo renders it (avx-512
|
|
333
|
+
* family, sha_ni, gfni and friends). */
|
|
334
|
+
const zen5flags = [
|
|
335
|
+
'fpu', 'vme', 'de', 'pse', 'tsc', 'msr', 'pae', 'mce', 'cx8', 'apic',
|
|
336
|
+
'sep', 'mtrr', 'pge', 'mca', 'cmov', 'pat', 'pse36', 'clflush', 'mmx',
|
|
337
|
+
'fxsr', 'sse', 'sse2', 'ht', 'syscall', 'nx', 'mmxext', 'fxsr_opt',
|
|
338
|
+
'pdpe1gb', 'rdtscp', 'lm', 'constant_tsc', 'rep_good', 'amd_lbr_v2',
|
|
339
|
+
'nopl', 'nonstop_tsc', 'cpuid', 'extd_apicid', 'aperfmperf', 'rapl',
|
|
340
|
+
'pclmulqdq', 'monitor', 'ssse3', 'fma', 'cx16', 'sse4_1', 'sse4_2',
|
|
341
|
+
'x2apic', 'movbe', 'popcnt', 'aes', 'xsave', 'avx', 'f16c', 'rdrand',
|
|
342
|
+
'lahf_lm', 'cmp_legacy', 'svm', 'extapic', 'cr8_legacy', 'abm', 'sse4a',
|
|
343
|
+
'misalignsse', '3dnowprefetch', 'osvw', 'ibs', 'skinit', 'wdt', 'tce',
|
|
344
|
+
'topoext', 'perfctr_core', 'perfctr_nb', 'bpext', 'perfctr_llc',
|
|
345
|
+
'mwaitx', 'cpb', 'cat_l3', 'cdp_l3', 'hw_pstate', 'ssbd', 'mba',
|
|
346
|
+
'perfmon_v2', 'ibrs', 'ibpb', 'stibp', 'vmmcall', 'fsgsbase', 'bmi1',
|
|
347
|
+
'avx2', 'smep', 'bmi2', 'erms', 'invpcid', 'cqm', 'rdt_a', 'avx512f',
|
|
348
|
+
'avx512dq', 'rdseed', 'adx', 'smap', 'clflushopt', 'clwb', 'avx512vl',
|
|
349
|
+
'avx512bw', 'avx512cd', 'avx512_bf16', 'avx512vbmi', 'avx512vbmi2',
|
|
350
|
+
'avx512ifma', 'avx512vp2intersect', 'sha_ni', 'gfni', 'vaes',
|
|
351
|
+
'vpclmulqdq', 'avx512vpopcntdq', 'avx512vnni', 'avx512bitalg', 'rdpid',
|
|
352
|
+
'movdiri', 'movdir64b', 'fsrm', 'cldemote', 'serialize', 'flush_l1d',
|
|
353
|
+
'arch_capabilities',
|
|
354
|
+
];
|
|
355
|
+
/** arrow lake / granite rapids flag set: avx2 with avx_vnni and no avx-512
|
|
356
|
+
* execution units; the ht flag is appended per model because arrow lake
|
|
357
|
+
* ships without hyper-threading while xeon 6 keeps it. */
|
|
358
|
+
const intelflags = [
|
|
359
|
+
'fpu', 'vme', 'de', 'pse', 'tsc', 'msr', 'pae', 'mce', 'cx8', 'apic',
|
|
360
|
+
'sep', 'mtrr', 'pge', 'mca', 'cmov', 'pat', 'pse36', 'clflush', 'mmx',
|
|
361
|
+
'fxsr', 'sse', 'sse2', 'syscall', 'nx', 'pdpe1gb', 'rdtscp', 'lm',
|
|
362
|
+
'constant_tsc', 'arch_perfmon', 'rep_good', 'nopl', 'xtopology',
|
|
363
|
+
'nonstop_tsc', 'cpuid', 'aperfmperf', 'tsc_known_freq', 'pclmulqdq',
|
|
364
|
+
'monitor', 'ssse3', 'fma', 'cx16', 'sse4_1', 'sse4_2', 'x2apic', 'movbe',
|
|
365
|
+
'popcnt', 'aes', 'xsave', 'avx', 'f16c', 'rdrand', 'lahf_lm', 'abm',
|
|
366
|
+
'3dnowprefetch', 'cpuid_fault', 'epb', 'invpcid_single', 'ssbd', 'ibrs',
|
|
367
|
+
'ibpb', 'stibp', 'ibrs_enhanced', 'tpr_shadow', 'vnmi', 'flexpriority',
|
|
368
|
+
'ept', 'vpid', 'ept_ad', 'fsgsbase', 'tsc_adjust', 'bmi1', 'avx2',
|
|
369
|
+
'smep', 'bmi2', 'erms', 'invpcid', 'rdseed', 'adx', 'smap',
|
|
370
|
+
'clflushopt', 'clwb', 'sha_ni', 'xsaveopt', 'xsavec', 'xgetbv1',
|
|
371
|
+
'xsaves', 'split_lock_detect', 'user_shstk', 'avx_vnni', 'waitpkg',
|
|
372
|
+
'gfni', 'vaes', 'vpclmulqdq', 'rdpid', 'movdiri', 'movdir64b', 'fsrm',
|
|
373
|
+
'md_clear', 'serialize', 'tsxldtrk', 'la57', 'cldemote',
|
|
374
|
+
'arch_capabilities',
|
|
375
|
+
];
|
|
376
|
+
/** arm64 feature line rendered for apple silicon cpuinfo blocks. */
|
|
377
|
+
const armfeatures = [
|
|
378
|
+
'fp', 'asimd', 'aes', 'pmull', 'sha1', 'sha2', 'sha3', 'sha512', 'crc32',
|
|
379
|
+
'atomics', 'fphp', 'asimdhp', 'cpuid', 'asimdrdm', 'jscvt', 'fcma',
|
|
380
|
+
'lrcpc', 'dcpop', 'sm3', 'sm4', 'asimddp', 'uscat', 'ilrcpc', 'flagm',
|
|
381
|
+
'ssbs', 'sb', 'paca', 'pacg', 'dcpodp', 'flagm2', 'frint', 'ecs', 'wfxt',
|
|
382
|
+
'bti',
|
|
383
|
+
];
|
|
384
|
+
/** vendor identity table consumed by cpuinfo and lscpu. */
|
|
385
|
+
const vendortables = {
|
|
386
|
+
amd: {
|
|
387
|
+
vendorid: 'AuthenticAMD',
|
|
388
|
+
flags: zen5flags,
|
|
389
|
+
bugs: 'sysret_ss_attrs spectre_v1 spectre_v2 spec_store_bypass srso',
|
|
390
|
+
tlbsize: '3584 2M/4M pages',
|
|
391
|
+
cpuidlevel: 16,
|
|
392
|
+
powermanagement: 'ts ttp tm hwpstate cpb eff_freq_ro [13] [14]',
|
|
393
|
+
},
|
|
394
|
+
intel: {
|
|
395
|
+
vendorid: 'GenuineIntel',
|
|
396
|
+
flags: intelflags,
|
|
397
|
+
bugs: 'spectre_v1 spectre_v2 spec_store_bypass swapgs eibrs_pbrsb',
|
|
398
|
+
tlbsize: null,
|
|
399
|
+
cpuidlevel: 32,
|
|
400
|
+
powermanagement: 'ts ttp tm hwpstate cpb eff_freq_ro [8] [9] [10] [11] [12] [13] [14]',
|
|
401
|
+
},
|
|
402
|
+
apple: {
|
|
403
|
+
vendorid: 'Apple',
|
|
404
|
+
flags: armfeatures,
|
|
405
|
+
bugs: '',
|
|
406
|
+
tlbsize: null,
|
|
407
|
+
cpuidlevel: 0,
|
|
408
|
+
powermanagement: '',
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
/* ------------------------------------------------------------------ */
|
|
412
|
+
/* context: topology */
|
|
413
|
+
/* ------------------------------------------------------------------ */
|
|
414
|
+
/**
|
|
415
|
+
* solves the package topology for a vcpu count against a spec; mirrors
|
|
416
|
+
* solvetopology from virtualcpu.ts (smt parts round core counts up,
|
|
417
|
+
* heterogeneous parts report one thread per core and the count is clamped
|
|
418
|
+
* to the model thread ceiling).
|
|
419
|
+
*/
|
|
420
|
+
export function solvetopology(spec, vcpus) {
|
|
421
|
+
const threadspercore = Math.max(1, Math.round(spec.threads / spec.cores));
|
|
422
|
+
const count = Math.max(1, Math.min(Math.round(vcpus) || 1, spec.threads));
|
|
423
|
+
return {
|
|
424
|
+
vcpus: count,
|
|
425
|
+
threadspercore,
|
|
426
|
+
coresonline: Math.ceil(count / threadspercore),
|
|
427
|
+
siblings: count,
|
|
428
|
+
sockets: 1,
|
|
429
|
+
fullypopulated: count === spec.threads,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/* ------------------------------------------------------------------ */
|
|
433
|
+
/* context: cpuinfo — /proc/cpuinfo generation */
|
|
434
|
+
/* ------------------------------------------------------------------ */
|
|
435
|
+
/** per-key tab alignment the kernel uses on x86 cpuinfo lines. */
|
|
436
|
+
const keytabs = {
|
|
437
|
+
processor: '\t', vendor_id: '\t', 'cpu family': '\t', model: '\t\t',
|
|
438
|
+
'model name': '\t', stepping: '\t', microcode: '\t', 'cpu MHz': '\t\t',
|
|
439
|
+
'cache size': '\t', 'physical id': '\t', siblings: '\t', 'core id': '\t\t',
|
|
440
|
+
'cpu cores': '\t', apicid: '\t\t', 'initial apicid': '\t', fpu: '\t\t',
|
|
441
|
+
fpu_exception: '\t', 'cpuid level': '\t', wp: '\t\t', flags: '\t\t',
|
|
442
|
+
bugs: '\t\t', bogomips: '\t', 'TLB size': '\t', 'clflush size': '\t',
|
|
443
|
+
cache_alignment: '', 'address sizes': '\t', 'power management': '',
|
|
444
|
+
};
|
|
445
|
+
/** renders one cpuinfo line exactly as procfs does: key, tabs, colon, value. */
|
|
446
|
+
function line(key, value) {
|
|
447
|
+
const tabs = keytabs[key] ?? '\t';
|
|
448
|
+
return `${key}${tabs}: ${value}`;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* generates a complete /proc/cpuinfo text for a bank model with an
|
|
452
|
+
* arbitrary vcpu count; x86_64 models render the full zen 5 / intel flag
|
|
453
|
+
* set per logical processor and the apple model renders the arm64 cpuinfo
|
|
454
|
+
* block shape instead.
|
|
455
|
+
*
|
|
456
|
+
* @param {string} model model name present in cpudata.
|
|
457
|
+
* @param {number} vcpus number of logical processors to render.
|
|
458
|
+
* @returns {string} the full procfs payload.
|
|
459
|
+
*/
|
|
460
|
+
export function cpuinfo(model, vcpus) {
|
|
461
|
+
const spec = getcpu(model);
|
|
462
|
+
if (spec === undefined) {
|
|
463
|
+
throw new Error(`unknown processor model "${model}"; known models: ${cpudata.map((entry) => entry.model).join(', ')}`);
|
|
464
|
+
}
|
|
465
|
+
const topology = solvetopology(spec, vcpus);
|
|
466
|
+
const table = vendortables[spec.vendor];
|
|
467
|
+
const blocks = [];
|
|
468
|
+
for (let processor = 0; processor < topology.vcpus; processor += 1) {
|
|
469
|
+
if (spec.vendor === 'apple') {
|
|
470
|
+
blocks.push([
|
|
471
|
+
`processor\t: ${processor}`,
|
|
472
|
+
'BogoMIPS\t: 48.00',
|
|
473
|
+
`Features\t: ${table.flags.join(' ')}`,
|
|
474
|
+
'CPU implementer\t: 0x41',
|
|
475
|
+
'CPU architecture: 8',
|
|
476
|
+
'CPU variant\t: 0x2',
|
|
477
|
+
'CPU part\t: 0x612',
|
|
478
|
+
`CPU revision\t: ${processor % 2 === 0 ? 0 : 3}`,
|
|
479
|
+
'',
|
|
480
|
+
`MIDR_EL1\t: 0x0000000041026${processor % 2 === 0 ? '00' : '03'}`,
|
|
481
|
+
'Revidr_EL1\t: 0x00000000',
|
|
482
|
+
`CPU affinity\t: ${processor}`,
|
|
483
|
+
].join('\n'));
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const currentmhz = rnd(Math.round(spec.baseclockmhz), Math.round(spec.boostclockmhz) + 1);
|
|
487
|
+
const bogomips = (spec.baseclockmhz * 2 + rnd(-40, 41) / 100).toFixed(2);
|
|
488
|
+
const coreid = Math.floor(processor / topology.threadspercore);
|
|
489
|
+
const flags = spec.vendor === 'intel' && topology.threadspercore === 2
|
|
490
|
+
? `${table.flags.join(' ')} ht`
|
|
491
|
+
: table.flags.join(' ');
|
|
492
|
+
const rows = [
|
|
493
|
+
line('processor', processor),
|
|
494
|
+
line('vendor_id', table.vendorid),
|
|
495
|
+
line('cpu family', spec.cpufamily),
|
|
496
|
+
line('model', spec.cpumodel),
|
|
497
|
+
line('model name', spec.displayname),
|
|
498
|
+
line('stepping', spec.stepping),
|
|
499
|
+
line('microcode', '0xffffffff'),
|
|
500
|
+
line('cpu MHz', currentmhz.toFixed(3)),
|
|
501
|
+
line('cache size', `${spec.l3mb} MB`),
|
|
502
|
+
line('physical id', 0),
|
|
503
|
+
line('siblings', topology.siblings),
|
|
504
|
+
line('core id', coreid),
|
|
505
|
+
line('cpu cores', topology.coresonline),
|
|
506
|
+
line('apicid', processor),
|
|
507
|
+
line('initial apicid', processor),
|
|
508
|
+
line('fpu', 'yes'),
|
|
509
|
+
line('fpu_exception', 'yes'),
|
|
510
|
+
line('cpuid level', table.cpuidlevel),
|
|
511
|
+
line('wp', 'yes'),
|
|
512
|
+
line('flags', flags),
|
|
513
|
+
line('bugs', table.bugs),
|
|
514
|
+
line('bogomips', bogomips),
|
|
515
|
+
];
|
|
516
|
+
if (table.tlbsize !== null) {
|
|
517
|
+
rows.push(line('TLB size', table.tlbsize));
|
|
518
|
+
}
|
|
519
|
+
rows.push(line('clflush size', 64), line('cache_alignment', 64), line('address sizes', `${spec.physicalline}, ${spec.virtualline}`), line('power management', table.powermanagement));
|
|
520
|
+
blocks.push(rows.join('\n'));
|
|
521
|
+
}
|
|
522
|
+
return blocks.join('\n\n');
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* generates the util-linux lscpu summary for a bank model with numa node
|
|
526
|
+
* ranges and vulnerability lines, mirroring generatevirtuallscpu.
|
|
527
|
+
*
|
|
528
|
+
* @param {string} model model name present in cpudata.
|
|
529
|
+
* @param {number} vcpus logical processor count.
|
|
530
|
+
* @param {number} [numanodes=1] number of numa nodes to spread over.
|
|
531
|
+
* @returns {string} the lscpu text block.
|
|
532
|
+
*/
|
|
533
|
+
export function lscpu(model, vcpus, numanodes = 1) {
|
|
534
|
+
const spec = getcpu(model);
|
|
535
|
+
if (spec === undefined) {
|
|
536
|
+
throw new Error(`unknown processor model "${model}"`);
|
|
537
|
+
}
|
|
538
|
+
const topology = solvetopology(spec, vcpus);
|
|
539
|
+
const corespersocket = Math.ceil(topology.coresonline / numanodes) * numanodes;
|
|
540
|
+
const rows = [
|
|
541
|
+
['Architecture:', spec.arch === 'arm64' ? 'aarch64' : 'x86_64'],
|
|
542
|
+
['CPU op-mode(s):', '32-bit, 64-bit'],
|
|
543
|
+
['Byte Order:', 'Little Endian'],
|
|
544
|
+
['Address sizes:', `${spec.physicalline}, ${spec.virtualline}`],
|
|
545
|
+
['CPU(s):', String(topology.vcpus)],
|
|
546
|
+
['On-line CPU(s) list:', topology.vcpus === 1 ? '0' : `0-${topology.vcpus - 1}`],
|
|
547
|
+
];
|
|
548
|
+
if (spec.vendor === 'apple') {
|
|
549
|
+
rows.push(['Vendor ID:', 'Apple'], ['Model name:', spec.displayname], ['Thread(s) per core:', '1'], ['Core(s) per socket:', String(topology.coresonline)], ['Socket(s):', '1'], ['Cluster(s)-wide:', '24 performance + 8 efficiency cores'], ['Frequency boost:', 'enabled'], ['CPU max MHz:', `${spec.boostclockmhz}.0000`], ['CPU min MHz:', '600.0000']);
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
rows.push(['Vendor ID:', vendortables[spec.vendor].vendorid], ['Model name:', spec.displayname], ['CPU family:', String(spec.cpufamily)], ['Model:', String(spec.cpumodel)], ['Thread(s) per core:', String(topology.threadspercore)], ['Core(s) per socket:', String(corespersocket)], ['Socket(s):', '1'], ['Stepping:', String(spec.stepping)], ['Frequency boost:', 'enabled'], ['CPU max MHz:', `${spec.boostclockmhz}.0000`], ['CPU min MHz:', `${(spec.baseclockmhz * 0.62).toFixed(4)}`], ['BogoMIPS:', `${(spec.baseclockmhz * 2).toFixed(2)}`], [
|
|
553
|
+
'L1d cache:',
|
|
554
|
+
`${Math.round((spec.l2mb / spec.cores) * 1024)} KiB (${spec.cores} instances)`,
|
|
555
|
+
], ['L2 cache:', `${spec.l2mb} MiB (${spec.cores} instances)`], ['L3 cache:', `${spec.l3mb} MiB (${Math.max(1, Math.round(spec.cores / 8))} instances)`]);
|
|
556
|
+
}
|
|
557
|
+
rows.push(['NUMA node(s):', String(numanodes)]);
|
|
558
|
+
const pernode = Math.ceil(topology.vcpus / numanodes);
|
|
559
|
+
for (let node = 0; node < numanodes; node += 1) {
|
|
560
|
+
const start = node * pernode;
|
|
561
|
+
const end = Math.min(start + pernode, topology.vcpus) - 1;
|
|
562
|
+
rows.push([`NUMA node${node} CPU(s):`, start === end ? String(start) : `${start}-${end}`]);
|
|
563
|
+
}
|
|
564
|
+
if (spec.vendor !== 'apple') {
|
|
565
|
+
rows.push(['Vulnerability Itlb multihit:', 'Not affected'], ['Vulnerability L1tf:', 'Not affected'], ['Vulnerability Mds:', 'Not affected'], ['Vulnerability Meltdown:', 'Not affected'], [
|
|
566
|
+
'Vulnerability Spectre v1:',
|
|
567
|
+
'Mitigation; usercopy/swapgs barriers and __user pointer sanitization',
|
|
568
|
+
], [
|
|
569
|
+
'Vulnerability Spectre v2:',
|
|
570
|
+
spec.vendor === 'amd' ? 'Mitigation; Retpolines, IBPB' : 'Mitigation; Enhanced IBRS',
|
|
571
|
+
]);
|
|
572
|
+
}
|
|
573
|
+
return rows.map(([key, value]) => `${key.padEnd(28)}${value}`).join('\n');
|
|
574
|
+
}
|
|
575
|
+
/* ------------------------------------------------------------------ */
|
|
576
|
+
/* context: gpudata and migprofiles */
|
|
577
|
+
/* ------------------------------------------------------------------ */
|
|
578
|
+
/**
|
|
579
|
+
* gpudata mirrors the verified gpu_bank: seven real identities with
|
|
580
|
+
* vrammib (physical capacity) and smireportedmib (the value after the
|
|
581
|
+
* firmware carveout) cross-checked on 2026-08-22.
|
|
582
|
+
*/
|
|
583
|
+
export const gpudata = [
|
|
584
|
+
{
|
|
585
|
+
id: 'rtx5090', name: 'NVIDIA GeForce RTX 5090', vendor: 'nvidia',
|
|
586
|
+
pcivendor: '10DE', pcidevice: '2B85', vrammib: 32768,
|
|
587
|
+
smireportedmib: 32607, memtype: 'GDDR7', busbits: 512,
|
|
588
|
+
bandwidthgbs: 1792, tdpwatts: 575, arch: 'GB202 Blackwell',
|
|
589
|
+
smarch: 'sm_120', smcount: 170, mig: false, driver: '575.57.08',
|
|
590
|
+
cuda: '12.9',
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
id: 'rtxpro6000', name: 'NVIDIA RTX PRO 6000 Blackwell', vendor: 'nvidia',
|
|
594
|
+
pcivendor: '10DE', pcidevice: '26B5', vrammib: 98304,
|
|
595
|
+
smireportedmib: 98304, memtype: 'GDDR7 ECC', busbits: 512,
|
|
596
|
+
bandwidthgbs: 1792, tdpwatts: 600, arch: 'GB202 Blackwell workstation',
|
|
597
|
+
smarch: 'sm_120', smcount: 188, mig: true, driver: '575.57.08',
|
|
598
|
+
cuda: '12.9',
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
id: 'b200', name: 'NVIDIA B200', vendor: 'nvidia',
|
|
602
|
+
pcivendor: '10DE', pcidevice: '2665', vrammib: 196608,
|
|
603
|
+
smireportedmib: 184320, memtype: 'HBM3e', busbits: 8192,
|
|
604
|
+
bandwidthgbs: 8000, tdpwatts: 1000, arch: 'GB100 Blackwell dual-die',
|
|
605
|
+
smarch: 'sm_100', smcount: 208, mig: true, driver: '575.57.08',
|
|
606
|
+
cuda: '12.9',
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
id: 'h100', name: 'NVIDIA H100 80GB HBM3', vendor: 'nvidia',
|
|
610
|
+
pcivendor: '10DE', pcidevice: '2330', vrammib: 81920,
|
|
611
|
+
smireportedmib: 81559, memtype: 'HBM3', busbits: 6144,
|
|
612
|
+
bandwidthgbs: 3350, tdpwatts: 700, arch: 'GH100 Hopper SXM5',
|
|
613
|
+
smarch: 'sm_90', smcount: 132, mig: true, driver: '575.57.08',
|
|
614
|
+
cuda: '12.9',
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
id: 'a100', name: 'NVIDIA A100-SXM4-40GB', vendor: 'nvidia',
|
|
618
|
+
pcivendor: '10DE', pcidevice: '20B0', vrammib: 40960,
|
|
619
|
+
smireportedmib: 40536, memtype: 'HBM2e', busbits: 5120,
|
|
620
|
+
bandwidthgbs: 1555, tdpwatts: 400, arch: 'GA100 Ampere SXM4',
|
|
621
|
+
smarch: 'sm_80', smcount: 108, mig: true, driver: '575.57.08',
|
|
622
|
+
cuda: '12.9',
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
id: 'rx9070xt', name: 'AMD Radeon RX 9070 XT', vendor: 'amd',
|
|
626
|
+
pcivendor: '1002', pcidevice: '748E', vrammib: 16384,
|
|
627
|
+
smireportedmib: 16384, memtype: 'GDDR6', busbits: 256,
|
|
628
|
+
bandwidthgbs: 640, tdpwatts: 304, arch: 'Navi 48 RDNA 4',
|
|
629
|
+
smarch: 'gfx1201', smcount: 64, mig: false,
|
|
630
|
+
driver: 'mesa 26.2.1 / rusticl', cuda: null,
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
id: 'mi350x', name: 'AMD Instinct MI350X', vendor: 'amd',
|
|
634
|
+
pcivendor: '1002', pcidevice: '75A0', vrammib: 294912,
|
|
635
|
+
smireportedmib: 294912, memtype: 'HBM3e', busbits: 8192,
|
|
636
|
+
bandwidthgbs: 8000, tdpwatts: 1000, arch: 'CDNA 4 Aqua Vanjaram',
|
|
637
|
+
smarch: 'gfx950', smcount: 304, mig: false, driver: 'rocm 7.x',
|
|
638
|
+
cuda: null,
|
|
639
|
+
},
|
|
640
|
+
];
|
|
641
|
+
/** resolves a gpu spec by id or name, case-insensitive. */
|
|
642
|
+
export function getgpu(gpu) {
|
|
643
|
+
const needle = normalizemodel(gpu);
|
|
644
|
+
return gpudata.find((spec) => normalizemodel(spec.id) === needle || normalizemodel(spec.name) === needle);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* migprofiles mirrors the mig catalog of the 96 gb-class virtual device:
|
|
648
|
+
* 1g.24gb exposes 24 gb slices (4 instances), 2g.48gb 48 gb slices (2
|
|
649
|
+
* instances) and 4g.96gb dedicates the full device to one instance.
|
|
650
|
+
*/
|
|
651
|
+
export const migprofiles = [
|
|
652
|
+
{ id: '1g.24gb', slicegb: 24, maxinstances: 4 },
|
|
653
|
+
{ id: '2g.48gb', slicegb: 48, maxinstances: 2 },
|
|
654
|
+
{ id: '4g.96gb', slicegb: 96, maxinstances: 1 },
|
|
655
|
+
];
|
|
656
|
+
/** resolves a mig profile by id; null for 'off' and unknown names. */
|
|
657
|
+
export function getmig(id) {
|
|
658
|
+
if (id === 'off' || id === undefined || id === null || id === '') {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
return migprofiles.find((profile) => profile.id === id) ?? null;
|
|
662
|
+
}
|
|
663
|
+
/* ------------------------------------------------------------------ */
|
|
664
|
+
/* context: meminfo and free — /proc/meminfo generation */
|
|
665
|
+
/* ------------------------------------------------------------------ */
|
|
666
|
+
/** rounds a kb value down to the nearest 4 kb page boundary. */
|
|
667
|
+
export function pagealign(kb) {
|
|
668
|
+
return Math.floor(kb / 4) * 4;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* generates a complete /proc/meminfo text for the requested total ram in
|
|
672
|
+
* gb, mirroring generatevirtualmeminfo: memtotal, memfree, memavailable,
|
|
673
|
+
* buffers, cached, the anon/file split, swap, slab, commit accounting
|
|
674
|
+
* under vm.overcommit_memory=2 semantics and the direct map breakdown.
|
|
675
|
+
*
|
|
676
|
+
* @param {number} ramgb total guest ram in gb (positive number).
|
|
677
|
+
* @param {{swapgb?: number, vcpus?: number, freefraction?: number,
|
|
678
|
+
* availablefraction?: number}} [options] optional overrides.
|
|
679
|
+
* @returns {string} the full meminfo payload.
|
|
680
|
+
*/
|
|
681
|
+
export function meminfo(ramgb, options = {}) {
|
|
682
|
+
if (!Number.isFinite(ramgb) || ramgb <= 0) {
|
|
683
|
+
throw new Error(`ramgb must be a positive number, received ${ramgb}`);
|
|
684
|
+
}
|
|
685
|
+
const vcpus = options.vcpus ?? 8;
|
|
686
|
+
const totalkb = pagealign(ramgb * 1024 * 1024 - 131072);
|
|
687
|
+
const freefraction = options.freefraction ?? rnd(38, 68) / 1000;
|
|
688
|
+
const availablefraction = options.availablefraction ?? rnd(860, 930) / 1000;
|
|
689
|
+
const memfree = pagealign(totalkb * freefraction);
|
|
690
|
+
const memavailable = pagealign(totalkb * availablefraction);
|
|
691
|
+
const buffers = pagealign(totalkb * (rnd(50, 80) / 10000));
|
|
692
|
+
const cached = pagealign(totalkb * (rnd(300, 420) / 1000));
|
|
693
|
+
const activeanon = pagealign(totalkb * (rnd(240, 320) / 1000));
|
|
694
|
+
const inactiveanon = pagealign(totalkb * (rnd(60, 120) / 1000));
|
|
695
|
+
const active = pagealign(activeanon + cached * 0.55);
|
|
696
|
+
const inactive = pagealign(inactiveanon + cached * 0.4);
|
|
697
|
+
const swapgb = options.swapgb ?? Math.min(Math.max(Math.round(ramgb / 8), 2), 128);
|
|
698
|
+
const swaptotal = swapgb * 1024 * 1024;
|
|
699
|
+
const swapfree = pagealign(swaptotal * (rnd(850, 980) / 1000));
|
|
700
|
+
const commitlimit = pagealign(swaptotal + (totalkb * 50) / 100);
|
|
701
|
+
const committedas = pagealign(totalkb * (rnd(95, 160) / 100));
|
|
702
|
+
const slab = pagealign(totalkb * (rnd(190, 240) / 10000));
|
|
703
|
+
const sreclaimable = pagealign(slab * 0.64);
|
|
704
|
+
const anonhuge = pagealign(activeanon * (rnd(20, 45) / 100));
|
|
705
|
+
const directmap1g = ramgb >= 64 ? pagealign(totalkb * 0.8) : 0;
|
|
706
|
+
const directmap2m = directmap1g > 0 ? pagealign(totalkb * 0.15) : pagealign(totalkb * 0.9);
|
|
707
|
+
const rows = [
|
|
708
|
+
['MemTotal', totalkb], ['MemFree', memfree],
|
|
709
|
+
['MemAvailable', memavailable], ['Buffers', buffers], ['Cached', cached],
|
|
710
|
+
['SwapCached', 0], ['Active', active], ['Inactive', inactive],
|
|
711
|
+
['Active(anon)', activeanon], ['Inactive(anon)', inactiveanon],
|
|
712
|
+
['Active(file)', pagealign(active - activeanon)],
|
|
713
|
+
['Inactive(file)', pagealign(inactive - inactiveanon)],
|
|
714
|
+
['Unevictable', pagealign(rnd(16384, 65536))],
|
|
715
|
+
['Mlocked', pagealign(rnd(4096, 16384))],
|
|
716
|
+
['SwapTotal', swaptotal], ['SwapFree', swapfree],
|
|
717
|
+
['Dirty', pagealign(rnd(20480, 262144))], ['Writeback', 0],
|
|
718
|
+
['AnonPages', pagealign(activeanon * 1.08)],
|
|
719
|
+
['Mapped', pagealign(totalkb * (rnd(20, 42) / 1000))],
|
|
720
|
+
['Shmem', pagealign(totalkb * (rnd(5, 12) / 1000))],
|
|
721
|
+
['KReclaimable', sreclaimable], ['Slab', slab],
|
|
722
|
+
['SReclaimable', sreclaimable],
|
|
723
|
+
['SUnreclaim', pagealign(slab - sreclaimable)],
|
|
724
|
+
['KernelStack', pagealign(vcpus * 320)],
|
|
725
|
+
['PageTables', pagealign(totalkb * (rnd(30, 60) / 10000))],
|
|
726
|
+
['SecPageTables', pagealign(totalkb * (rnd(8, 16) / 10000))],
|
|
727
|
+
['NFS_Unstable', 0], ['Bounce', 0], ['WritebackTmp', 0],
|
|
728
|
+
['CommitLimit', commitlimit], ['Committed_AS', committedas],
|
|
729
|
+
['VmallocTotal', 34359738367],
|
|
730
|
+
['VmallocUsed', pagealign(rnd(49152, 131072))], ['VmallocChunk', 0],
|
|
731
|
+
['Percpu', pagealign(vcpus * rnd(20480, 40960))],
|
|
732
|
+
['HardwareCorrupted', 0], ['AnonHugePages', anonhuge],
|
|
733
|
+
['ShmemHugePages', 0], ['ShmemPmdMapped', 0], ['FileHugePages', 0],
|
|
734
|
+
['FilePmdMapped', 0], ['Unaccepted', 0], ['HugePages_Total', 0],
|
|
735
|
+
['HugePages_Free', 0], ['HugePages_Rsvd', 0], ['HugePages_Surp', 0],
|
|
736
|
+
['Hugepagesize', 2048], ['Hugetlb', 0],
|
|
737
|
+
['DirectMap4k', pagealign(rnd(131072, 262144))],
|
|
738
|
+
['DirectMap2M', directmap2m], ['DirectMap1G', directmap1g],
|
|
739
|
+
];
|
|
740
|
+
return rows
|
|
741
|
+
.map(([key, value]) => `${key}:${String(value).padStart(24 - key.length - 1)} kB`)
|
|
742
|
+
.join('\n');
|
|
743
|
+
}
|
|
744
|
+
/** converts a kb value to the largest human unit for free -h. */
|
|
745
|
+
function human(kb) {
|
|
746
|
+
if (kb >= 1024 * 1024) {
|
|
747
|
+
const gb = kb / (1024 * 1024);
|
|
748
|
+
return `${gb >= 100 ? Math.round(gb) : gb.toFixed(1)}Gi`;
|
|
749
|
+
}
|
|
750
|
+
if (kb >= 1024) {
|
|
751
|
+
return `${(kb / 1024).toFixed(1)}Mi`;
|
|
752
|
+
}
|
|
753
|
+
return `${Math.round(kb)}Ki`;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* renders the free -h table derived from a memoized meminfo snapshot so
|
|
757
|
+
* cat /proc/meminfo and free -h always agree inside one sandbox.
|
|
758
|
+
*
|
|
759
|
+
* @param {string} meminfotext a payload previously returned by meminfo().
|
|
760
|
+
* @returns {string} the free -h table.
|
|
761
|
+
*/
|
|
762
|
+
export function freeh(meminfotext) {
|
|
763
|
+
const map = {};
|
|
764
|
+
for (const row of meminfotext.split('\n')) {
|
|
765
|
+
const match = /^([A-Za-z_()]+):\s*(\d+)\skB$/.exec(row);
|
|
766
|
+
if (match !== null) {
|
|
767
|
+
map[match[1]] = Number(match[2]);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
const total = map.MemTotal ?? 0;
|
|
771
|
+
const freekb = map.MemFree ?? 0;
|
|
772
|
+
const buffers = map.Buffers ?? 0;
|
|
773
|
+
const cached = (map.Cached ?? 0) + (map.SReclaimable ?? 0);
|
|
774
|
+
const available = map.MemAvailable ?? 0;
|
|
775
|
+
const used = Math.max(0, total - freekb - buffers - cached);
|
|
776
|
+
const swap = map.SwapTotal ?? 0;
|
|
777
|
+
const swapfreekb = map.SwapFree ?? 0;
|
|
778
|
+
const swapused = swap - swapfreekb;
|
|
779
|
+
const w = (text) => padstart(String(text), 13);
|
|
780
|
+
return [
|
|
781
|
+
`${w('total')}${w('used')}${w('free')}${w('shared')}${w('buff/cache')}${w('available')}`,
|
|
782
|
+
['Mem:', w(human(total)), w(human(used)), w(human(freekb)),
|
|
783
|
+
w(human(map.Shmem ?? 0)), w(human(buffers + cached)), w(human(available))].join(''),
|
|
784
|
+
['Swap:', w(human(swap)), w(human(swapused)), w(human(swapfreekb))].join(''),
|
|
785
|
+
].join('\n');
|
|
786
|
+
}
|
|
787
|
+
/* ------------------------------------------------------------------ */
|
|
788
|
+
/* context: smiformat — the exact 89-char summary table */
|
|
789
|
+
/* ------------------------------------------------------------------ */
|
|
790
|
+
const smiwidth = 89;
|
|
791
|
+
const smicolumns = [40, 24, 21];
|
|
792
|
+
const procwidth = 75;
|
|
793
|
+
const smidriver = '575.57.08';
|
|
794
|
+
const cudaversion = '12.9';
|
|
795
|
+
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
796
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
797
|
+
/** formats the nvidia-smi timestamp "weekday month day hh:mm:ss year". */
|
|
798
|
+
function formatstamp(date) {
|
|
799
|
+
const two = (value) => String(value).padStart(2, '0');
|
|
800
|
+
return `${weekdays[date.getDay()]} ${months[date.getMonth()]} ${two(date.getDate())} ${two(date.getHours())}:${two(date.getMinutes())}:${two(date.getSeconds())} ${date.getFullYear()}`;
|
|
801
|
+
}
|
|
802
|
+
/** full-width border line: "+" plus 87 fill characters plus "+". */
|
|
803
|
+
function smiborder(fill) {
|
|
804
|
+
return `+${fill.repeat(smiwidth - 2)}+`;
|
|
805
|
+
}
|
|
806
|
+
/** full-width content line padded to the 89-char table width. */
|
|
807
|
+
function smifull(content) {
|
|
808
|
+
return `|${pad(content, smiwidth - 2)}|`;
|
|
809
|
+
}
|
|
810
|
+
/** three-column separator: pipes at fixed offsets 1, 42 and 67. */
|
|
811
|
+
function smisep(fill) {
|
|
812
|
+
return `|${fill.repeat(smicolumns[0])}+${fill.repeat(smicolumns[1])}+${fill.repeat(smicolumns[2])}|`;
|
|
813
|
+
}
|
|
814
|
+
/** three-column data row within the 89-char width. */
|
|
815
|
+
function smirow(left, mid, right) {
|
|
816
|
+
return `|${pad(left, smicolumns[0])}|${pad(mid, smicolumns[1])}|${pad(right, smicolumns[2])}|`;
|
|
817
|
+
}
|
|
818
|
+
/** process table content line within the 75-char width. */
|
|
819
|
+
function procline(content) {
|
|
820
|
+
return `|${pad(content, procwidth - 2)}|`;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* renders the complete virtual nvidia-smi adapter report; the summary
|
|
824
|
+
* table is exactly 89 characters wide and the header pins driver
|
|
825
|
+
* 575.57.08 and cuda 12.9 exactly as the native smiadapterbuilder does.
|
|
826
|
+
* when a mig profile is active the memory column reflects the slice size
|
|
827
|
+
* and a mig line joins the table.
|
|
828
|
+
*
|
|
829
|
+
* @param {string} gpu gpu id or name present in gpudata.
|
|
830
|
+
* @param {string} [migprofile='off'] one of 1g.24gb, 2g.48gb, 4g.96gb or off.
|
|
831
|
+
* @returns {string} the full report text.
|
|
832
|
+
*/
|
|
833
|
+
export function nvidiaSmiTable(gpu, migprofile = 'off') {
|
|
834
|
+
const spec = getgpu(gpu);
|
|
835
|
+
if (spec === undefined) {
|
|
836
|
+
throw new Error(`unknown gpu "${gpu}"; known gpus: ${gpudata.map((entry) => entry.id).join(', ')}`);
|
|
837
|
+
}
|
|
838
|
+
const mig = getmig(migprofile);
|
|
839
|
+
const vrammib = mig !== null ? mig.slicegb * 1024 : spec.smireportedmib;
|
|
840
|
+
const memusedmib = rnd(0, 24);
|
|
841
|
+
const util = rnd(0, 3);
|
|
842
|
+
const temperature = rnd(38, 47);
|
|
843
|
+
const fanspeed = rnd(24, 38);
|
|
844
|
+
const powerw = Math.max(10, Math.round(spec.tdpwatts * 0.04));
|
|
845
|
+
const head = ` NVIDIA-SMI ${smidriver} ${' '.repeat(13)}Driver Version: ${smidriver}${' '.repeat(6)}CUDA Version: ${cudaversion}`;
|
|
846
|
+
const lines = [
|
|
847
|
+
formatstamp(new Date()),
|
|
848
|
+
smiborder('-'),
|
|
849
|
+
smifull(head),
|
|
850
|
+
smisep('-'),
|
|
851
|
+
smirow(`${pad(' GPU Name', 27)}${pad('Persistence-M', 13)}`, pad(' Bus-Id Disp.A', 24), ` ${padstart('Volatile Uncorr. ECC', 20)}`),
|
|
852
|
+
smirow(pad(' Fan Temp Perf Pwr:Usage/Cap', 40), pad(' Memory-Usage', 24), ` ${padstart('GPU-Util Compute M.', 20)}`),
|
|
853
|
+
smisep('='),
|
|
854
|
+
smirow(`${pad(` 0 ${spec.name}`, 36)}${pad('Off', 4)}`, pad(' 00000000:01:00.0 Off', 24), ` ${padstart('N/A', 20)}`),
|
|
855
|
+
smirow(pad(` ${fanspeed}% ${temperature}C P0 ${powerw}W / ${spec.tdpwatts}W`, 40), padstart(`${memusedmib}MiB / ${vrammib}MiB`, 24), `${padstart(`${util}%`, 10)}${pad(' Default', 11)}`),
|
|
856
|
+
];
|
|
857
|
+
if (mig !== null) {
|
|
858
|
+
lines.push(smifull(` MIG: enabled, profile ${mig.id}, ${mig.maxinstances} instance(s) of ${mig.slicegb} GB`));
|
|
859
|
+
}
|
|
860
|
+
lines.push(smirow('', '', ''), smiborder('-'), '', 'Processes:', procline(`${pad(' GPU GI CI', 20)}${pad('PID', 7)}${pad('Type', 8)}${pad('Process name', 27)}${pad('GPU Memory', 11)}`), procline('='.repeat(procwidth - 2)));
|
|
861
|
+
if (mig !== null) {
|
|
862
|
+
for (let instance = 0; instance < mig.maxinstances; instance += 1) {
|
|
863
|
+
lines.push(procline(`${pad(` 0 ${instance} 0`, 20)}${pad(String(1200 + instance), 7)}${pad('MIG', 8)}${pad('mig-worker', 27)}${pad(`${rnd(0, 512)}MiB`, 11)}`));
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
lines.push(procline(' No running processes found'));
|
|
868
|
+
}
|
|
869
|
+
lines.push(`+${'-'.repeat(procwidth - 2)}+`);
|
|
870
|
+
return lines.join('\n');
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* renders the nvidia-smi -l device listing; with an active mig profile
|
|
874
|
+
* the mig device lines follow the parent gpu line.
|
|
875
|
+
*
|
|
876
|
+
* @param {string} gpu gpu id or name present in gpudata.
|
|
877
|
+
* @param {string} [migprofile='off'] mig profile id or off.
|
|
878
|
+
* @returns {string} the -l listing.
|
|
879
|
+
*/
|
|
880
|
+
export function nvidiaSmiList(gpu, migprofile = 'off') {
|
|
881
|
+
const spec = getgpu(gpu);
|
|
882
|
+
if (spec === undefined) {
|
|
883
|
+
throw new Error(`unknown gpu "${gpu}"`);
|
|
884
|
+
}
|
|
885
|
+
const mig = getmig(migprofile);
|
|
886
|
+
const lines = [`GPU 0: ${spec.name} (UUID: ${token('GPU-')})`];
|
|
887
|
+
if (mig !== null) {
|
|
888
|
+
for (let instance = 0; instance < mig.maxinstances; instance += 1) {
|
|
889
|
+
lines.push(` MIG ${mig.id} Device ${instance}: (UUID: ${token('MIG-')})`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return lines.join('\n');
|
|
893
|
+
}
|
|
894
|
+
/* ------------------------------------------------------------------ */
|
|
895
|
+
/* context: mesa stack summaries */
|
|
896
|
+
/* ------------------------------------------------------------------ */
|
|
897
|
+
/** the software rendering stack pinned from primary sources. */
|
|
898
|
+
const mesastack = {
|
|
899
|
+
mesa: '26.2.1', llvm: '22.1.8', gl: '4.6', gles: '3.2', vulkan: '1.4',
|
|
900
|
+
opencl: '3.1',
|
|
901
|
+
};
|
|
902
|
+
/**
|
|
903
|
+
* renders the clinfo summary for the rusticl platform on llvmpipe: opencl
|
|
904
|
+
* 3.1 (same-day spec support in mesa 26.2), fp16 default since 25.2 and
|
|
905
|
+
* fp64 through rusticl_features.
|
|
906
|
+
*
|
|
907
|
+
* @param {number} [vcpus=8] vcpu count reported as compute units.
|
|
908
|
+
* @returns {string} the clinfo summary block.
|
|
909
|
+
*/
|
|
910
|
+
export function clinfoSummary(vcpus = 8) {
|
|
911
|
+
const units = Math.max(1, Math.min(vcpus, 32));
|
|
912
|
+
return [
|
|
913
|
+
'Number of platforms 1',
|
|
914
|
+
' Platform Name rusticl',
|
|
915
|
+
' Platform Vendor Mesa/X.org',
|
|
916
|
+
' Platform Version OpenCL 3.1',
|
|
917
|
+
' Platform Profile FULL_PROFILE',
|
|
918
|
+
' Platform Extensions cl_khr_fp64 cl_khr_fp16 cl_khr_icd cl_khr_command_buffer',
|
|
919
|
+
'Number of devices 1',
|
|
920
|
+
' Device Name llvmpipe (CPU)',
|
|
921
|
+
' Device Vendor Mesa/X.org',
|
|
922
|
+
' Device Vendor ID 0x10005 (Mesa)',
|
|
923
|
+
' Device Version OpenCL 3.1',
|
|
924
|
+
' Device Type GPU',
|
|
925
|
+
` Max compute units ${units}`,
|
|
926
|
+
' Max work item dimensions 3',
|
|
927
|
+
' Max work group size 1024',
|
|
928
|
+
' Preferred vector width char/int/float 16/16/16',
|
|
929
|
+
' Max clock frequency 3700MHz',
|
|
930
|
+
' Address bits 64',
|
|
931
|
+
' Half precision support cl_khr_fp16 (default since mesa 25.2)',
|
|
932
|
+
' Double precision support cl_khr_fp64 (rusticl_features=fp64)',
|
|
933
|
+
' Device available Yes',
|
|
934
|
+
' Compiler available Yes',
|
|
935
|
+
' SPIR versions 1.2',
|
|
936
|
+
].join('\n');
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* renders the vulkaninfo --summary block for lavapipe: vulkan 1.4 surface
|
|
940
|
+
* since mesa 25.1, khronos-conformant on the vulkan 1.3 cts, selected
|
|
941
|
+
* through vk_driver_files (vk_icd_filenames is deprecated).
|
|
942
|
+
*
|
|
943
|
+
* @returns {string} the vulkan summary block.
|
|
944
|
+
*/
|
|
945
|
+
export function vulkanSummary() {
|
|
946
|
+
return [
|
|
947
|
+
'==========',
|
|
948
|
+
'VULKANINFO',
|
|
949
|
+
'==========',
|
|
950
|
+
'',
|
|
951
|
+
'Instance Extensions: count=10',
|
|
952
|
+
'VK_KHR_surface : extension revision 25',
|
|
953
|
+
'VK_KHR_get_physical_device_properties2 : extension revision 2',
|
|
954
|
+
'VK_EXT_headless_surface : extension revision 1',
|
|
955
|
+
'',
|
|
956
|
+
'API Version: 1.4.313',
|
|
957
|
+
'',
|
|
958
|
+
'GPU0:',
|
|
959
|
+
' apiVersion = 1.4.313',
|
|
960
|
+
' driverVersion = 26.2.1',
|
|
961
|
+
' vendorID = 0x10005',
|
|
962
|
+
' deviceID = 0x0000',
|
|
963
|
+
' deviceType = CPU',
|
|
964
|
+
' deviceName = llvmpipe (CPU (x86_64))',
|
|
965
|
+
' driverName = mesa_llvmpipe',
|
|
966
|
+
` driverInfo = Mesa ${mesastack.mesa} (LLVM ${mesastack.llvm})`,
|
|
967
|
+
' conformantVersion = 1.3.0.0',
|
|
968
|
+
'',
|
|
969
|
+
'Device Extensions: count=204',
|
|
970
|
+
'VK_KHR_8bit_storage VK_KHR_16bit_storage VK_KHR_shader_float16_int8',
|
|
971
|
+
'VK_KHR_spirv_1_4 VK_KHR_storage_buffer_storage_class',
|
|
972
|
+
].join('\n');
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* renders the glxinfo header block for llvmpipe: opengl 4.6 core profile
|
|
976
|
+
* 100% (161/161 extensions tracked by mesamatrix) and the llvmpipe
|
|
977
|
+
* renderer string with the llvm version.
|
|
978
|
+
*
|
|
979
|
+
* @returns {string} the glxinfo block.
|
|
980
|
+
*/
|
|
981
|
+
export function glxinfoSummary() {
|
|
982
|
+
return [
|
|
983
|
+
'name of display: :0',
|
|
984
|
+
'display: :0 screen: 0',
|
|
985
|
+
'direct rendering: Yes',
|
|
986
|
+
'server glx vendor string: SGI',
|
|
987
|
+
'client glx vendor string: Mesa Project and SGI',
|
|
988
|
+
'OpenGL vendor string: Mesa',
|
|
989
|
+
`OpenGL renderer string: llvmpipe (LLVM ${mesastack.llvm}, 256 bits)`,
|
|
990
|
+
`OpenGL core profile version string: ${mesastack.gl} (Core Profile) Mesa ${mesastack.mesa}`,
|
|
991
|
+
'OpenGL core profile shading language version string: 4.60',
|
|
992
|
+
'',
|
|
993
|
+
'OpenGL core profile context flags: (none)',
|
|
994
|
+
'OpenGL core profile profile mask: core profile',
|
|
995
|
+
'',
|
|
996
|
+
'OpenGL core profile extensions: 161',
|
|
997
|
+
`OpenGL version string: ${mesastack.gl} (Compatibility Profile) Mesa ${mesastack.mesa}`,
|
|
998
|
+
'OpenGL shading language version string: 4.60',
|
|
999
|
+
'OpenGL context flags: (none)',
|
|
1000
|
+
'',
|
|
1001
|
+
'Extended renderer info (GLX_MESA_query_renderer):',
|
|
1002
|
+
' Vendor: Mesa/X.org (0x1002)',
|
|
1003
|
+
` Device: llvmpipe (LLVM ${mesastack.llvm}, 256 bits) (0xffffffff)`,
|
|
1004
|
+
` Version: ${mesastack.mesa}`,
|
|
1005
|
+
' Accelerated: no',
|
|
1006
|
+
' Video memory: 32768MB',
|
|
1007
|
+
' Unified memory: yes',
|
|
1008
|
+
' Texture from pixmap: yes',
|
|
1009
|
+
].join('\n');
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* assembles the validated mesa environment exactly as mesaenvbuilder +
|
|
1013
|
+
* renderenv('opengl') do: llvmpipe forced, gl 4.6, glsl 460, vector width
|
|
1014
|
+
* 512 to exercise the avx-512 paths, lavapipe through vk_driver_files and
|
|
1015
|
+
* rusticl opencl 3.1 with the fp64 feature.
|
|
1016
|
+
*
|
|
1017
|
+
* @param {number} [vcpus=8] vcpu count used for lp_num_threads (0 uses
|
|
1018
|
+
* every core up to the lp_max_threads=32 ceiling).
|
|
1019
|
+
* @returns {Record<string, string>} the environment record.
|
|
1020
|
+
*/
|
|
1021
|
+
export function mesaenv(vcpus = 8) {
|
|
1022
|
+
const threads = vcpus <= 0 ? 0 : Math.min(vcpus, 32);
|
|
1023
|
+
return {
|
|
1024
|
+
LIBGL_ALWAYS_SOFTWARE: 'true',
|
|
1025
|
+
GALLIUM_DRIVER: 'llvmpipe',
|
|
1026
|
+
MESA_GL_VERSION_OVERRIDE: mesastack.gl,
|
|
1027
|
+
MESA_GLSL_VERSION_OVERRIDE: '460',
|
|
1028
|
+
MESA_GLES_VERSION_OVERRIDE: mesastack.gles,
|
|
1029
|
+
MESA_VK_VERSION_OVERRIDE: mesastack.vulkan,
|
|
1030
|
+
LP_NUM_THREADS: String(threads),
|
|
1031
|
+
LP_NATIVE_VECTOR_WIDTH: '512',
|
|
1032
|
+
MESA_NO_ERROR: '1',
|
|
1033
|
+
VK_DRIVER_FILES: '/usr/share/vulkan/icd.d/lvp_icd.x86_64.json',
|
|
1034
|
+
MESA_VK_WSI_HEADLESS_SWAPCHAIN: '1',
|
|
1035
|
+
RUSTICL_ENABLE: 'llvmpipe',
|
|
1036
|
+
RUSTICL_DEVICE_TYPE: 'gpu',
|
|
1037
|
+
RUSTICL_CL_VERSION: mesastack.opencl,
|
|
1038
|
+
RUSTICL_FEATURES: 'fp64',
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
/* ------------------------------------------------------------------ */
|
|
1042
|
+
/* context: bootsequence — dmesg-style boot lines */
|
|
1043
|
+
/* ------------------------------------------------------------------ */
|
|
1044
|
+
/**
|
|
1045
|
+
* renders the firecracker-style boot log for a sandbox: kernel banner,
|
|
1046
|
+
* 125 ms microvm bring-up, memory sizing, vcpu online lines, the mesa
|
|
1047
|
+
* software stack bring-up and the virtual gpu identity binding. every
|
|
1048
|
+
* line carries the kernel [ 0.000000] timestamp style.
|
|
1049
|
+
*
|
|
1050
|
+
* @param {string} model cpu model name present in cpudata.
|
|
1051
|
+
* @param {number} vcpus logical processor count.
|
|
1052
|
+
* @param {number} [ramgb=32] guest ram in gb.
|
|
1053
|
+
* @param {string} [gpu='rtx5090'] gpu id present in gpudata.
|
|
1054
|
+
* @param {number} [quota=16777216] persistent workspace quota in bytes.
|
|
1055
|
+
* @returns {string[]} ordered dmesg lines.
|
|
1056
|
+
*/
|
|
1057
|
+
export function bootSequence(model, vcpus, ramgb = 32, gpu = 'rtx5090', quota = 16 * 1024 * 1024) {
|
|
1058
|
+
const spec = getcpu(model) ?? cpudata[0];
|
|
1059
|
+
const gpuspec = getgpu(gpu) ?? gpudata[0];
|
|
1060
|
+
const topology = solvetopology(spec, vcpus);
|
|
1061
|
+
const arch = spec.arch === 'arm64' ? 'aarch64' : 'x86_64';
|
|
1062
|
+
const stamp = (seconds) => `[${seconds.toFixed(6).padStart(12)}]`;
|
|
1063
|
+
const onlinetime = 0.039 + 0.00042 * topology.vcpus;
|
|
1064
|
+
return [
|
|
1065
|
+
`${stamp(0)} Linux version 6.12.0-saddle (root@saddle) (gcc 15.1.0, ld 2.44) #1 SMP PREEMPT_DYNAMIC ${arch}`,
|
|
1066
|
+
`${stamp(0.000049)} Command line: root=/dev/vda rw console=ttyS0 reboot=k panic=1 pci=off i8042.noaux i8042.nomux i8042.nopnp i8042.dumbkbd`,
|
|
1067
|
+
`${stamp(0.004201)} BIOS-provided physical RAM map (virtio-mem + memfd hugetlb backend)`,
|
|
1068
|
+
`${stamp(0.008913)} Memory: ${pagealign(ramgb * 1024 * 1024 - 131072)}K available (${ramgb} GB guest plan, 4 kB pages)`,
|
|
1069
|
+
`${stamp(0.012451)} firecracker: microvm vm_state=Running boot_time=125ms vmm=fc v1.16.1`,
|
|
1070
|
+
`${stamp(0.02133)} virtio_blk virtio1: [vda] 8589934592 512-byte logical blocks (8.6 GB)`,
|
|
1071
|
+
`${stamp(0.038774)} smpboot: ${topology.coresonline} cores, ${topology.vcpus} threads online (${spec.model})`,
|
|
1072
|
+
`${stamp(0.039001)} cpu cpu0: ${spec.microarch}, boost ${spec.boostclockmhz} MHz, l3 ${spec.l3mb} MB`,
|
|
1073
|
+
`${stamp(onlinetime)} smp: brought up ${topology.vcpus} cpus, cpu mask 0-${topology.vcpus - 1}`,
|
|
1074
|
+
`${stamp(onlinetime + 0.0148)} NUMA: ${Math.max(1, Math.round(topology.vcpus / 64))} node(s), interleave enabled`,
|
|
1075
|
+
`${stamp(onlinetime + 0.0283)} hugepage: 2048 kB pages reserved, thp=always`,
|
|
1076
|
+
`${stamp(onlinetime + 0.0422)} docker: engine 29.7.2 ready, containerd 2.3.1, memswap=-1 shm=2g`,
|
|
1077
|
+
`${stamp(onlinetime + 0.0551)} qemu-system-${arch}: bridge 11.1.0 attached (qmp unix:/run/saddle/qemu-0.sock)`,
|
|
1078
|
+
`${stamp(onlinetime + 0.0784)} mesa ${mesastack.mesa}: llvmpipe rasterizer, lp_num_threads=${Math.min(topology.vcpus, 32)}, lp_max_threads=32`,
|
|
1079
|
+
`${stamp(onlinetime + 0.0789)} llvmpipe: avx-512 vector width 512 selected by gallivm (llvm ${mesastack.llvm})`,
|
|
1080
|
+
`${stamp(onlinetime + 0.0812)} lavapipe: vulkan ${mesastack.vulkan}, icd lvp_icd.${arch}.json, wsi headless`,
|
|
1081
|
+
`${stamp(onlinetime + 0.0837)} rusticl: opencl ${mesastack.opencl} on llvmpipe, features fp64 (fp16 default since 25.2)`,
|
|
1082
|
+
`${stamp(onlinetime + 0.091)} virtualgpu: identity ${gpuspec.name} [${gpuspec.pcivendor}:${gpuspec.pcidevice}] ${gpuspec.memtype} ${gpuspec.vrammib / 1024} GB`,
|
|
1083
|
+
`${stamp(onlinetime + 0.0922)} nvml shim: FAKE_MODEL="${gpuspec.name}" FAKE_VRAM=${gpuspec.smireportedmib} (fake-nvidia-smi adapter ready)`,
|
|
1084
|
+
`${stamp(onlinetime + 0.1045)} saddle: virtual hardware engine v2.0.127, sandbox created -> running`,
|
|
1085
|
+
`${stamp(onlinetime + 0.1047)} saddle: persistent workspace: ${humanbytes(quota)} quota, data stays with the sandbox id`,
|
|
1086
|
+
`${stamp(onlinetime + 0.1046)} Freeing unused kernel image memory`,
|
|
1087
|
+
];
|
|
1088
|
+
}
|
|
1089
|
+
/* ------------------------------------------------------------------ */
|
|
1090
|
+
/* context: sandboxstate and dispatcher */
|
|
1091
|
+
/* ------------------------------------------------------------------ */
|
|
1092
|
+
/**
|
|
1093
|
+
* creates the state object shared by the browser terminal and the api
|
|
1094
|
+
* exec endpoint: resolved specs, a stable hostname, the boot timestamp and
|
|
1095
|
+
* a memoized meminfo snapshot so memory reports stay consistent within
|
|
1096
|
+
* one sandbox session.
|
|
1097
|
+
*
|
|
1098
|
+
* @param {{model: string, vcpus: number, ramgb: number, gpu: string,
|
|
1099
|
+
* mig?: string, id?: string}} spec the sandbox specification.
|
|
1100
|
+
* @returns {object} the sandbox state consumed by dispatch.
|
|
1101
|
+
*/
|
|
1102
|
+
export function createSandboxState(spec) {
|
|
1103
|
+
// the reviewed catalog is the only source of processor identity; ram
|
|
1104
|
+
// accepts any user chosen plan up to the 18 tb virtual ceiling.
|
|
1105
|
+
const cpu = getcpu(spec.model) ?? cpudata[0];
|
|
1106
|
+
const gpu = getgpu(spec.gpu) ?? gpudata[0];
|
|
1107
|
+
const topology = solvetopology(cpu, spec.vcpus ?? 8);
|
|
1108
|
+
const ramgb = Math.max(1, Math.min(Math.round(spec.ramgb ?? 32), 18432));
|
|
1109
|
+
const id = spec.id ?? token('sb-');
|
|
1110
|
+
return {
|
|
1111
|
+
model: cpu.model,
|
|
1112
|
+
cpuspec: cpu,
|
|
1113
|
+
gpuspec: gpu,
|
|
1114
|
+
vcpus: topology.vcpus,
|
|
1115
|
+
ramgb,
|
|
1116
|
+
gpu: gpu.id,
|
|
1117
|
+
mig: getmig(spec.mig) === null ? 'off' : spec.mig,
|
|
1118
|
+
id,
|
|
1119
|
+
hostname: `saddle-${id.replace(/^sb-/, '').slice(0, 8)}`,
|
|
1120
|
+
boottime: Date.now(),
|
|
1121
|
+
kernel: cpu.arch === 'arm64' ? '6.12.0-saddle-aarch64' : '6.12.0-saddle',
|
|
1122
|
+
memsnapshot: meminfo(ramgb, { vcpus: topology.vcpus }),
|
|
1123
|
+
history: [],
|
|
1124
|
+
files: new Map(),
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
/** the supported command list, reused by help and the terminal. */
|
|
1128
|
+
export const commands = [
|
|
1129
|
+
'help', 'lscpu', 'cat /proc/cpuinfo', 'cat /proc/meminfo', 'free -h',
|
|
1130
|
+
'nvidia-smi', 'nvidia-smi -L', 'clinfo', 'vulkaninfo --summary',
|
|
1131
|
+
'glxinfo', 'uname -a', 'ls /etc/virtual', 'env', 'docker --version',
|
|
1132
|
+
'qemu-system-x86_64 --version', 'uptime', 'whoami', 'neofetch', 'clear',
|
|
1133
|
+
'echo', 'echo <text> > <file>', 'echo <text> >> <file>', 'touch <file>',
|
|
1134
|
+
'cat <file>', 'ls', 'rm <file>', 'df -h', 'stat <file>', 'pwd',
|
|
1135
|
+
'history', 'streaming', 'man <command>',
|
|
1136
|
+
];
|
|
1137
|
+
/** renders the help text with every supported command. */
|
|
1138
|
+
function helpText() {
|
|
1139
|
+
const rows = [
|
|
1140
|
+
['help', 'this command list'],
|
|
1141
|
+
['lscpu', 'virtual processor topology summary'],
|
|
1142
|
+
['cat /proc/cpuinfo', 'full procfs cpuinfo payload'],
|
|
1143
|
+
['cat /proc/meminfo', 'full procfs meminfo payload'],
|
|
1144
|
+
['free -h', 'human readable memory table'],
|
|
1145
|
+
['nvidia-smi', 'virtual gpu adapter report (89-char table)'],
|
|
1146
|
+
['nvidia-smi -L', 'virtual gpu device listing'],
|
|
1147
|
+
['clinfo', 'rusticl opencl 3.1 summary on llvmpipe'],
|
|
1148
|
+
['vulkaninfo --summary', 'lavapipe vulkan 1.4 summary'],
|
|
1149
|
+
['glxinfo', 'llvmpipe opengl 4.6 summary'],
|
|
1150
|
+
['uname -a', 'kernel and architecture line'],
|
|
1151
|
+
['ls /etc/virtual', 'virtual hardware definition files'],
|
|
1152
|
+
['env', 'mesa llvmpipe environment variables'],
|
|
1153
|
+
['docker --version', 'docker engine version'],
|
|
1154
|
+
['qemu-system-x86_64 --version', 'qemu bridge version'],
|
|
1155
|
+
['uptime', 'sandbox uptime and load average'],
|
|
1156
|
+
['whoami', 'current user'],
|
|
1157
|
+
['neofetch', 'saddle ascii logo and sandbox specs'],
|
|
1158
|
+
['clear', 'clear the terminal'],
|
|
1159
|
+
['echo <text>', 'print text'],
|
|
1160
|
+
['echo <text> > <file>', 'write text to a workspace file'],
|
|
1161
|
+
['echo <text> >> <file>', 'append text to a workspace file'],
|
|
1162
|
+
['touch <file>', 'create an empty workspace file'],
|
|
1163
|
+
['cat <file>', 'print a persistent workspace file'],
|
|
1164
|
+
['ls', 'list the workspace root with sizes and dates'],
|
|
1165
|
+
['rm <file>', 'delete one workspace file'],
|
|
1166
|
+
['df -h', 'workspace quota usage as a filesystem table'],
|
|
1167
|
+
['stat <file>', 'size and timestamps of one workspace file'],
|
|
1168
|
+
['pwd', 'print the working directory (always /)'],
|
|
1169
|
+
['history', 'streaming', 'the last 50 commands of this session'],
|
|
1170
|
+
['man <command>', 'short manual for one command'],
|
|
1171
|
+
];
|
|
1172
|
+
return rows.map(([name, about]) => ` ${pad(name, 32)}${about}`).join('\n');
|
|
1173
|
+
}
|
|
1174
|
+
/** renders the neofetch block: the saddle ascii logo beside sandbox specs. */
|
|
1175
|
+
function neofetch(state) {
|
|
1176
|
+
const cpu = state.cpuspec;
|
|
1177
|
+
const gpu = state.gpuspec;
|
|
1178
|
+
const logo = [
|
|
1179
|
+
' ``-://////:-`` ',
|
|
1180
|
+
' ./+++++++++++++/-. ',
|
|
1181
|
+
' -+++++++++++++++++++-` ',
|
|
1182
|
+
' ./+++ saddle sandbox ++/. ',
|
|
1183
|
+
' -+++++++++++++++++++++-` ',
|
|
1184
|
+
' `-:/+++++++++++++/-. ',
|
|
1185
|
+
' .-://////:-. ',
|
|
1186
|
+
' ',
|
|
1187
|
+
];
|
|
1188
|
+
const uptime = Math.max(1, Math.round((Date.now() - state.boottime) / 1000));
|
|
1189
|
+
const mins = Math.floor(uptime / 60);
|
|
1190
|
+
const hours = Math.floor(mins / 60);
|
|
1191
|
+
const uptimeText = hours > 0 ? `${hours} hour(s), ${mins % 60} min` : `${mins} min`;
|
|
1192
|
+
const total = pagealign(state.ramgb * 1024 * 1024 - 131072);
|
|
1193
|
+
const mig = getmig(state.mig);
|
|
1194
|
+
const info = [
|
|
1195
|
+
`root@${state.hostname}`,
|
|
1196
|
+
'-----------------',
|
|
1197
|
+
'OS: saddle linux (virtual hardware engine v2.0.127)',
|
|
1198
|
+
'Host: firecracker microvm (125 ms boot)',
|
|
1199
|
+
`Kernel: ${state.kernel}`,
|
|
1200
|
+
`Uptime: ${uptimeText}`,
|
|
1201
|
+
'Packages: 412 (dpkg), 8 (snap)',
|
|
1202
|
+
'Shell: bash 5.2.37',
|
|
1203
|
+
`CPU: ${cpu.displayname} (${state.vcpus}) @ ${cpu.boostclockmhz}.00MHz`,
|
|
1204
|
+
`GPU: ${gpu.name} [${gpu.pcivendor}:${gpu.pcidevice}]`,
|
|
1205
|
+
`Memory: ${human(Math.round(total * 0.31))} / ${human(total)}`,
|
|
1206
|
+
`vRAM: ${gpu.memtype} ${gpu.vrammib / 1024} GB${mig !== null ? ` (MIG ${mig.id})` : ''}`,
|
|
1207
|
+
'Renderer: llvmpipe (LLVM 22.1.8) / lavapipe 1.4 / rusticl 3.1',
|
|
1208
|
+
];
|
|
1209
|
+
const rows = Math.max(logo.length, info.length);
|
|
1210
|
+
const out = [];
|
|
1211
|
+
for (let index = 0; index < rows; index += 1) {
|
|
1212
|
+
out.push(`${(logo[index] ?? '').padEnd(30)}${info[index] ?? ''}`);
|
|
1213
|
+
}
|
|
1214
|
+
return out.join('\n');
|
|
1215
|
+
}
|
|
1216
|
+
/** renders the uptime line with a load average derived from the vcpus. */
|
|
1217
|
+
function uptimeLine(state) {
|
|
1218
|
+
const totalSeconds = Math.max(0, Math.floor((Date.now() - state.boottime) / 1000));
|
|
1219
|
+
const now = new Date();
|
|
1220
|
+
const mins = Math.max(1, Math.round(totalSeconds / 60));
|
|
1221
|
+
const load = (decay) => (Math.random() * state.vcpus * 0.18 * Math.exp(-decay / 30)).toFixed(2);
|
|
1222
|
+
return ` ${now.toTimeString().slice(0, 8)} up ${mins} min, 1 user, load average: ${load(1)}, ${load(5)}, ${load(15)}`;
|
|
1223
|
+
}
|
|
1224
|
+
/** renders the env listing: the mesa stack plus the sandbox variables. */
|
|
1225
|
+
function envText(state) {
|
|
1226
|
+
const mig = getmig(state.mig);
|
|
1227
|
+
const entries = {
|
|
1228
|
+
...mesaenv(state.vcpus),
|
|
1229
|
+
FAKE_MODEL: state.gpuspec.name,
|
|
1230
|
+
FAKE_VRAM: String(mig !== null ? mig.slicegb * 1024 : state.gpuspec.smireportedmib),
|
|
1231
|
+
SADDLE_VERSION: '2.0.127',
|
|
1232
|
+
SADDLE_SANDBOX: state.id,
|
|
1233
|
+
SADDLE_STATE: 'running',
|
|
1234
|
+
SADDLE_CPU: state.model,
|
|
1235
|
+
SADDLE_GPU: state.gpu,
|
|
1236
|
+
SADDLE_MIG: state.mig,
|
|
1237
|
+
HOSTNAME: state.hostname,
|
|
1238
|
+
TERM: 'xterm-256color',
|
|
1239
|
+
SHELL: '/bin/bash',
|
|
1240
|
+
USER: 'root',
|
|
1241
|
+
HOME: '/root',
|
|
1242
|
+
PATH: '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
|
|
1243
|
+
LANG: 'C.UTF-8',
|
|
1244
|
+
};
|
|
1245
|
+
return Object.keys(entries)
|
|
1246
|
+
.sort()
|
|
1247
|
+
.map((key) => `${key}=${entries[key]}`)
|
|
1248
|
+
.join('\n');
|
|
1249
|
+
}
|
|
1250
|
+
/* ------------------------------------------------------------------ */
|
|
1251
|
+
/* context: virtualfs — the persistent workspace filesystem */
|
|
1252
|
+
/* ------------------------------------------------------------------ */
|
|
1253
|
+
/** the default workspace quota mirrored from the api contract. */
|
|
1254
|
+
const defaultquota = 16 * 1024 * 1024;
|
|
1255
|
+
/** the per-session history cap shown by the history command. */
|
|
1256
|
+
const historycap = 50;
|
|
1257
|
+
/**
|
|
1258
|
+
* normalizes one workspace path: the working directory is always the
|
|
1259
|
+
* sandbox root, so relative names gain the leading slash, duplicated
|
|
1260
|
+
* slashes collapse, trailing slashes drop and any traversal attempt
|
|
1261
|
+
* ("..") or oversized path is rejected.
|
|
1262
|
+
*
|
|
1263
|
+
* @param {string} input the raw path typed by the user.
|
|
1264
|
+
* @returns {{ok: true, path: string} | {ok: false, error: string}} the verdict.
|
|
1265
|
+
*/
|
|
1266
|
+
function normalizepath(input) {
|
|
1267
|
+
const raw = String(input ?? '').trim();
|
|
1268
|
+
if (raw.length === 0) {
|
|
1269
|
+
return { ok: false, error: 'empty path' };
|
|
1270
|
+
}
|
|
1271
|
+
if (raw.includes('..')) {
|
|
1272
|
+
return { ok: false, error: 'path traversal is not allowed' };
|
|
1273
|
+
}
|
|
1274
|
+
let path = raw.replace(/\/+/, '/');
|
|
1275
|
+
if (!path.startsWith('/')) {
|
|
1276
|
+
path = `/${path}`;
|
|
1277
|
+
}
|
|
1278
|
+
if (path.length > 1 && path.endsWith('/')) {
|
|
1279
|
+
path = path.slice(0, -1);
|
|
1280
|
+
}
|
|
1281
|
+
if (path.length > 128) {
|
|
1282
|
+
return { ok: false, error: 'path is too long (max 128 characters)' };
|
|
1283
|
+
}
|
|
1284
|
+
return { ok: true, path };
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* builds the fallback filesystem for contexts without callbacks (the
|
|
1288
|
+
* browser terminal): a per-state map that lives exactly as long as the
|
|
1289
|
+
* sandbox session and mirrors the {write, read, list, del} contract.
|
|
1290
|
+
*
|
|
1291
|
+
* @param {object} state a state built by createsandboxstate.
|
|
1292
|
+
* @returns {{write: Function, read: Function, list: Function, del: Function}}
|
|
1293
|
+
* the in-memory filesystem adapter.
|
|
1294
|
+
*/
|
|
1295
|
+
function memoryfs(state) {
|
|
1296
|
+
return {
|
|
1297
|
+
write(path, content) {
|
|
1298
|
+
const text = content === null || content === undefined ? '' : String(content);
|
|
1299
|
+
const size = new TextEncoder().encode(text).length;
|
|
1300
|
+
const updatedat = new Date().toISOString();
|
|
1301
|
+
state.files.set(path, { content: text, size, updatedat });
|
|
1302
|
+
return { path, size, updatedat };
|
|
1303
|
+
},
|
|
1304
|
+
read(path) {
|
|
1305
|
+
const entry = state.files.get(path);
|
|
1306
|
+
return entry === undefined ? null : { path, ...entry };
|
|
1307
|
+
},
|
|
1308
|
+
list() {
|
|
1309
|
+
return [...state.files.entries()]
|
|
1310
|
+
.map(([path, entry]) => ({ path, size: entry.size, updatedat: entry.updatedat }))
|
|
1311
|
+
.sort((a, b) => (a.path < b.path ? -1 : 1));
|
|
1312
|
+
},
|
|
1313
|
+
del(path) {
|
|
1314
|
+
return state.files.delete(path);
|
|
1315
|
+
},
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* renders one ls row in the long listing style with size and date.
|
|
1320
|
+
*
|
|
1321
|
+
* @param {{path: string, size: number, updatedat: string}} file the file row.
|
|
1322
|
+
* @returns {string} the formatted line.
|
|
1323
|
+
*/
|
|
1324
|
+
function lsrow(file) {
|
|
1325
|
+
const when = String(file.updatedat ?? '').slice(0, 16).replace('T', ' ');
|
|
1326
|
+
return `-rw-r--r-- 1 root root ${padstart(String(file.size), 9)} ${when} ${file.path}`;
|
|
1327
|
+
}
|
|
1328
|
+
/** the short manual pages served by the man command. */
|
|
1329
|
+
const manualpages = {
|
|
1330
|
+
streaming: 'streaming: shows the streaming memory plan - any workload size runs inside a small hot window (mmap layers, evict after last consumer)',
|
|
1331
|
+
quantum: 'quantum: quantum layer summary - simulator, bb84, grover, dna vault, optical planner',
|
|
1332
|
+
tiers: 'tiers: shows the layered virtual memory - l1 ram to l4 buckets, the free pool and the npm-as-disk farm',
|
|
1333
|
+
touch: 'touch <file> - create an empty file in the persistent workspace (or refresh its timestamp)',
|
|
1334
|
+
echo: 'echo <text> - print text; echo <text> > <file> writes and echo <text> >> <file> appends (stored verbatim, no implicit newline)',
|
|
1335
|
+
cat: 'cat <file> - print one persistent workspace file (also /proc/cpuinfo and /proc/meminfo)',
|
|
1336
|
+
ls: 'ls [path] - list the persistent workspace root with sizes and dates; ls /etc/virtual keeps the hardware table',
|
|
1337
|
+
rm: 'rm <file> - delete one persistent workspace file',
|
|
1338
|
+
df: 'df [-h] - workspace quota usage as a filesystem table; the quota is fixed per sandbox id',
|
|
1339
|
+
stat: 'stat <file> - size and timestamps of one persistent workspace file',
|
|
1340
|
+
pwd: 'pwd - print the working directory; the sandbox is always rooted at /',
|
|
1341
|
+
history: 'history - the last 50 commands typed in this session',
|
|
1342
|
+
man: 'man <command> - this short manual',
|
|
1343
|
+
};
|
|
1344
|
+
/**
|
|
1345
|
+
* dispatches one shell command against a sandbox state; the return shape
|
|
1346
|
+
* matches the api exec contract {output, exitCode}. the optional context
|
|
1347
|
+
* carries the persistent filesystem callbacks {write, read, list, del}
|
|
1348
|
+
* bound to the sandbox id by the api host; without a context the same
|
|
1349
|
+
* commands run on the per-session in-memory workspace so the browser
|
|
1350
|
+
* terminal keeps working unchanged.
|
|
1351
|
+
*
|
|
1352
|
+
* @param {string} command the raw command line typed by the user.
|
|
1353
|
+
* @param {object} state a state built by createsandboxstate.
|
|
1354
|
+
* @param {{fs?: {write: Function, read: Function, list: Function,
|
|
1355
|
+
* del: Function}, quota?: number}} [context] the optional filesystem
|
|
1356
|
+
* context provided by the api host.
|
|
1357
|
+
* @returns {{output: string, exitCode: number, clear?: boolean}} the result.
|
|
1358
|
+
*/
|
|
1359
|
+
export function dispatch(command, state, context) {
|
|
1360
|
+
const raw = String(command ?? '').trim();
|
|
1361
|
+
if (raw.length === 0) {
|
|
1362
|
+
return { output: '', exitCode: 0 };
|
|
1363
|
+
}
|
|
1364
|
+
try {
|
|
1365
|
+
if (!Array.isArray(state.history)) {
|
|
1366
|
+
state.history = [];
|
|
1367
|
+
}
|
|
1368
|
+
state.history.push(raw);
|
|
1369
|
+
if (state.history.length > historycap) {
|
|
1370
|
+
state.history.splice(0, state.history.length - historycap);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
catch {
|
|
1374
|
+
/* history is best effort; a frozen state must not break exec */
|
|
1375
|
+
}
|
|
1376
|
+
const fs = context?.fs ?? memoryfs(state);
|
|
1377
|
+
const argv = raw.split(/\s+/);
|
|
1378
|
+
const head = argv[0];
|
|
1379
|
+
const rest = argv.slice(1).join(' ');
|
|
1380
|
+
try {
|
|
1381
|
+
/* echo is the one command with output redirection: the text is
|
|
1382
|
+
* stored verbatim ("echo a > f" then "echo b >> f" yields "ab").
|
|
1383
|
+
* The split is a linear scan off the last '>' — the lazy regex it
|
|
1384
|
+
* replaces walked every boundary of a space run and the redos gate
|
|
1385
|
+
* refused the polynomial detour: the operator is the last '>' doubled
|
|
1386
|
+
* or single, the filename the single whitespace-free token after it
|
|
1387
|
+
* and the command everything before the operator. */
|
|
1388
|
+
const gt = raw.lastIndexOf('>');
|
|
1389
|
+
let redirect = null;
|
|
1390
|
+
if (gt >= 0) {
|
|
1391
|
+
const file = raw.slice(gt + 1).trim();
|
|
1392
|
+
if (file.length > 0 && !/\s/.test(file)) {
|
|
1393
|
+
const doubled = gt > 0 && raw[gt - 1] === '>';
|
|
1394
|
+
redirect = {
|
|
1395
|
+
command: raw.slice(0, doubled ? gt - 1 : gt).trim(),
|
|
1396
|
+
operator: doubled ? '>>' : '>',
|
|
1397
|
+
file,
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
if (redirect !== null) {
|
|
1402
|
+
const commandpart = redirect.command;
|
|
1403
|
+
const redirhead = commandpart.split(/\s+/)[0];
|
|
1404
|
+
if (redirhead !== 'echo') {
|
|
1405
|
+
return {
|
|
1406
|
+
output: `bash: output redirection is only supported for echo (got ${redirhead})`,
|
|
1407
|
+
exitCode: 1,
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
const text = commandpart.slice(redirhead.length).trim();
|
|
1411
|
+
const norm = normalizepath(redirect.file);
|
|
1412
|
+
if (!norm.ok) {
|
|
1413
|
+
return { output: `bash: echo: ${redirect.file}: ${norm.error}`, exitCode: 1 };
|
|
1414
|
+
}
|
|
1415
|
+
try {
|
|
1416
|
+
if (redirect.operator === '>>') {
|
|
1417
|
+
const existing = fs.read(norm.path);
|
|
1418
|
+
fs.write(norm.path, `${existing === null ? '' : existing.content}${text}`);
|
|
1419
|
+
}
|
|
1420
|
+
else {
|
|
1421
|
+
fs.write(norm.path, text);
|
|
1422
|
+
}
|
|
1423
|
+
return { output: '', exitCode: 0 };
|
|
1424
|
+
}
|
|
1425
|
+
catch (writeerror) {
|
|
1426
|
+
return {
|
|
1427
|
+
output: `bash: echo: ${writeerror instanceof Error ? writeerror.message : String(writeerror)}`,
|
|
1428
|
+
exitCode: 1,
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
switch (head) {
|
|
1433
|
+
case 'help':
|
|
1434
|
+
return { output: `saddle sandbox commands:\n${helpText()}`, exitCode: 0 };
|
|
1435
|
+
case 'lscpu':
|
|
1436
|
+
return { output: lscpu(state.model, state.vcpus, 1), exitCode: 0 };
|
|
1437
|
+
case 'cat': {
|
|
1438
|
+
if (argv[1] === '/proc/cpuinfo') {
|
|
1439
|
+
return { output: cpuinfo(state.model, state.vcpus), exitCode: 0 };
|
|
1440
|
+
}
|
|
1441
|
+
if (argv[1] === '/proc/meminfo') {
|
|
1442
|
+
return { output: state.memsnapshot, exitCode: 0 };
|
|
1443
|
+
}
|
|
1444
|
+
if (argv[1] === undefined) {
|
|
1445
|
+
return { output: 'cat: missing file operand', exitCode: 1 };
|
|
1446
|
+
}
|
|
1447
|
+
const catnorm = normalizepath(argv[1]);
|
|
1448
|
+
if (!catnorm.ok) {
|
|
1449
|
+
return { output: `cat: ${argv[1]}: ${catnorm.error}`, exitCode: 1 };
|
|
1450
|
+
}
|
|
1451
|
+
const catfile = fs.read(catnorm.path);
|
|
1452
|
+
if (catfile === null) {
|
|
1453
|
+
return {
|
|
1454
|
+
output: `cat: ${argv[1]}: No such file or directory`,
|
|
1455
|
+
exitCode: 1,
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
return { output: catfile.content, exitCode: 0 };
|
|
1459
|
+
}
|
|
1460
|
+
case 'free':
|
|
1461
|
+
return { output: freeh(state.memsnapshot), exitCode: 0 };
|
|
1462
|
+
case 'nvidia-smi':
|
|
1463
|
+
if (argv[1] === '-L') {
|
|
1464
|
+
return { output: nvidiaSmiList(state.gpu, state.mig), exitCode: 0 };
|
|
1465
|
+
}
|
|
1466
|
+
return { output: nvidiaSmiTable(state.gpu, state.mig), exitCode: 0 };
|
|
1467
|
+
case 'clinfo':
|
|
1468
|
+
return { output: clinfoSummary(state.vcpus), exitCode: 0 };
|
|
1469
|
+
case 'vulkaninfo':
|
|
1470
|
+
return { output: vulkanSummary(), exitCode: 0 };
|
|
1471
|
+
case 'glxinfo':
|
|
1472
|
+
return { output: glxinfoSummary(), exitCode: 0 };
|
|
1473
|
+
case 'uname':
|
|
1474
|
+
return {
|
|
1475
|
+
output: `Linux ${state.hostname} ${state.kernel} #1 SMP PREEMPT_DYNAMIC ${state.cpuspec.arch === 'arm64' ? 'aarch64' : 'x86_64'} GNU/Linux`,
|
|
1476
|
+
exitCode: 0,
|
|
1477
|
+
};
|
|
1478
|
+
case 'ls': {
|
|
1479
|
+
const target = argv[1] ?? '/';
|
|
1480
|
+
if (target === '/etc/virtual') {
|
|
1481
|
+
return {
|
|
1482
|
+
output: [
|
|
1483
|
+
'boards.json cores.json gpus.json',
|
|
1484
|
+
'processors.json virtualhardware.json vm.config.json',
|
|
1485
|
+
'cpuinfo meminfo lscpu nvidia-smi',
|
|
1486
|
+
].join('\n'),
|
|
1487
|
+
exitCode: 0,
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
const lsnorm = normalizepath(target);
|
|
1491
|
+
if (!lsnorm.ok) {
|
|
1492
|
+
return { output: `ls: ${target}: ${lsnorm.error}`, exitCode: 2 };
|
|
1493
|
+
}
|
|
1494
|
+
const rows = fs.list().filter((entry) => lsnorm.path === '/' || entry.path === lsnorm.path);
|
|
1495
|
+
if (rows.length === 0) {
|
|
1496
|
+
if (lsnorm.path !== '/') {
|
|
1497
|
+
return {
|
|
1498
|
+
output: `ls: cannot access '${target}': No such file or directory`,
|
|
1499
|
+
exitCode: 2,
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
return { output: '', exitCode: 0 };
|
|
1503
|
+
}
|
|
1504
|
+
const blocks = Math.max(1, Math.ceil(rows.reduce((sum, row) => sum + row.size, 0) / 1024));
|
|
1505
|
+
return {
|
|
1506
|
+
output: [`total ${blocks}`, ...rows.map(lsrow)].join('\n'),
|
|
1507
|
+
exitCode: 0,
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
case 'env':
|
|
1511
|
+
return { output: envText(state), exitCode: 0 };
|
|
1512
|
+
case 'docker':
|
|
1513
|
+
return { output: 'Docker version 29.7.2, build 5ea4c1f', exitCode: 0 };
|
|
1514
|
+
case 'qemu-system-x86_64':
|
|
1515
|
+
return {
|
|
1516
|
+
output: [
|
|
1517
|
+
'QEMU emulator version 11.1.0 (saddle bridge)',
|
|
1518
|
+
'Copyright (c) 2003-2026 Fabrice Bellard and the QEMU Project developers',
|
|
1519
|
+
].join('\n'),
|
|
1520
|
+
exitCode: 0,
|
|
1521
|
+
};
|
|
1522
|
+
case 'uptime':
|
|
1523
|
+
return { output: uptimeLine(state), exitCode: 0 };
|
|
1524
|
+
case 'whoami':
|
|
1525
|
+
return { output: 'root', exitCode: 0 };
|
|
1526
|
+
case 'neofetch':
|
|
1527
|
+
return { output: neofetch(state), exitCode: 0 };
|
|
1528
|
+
case 'echo':
|
|
1529
|
+
return { output: rest, exitCode: 0 };
|
|
1530
|
+
case 'touch': {
|
|
1531
|
+
if (argv[1] === undefined) {
|
|
1532
|
+
return { output: 'touch: missing file operand', exitCode: 1 };
|
|
1533
|
+
}
|
|
1534
|
+
const touchnorm = normalizepath(argv[1]);
|
|
1535
|
+
if (!touchnorm.ok) {
|
|
1536
|
+
return { output: `touch: ${argv[1]}: ${touchnorm.error}`, exitCode: 1 };
|
|
1537
|
+
}
|
|
1538
|
+
try {
|
|
1539
|
+
const existing = fs.read(touchnorm.path);
|
|
1540
|
+
fs.write(touchnorm.path, existing === null ? '' : existing.content);
|
|
1541
|
+
return { output: '', exitCode: 0 };
|
|
1542
|
+
}
|
|
1543
|
+
catch (toucherror) {
|
|
1544
|
+
return {
|
|
1545
|
+
output: `touch: ${toucherror instanceof Error ? toucherror.message : String(toucherror)}`,
|
|
1546
|
+
exitCode: 1,
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
case 'rm': {
|
|
1551
|
+
if (argv[1] === undefined) {
|
|
1552
|
+
return { output: 'rm: missing operand', exitCode: 1 };
|
|
1553
|
+
}
|
|
1554
|
+
const rmnorm = normalizepath(argv[1]);
|
|
1555
|
+
if (!rmnorm.ok) {
|
|
1556
|
+
return { output: `rm: ${argv[1]}: ${rmnorm.error}`, exitCode: 1 };
|
|
1557
|
+
}
|
|
1558
|
+
try {
|
|
1559
|
+
const removed = fs.del(rmnorm.path);
|
|
1560
|
+
if (!removed) {
|
|
1561
|
+
return {
|
|
1562
|
+
output: `rm: cannot remove '${argv[1]}': No such file or directory`,
|
|
1563
|
+
exitCode: 1,
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
return { output: '', exitCode: 0 };
|
|
1567
|
+
}
|
|
1568
|
+
catch (rmerror) {
|
|
1569
|
+
return {
|
|
1570
|
+
output: `rm: ${rmerror instanceof Error ? rmerror.message : String(rmerror)}`,
|
|
1571
|
+
exitCode: 1,
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
case 'quantum':
|
|
1576
|
+
return { output: quantumdemo(), exitCode: 0 };
|
|
1577
|
+
case 'tiers':
|
|
1578
|
+
return { output: tiersdemo(), exitCode: 0 };
|
|
1579
|
+
case 'streaming':
|
|
1580
|
+
return { output: streamingdemo(), exitCode: 0 };
|
|
1581
|
+
case 'df': {
|
|
1582
|
+
const rows = fs.list();
|
|
1583
|
+
const used = rows.reduce((sum, row) => sum + row.size, 0);
|
|
1584
|
+
const quota = Number(context?.quota ?? defaultquota);
|
|
1585
|
+
const avail = Math.max(0, quota - used);
|
|
1586
|
+
const percent = quota > 0 ? Math.min(100, Math.floor((used / quota) * 100)) : 0;
|
|
1587
|
+
const col = (text, width) => padstart(String(text), width);
|
|
1588
|
+
return {
|
|
1589
|
+
output: [
|
|
1590
|
+
`Filesystem ${col('Size', 10)}${col('Used', 10)}${col('Avail', 10)}${col('Use%', 6)} Mounted on`,
|
|
1591
|
+
`${pad(state.id, 11)}${col(humanbytes(quota), 10)}${col(humanbytes(used), 10)}${col(humanbytes(avail), 10)}${col(`${percent}%`, 6)} /`,
|
|
1592
|
+
].join('\n'),
|
|
1593
|
+
exitCode: 0,
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
case 'stat': {
|
|
1597
|
+
if (argv[1] === undefined) {
|
|
1598
|
+
return { output: 'stat: missing operand', exitCode: 1 };
|
|
1599
|
+
}
|
|
1600
|
+
const statnorm = normalizepath(argv[1]);
|
|
1601
|
+
if (!statnorm.ok) {
|
|
1602
|
+
return { output: `stat: ${argv[1]}: ${statnorm.error}`, exitCode: 1 };
|
|
1603
|
+
}
|
|
1604
|
+
const file = fs.read(statnorm.path);
|
|
1605
|
+
if (file === null) {
|
|
1606
|
+
return {
|
|
1607
|
+
output: `stat: cannot stat '${argv[1]}': No such file or directory`,
|
|
1608
|
+
exitCode: 1,
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
return {
|
|
1612
|
+
output: [
|
|
1613
|
+
` File: ${file.path ?? statnorm.path}`,
|
|
1614
|
+
` Size: ${padstart(String(file.size), 10)}\tBlocks: ${padstart(String(Math.ceil(file.size / 512)), 6)} IO Block: 4096 regular file`,
|
|
1615
|
+
`Device: saddle/virtual\tInode: ${padstart(String(state.files instanceof Map ? state.files.size + 1 : 1), 12)} Links: 1`,
|
|
1616
|
+
`Modify: ${file.updatedat}`,
|
|
1617
|
+
].join('\n'),
|
|
1618
|
+
exitCode: 0,
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
case 'pwd':
|
|
1622
|
+
return { output: '/', exitCode: 0 };
|
|
1623
|
+
case 'history': {
|
|
1624
|
+
const lines = (Array.isArray(state.history) ? state.history : [])
|
|
1625
|
+
.map((entry, index) => `${padstart(String(index + 1), 5)} ${entry}`);
|
|
1626
|
+
return { output: lines.join('\n'), exitCode: 0 };
|
|
1627
|
+
}
|
|
1628
|
+
case 'man': {
|
|
1629
|
+
const topic = argv[1];
|
|
1630
|
+
if (topic === undefined) {
|
|
1631
|
+
return { output: 'What manual page do you want?', exitCode: 1 };
|
|
1632
|
+
}
|
|
1633
|
+
const page = manualpages[topic];
|
|
1634
|
+
if (page === undefined) {
|
|
1635
|
+
return { output: `man: no manual for ${topic} (try: ${Object.keys(manualpages).join(', ')})`, exitCode: 1 };
|
|
1636
|
+
}
|
|
1637
|
+
return { output: `${topic.toUpperCase()}(1)\n\n ${page}`, exitCode: 0 };
|
|
1638
|
+
}
|
|
1639
|
+
case 'clear':
|
|
1640
|
+
return { output: '', exitCode: 0, clear: true };
|
|
1641
|
+
default:
|
|
1642
|
+
return {
|
|
1643
|
+
output: `bash: ${head}: command not found (try "help")`,
|
|
1644
|
+
exitCode: 127,
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
catch (error) {
|
|
1649
|
+
return {
|
|
1650
|
+
output: `bash: ${head}: engine error: ${error instanceof Error ? error.message : String(error)}`,
|
|
1651
|
+
exitCode: 1,
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* renders the streaming plan for a workload larger than any host: the
|
|
1657
|
+
* demo the terminal shows for "how does a 1.5 tb model run here" - layers
|
|
1658
|
+
* stream through a fixed window, the workload size is unbounded.
|
|
1659
|
+
* @param {number} totalbytes workload size in bytes.
|
|
1660
|
+
* @param {number} windowbytes hot-set budget in bytes.
|
|
1661
|
+
* @returns {string} the plan summary lines.
|
|
1662
|
+
*/
|
|
1663
|
+
/**
|
|
1664
|
+
* renders the layered virtual memory architecture: everything is vram and
|
|
1665
|
+
* the only physical layer is the free pool (github, npm cdn and friends -
|
|
1666
|
+
* the saddle storage doctrine applied to this engine).
|
|
1667
|
+
* @returns {string} the tier summary lines.
|
|
1668
|
+
*/
|
|
1669
|
+
/**
|
|
1670
|
+
* renders the quantum layer summary: a classical statevector simulator
|
|
1671
|
+
* (1-20 qubits), the bb84/e91 key exchange, grover search, dna and 5d
|
|
1672
|
+
* optical storage planners - every "quantum" capability is software.
|
|
1673
|
+
* @returns {string} the quantum summary lines.
|
|
1674
|
+
*/
|
|
1675
|
+
export function quantumdemo() {
|
|
1676
|
+
return [
|
|
1677
|
+
'saddle quantum layer (100 percent classical simulation)',
|
|
1678
|
+
' simulator statevector, float64 interleaved re/im, 1-20 qubits',
|
|
1679
|
+
' gates h x y z s t rx ry rz cnot cz toffoli swap',
|
|
1680
|
+
' algorithms bellstate, ghz, grover2 (provable), grover3, deutsch, teleport',
|
|
1681
|
+
' protocols bb84 (1984) with intercept-resend detection, e91 (1991) + chsh',
|
|
1682
|
+
' randomness quantum-inspired (csprng) with honest anu qrng fallback note',
|
|
1683
|
+
' storage dna vault (goldman 2013, 215 pb/g, 4 strands, roundtrip proven)',
|
|
1684
|
+
' 5d quartz planner (360 tb/disc, 13.8 billion years at room)',
|
|
1685
|
+
' qram bucket-brigade model (giovannetti prl 2008)',
|
|
1686
|
+
' engine: import { quantumsim, runbb84, dnavaultencode } from ./quantum',
|
|
1687
|
+
].join('\n');
|
|
1688
|
+
}
|
|
1689
|
+
export function tiersdemo() {
|
|
1690
|
+
const ladder = ['ram ~100ns', 'zram ~500ns', 'tmpfs ~1us', 'mmap ~5us', 'sqlite ~10us', 'r2 ~50us'];
|
|
1691
|
+
return [
|
|
1692
|
+
'saddle tiered virtual memory (everything is vram)',
|
|
1693
|
+
' l1 ram working set, ephemeral ~100 ns',
|
|
1694
|
+
' l2 vram compute-bound identity layer spoofed',
|
|
1695
|
+
' l3 storage repos as ram (sqlite kv, lru) ~10 us',
|
|
1696
|
+
' l4 buckets hf 10tb + kaggle 20tb + terabox ~50 us',
|
|
1697
|
+
' physical github artifacts + ghcr + npm cdn (the only disks)',
|
|
1698
|
+
` ladder ${ladder.join(' | ')}`,
|
|
1699
|
+
' autoscale <64mb memfs / <1gb mmap / larger sqlite+r2',
|
|
1700
|
+
' pool >33 tb free across accounts (see tiers.ts FREEPOOL)',
|
|
1701
|
+
' npm-as-disk 200 mb chunks as .bin.js packages on the cdn farm',
|
|
1702
|
+
' vdr 64-bit bigint addressing up to 9.22 eb',
|
|
1703
|
+
' engine: import { creatiersengine } from the package (./tiers)',
|
|
1704
|
+
].join('\n');
|
|
1705
|
+
}
|
|
1706
|
+
export function streamingdemo(totalbytes = 1649267441664, windowbytes = 4 * 1024 * 1024 * 1024) {
|
|
1707
|
+
const layerbytes = 512 * 1024 * 1024;
|
|
1708
|
+
const count = Math.ceil(totalbytes / layerbytes);
|
|
1709
|
+
const batches = Math.ceil(count / Math.max(1, Math.floor(windowbytes / layerbytes)));
|
|
1710
|
+
const fmt = (b) => (b >= 1024 ** 4 ? (b / 1024 ** 4).toFixed(2) + ' TB' : (b / 1024 ** 3).toFixed(1) + ' GB');
|
|
1711
|
+
return [
|
|
1712
|
+
'saddle streaming memory plan',
|
|
1713
|
+
` workload: ${fmt(totalbytes)} decomposed into ${count} layers of ${layerbytes / 1024 ** 2} MiB`,
|
|
1714
|
+
` window: ${fmt(windowbytes)} hot-set budget (peak resident)`,
|
|
1715
|
+
` batches: ${batches} load/run/evict passes over the weights`,
|
|
1716
|
+
' technique: mmap + page cache streams each layer on demand; eviction',
|
|
1717
|
+
' after the last consumer keeps the resident set inside the',
|
|
1718
|
+
' window - the workload size is unbounded, only the window',
|
|
1719
|
+
' is fixed. this is how engines run models larger than ram.',
|
|
1720
|
+
].join('\n');
|
|
1721
|
+
}
|