From 8dec878ecfa272f121c953e4358977ab5868fc3a Mon Sep 17 00:00:00 2001 From: mektpoy Date: Thu, 22 Oct 2026 20:04:10 +0800 Subject: [PATCH] fix(experimental): keep totals cumulative and switch token units to M --- .../client-ui-perf/README.i18n.yaml | 4 ++-- .../experimental/client-ui-perf/README.md | 2 +- .../experimental/client-ui-perf/README.zh.md | 2 +- .../client-ui-perf/src/client/PerfView.tsx | 2 +- .../client-ui-perf/src/client/locales.ts | 18 ++++++++-------- .../src/client/perf-aggregate.ts | 21 +++++++------------ .../client-ui-perf/src/client/perf-format.ts | 6 +++--- .../tests/perf-fold.client.spec.ts | 3 +++ .../tests/perf-metrics.client.spec.ts | 17 +++++++-------- .../tests/perf-view.client.spec.tsx | 2 +- 10 files changed, 36 insertions(+), 41 deletions(-) diff --git a/packages/experimental/client-ui-perf/README.i18n.yaml b/packages/experimental/client-ui-perf/README.i18n.yaml index f4be7a02f0..e0facf5ff1 100644 --- a/packages/experimental/client-ui-perf/README.i18n.yaml +++ b/packages/experimental/client-ui-perf/README.i18n.yaml @@ -24,8 +24,8 @@ en: e001601f2f9779b1 zh: 077d3703b8c82774 /deepseek-ai-dsh-experimental-client-ui-perf/understand-the-implementation: - en: 4d14d791cd6dd206 - zh: e7d54702502d8459 + en: c68d4c69e3820208 + zh: 58b9c35c9d49b512 /deepseek-ai-dsh-experimental-client-ui-perf/further-exploration: en: 285cfe1d7a99af34 zh: fbcee76e0c2f20fb diff --git a/packages/experimental/client-ui-perf/README.md b/packages/experimental/client-ui-perf/README.md index 8141bdef12..430b55386d 100644 --- a/packages/experimental/client-ui-perf/README.md +++ b/packages/experimental/client-ui-perf/README.md @@ -59,7 +59,7 @@ Four Conversation Definitions fold the event window into nodes for one view targ The footer switches control the totals row and the context basis, and the footer stays pinned above the composer while the table scrolls. An expanded step row fills the Trigger column with its start clock and the gap since the previous step. -The footer switches control the totals row and the context basis. `Context basis: Latest` shows each turn's final prompt size; `Cumulative` sums that turn's step prompt sizes, so a row shows the tokens processed rather than the final window. The totals row applies the same basis to every loaded turn and sums the numeric columns (cache stays token-weighted, speed stays output over decode time). +The footer switches control the totals row and the context basis. `Context basis` changes only how a turn row aggregates its steps: `Latest` shows the turn's final prompt size, while `Cumulative` sums that turn's step prompt sizes. The totals row is not affected by this switch: its context cell is always the cumulative step context of the loaded turns, and the other numeric columns are sums (cache stays token-weighted, speed stays output over decode time). Token cells switch from `K` to `M` at ten million. The turn cache-hit cell is token-weighted across the steps that reported usage (summed cache reads ÷ summed prompt tokens), so one transient cold step is not hidden by a warmer neighbour; the tooltip shows the summed read/input/write breakdown. A step row keeps its own measured share. diff --git a/packages/experimental/client-ui-perf/README.zh.md b/packages/experimental/client-ui-perf/README.zh.md index cb1937fdd8..0f3e151a6f 100644 --- a/packages/experimental/client-ui-perf/README.zh.md +++ b/packages/experimental/client-ui-perf/README.zh.md @@ -59,7 +59,7 @@ kind: "package-reference" 页脚的开关控制合计行与上下文口径,滚表格时页脚钉在输入框上方。展开的步骤行在「触发」列显示该步开始时刻和距上一步的间隔。 -页脚的开关控制合计行与上下文口径。「上下文口径:最新」显示每轮最后一步的提示词大小;「累计」显示该轮各步提示词大小之和,即这一轮实际处理过的 token 量。合计行对已加载的轮次套用同一口径并汇总数值列(缓存仍按 token 加权,速度仍按输出 ÷ 生成时长)。 +页脚的开关控制合计行与上下文口径。「上下文口径」只改变轮次行对其各步的聚合方式:「最新」显示该轮最后一步的提示词大小,「累计」显示该轮各步提示词大小之和。合计行不受这个开关影响:上下文列始终是已加载轮次的步骤累计值,其余数值列是求和(缓存仍按 token 加权,速度仍按输出 ÷ 生成时长)。token 超过一千万时从 K 切换为 M。 轮次的缓存命中率按报了 usage 的各步做 token 加权(缓存读之和 ÷ 提示词 token 之和),不会因为相邻步命中高就把某一次冷启动盖掉;tooltip 展示汇总后的读/输入/写明细。步骤行仍显示该步自己的实测比例。 diff --git a/packages/experimental/client-ui-perf/src/client/PerfView.tsx b/packages/experimental/client-ui-perf/src/client/PerfView.tsx index 6b43b93151..ee2942ecef 100644 --- a/packages/experimental/client-ui-perf/src/client/PerfView.tsx +++ b/packages/experimental/client-ui-perf/src/client/PerfView.tsx @@ -1614,7 +1614,7 @@ export function PerfView(props: PerfViewProps): ReactElement { return () => { clearInterval(timer) } }, [hasRunning]) // Turns compare only with the other turns in the window. - const totals = view.showTotals && turns.length > 0 ? aggregateTotals(turns, view.contextMode) : null + const totals = view.showTotals && turns.length > 0 ? aggregateTotals(turns) : null const scaleTurns = visibleTurns.size === 0 ? turns : turns.filter(turn => visibleTurns.has(turn.turn)) const turnScale = scaleTurns.reduce((max, turn) => { const span = totalSpan(turn, now) diff --git a/packages/experimental/client-ui-perf/src/client/locales.ts b/packages/experimental/client-ui-perf/src/client/locales.ts index 7e95183794..bc82180b45 100644 --- a/packages/experimental/client-ui-perf/src/client/locales.ts +++ b/packages/experimental/client-ui-perf/src/client/locales.ts @@ -62,11 +62,11 @@ export const zh = { 'table.showTotals': '显示合计行', 'contextMode.label': '上下文口径', 'contextMode.latest': '最新', - 'contextMode.latest.title': '每轮最后一步的提示词大小(最终上下文)', + 'contextMode.latest.title': '轮次行显示最后一步的提示词大小(最终上下文);合计行始终为累计值', 'contextMode.cumulative': '累计', - 'contextMode.cumulative.title': '该轮各步提示词大小之和(累计处理量)', + 'contextMode.cumulative.title': '轮次行显示该轮各步提示词大小之和(累计处理量);合计行始终为累计值', 'totals.label': '合计', - 'totals.title': '当前已加载轮次的合计', + 'totals.title': '当前已加载轮次的合计;上下文为各步累计值', 'expand.all': '展开全部', 'collapse.all': '折叠全部', @@ -206,11 +206,11 @@ export const en: Record = { 'table.showTotals': 'Show total row', 'contextMode.label': 'Context basis', 'contextMode.latest': 'Latest', - 'contextMode.latest.title': "Prompt size of the turn's last measured step (final context)", + 'contextMode.latest.title': "Turn rows show the last measured step's prompt size (final context); the total row stays cumulative", 'contextMode.cumulative': 'Cumulative', - 'contextMode.cumulative.title': "Sum of prompt sizes across the turn's steps (cumulative processing)", + 'contextMode.cumulative.title': "Turn rows show the sum of prompt sizes across the turn's steps (cumulative processing); the total row stays cumulative", 'totals.label': 'Total', - 'totals.title': 'Totals for the turns loaded in this window', + 'totals.title': 'Totals for the turns loaded in this window; the context cell is cumulative across steps', 'expand.all': 'Expand all', 'collapse.all': 'Collapse all', @@ -337,11 +337,11 @@ export const ja: Record = { 'table.showTotals': '合計行を表示', 'contextMode.label': 'コンテキスト基準', 'contextMode.latest': '最新', - 'contextMode.latest.title': '各ターンの最後に測定したステップのプロンプトサイズ(最終コンテキスト)', + 'contextMode.latest.title': 'ターン行は最後に測定したステップのプロンプトサイズ(最終コンテキスト)。合計行は常に累計', 'contextMode.cumulative': '累計', - 'contextMode.cumulative.title': 'ターン内の各ステップのプロンプトサイズの合計(累計処理量)', + 'contextMode.cumulative.title': 'ターン行はターン内の各ステップのプロンプトサイズの合計(累計処理量)。合計行は常に累計', 'totals.label': '合計', - 'totals.title': '読み込み済みターンの合計', + 'totals.title': '読み込み済みターンの合計。コンテキストは各ステップの累計', 'expand.all': 'すべて展開', 'collapse.all': 'すべて折りたたむ', diff --git a/packages/experimental/client-ui-perf/src/client/perf-aggregate.ts b/packages/experimental/client-ui-perf/src/client/perf-aggregate.ts index d2d57deddb..78744e2674 100644 --- a/packages/experimental/client-ui-perf/src/client/perf-aggregate.ts +++ b/packages/experimental/client-ui-perf/src/client/perf-aggregate.ts @@ -363,11 +363,9 @@ export function turnContext(turn: PerfTurnMetrics, mode: ContextMode): number | /** * Aggregate the loaded turns into one totals row. * @param turns - aggregated turns in display order. - * @param mode - context basis shared with the turn rows. - * @returns summed metrics and the context value for the selected basis. + * @returns summed metrics; the context column is always the cumulative step context. */ -export function aggregateTotals(turns: readonly PerfTurnMetrics[], mode: ContextMode): PerfTotals { - let context: number | null = null +export function aggregateTotals(turns: readonly PerfTurnMetrics[]): PerfTotals { let stepCount = 0 let outputSum = 0 let outputMeasured = false @@ -391,7 +389,6 @@ export function aggregateTotals(turns: readonly PerfTurnMetrics[], mode: Context let wallMeasured = false for (const turn of turns) { stepCount += turn.steps.length - if (isNumber(turn.context)) context = turn.context if (isNumber(turn.output)) { outputSum += turn.output outputMeasured = true @@ -429,19 +426,17 @@ export function aggregateTotals(turns: readonly PerfTurnMetrics[], mode: Context } let cumulative = 0 let cumulativeMeasured = false - if (mode === 'cumulative') { - for (const turn of turns) { - for (const step of turn.steps) { - if (!isNumber(step.context)) continue - cumulative += step.context - cumulativeMeasured = true - } + for (const turn of turns) { + for (const step of turn.steps) { + if (!isNumber(step.context)) continue + cumulative += step.context + cumulativeMeasured = true } } return { turnCount: turns.length, stepCount, - context: mode === 'cumulative' ? (cumulativeMeasured ? cumulative : null) : context, + context: cumulativeMeasured ? cumulative : null, output: outputMeasured ? outputSum : null, cacheHit: cacheMeasured && contextSum > 0 ? Math.min(1, read / contextSum) : null, cacheRead: cacheMeasured ? read : null, diff --git a/packages/experimental/client-ui-perf/src/client/perf-format.ts b/packages/experimental/client-ui-perf/src/client/perf-format.ts index 675ce7dac4..d277884b83 100644 --- a/packages/experimental/client-ui-perf/src/client/perf-format.ts +++ b/packages/experimental/client-ui-perf/src/client/perf-format.ts @@ -30,13 +30,13 @@ export function formatTokens(value: number | null): string { } /** - * Token count in K: one decimal at or above 1K, two decimals below so a - * short reply does not print as 0.0K. + * Token count in K, switching to M once the value reaches ten million. * @param value - token count, or null when unrecorded. - * @returns the K-suffixed count, or the dash placeholder. + * @returns the K/M-suffixed count, or the dash placeholder. */ export function formatTokensShort(value: number | null): string { if (!isNumber(value)) return DASH + if (value >= 10_000_000) return `${(value / 1_000_000).toFixed(1)}M` return value >= 1_000 ? `${(value / 1_000).toFixed(1)}K` : `${(value / 1_000).toFixed(2)}K` } diff --git a/packages/experimental/client-ui-perf/tests/perf-fold.client.spec.ts b/packages/experimental/client-ui-perf/tests/perf-fold.client.spec.ts index 3abac3d09c..8f6664d27b 100644 --- a/packages/experimental/client-ui-perf/tests/perf-fold.client.spec.ts +++ b/packages/experimental/client-ui-perf/tests/perf-fold.client.spec.ts @@ -328,6 +328,9 @@ describe('display formatting boundaries', () => { expect(formatTps(Number.NaN)).toBe(DASH) expect(formatTps(0)).toBe('0.0 tok/s') expect(formatTps(268.94)).toBe('268.9 tok/s') + expect(formatTokensShort(9_999_999)).toBe('10000.0K') + expect(formatTokensShort(10_000_000)).toBe('10.0M') + expect(formatTokensShort(65_300_000)).toBe('65.3M') expect(formatPercent(null)).toBe(DASH) expect(formatPercent(0)).toBe('0.00%') expect(formatPercent(0.962)).toBe('96.20%') diff --git a/packages/experimental/client-ui-perf/tests/perf-metrics.client.spec.ts b/packages/experimental/client-ui-perf/tests/perf-metrics.client.spec.ts index 1fad13ac2c..28641d893c 100644 --- a/packages/experimental/client-ui-perf/tests/perf-metrics.client.spec.ts +++ b/packages/experimental/client-ui-perf/tests/perf-metrics.client.spec.ts @@ -239,7 +239,7 @@ describe('turn aggregation', () => { const bare = aggregateTurn(3, [metricsOf({ step: 1 })]) expect(turnContext(bare, 'cumulative')).toBeNull() expect(turnContext(bare, 'latest')).toBeNull() - const totals = aggregateTotals([bare], 'cumulative') + const totals = aggregateTotals([bare]) expect(totals.context).toBeNull() expect(totals.output).toBeNull() expect(totals.cacheHit).toBeNull() @@ -252,14 +252,14 @@ describe('turn aggregation', () => { const partial = aggregateTurn(4, [metricsOf({ step: 1, context: 100, cacheRead: 40 })]) expect(turnContext(partial, 'cumulative')).toBe(100) - const mixed = aggregateTotals([partial], 'cumulative') + const mixed = aggregateTotals([partial]) expect(mixed.cacheWrite).toBe(0) expect(mixed.cacheHit).toBe(0.4) const forged = { ...bare, cacheRead: 10, cacheContext: 100, cacheWrite: null } - expect(aggregateTotals([forged], 'latest').cacheWrite).toBe(0) + expect(aggregateTotals([forged]).cacheWrite).toBe(0) }) - it('aggregates loaded turns into totals and switches the context basis', () => { + it('aggregates loaded turns into totals with cumulative context', () => { const first = aggregateTurn(1, [ metricsOf({ step: 1, startTime: 0, endTime: 300, context: 100, output: 10, decode: 1_000, @@ -276,10 +276,10 @@ describe('turn aggregation', () => { cacheRead: 250, cacheWrite: 10, tool: 75, toolWall: 60, wall: 500, }), ]) - const latest = aggregateTotals([first, second], 'latest') + const latest = aggregateTotals([first, second]) expect(latest.turnCount).toBe(2) expect(latest.stepCount).toBe(3) - expect(latest.context).toBe(300) + expect(latest.context).toBe(600) expect(turnContext(first, 'latest')).toBe(200) expect(turnContext(first, 'cumulative')).toBe(300) expect(latest.output).toBe(60) @@ -297,10 +297,7 @@ describe('turn aggregation', () => { expect(latest.toolWall).toBe(100) expect(latest.wall).toBe(1_200) - const cumulative = aggregateTotals([first, second], 'cumulative') - expect(cumulative.context).toBe(600) - - const empty = aggregateTotals([], 'cumulative') + const empty = aggregateTotals([]) expect(empty).toEqual({ turnCount: 0, stepCount: 0, diff --git a/packages/experimental/client-ui-perf/tests/perf-view.client.spec.tsx b/packages/experimental/client-ui-perf/tests/perf-view.client.spec.tsx index 79b617e791..969b873aac 100644 --- a/packages/experimental/client-ui-perf/tests/perf-view.client.spec.tsx +++ b/packages/experimental/client-ui-perf/tests/perf-view.client.spec.tsx @@ -780,7 +780,7 @@ describe('turn rows', () => { const totalLabel = screen.getByText(say('totals.label')) expect(totalLabel.parentElement?.textContent).toContain(say('summary', { turns: 2, steps: 4 })) const totalRow = totalLabel.closest('tr') as HTMLTableRowElement - expect(cellsOf(totalRow)[2]?.textContent).toBe(formatTokensShort(300)) + expect(cellsOf(totalRow)[2]?.textContent).toBe(formatTokensShort(600)) const first = rowWithPrefix(say('turn.label', { turn: 1 })) expect(cellsOf(first)[3]?.textContent).toBe(formatTokensShort(200))