From a6b9ca43155b5e9357570705b6f45ba4fc3490a8 Mon Sep 17 00:00:00 2001 From: Max Richter Date: Mon, 4 May 2026 15:12:51 +0200 Subject: [PATCH] feat: capture system stats in benchmark --- app/benchmark/index.ts | 144 ++++++++++++++++++++++++++++++++--- app/benchmark/systemStats.ts | 136 +++++++++++++++++++++++++++++++++ 2 files changed, 271 insertions(+), 9 deletions(-) create mode 100644 app/benchmark/systemStats.ts diff --git a/app/benchmark/index.ts b/app/benchmark/index.ts index e06c003..bd6070c 100644 --- a/app/benchmark/index.ts +++ b/app/benchmark/index.ts @@ -1,9 +1,21 @@ import type { Graph, Graph as GraphType, NodeId } from '@nodarium/types'; import { createLogger, createPerformanceStore, splitNestedArray } from '@nodarium/utils'; + import { mkdir, writeFile } from 'node:fs/promises'; +import { freemem, loadavg, totalmem } from 'node:os'; import { resolve } from 'node:path'; + import { MemoryRuntimeExecutor } from '../src/lib/runtime/runtime-executor.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 lottaFacesTemplate from './templates/lotta-faces.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 templates: Record = { - 'plant': plantTemplate as unknown as GraphType, + plant: plantTemplate 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); + let totalVertices = 0; let totalFaces = 0; + for (const part of parts) { 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 faceCount = part[2] >>> 0; + if (type === 2) { const instanceCount = part[3] >>> 0; + totalVertices += vertexCount * instanceCount; totalFaces += faceCount * instanceCount; } else { @@ -38,41 +61,144 @@ function countGeometry(result: Int32Array): { totalVertices: number; totalFaces: totalFaces += faceCount; } } - return { totalVertices, totalFaces }; + + return { + totalVertices, + totalFaces + }; } async function run(g: GraphType, amount: number) { await registry.load(g.nodes.map(n => n.type) as NodeId[]); + log.log('loaded ' + g.nodes.length + ' nodes'); log.log('warming up'); + for (let index = 0; index < 10; index++) { 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'); + const perfStore = createPerformanceStore(); + r.perf = perfStore; - let res; + + let res: Int32Array | undefined; + + const cgroupBefore = await readCgroupCpuStat(); + for (let i = 0; i < amount; i++) { r.perf?.startRun(); + res = await r.execute(g, { randomSeed: true }); + r.perf?.stopRun(); + const { totalVertices, totalFaces } = countGeometry(res!); + r.perf?.addToLastRun('total-vertices', totalVertices); r.perf?.addToLastRun('total-faces', totalFaces); } + + const cgroupAfter = await readCgroupCpuStat(); + + clearInterval(sampler); + 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() { const outPath = resolve('benchmark/out/'); + await mkdir(outPath, { recursive: true }); + for (const key in templates) { log.log('executing ' + key); + 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)); } } diff --git a/app/benchmark/systemStats.ts b/app/benchmark/systemStats.ts new file mode 100644 index 0000000..f78bfad --- /dev/null +++ b/app/benchmark/systemStats.ts @@ -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 { + 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 = {}; + + 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 + } + }; +}