feat: capture system stats in benchmark
📊 Benchmark the Runtime / benchmark (pull_request) Successful in 1m3s
🚀 Lint & Test & Deploy / quality (pull_request) Failing after 52s
🚀 Lint & Test & Deploy / test-unit (pull_request) Successful in 31s
🚀 Lint & Test & Deploy / test-e2e (pull_request) Failing after 33s
🚀 Lint & Test & Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
2026-05-04 15:12:51 +02:00
parent d4910aba8c
commit a6b9ca4315
2 changed files with 271 additions and 9 deletions
+135 -9
View File
@@ -1,9 +1,21 @@
import type { Graph, Graph as GraphType, NodeId } from '@nodarium/types'; import type { Graph, Graph as GraphType, NodeId } from '@nodarium/types';
import { createLogger, createPerformanceStore, splitNestedArray } from '@nodarium/utils'; import { createLogger, createPerformanceStore, splitNestedArray } from '@nodarium/utils';
import { mkdir, writeFile } from 'node:fs/promises'; import { mkdir, writeFile } from 'node:fs/promises';
import { freemem, loadavg, totalmem } from 'node:os';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { MemoryRuntimeExecutor } from '../src/lib/runtime/runtime-executor.ts'; import { MemoryRuntimeExecutor } from '../src/lib/runtime/runtime-executor.ts';
import { BenchmarkRegistry } from './benchmarkRegistry.ts'; import { BenchmarkRegistry } from './benchmarkRegistry.ts';
import {
getMachineInfo,
measureCpuUsage,
readCgroupCpuStat,
readCpuSnapshot,
readProcMemInfo,
SystemSample
} from './systemStats.ts';
import defaultPlantTemplate from './templates/default.json' assert { type: 'json' }; import defaultPlantTemplate from './templates/default.json' assert { type: 'json' };
import lottaFacesTemplate from './templates/lotta-faces.json' assert { type: 'json' }; import lottaFacesTemplate from './templates/lotta-faces.json' assert { type: 'json' };
import plantTemplate from './templates/plant.json' assert { type: 'json' }; import plantTemplate from './templates/plant.json' assert { type: 'json' };
@@ -14,23 +26,34 @@ const r = new MemoryRuntimeExecutor(registry);
const log = createLogger('bench'); const log = createLogger('bench');
const templates: Record<string, Graph> = { const templates: Record<string, Graph> = {
'plant': plantTemplate as unknown as GraphType, plant: plantTemplate as unknown as GraphType,
'lotta-faces': lottaFacesTemplate as unknown as GraphType, 'lotta-faces': lottaFacesTemplate as unknown as GraphType,
'default': defaultPlantTemplate as unknown as GraphType default: defaultPlantTemplate as unknown as GraphType
}; };
function countGeometry(result: Int32Array): { totalVertices: number; totalFaces: number } { function average(values: number[]) {
if (values.length === 0) return 0;
return values.reduce((a, b) => a + b, 0) / values.length;
}
function countGeometry(result: Int32Array): {
totalVertices: number;
totalFaces: number;
} {
const parts = splitNestedArray(result); const parts = splitNestedArray(result);
let totalVertices = 0; let totalVertices = 0;
let totalFaces = 0; let totalFaces = 0;
for (const part of parts) { for (const part of parts) {
const type = part[0]; const type = part[0];
// Values are stored as uint32 in the wasm output but read as signed int32;
// >>> 0 reinterprets the bit pattern as unsigned.
const vertexCount = part[1] >>> 0; const vertexCount = part[1] >>> 0;
const faceCount = part[2] >>> 0; const faceCount = part[2] >>> 0;
if (type === 2) { if (type === 2) {
const instanceCount = part[3] >>> 0; const instanceCount = part[3] >>> 0;
totalVertices += vertexCount * instanceCount; totalVertices += vertexCount * instanceCount;
totalFaces += faceCount * instanceCount; totalFaces += faceCount * instanceCount;
} else { } else {
@@ -38,41 +61,144 @@ function countGeometry(result: Int32Array): { totalVertices: number; totalFaces:
totalFaces += faceCount; totalFaces += faceCount;
} }
} }
return { totalVertices, totalFaces };
return {
totalVertices,
totalFaces
};
} }
async function run(g: GraphType, amount: number) { async function run(g: GraphType, amount: number) {
await registry.load(g.nodes.map(n => n.type) as NodeId[]); await registry.load(g.nodes.map(n => n.type) as NodeId[]);
log.log('loaded ' + g.nodes.length + ' nodes'); log.log('loaded ' + g.nodes.length + ' nodes');
log.log('warming up'); log.log('warming up');
for (let index = 0; index < 10; index++) { for (let index = 0; index < 10; index++) {
await r.execute(g, { randomSeed: true }); await r.execute(g, { randomSeed: true });
} }
const systemSamples: SystemSample[] = [];
let previousCpuSnapshot = await readCpuSnapshot();
const sampler = setInterval(async () => {
try {
const cpu = await measureCpuUsage(previousCpuSnapshot);
previousCpuSnapshot = cpu.snapshot;
const [l1, l5, l15] = loadavg();
systemSamples.push({
timestamp: Date.now(),
cpuUsagePercent: cpu.usagePercent,
cpuStealPercent: cpu.stealPercent,
load1: l1,
load5: l5,
load15: l15,
freeMemory: freemem(),
totalMemory: totalmem()
});
} catch (err) {
console.error(err);
}
}, 1000);
log.log('executing'); log.log('executing');
const perfStore = createPerformanceStore(); const perfStore = createPerformanceStore();
r.perf = perfStore; r.perf = perfStore;
let res;
let res: Int32Array | undefined;
const cgroupBefore = await readCgroupCpuStat();
for (let i = 0; i < amount; i++) { for (let i = 0; i < amount; i++) {
r.perf?.startRun(); r.perf?.startRun();
res = await r.execute(g, { randomSeed: true }); res = await r.execute(g, { randomSeed: true });
r.perf?.stopRun(); r.perf?.stopRun();
const { totalVertices, totalFaces } = countGeometry(res!); const { totalVertices, totalFaces } = countGeometry(res!);
r.perf?.addToLastRun('total-vertices', totalVertices); r.perf?.addToLastRun('total-vertices', totalVertices);
r.perf?.addToLastRun('total-faces', totalFaces); r.perf?.addToLastRun('total-faces', totalFaces);
} }
const cgroupAfter = await readCgroupCpuStat();
clearInterval(sampler);
log.log('finished'); log.log('finished');
return r.perf.get();
return {
data: r.perf.get(),
metadata: {
timestamp: new Date().toISOString(),
machine: getMachineInfo(),
process: {
pid: process.pid,
uptime: process.uptime(),
memoryUsage: process.memoryUsage()
},
system: {
averages: {
cpuUsagePercent: average(
systemSamples.map(s => s.cpuUsagePercent)
),
cpuStealPercent: average(
systemSamples.map(s => s.cpuStealPercent)
),
load1: average(systemSamples.map(s => s.load1)),
load5: average(systemSamples.map(s => s.load5)),
load15: average(systemSamples.map(s => s.load15)),
freeMemory: average(
systemSamples.map(s => s.freeMemory)
)
},
samples: systemSamples,
meminfo: await readProcMemInfo()
},
cgroup: {
before: cgroupBefore,
after: cgroupAfter
}
}
};
} }
async function main() { async function main() {
const outPath = resolve('benchmark/out/'); const outPath = resolve('benchmark/out/');
await mkdir(outPath, { recursive: true }); await mkdir(outPath, { recursive: true });
for (const key in templates) { for (const key in templates) {
log.log('executing ' + key); log.log('executing ' + key);
const perfData = await run(templates[key], 100); const perfData = await run(templates[key], 100);
await writeFile(resolve(outPath, key + '.json'), JSON.stringify(perfData));
await writeFile(
resolve(outPath, key + '.json'),
JSON.stringify(perfData, null, 2)
);
await new Promise(res => setTimeout(res, 200)); await new Promise(res => setTimeout(res, 200));
} }
} }
+136
View File
@@ -0,0 +1,136 @@
import { readFile } from 'node:fs/promises';
import { cpus, totalmem } from 'node:os';
export type CpuSnapshot = {
idle: number;
total: number;
steal: number;
};
export type SystemSample = {
timestamp: number;
cpuUsagePercent: number;
cpuStealPercent: number;
load1: number;
load5: number;
load15: number;
freeMemory: number;
totalMemory: number;
};
export async function readCpuSnapshot(): Promise<CpuSnapshot> {
const stat = await readFile('/proc/stat', 'utf8');
const line = stat.split('\n')[0];
const parts = line
.trim()
.split(/\s+/)
.slice(1)
.map(v => Number(v));
const [
user,
nice,
system,
idle,
iowait,
irq,
softirq,
steal
] = parts;
return {
idle: idle + iowait,
total: parts.reduce((a, b) => a + b, 0),
steal: steal ?? 0
};
}
export async function measureCpuUsage(
previous: CpuSnapshot
): Promise<{
snapshot: CpuSnapshot;
usagePercent: number;
stealPercent: number;
}> {
const current = await readCpuSnapshot();
const idle = current.idle - previous.idle;
const total = current.total - previous.total;
const steal = current.steal - previous.steal;
return {
snapshot: current,
usagePercent: total === 0 ? 0 : 100 * (1 - idle / total),
stealPercent: total === 0 ? 0 : 100 * (steal / total)
};
}
export async function readCgroupCpuStat() {
const possiblePaths = [
'/sys/fs/cgroup/cpu.stat',
'/sys/fs/cgroup/cpu/cpu.stat'
];
for (const path of possiblePaths) {
try {
const txt = await readFile(path, 'utf8');
return Object.fromEntries(
txt
.trim()
.split('\n')
.map(line => {
const [k, v] = line.trim().split(/\s+/);
return [k, Number(v)];
})
);
} catch {
// continue
}
}
return null;
}
export async function readProcMemInfo() {
try {
const txt = await readFile('/proc/meminfo', 'utf8');
const result: Record<string, number> = {};
for (const line of txt.split('\n')) {
const match = line.match(/^(\w+):\s+(\d+)/);
if (!match) continue;
result[match[1]] = Number(match[2]);
}
return result;
} catch {
return null;
}
}
export function getMachineInfo() {
const cpuInfo = cpus();
return {
platform: process.platform,
arch: process.arch,
nodeVersion: process.version,
cpuModel: cpuInfo[0]?.model ?? 'unknown',
cpuCount: cpuInfo.length,
totalMemory: totalmem(),
ci: {
githubActions: process.env.GITHUB_ACTIONS ?? false,
runnerName: process.env.RUNNER_NAME ?? null,
runnerOs: process.env.RUNNER_OS ?? null,
runnerArch: process.env.RUNNER_ARCH ?? null
}
};
}