diff --git a/apps/host/server/energy-meter.mjs b/apps/host/server/energy-meter.mjs index 9bed3eef4d7..5de02071993 100644 --- a/apps/host/server/energy-meter.mjs +++ b/apps/host/server/energy-meter.mjs @@ -4,6 +4,12 @@ */ const meter = { wh: 0, at: 0 }; +/** Sum the cards that report power. An unmetered iGPU must not hide a measured discrete GPU. */ +export function reportedPower(readings, key = 'powerW') { + const values = readings.map(gpu => gpu[key]).filter(value => typeof value === 'number' && Number.isFinite(value) && value >= 0); + return values.length ? values.reduce((total, value) => total + value, 0) : null; +} + export function addEnergy(powerW, at) { const dt = meter.at ? Math.min((at - meter.at) / 1000, 5) : 0; meter.at = at; diff --git a/apps/host/server/monitor.mjs b/apps/host/server/monitor.mjs index c861d9b97f6..ee73b980d12 100644 --- a/apps/host/server/monitor.mjs +++ b/apps/host/server/monitor.mjs @@ -5,7 +5,7 @@ import { readGpuTelemetry, gpuMemoryTotals } from './device-probes.mjs'; import { createSystemSampler } from './system-telemetry.mjs'; import { sensorPace, WATCH_MS } from './sensor-pace.mjs'; import { readTemperatures } from './temperature-telemetry.mjs'; -import { addEnergy, energyWh } from './energy-meter.mjs'; +import { addEnergy, energyWh, reportedPower } from './energy-meter.mjs'; import { state as servingState } from './serving.mjs'; import { originalDashboard } from './monitor-dashboard.mjs'; import { get as getStore } from './store.mjs'; @@ -93,7 +93,7 @@ function hardwareSnapshot() { vramTotalGb:memory.total, ramUsedGb:systemReading?.ramUsedGb??null,ramTotalGb:systemReading?.ramTotalGb??hw.ramTotalGb, gpuTempC:aggregate(readings,'temperatureC','max'),tempLimitC:hw.tempLimitC, - powerW:aggregate(readings,'powerW'),powerLimitW:aggregate(readings,'powerLimitW'), + powerW:reportedPower(readings),powerLimitW:reportedPower(readings,'powerLimitW'), pcieRxMbs:aggregate(readings,'pcieRxMbs'), cpuTempC:demo?54:temperatures.cpuTempC,ssdTempC:demo?41:temperatures.ssdTempC,energyWh:round(energyWh(),3), diskReadGbs:systemReading?.diskReadGbs??null,diskWriteMbs:systemReading?.diskWriteMbs??null, diff --git a/apps/host/tests/energy-meter.test.mjs b/apps/host/tests/energy-meter.test.mjs new file mode 100644 index 00000000000..54758fd16b3 --- /dev/null +++ b/apps/host/tests/energy-meter.test.mjs @@ -0,0 +1,20 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { addEnergy, reportedPower } from '../server/energy-meter.mjs'; + +test('an unmetered iGPU does not prevent energy accumulation from a metered GPU', () => { + const readings = [ + { id: 'GPU-discrete', powerW: 72, powerLimitW: 575 }, + { id: 'GPU-integrated', powerW: null, powerLimitW: null }, + ]; + assert.equal(reportedPower(readings), 72); + assert.equal(reportedPower(readings, 'powerLimitW'), 575); + const start = addEnergy(reportedPower(readings), 1_000); + assert.equal(addEnergy(reportedPower(readings), 6_000) - start, 0.1); +}); + +test('power remains unavailable when no GPU reports it, and multiple readings are summed', () => { + assert.equal(reportedPower([]), null); + assert.equal(reportedPower([{ powerW: null }, { powerW: NaN }]), null); + assert.equal(reportedPower([{ powerW: 20 }, { powerW: 30 }]), 50); +});