mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-10-17 05:14:19 +02:00
Phase 3 of the live benchmark run experience. - ScoreReveal: replaces the abrupt run-view unmount with a deliberate end-of-run "REPORT" card -- animated NOMAD score gauge + odometer count-up number, sub-score gauges cascading in, Continue button + 5s auto-dismiss. Takes score scale as a prop so it survives Score v2. - GPU-util overlay (NVIDIA): during the AI stage, a ~1Hz nvidia-smi poll inside the Ollama container feeds live GPU utilization + VRAM into the telemetry frames; shown in the AI hero, hidden when absent (AMD/none). Poller is side-effect-only and cleared in a finally -- scored numbers unchanged. - Disk polish: reset in-test buffers on stage transition so the write stage no longer briefly shows the carried disk-read value. Browser-validated on NOMAD3 (RTX 5060): GPU overlay live (1% util, 0.2/8.0 GB VRAM during model load); reveal cascade + count-up; AI-only score 39.8 (scored path unchanged). Part of #1082 (tracker stays open). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
import transmit from '@adonisjs/transmit/services/main'
|
|
import si from 'systeminformation'
|
|
import logger from '@adonisjs/core/services/logger'
|
|
import { BROADCAST_CHANNELS } from '../../constants/broadcast.js'
|
|
import type { BenchmarkStatus, BenchmarkTelemetry } from '../../types/benchmark.js'
|
|
|
|
const SAMPLE_INTERVAL_MS = 1000
|
|
|
|
/**
|
|
* Samples host telemetry (per-core CPU load, temperature, disk throughput) on a
|
|
* timer and broadcasts it over SSE while a benchmark runs. Read-only: the sampler
|
|
* runs in the orchestration process, never inside the sysbench container, so it
|
|
* cannot influence the scored numbers.
|
|
*
|
|
* Signals come from `systeminformation`, which reads the host's /proc and /sys
|
|
* (not namespaced for these counters), so this works from inside a container too.
|
|
* Temperature legitimately isn't available on every host (VMs, some hwmon-less
|
|
* boards) and is reported as null rather than faked.
|
|
*/
|
|
export class BenchmarkTelemetrySampler {
|
|
private timer: NodeJS.Timeout | null = null
|
|
private readonly benchmarkId: string | null
|
|
private status: BenchmarkStatus = 'starting'
|
|
private startedAt = Date.now()
|
|
private stageMetric: BenchmarkTelemetry['stage_metric'] | undefined
|
|
private gpu: BenchmarkTelemetry['gpu'] | undefined
|
|
private sampling = false
|
|
|
|
constructor(benchmarkId: string | null) {
|
|
this.benchmarkId = benchmarkId
|
|
}
|
|
|
|
start() {
|
|
if (this.timer) return
|
|
this.startedAt = Date.now()
|
|
// Prime systeminformation's per-second deltas and emit an initial frame,
|
|
// then continue on the interval.
|
|
void this._sample()
|
|
this.timer = setInterval(() => void this._sample(), SAMPLE_INTERVAL_MS)
|
|
}
|
|
|
|
/** Tag subsequent frames with the current stage; clears any prior in-test state. */
|
|
setStage(status: BenchmarkStatus) {
|
|
this.status = status
|
|
this.stageMetric = undefined
|
|
this.gpu = undefined
|
|
}
|
|
|
|
/** Inject an in-test metric (e.g. live AI tokens/sec) into subsequent frames. */
|
|
setStageMetric(kind: NonNullable<BenchmarkTelemetry['stage_metric']>['kind'], value: number, ttftMs?: number) {
|
|
this.stageMetric = { kind, value, ...(ttftMs !== undefined ? { ttft_ms: ttftMs } : {}) }
|
|
}
|
|
|
|
/** Inject NVIDIA GPU stats into subsequent frames (null clears them). */
|
|
setGpuStats(gpu: NonNullable<BenchmarkTelemetry['gpu']> | null) {
|
|
this.gpu = gpu ?? undefined
|
|
}
|
|
|
|
stop() {
|
|
if (this.timer) {
|
|
clearInterval(this.timer)
|
|
this.timer = null
|
|
}
|
|
}
|
|
|
|
private async _sample() {
|
|
// Skip if the previous sample is still in flight (currentLoad can take a
|
|
// couple hundred ms); never let samples pile up.
|
|
if (this.sampling) return
|
|
this.sampling = true
|
|
try {
|
|
const [load, temp, fs] = await Promise.all([
|
|
si.currentLoad().catch(() => null),
|
|
si.cpuTemperature().catch(() => null),
|
|
si.fsStats().catch(() => null),
|
|
])
|
|
|
|
const overall = load ? Math.max(0, Math.round(load.currentLoad)) : 0
|
|
const perCore = load?.cpus?.map((c) => Math.max(0, Math.round(c.load))) ?? []
|
|
const tempC = temp && typeof temp.main === 'number' && temp.main > 0 ? Math.round(temp.main) : null
|
|
const readMb = fs && typeof fs.rx_sec === 'number' && fs.rx_sec >= 0 ? Number((fs.rx_sec / 1e6).toFixed(1)) : 0
|
|
const writeMb = fs && typeof fs.wx_sec === 'number' && fs.wx_sec >= 0 ? Number((fs.wx_sec / 1e6).toFixed(1)) : 0
|
|
|
|
const payload: BenchmarkTelemetry = {
|
|
benchmark_id: this.benchmarkId,
|
|
status: this.status,
|
|
t: Date.now() - this.startedAt,
|
|
cpu: { overall, per_core: perCore },
|
|
temp_c: tempC,
|
|
disk: { read_mb_s: readMb, write_mb_s: writeMb },
|
|
...(this.stageMetric ? { stage_metric: this.stageMetric } : {}),
|
|
...(this.gpu ? { gpu: this.gpu } : {}),
|
|
}
|
|
|
|
transmit.broadcast(BROADCAST_CHANNELS.BENCHMARK_TELEMETRY, payload)
|
|
} catch (err) {
|
|
logger.debug(`[BenchmarkTelemetry] sample failed: ${err.message}`)
|
|
} finally {
|
|
this.sampling = false
|
|
}
|
|
}
|
|
}
|