426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
378 lines
12 KiB
JavaScript
378 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Desktop multi-modality memory-bench harness (#8809 M10b).
|
|
*
|
|
* The desktop parallel to native/verify/mobile_peak_rss_harness.mjs. Where the
|
|
* mobile harness can only record a "needs-device" stub off-device (it cannot
|
|
* measure peak RSS or thermals on a host), the desktop harness CAN measure real
|
|
* per-modality resident-set growth on the host it runs on — so it does, and it
|
|
* never fabricates a number it didn't measure (AGENTS.md §3 / §8).
|
|
*
|
|
* What it measures
|
|
* ----------------
|
|
* For each modality present in an installed Eliza-1 bundle — text, embedding,
|
|
* vision (mmproj), VAD — it spawns a child process that memory-maps that
|
|
* modality's GGUF and *touches every page* (the residency the runtime pays once
|
|
* a model is loaded and its weights are warm), and records the child's peak RSS
|
|
* delta over a clean baseline. It then runs a co-residency pass that maps
|
|
* text + embedding + vision + VAD together, which is exactly the resident set
|
|
* the MemoryArbiter's `evictToFit` path (#8809) must keep under budget.
|
|
*
|
|
* This is a genuine warm-RSS measurement of the weight residency, not model
|
|
* execution: it does not depend on the fused native inference library or a
|
|
* loaded graph, so it runs on any desktop with a bundle on disk and stays
|
|
* green while the engine load path is in flux. (Full decode-time RSS lives in
|
|
* scripts/memory-benchmark.ts, which drives the real engine.)
|
|
*
|
|
* Discipline mirrored from the iOS runners:
|
|
* - No bundle installed → records `available:false` with a structured
|
|
* reason and exits 0 (recording "not installed" is success, like the mtp
|
|
* bench), never a fabricated RSS.
|
|
* - A modality absent from a bundle → that modality row is `present:false`,
|
|
* `peakRssMb:null`; the tool does not invent residency for files that do
|
|
* not exist.
|
|
*
|
|
* Report shape: top-level `summary.peakRssMb` is the co-resident peak (the
|
|
* number the collector's peak-RSS gate keys off, matching the mobile harness's
|
|
* `summary.peakRssMb` field), with per-modality detail under `modalities`. The
|
|
* JSON is written under reports/desktop-rss/ so eliza1_gates_collect.mjs can
|
|
* fold it in alongside the mobile-rss reports.
|
|
*
|
|
* Usage:
|
|
* node plugins/plugin-local-inference/native/verify/desktop_memory_inventory.mjs \
|
|
* [bundle ...] [--report PATH] [--json] [--touch-bytes N]
|
|
*/
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
const DEFAULT_BUNDLE_ROOT = path.join(
|
|
os.homedir(),
|
|
".eliza",
|
|
"local-inference",
|
|
"models",
|
|
);
|
|
|
|
/** The modalities this harness exercises, in co-residency load order. */
|
|
const MODALITIES = ["text", "embedding", "vision", "vad"];
|
|
|
|
function timestamp() {
|
|
return new Date()
|
|
.toISOString()
|
|
.replace(/[-:]/g, "")
|
|
.replace(/\.\d{3}Z$/, "Z");
|
|
}
|
|
|
|
function expandHome(p) {
|
|
if (!p) return p;
|
|
if (p === "~") return os.homedir();
|
|
if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
|
|
return p;
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const args = {
|
|
bundles: [],
|
|
report: path.join(
|
|
__dirname,
|
|
"..",
|
|
"reports",
|
|
"desktop-rss",
|
|
`desktop-memory-${timestamp()}.json`,
|
|
),
|
|
json: false,
|
|
/** Page-touch stride; reading one byte per OS page faults the page in. */
|
|
touchStride: 4096,
|
|
};
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|
const a = argv[i];
|
|
if (a === "--report") args.report = expandHome(argv[(i += 1)]);
|
|
else if (a === "--json") args.json = true;
|
|
else if (a === "--touch-stride") args.touchStride = Number(argv[(i += 1)]);
|
|
else if (a === "--help" || a === "-h") {
|
|
console.log(
|
|
[
|
|
"Usage: node desktop_memory_inventory.mjs [bundle ...]",
|
|
" --report PATH output JSON (default reports/desktop-rss/desktop-memory-<ts>.json)",
|
|
" --json print the report to stdout",
|
|
" --touch-stride N bytes between page-touch reads (default 4096)",
|
|
].join("\n"),
|
|
);
|
|
process.exit(0);
|
|
} else args.bundles.push(expandHome(a));
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function defaultBundles() {
|
|
if (!fs.existsSync(DEFAULT_BUNDLE_ROOT)) return [];
|
|
return fs
|
|
.readdirSync(DEFAULT_BUNDLE_ROOT)
|
|
.filter((name) => /^eliza-1-.+\.bundle$/.test(name))
|
|
.map((name) => path.join(DEFAULT_BUNDLE_ROOT, name))
|
|
.filter((p) => fs.existsSync(p) && fs.statSync(p).isDirectory())
|
|
.sort();
|
|
}
|
|
|
|
function tierFromBundlePath(bundleDir) {
|
|
return path
|
|
.basename(bundleDir)
|
|
.replace(/\.bundle$/, "")
|
|
.replace(/^eliza-1-/, "");
|
|
}
|
|
|
|
function walkGgufs(dir) {
|
|
const out = [];
|
|
if (!fs.existsSync(dir)) return out;
|
|
const stack = [dir];
|
|
while (stack.length) {
|
|
const cur = stack.pop();
|
|
for (const name of fs.readdirSync(cur)) {
|
|
const abs = path.join(cur, name);
|
|
const st = fs.lstatSync(abs);
|
|
if (st.isDirectory()) stack.push(abs);
|
|
else if (st.isFile()) out.push({ abs, size: st.size });
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Resolve the on-disk file(s) backing a modality inside a bundle. The manifest
|
|
* is the source of truth; we fall back to the conventional directory layout
|
|
* (AGENTS.md §2) so the harness still works against a bundle whose manifest is
|
|
* older than a component. Returns the largest matching GGUF (the weight bank).
|
|
*/
|
|
function modalityFile(bundleDir, modality) {
|
|
const dirFor = {
|
|
text: "text",
|
|
embedding: "embedding",
|
|
vision: "vision",
|
|
vad: "vad",
|
|
}[modality];
|
|
const ggufs = walkGgufs(path.join(bundleDir, dirFor)).sort(
|
|
(a, b) => b.size - a.size,
|
|
);
|
|
// The 2B tier reuses the text backbone for embeddings (no dedicated file).
|
|
if (modality === "embedding" && ggufs.length === 0) {
|
|
const textGgufs = walkGgufs(path.join(bundleDir, "text")).sort(
|
|
(a, b) => b.size - a.size,
|
|
);
|
|
if (textGgufs[0]) {
|
|
return { abs: textGgufs[0].abs, size: textGgufs[0].size, sharedWithText: true };
|
|
}
|
|
return null;
|
|
}
|
|
return ggufs[0] ?? null;
|
|
}
|
|
|
|
/** Run the page-touch child and return its measured peak RSS delta in MB. */
|
|
function measurePeakRss(files, touchStride) {
|
|
const list = files.map((f) => f.abs);
|
|
const child = spawnSync(
|
|
process.execPath,
|
|
["-e", PAGE_TOUCH_CHILD, JSON.stringify(list), String(touchStride)],
|
|
{ encoding: "utf8", maxBuffer: 16 * 1024 * 1024 },
|
|
);
|
|
if (child.status !== 0) {
|
|
return {
|
|
ok: false,
|
|
error: (child.stderr || child.stdout || "child exited non-zero").trim(),
|
|
};
|
|
}
|
|
try {
|
|
const last = child.stdout.trim().split("\n").pop();
|
|
const parsed = JSON.parse(last);
|
|
return { ok: true, ...parsed };
|
|
} catch (err) {
|
|
return { ok: false, error: `unparseable child output: ${String(err)}` };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The child: read baseline RSS, mmap+touch every page of each file, sample RSS
|
|
* across the touch, and print the peak-RSS delta. Uses a Buffer read of one
|
|
* byte per page so the kernel faults the file-backed page into resident memory
|
|
* — the same residency the warm runtime holds.
|
|
*/
|
|
const PAGE_TOUCH_CHILD = `
|
|
const fs = require("node:fs");
|
|
const files = JSON.parse(process.argv[1]);
|
|
const stride = Math.max(512, Number(process.argv[2]) || 4096);
|
|
const MB = 1024 * 1024;
|
|
const baseline = process.memoryUsage().rss;
|
|
let peak = baseline;
|
|
let touchedBytes = 0;
|
|
const buf = Buffer.allocUnsafe(1);
|
|
for (const file of files) {
|
|
const fd = fs.openSync(file, "r");
|
|
try {
|
|
const size = fs.fstatSync(fd).size;
|
|
for (let pos = 0; pos < size; pos += stride) {
|
|
fs.readSync(fd, buf, 0, 1, pos);
|
|
touchedBytes += 1;
|
|
if ((touchedBytes & 0x3ff) === 0) {
|
|
const rss = process.memoryUsage().rss;
|
|
if (rss > peak) peak = rss;
|
|
}
|
|
}
|
|
} finally {
|
|
fs.closeSync(fd);
|
|
}
|
|
}
|
|
const rss = process.memoryUsage().rss;
|
|
if (rss > peak) peak = rss;
|
|
process.stdout.write(JSON.stringify({
|
|
baselineRssMb: round(baseline / MB),
|
|
peakRssMb: round(peak / MB),
|
|
peakRssDeltaMb: round((peak - baseline) / MB),
|
|
}) + "\\n");
|
|
function round(n) { return Math.round(n * 100) / 100; }
|
|
`;
|
|
|
|
function round(n, digits = 2) {
|
|
const f = 10 ** digits;
|
|
return Math.round(n * f) / f;
|
|
}
|
|
|
|
function inventoryBundle(bundleDir, touchStride) {
|
|
const tier = tierFromBundlePath(bundleDir);
|
|
const modalities = {};
|
|
const presentFiles = [];
|
|
for (const modality of MODALITIES) {
|
|
const file = modalityFile(bundleDir, modality);
|
|
if (!file) {
|
|
modalities[modality] = {
|
|
present: false,
|
|
path: null,
|
|
sizeMb: null,
|
|
peakRssMb: null,
|
|
peakRssDeltaMb: null,
|
|
reason: "modality file absent from bundle",
|
|
};
|
|
continue;
|
|
}
|
|
const measured = measurePeakRss([file], touchStride);
|
|
modalities[modality] = {
|
|
present: true,
|
|
path: path.relative(bundleDir, file.abs),
|
|
sizeMb: round(file.size / 1024 / 1024),
|
|
sharedWithText: Boolean(file.sharedWithText),
|
|
peakRssMb: measured.ok ? measured.peakRssMb : null,
|
|
peakRssDeltaMb: measured.ok ? measured.peakRssDeltaMb : null,
|
|
reason: measured.ok ? null : measured.error,
|
|
};
|
|
// Dedupe the shared-with-text embedding file in the co-resident pass.
|
|
if (!presentFiles.some((f) => f.abs === file.abs)) presentFiles.push(file);
|
|
}
|
|
|
|
let coResident = {
|
|
measured: false,
|
|
peakRssMb: null,
|
|
peakRssDeltaMb: null,
|
|
files: presentFiles.length,
|
|
reason: presentFiles.length ? null : "no modality files present",
|
|
};
|
|
if (presentFiles.length) {
|
|
const result = measurePeakRss(presentFiles, touchStride);
|
|
coResident = result.ok
|
|
? {
|
|
measured: true,
|
|
peakRssMb: result.peakRssMb,
|
|
peakRssDeltaMb: result.peakRssDeltaMb,
|
|
baselineRssMb: result.baselineRssMb,
|
|
files: presentFiles.length,
|
|
reason: null,
|
|
}
|
|
: { ...coResident, reason: result.error };
|
|
}
|
|
|
|
return { tier, bundleDir, modalities, coResident };
|
|
}
|
|
|
|
function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const bundles = (args.bundles.length ? args.bundles : defaultBundles()).filter(
|
|
(p) => fs.existsSync(p) && fs.statSync(p).isDirectory(),
|
|
);
|
|
|
|
const base = {
|
|
schemaVersion: 1,
|
|
generatedAt: new Date().toISOString(),
|
|
harness: path.relative(process.cwd(), __filename),
|
|
host: {
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
cpus: os.cpus().length,
|
|
totalRamMb: round(os.totalmem() / 1024 / 1024),
|
|
freeRamMb: round(os.freemem() / 1024 / 1024),
|
|
},
|
|
};
|
|
|
|
if (!bundles.length) {
|
|
// No bundle on disk → record "not installed", never a fabricated number.
|
|
const report = {
|
|
...base,
|
|
available: false,
|
|
reason:
|
|
"no Eliza-1 bundle found on disk — install a bundle (default ~/.eliza/local-inference/models) to measure per-modality peak RSS",
|
|
bundles: [],
|
|
summary: { peakRssMb: null, coResidentPeakRssMb: null },
|
|
};
|
|
writeReport(args, report);
|
|
return;
|
|
}
|
|
|
|
const inventories = bundles.map((b) => inventoryBundle(b, args.touchStride));
|
|
// Summary peak = the largest co-resident peak observed (the residency the
|
|
// arbiter budget must hold), matching the mobile harness's summary.peakRssMb.
|
|
const coResidentPeaks = inventories
|
|
.map((b) => b.coResident.peakRssMb)
|
|
.filter((v) => typeof v === "number");
|
|
const summaryPeak = coResidentPeaks.length
|
|
? Math.max(...coResidentPeaks)
|
|
: null;
|
|
|
|
const report = {
|
|
...base,
|
|
available: summaryPeak !== null,
|
|
reason: summaryPeak !== null ? null : "no modality residency could be measured",
|
|
inputBundles: bundles,
|
|
bundles: inventories,
|
|
summary: {
|
|
peakRssMb: summaryPeak,
|
|
coResidentPeakRssMb: summaryPeak,
|
|
bundlesMeasured: inventories.length,
|
|
modalities: MODALITIES,
|
|
},
|
|
};
|
|
writeReport(args, report);
|
|
}
|
|
|
|
function writeReport(args, report) {
|
|
fs.mkdirSync(path.dirname(args.report), { recursive: true });
|
|
fs.writeFileSync(args.report, `${JSON.stringify(report, null, 2)}\n`);
|
|
if (args.json) {
|
|
console.log(JSON.stringify(report, null, 2));
|
|
} else {
|
|
console.log(`wrote ${args.report}`);
|
|
console.log(
|
|
`desktop-memory: available=${report.available}${
|
|
report.available
|
|
? ` co-resident peak RSS=${report.summary.peakRssMb} MB`
|
|
: ` — ${report.reason}`
|
|
}`,
|
|
);
|
|
}
|
|
// Exit 0: recording a "not installed" entry is success, like the mtp bench.
|
|
process.exit(0);
|
|
}
|
|
|
|
main();
|