igneum/tools/harness/scenarios/s8-steady.mjs
igneum-labs 90478b5cf4 Harness s8 steady state and the bench-log paragraph: RSS per 1,000 blocks before and after the M30 fix
s8-steady.mjs: two nodes, one honest vmine miner at 1 block/s, no flood,
RSS and cache-build count every 60 s, vmmap -summary at 0, 500, 1,000 and
1,500 blocks. Both builds ran 1,500 blocks on the 60x profile: before
41 to 1,342 MB by 514 blocks (9 cache builds, five 256 MiB chunks resident:
KEEP 4 plus one evicted chunk the allocator keeps) then flat, 27 builds in
1,529 blocks; after 319 MB at 510 blocks (1 build), 589 at 1,029 (the second
day's cache, by design), 603 at 1,526, 2 builds. Residual 30 MB per 1,000
blocks on both builds, read as the consensus database and caches filling,
not the PoW cache. JSON and vmmap files under
docs/benchmarks/memory-floods-2026-10-04/.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-05 01:49:46 +00:00

77 lines
4.7 KiB
JavaScript

// Scenario 8: steady state, no flood (ledger M30 follow-up, 5 October 2026). Node A takes one honest virtual miner
// at 1 block/s; node B follows. Every 60 s: RSS of both nodes, the block count, the count of "PoW cache built" lines
// in each node log, and (at the block milestones) `vmmap -summary` of both nodes, so malloc-held memory can be told
// from mapped. Runs until `blocks` chain blocks (IGNEUM_STEADY_BLOCKS, default 1,500) or `maxMinutes`
// (IGNEUM_STEADY_MAX_MIN, default 40), whichever first. No pass criterion: the row records RSS at 0, 500, 1,000 and
// 1,500 blocks, so the ledger can state MB per 1,000 blocks before and after a change.
import { Node, stopAll, dagInfo, log, sleep, assertBinaries, TMP } from '../lib/net.mjs';
import { Miner } from '../lib/miner.mjs';
import { saveResult } from '../lib/report.mjs';
import { execSync } from 'node:child_process';
import { mkdirSync, writeFileSync } from 'node:fs';
const MILESTONES = [0, 500, 1000, 1500];
function vmmapSummary(node, tag) {
if (!node.alive()) return null;
try {
const out = execSync(`vmmap -summary ${node.proc.pid}`, { timeout: 60000, stdio: ['ignore', 'pipe', 'ignore'] }).toString();
mkdirSync(`${TMP}/vmmap`, { recursive: true });
writeFileSync(`${TMP}/vmmap/${node.name}-${tag}.txt`, out);
const pick = (re) => { const m = out.match(re); return m ? m[1].trim() : null; };
return {
physical_footprint: pick(/Physical footprint:\s+([^\n]+)/),
malloc_total: pick(/\nMALLOC\s+[^\n]*?\s(\S+)\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s*\n/) || pick(/TOTAL\s+([^\n]+)/),
file: `${TMP}/vmmap/${node.name}-${tag}.txt`,
};
} catch (e) { return { error: String(e.message).slice(0, 120) }; }
}
export async function run({ blocks = parseInt(process.env.IGNEUM_STEADY_BLOCKS || '1500', 10), maxMinutes = parseInt(process.env.IGNEUM_STEADY_MAX_MIN || '40', 10) } = {}) {
assertBinaries();
const a = await new Node(0, { name: 's8a' }).start();
const b = await new Node(1, { name: 's8b', connect: [a.p2p] }).start();
await sleep(5000);
const builds = (n) => n.grepLog(/PoW cache built/).length;
const samples = []; const milestones = {};
const t0 = Date.now();
const sample = async (tag) => {
const ia = await dagInfo(a); const ib = await dagInfo(b);
const s = { t_s: Math.round((Date.now() - t0) / 1000), blocks_a: ia.blockCount, blocks_b: ib.blockCount, daa_a: ia.virtualDaaScore ?? null, rss_a: a.rssMb(), rss_b: b.rssMb(), cache_builds_a: builds(a), cache_builds_b: builds(b), same_sink: ia.sink === ib.sink, load: (() => { try { return execSync('uptime').toString().match(/load averages?: ([\d. ]+)/)?.[1].trim(); } catch { return ''; } })() };
samples.push(s);
log(`s8 ${s.t_s}s: blocks ${s.blocks_a}/${s.blocks_b} rss ${s.rss_a}/${s.rss_b} MB cache builds ${s.cache_builds_a}/${s.cache_builds_b} same=${s.same_sink} load ${s.load}`);
if (tag) { s.vmmap_a = vmmapSummary(a, tag); s.vmmap_b = vmmapSummary(b, tag); milestones[tag] = s; }
return s;
};
await sample('0');
const honest = new Miner({ node: a, share: 1, label: 'honest-s8' }); await honest.start();
let nextMilestone = 1;
while (Date.now() - t0 < maxMinutes * 60000) {
await sleep(60000);
const s = await sample(null);
while (nextMilestone < MILESTONES.length && s.blocks_a >= MILESTONES[nextMilestone]) {
const tag = String(MILESTONES[nextMilestone]); s.vmmap_a = vmmapSummary(a, tag); s.vmmap_b = vmmapSummary(b, tag); milestones[tag] = s; nextMilestone++;
}
if (s.blocks_a >= blocks) break;
}
honest.stop();
const end = await sample('end');
const alive = a.alive() && b.alive();
await stopAll();
const at = (k) => milestones[k] ? `${milestones[k].rss_a}/${milestones[k].rss_b} MB at ${milestones[k].blocks_a} blocks (${milestones[k].cache_builds_a}/${milestones[k].cache_builds_b} builds)` : 'not reached';
const row = {
scenario: '8 steady state, one honest miner at 1 block/s, no flood (RSS per 1,000 blocks)',
criterion: 'recorded: RSS of the mining node / the follower at 0, 500, 1,000 and 1,500 blocks, cache builds',
result: `0: ${at('0')}; 500: ${at('500')}; 1000: ${at('1000')}; 1500: ${at('1500')}; end ${end.blocks_a} blocks in ${end.t_s} s: ${end.rss_a}/${end.rss_b} MB; honest accepted ${honest.accepted} rejected ${honest.rejected} errors ${honest.errors}; alive ${alive}`,
pass: null,
};
saveResult('s8-steady', { rows: [row], data: { samples, milestones, honest: { accepted: honest.accepted, rejected: honest.rejected, errors: honest.errors }, alive } });
return { rows: [row], data: { samples, milestones } };
}
if (import.meta.url === `file://${process.argv[1]}`) {
const r = await run({});
console.log(JSON.stringify(r.rows, null, 2));
process.exit(0);
}