From 4c48fe0c41c90cea4fb44f0bdf0463d84fc6e0f1 Mon Sep 17 00:00:00 2001 From: Luke Wass Date: Mon, 24 Aug 2026 09:33:00 -0500 Subject: [PATCH] fix(agent): carry Intel GPU averages forward between samples (#2256) Intel GPUs (intel_gpu_top) never report temperature or memory, so the "suspended card" heuristic in calculateGPUAverage (temp == 0 && memoryUsed == 0) fired on every collection that landed between samples. intel_gpu_top samples every 3.3s (intelGpuStatsInterval) while the hub's realtime worker collects every 1s, so most realtime collections had no new sample (delta count 0) and returned an empty GPUData with power omitted (json "p"/"pp" are omitempty). The frontend derives the GPU Power Draw series and legend from the latest sample, so the chart and legend blanked on roughly two of every three or four one-second cycles. NVIDIA/AMD were unaffected because they report temperature even when idle, so the heuristic never fired and the last average was already carried forward. Gate the zero-return on non-engine (discrete) GPUs so Intel GPUs carry the last average forward during between-sample gaps, matching the existing NVIDIA/AMD behavior. Add a regression test. --- agent/gpu.go | 12 ++++++++---- agent/gpu_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/agent/gpu.go b/agent/gpu.go index b5ace45a..d1b7b422 100644 --- a/agent/gpu.go +++ b/agent/gpu.go @@ -361,12 +361,16 @@ func (gm *GPUManager) calculateGPUAverage(id string, gpu *system.GPUData, cacheK // If no new data arrived if deltaCount == 0 { - // If GPU appears suspended (instantaneous values are 0), return zero values - // Otherwise return last known average for temporary collection gaps - if gpu.Temperature == 0 && gpu.MemoryUsed == 0 { + // Only discrete GPUs report temp/memory, so treat all-zero as suspended (return zeros). + // Engine-based (Intel) GPUs don't, so carry the last average forward across sample gaps. + if gpu.Engines == nil && gpu.Temperature == 0 && gpu.MemoryUsed == 0 { return system.GPUData{Name: gpu.Name} } - return gm.lastAvgData[id] // zero value if not found + lastAvg := gm.lastAvgData[id] // zero value if not found + if lastAvg.Name == "" { + lastAvg.Name = gpu.Name + } + return lastAvg } // Calculate new average diff --git a/agent/gpu_test.go b/agent/gpu_test.go index 075228c3..1ac6e018 100644 --- a/agent/gpu_test.go +++ b/agent/gpu_test.go @@ -566,6 +566,42 @@ func TestGetCurrentData(t *testing.T) { assert.EqualValues(t, 2, gm.GpuDataMap["0"].Count, "Count should still be 2") }) + t.Run("carries Intel GPU average forward between samples", func(t *testing.T) { + // Intel GPUs report no temp/memory, so between-sample gaps (delta 0) must + // reuse the last average instead of returning zeros and blanking the chart. + gm := &GPUManager{ + GpuDataMap: map[string]*system.GPUData{ + "0": { + Name: "GPU", + Usage: 0, // derived from engines for Intel + Power: 200, // averages to 100 over 2 counts + PowerPkg: 60, // averages to 30 over 2 counts + Count: 2, + Engines: map[string]float64{ + "Render/3D": 80, // averages to 40 + "Video": 20, // averages to 10 + }, + }, + }, + } + + cacheKey := uint16(1000) // realtime cache key + + // First collection - computes and stores averages + result1 := gm.GetCurrentData(cacheKey) + assert.InDelta(t, 100.0, result1["0"].Power, 0.01) + assert.InDelta(t, 30.0, result1["0"].PowerPkg, 0.01) + assert.InDelta(t, 40.0, result1["0"].Engines["Render/3D"], 0.01) + + // Second collection with no new sample (count unchanged, temp/mem still 0). + // Must carry the last average forward rather than blanking to zero. + result2 := gm.GetCurrentData(cacheKey) + assert.Equal(t, "GPU", result2["0"].Name, "Name should be preserved") + assert.InDelta(t, 100.0, result2["0"].Power, 0.01, "Should reuse last average power, not 0") + assert.InDelta(t, 30.0, result2["0"].PowerPkg, 0.01, "Should reuse last average package power, not 0") + assert.InDelta(t, 40.0, result2["0"].Engines["Render/3D"], 0.01, "Should reuse last average engine usage") + }) + t.Run("tracks separate averages per cache key", func(t *testing.T) { gm := &GPUManager{ GpuDataMap: map[string]*system.GPUData{