From a5dfaf360642387c324ff57239d5a530bd424941 Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Sun, 20 Sep 2026 23:56:24 -0700 Subject: [PATCH 1/2] fix: keep constant telemetry axes readable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 中文:为恒定及近恒定遥测值保留最小坐标范围,避免显存时钟刻度坍缩;补充桌面、手机、全零值和 TDP 参考线的组件回归测试。 --- .../cypress/component/gpu-metrics-axis.cy.tsx | 89 +++++++++++++++++++ .../components/gpu-power/GpuPowerChart.tsx | 10 ++- 2 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 packages/app/cypress/component/gpu-metrics-axis.cy.tsx diff --git a/packages/app/cypress/component/gpu-metrics-axis.cy.tsx b/packages/app/cypress/component/gpu-metrics-axis.cy.tsx new file mode 100644 index 000000000..900377614 --- /dev/null +++ b/packages/app/cypress/component/gpu-metrics-axis.cy.tsx @@ -0,0 +1,89 @@ +import GpuMetricsChart from '@/components/gpu-power/GpuPowerChart'; +import type { GpuMetricRow } from '@/components/gpu-power/types'; +import { HW_REGISTRY } from '@semianalysisai/inferencex-constants'; + +const svg = () => cy.get('[data-testid="gpu-metrics-chart-svg"]'); + +function samples(clock: (sample: number) => number): GpuMetricRow[] { + return Array.from({ length: 8 }, (_chip, index) => + Array.from({ length: 21 }, (_, sample) => ({ + timestamp: new Date(Date.UTC(2026, 8, 20) + sample * 30_000).toISOString(), + index, + power: 300 + sample * 30, + memClock: clock(sample), + })), + ).flat(); +} + +function mountClock(data: GpuMetricRow[]) { + cy.mount( + , + ); +} + +function assertReadableAxis(value: number) { + svg().should(($svg) => { + const labels = $svg.find('.y-axis .tick text').toArray(); + expect(labels.length, 'multiple useful ticks').to.be.greaterThan(1); + const values = labels.map((label) => Number(label.textContent?.replaceAll(',', ''))); + expect(values.every(Number.isFinite), 'numeric ticks').to.eq(true); + expect(new Set(values).size, 'distinct ticks').to.eq(values.length); + expect(Math.min(...values)).to.be.at.most(value); + expect(Math.max(...values)).to.be.greaterThan(value); + const title = $svg.find('.y-axis-label')[0]!.getBoundingClientRect(); + for (const label of labels) { + expect(label.getBoundingClientRect().left, 'tick clears axis title').to.be.greaterThan( + title.right, + ); + } + }); + svg().find('path.line-path').should('have.length', 8); +} + +describe('PowerX telemetry axis', () => { + for (const width of [1280, 375]) { + it(`keeps constant memory clocks readable at ${width}px`, () => { + cy.viewport(width, 800); + mountClock(samples(() => 390)); + assertReadableAxis(390); + }); + } + + it('does not magnify floating-point noise after rolling averages', () => { + mountClock(samples((sample) => 390 - (sample % 2) * Number.EPSILON * 256)); + assertReadableAxis(390); + }); + + it('gives all-zero telemetry a nonzero, nonnegative range', () => { + mountClock(samples(() => 0)); + assertReadableAxis(0); + svg().find('.y-axis .tick text').first().should('have.text', '0.0'); + }); + + it('preserves varying power measurements and the hardware TDP reference', () => { + cy.mount( + 390)} + visibleGpus={new Set([0, 1, 2, 3, 4, 5, 6, 7])} + metricKey="power" + artifactName="gpu_metrics_b200" + display={{ mode: 'points', windowS: 300, series: 'chips' }} + />, + ); + svg().find('.tdp-line text').should('contain.text', `${HW_REGISTRY.b200.tdp}W`); + svg().should(($svg) => { + const values = $svg + .find('.y-axis .tick text') + .toArray() + .map((label) => Number(label.textContent?.replaceAll(',', ''))); + expect(Math.min(...values)).to.be.at.most(300); + expect(Math.max(...values)).to.be.at.least(HW_REGISTRY.b200.tdp); + }); + }); +}); diff --git a/packages/app/src/components/gpu-power/GpuPowerChart.tsx b/packages/app/src/components/gpu-power/GpuPowerChart.tsx index addb25953..68d622d21 100644 --- a/packages/app/src/components/gpu-power/GpuPowerChart.tsx +++ b/packages/app/src/components/gpu-power/GpuPowerChart.tsx @@ -387,8 +387,14 @@ const GpuMetricsChart = React.memo( if (allPoints.length === 0) return [0, 100] as [number, number]; const ext = d3.extent(allPoints, (d) => d.value) as [number, number]; const range = ext[1] - ext[0]; - const yMin = Math.max(0, ext[0] - range * 0.05); - let yMax = ext[1] + range * 0.05; + // Constant telemetry and floating-point noise still need a readable axis. + const padding = Math.max( + range * 0.05, + Math.max(Math.abs(ext[0]), Math.abs(ext[1])) * 0.01, + 1, + ); + const yMin = Math.max(0, ext[0] - padding); + let yMax = ext[1] + padding; if (tdpInfo && tdpInfo.tdp > yMax) yMax = tdpInfo.tdp * 1.05; return [yMin, yMax] as [number, number]; }, [allPoints, tdpInfo]); From e5c3bcb0e2716bb957c734a08459926f00627456 Mon Sep 17 00:00:00 2001 From: Wenyao Gao Date: Mon, 21 Sep 2026 00:16:18 -0700 Subject: [PATCH 2/2] fix: clear power reference when switching telemetry metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 切换到显存频率时清除旧 TDP 参考线,并验证切回功耗后仅恢复一条参考线。 --- .../cypress/component/gpu-metrics-axis.cy.tsx | 28 ++++++++++ .../components/gpu-power/GpuPowerChart.tsx | 51 +++++++++---------- 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/packages/app/cypress/component/gpu-metrics-axis.cy.tsx b/packages/app/cypress/component/gpu-metrics-axis.cy.tsx index 900377614..2d7443411 100644 --- a/packages/app/cypress/component/gpu-metrics-axis.cy.tsx +++ b/packages/app/cypress/component/gpu-metrics-axis.cy.tsx @@ -1,6 +1,7 @@ import GpuMetricsChart from '@/components/gpu-power/GpuPowerChart'; import type { GpuMetricRow } from '@/components/gpu-power/types'; import { HW_REGISTRY } from '@semianalysisai/inferencex-constants'; +import { useState } from 'react'; const svg = () => cy.get('[data-testid="gpu-metrics-chart-svg"]'); @@ -86,4 +87,31 @@ describe('PowerX telemetry axis', () => { expect(Math.max(...values)).to.be.at.least(HW_REGISTRY.b200.tdp); }); }); + + it('removes the power reference when switching to memory clock and restores it once', () => { + function SwitchMetric() { + const [metric, setMetric] = useState<'power' | 'memClock'>('power'); + return ( + <> + + 3996)} + visibleGpus={new Set([0, 1, 2, 3, 4, 5, 6, 7])} + metricKey={metric} + artifactName="gpu_metrics_b200" + display={{ mode: 'rolling', windowS: 300, series: 'chips' }} + /> + + ); + } + cy.mount(); + svg().find('.tdp-line').should('have.length', 1); + cy.contains('button', 'Switch metric').click(); + svg().find('.tdp-line').should('not.exist'); + assertReadableAxis(3996); + cy.contains('button', 'Switch metric').click(); + svg().find('.tdp-line').should('have.length', 1); + }); }); diff --git a/packages/app/src/components/gpu-power/GpuPowerChart.tsx b/packages/app/src/components/gpu-power/GpuPowerChart.tsx index 68d622d21..1cba651ec 100644 --- a/packages/app/src/components/gpu-power/GpuPowerChart.tsx +++ b/packages/app/src/components/gpu-power/GpuPowerChart.tsx @@ -426,32 +426,31 @@ const GpuMetricsChart = React.memo( { type: 'custom', key: 'tdp-line', - render: tdpInfo - ? (group, ctx) => { - const yScale = ctx.yScale as d3.ScaleLinear; - const tdpY = yScale(tdpInfo.tdp); - group.selectAll('.tdp-line').remove(); - const tdpGroup = group.append('g').attr('class', 'tdp-line'); - tdpGroup - .append('line') - .attr('x1', 0) - .attr('x2', ctx.width) - .attr('y1', tdpY) - .attr('y2', tdpY) - .attr('stroke', '#ef4444') - .attr('stroke-width', 1.5) - .attr('stroke-dasharray', '6,4'); - tdpGroup - .append('text') - .attr('x', ctx.width - 4) - .attr('y', tdpY - 6) - .attr('text-anchor', 'end') - .attr('fill', '#ef4444') - .attr('font-size', '11px') - .attr('font-weight', '600') - .text(`${tdpInfo.sku} TDP: ${tdpInfo.tdp}W`); - } - : null, + render: (group, ctx) => { + group.selectAll('.tdp-line').remove(); + if (!tdpInfo) return; + const yScale = ctx.yScale as d3.ScaleLinear; + const tdpY = yScale(tdpInfo.tdp); + const tdpGroup = group.append('g').attr('class', 'tdp-line'); + tdpGroup + .append('line') + .attr('x1', 0) + .attr('x2', ctx.width) + .attr('y1', tdpY) + .attr('y2', tdpY) + .attr('stroke', '#ef4444') + .attr('stroke-width', 1.5) + .attr('stroke-dasharray', '6,4'); + tdpGroup + .append('text') + .attr('x', ctx.width - 4) + .attr('y', tdpY - 6) + .attr('text-anchor', 'end') + .attr('fill', '#ef4444') + .attr('font-size', '11px') + .attr('font-weight', '600') + .text(`${tdpInfo.sku} TDP: ${tdpInfo.tdp}W`); + }, }, // GPU lines {