diff --git a/packages/experimental/client-ui-perf/README.i18n.yaml b/packages/experimental/client-ui-perf/README.i18n.yaml index c3ccf43d78..868b8e40fd 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: 289e8a539820fa4d - zh: 5dad2cfac77761e8 + en: 611ac4f20831030d + zh: 11051434b5271feb /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 5933f3a1ac..15437c315d 100644 --- a/packages/experimental/client-ui-perf/README.md +++ b/packages/experimental/client-ui-perf/README.md @@ -57,6 +57,8 @@ Four Conversation Definitions fold the event window into nodes for one view targ | `src/client/PerfView.tsx` | Scrollport with scroll-to-top paging, table, time bars, step accordion, copy controls, and the prompt image pill with its preview and viewer | | `src/client/locales.ts` | Every visible string, in Chinese, English, and Japanese | +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. + Windows are bounded: when a step's `step/start` is outside the loaded window, its start-dependent columns show `—` with a `start unknown` marker rather than a guessed value. Scrolling to the top asks the Session for one older page; the view offsets the scroll position by the height it prepended, so the row being read stays in place. diff --git a/packages/experimental/client-ui-perf/README.zh.md b/packages/experimental/client-ui-perf/README.zh.md index 53ba30d94c..4427e2454f 100644 --- a/packages/experimental/client-ui-perf/README.zh.md +++ b/packages/experimental/client-ui-perf/README.zh.md @@ -57,6 +57,8 @@ kind: "package-reference" | `src/client/PerfView.tsx` | 表格、时间条、步骤手风琴、复制按钮,以及提问图片标签、预览与看图器 | | `src/client/locales.ts` | 全部可见文案,中、英、日三份 | +轮次的缓存命中率按报了 usage 的各步做 token 加权(缓存读之和 ÷ 提示词 token 之和),不会因为相邻步命中高就把某一次冷启动盖掉;tooltip 展示汇总后的读/输入/写明细。步骤行仍显示该步自己的实测比例。 + 会话窗口是有界的:某一步的 `step/start` 不在已加载窗口里时,与起点有关的列显示 `—` 并标「起止未知」,不估算数值。滚到顶部会向 Session 请求更早的一页;视图按新增内容的高度补偿滚动位置,正在读的行不会跳走。 diff --git a/packages/experimental/client-ui-perf/src/client/PerfView.tsx b/packages/experimental/client-ui-perf/src/client/PerfView.tsx index a022ca18c9..ea9ae378f2 100644 --- a/packages/experimental/client-ui-perf/src/client/PerfView.tsx +++ b/packages/experimental/client-ui-perf/src/client/PerfView.tsx @@ -150,6 +150,8 @@ interface MetricsValues { readonly tool: number | null readonly cacheRead: number | null readonly cacheWrite: number | null + /** Weighted prompt denominator of a turn's cache rate; step rows use `context`. */ + readonly cacheContext?: number | null readonly ttftPending?: number readonly modelPending?: number readonly toolPending?: number @@ -802,7 +804,9 @@ const COLUMN_DETAIL: Readonly< if (!isNumber(value.cacheRead) && !isNumber(value.cacheWrite)) return t('column.cache.title') const read = value.cacheRead ?? 0 const write = value.cacheWrite ?? 0 - const context = isNumber(value.context) ? value.context : read + write + const context = isNumber(value.cacheContext) + ? value.cacheContext + : isNumber(value.context) ? value.context : read + write const input = Math.max(0, context - read - write) return t('cache.detail', { read: formatTokens(read), 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 c57f1e4c6a..e2f6f23702 100644 --- a/packages/experimental/client-ui-perf/src/client/perf-aggregate.ts +++ b/packages/experimental/client-ui-perf/src/client/perf-aggregate.ts @@ -44,16 +44,26 @@ function turnContext(steps: readonly PerfStepMetrics[]): number | null { return context } -/** Cache breakdown measured last in the turn, from the same step whose hit rate the row shows. */ -function turnCache(steps: readonly PerfStepMetrics[]): { read: number | null; write: number | null } { - let read: number | null = null - let write: number | null = null +/** Token-weighted cache level of the turn, over the steps that reported both read and prompt tokens. */ +function turnCache(steps: readonly PerfStepMetrics[]): { + rate: number | null + read: number | null + write: number | null + context: number | null +} { + let read = 0 + let write = 0 + let context = 0 + let measured = false for (const step of steps) { - if (!isNumber(step.cacheHit)) continue - read = step.cacheRead - write = step.cacheWrite + if (!isNumber(step.cacheRead) || !isNumber(step.context)) continue + read += step.cacheRead + write += step.cacheWrite ?? 0 + context += step.context + measured = true } - return { read, write } + const level = measured && context > 0 ? Math.min(1, read / context) : null + return { rate: level, read: measured ? read : null, write: measured ? write : null, context: measured ? context : null } } /** Tool tallies by descending count; ties keep call order. */ @@ -144,16 +154,12 @@ export function aggregateTurn(turn: number, steps: readonly PerfStepMetrics[]): && !last.running && isNumber(first.startTime) && isNumber(last.endTime) ? Math.max(0, last.endTime - first.startTime) : null - // Last cache hit measured in the turn: the prompt grows, so the tail value is - // the turn's actual level rather than the first step's cold start. - let cacheHit: number | null = null let answer: string | null = null let retries = 0 let toolErrors = 0 let toolOutput = 0 let lastStepWall: number | null = null for (const step of steps) { - if (isNumber(step.cacheHit)) cacheHit = step.cacheHit if (step.answer !== null) answer = step.answer retries += step.retries toolErrors += step.toolErrors @@ -182,9 +188,10 @@ export function aggregateTurn(turn: number, steps: readonly PerfStepMetrics[]): toolWall: toolWallMeasured ? toolWallSum : null, toolPending: toolPairable === 0 ? 0 : toolPending, tools: toolCounts, - cacheHit, + cacheHit: cache.rate, cacheRead: cache.read, cacheWrite: cache.write, + cacheContext: cache.context, retries, toolErrors, toolOutput, diff --git a/packages/experimental/client-ui-perf/src/client/perf-contract.ts b/packages/experimental/client-ui-perf/src/client/perf-contract.ts index 84c77b10ec..59ef8c68ff 100644 --- a/packages/experimental/client-ui-perf/src/client/perf-contract.ts +++ b/packages/experimental/client-ui-perf/src/client/perf-contract.ts @@ -139,10 +139,14 @@ export interface PerfTurnMetrics { readonly toolWall: number | null readonly toolPending: number readonly tools: readonly PerfToolCount[] + /** Token-weighted cache-read share across the steps that reported usage. */ readonly cacheHit: number | null - /** Cache breakdown of the last step that reported usage, for the turn tooltip. */ + /** Cache-read tokens summed across the steps that reported usage. */ readonly cacheRead: number | null + /** Cache-write tokens summed across the steps that reported usage. */ readonly cacheWrite: number | null + /** Prompt tokens summed across the steps that reported usage; the weight of `cacheHit`. */ + readonly cacheContext: number | null readonly retries: number readonly toolErrors: number readonly toolOutput: number 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 f95709e1a4..056febebed 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 @@ -204,30 +204,35 @@ describe('turn aggregation', () => { expect(noOutput.tps).toBeNull() }) - it('keeps the last measured context and cache level of the turn', () => { + it('weights the turn cache level by measured prompt tokens and keeps the last context', () => { const turn = aggregateTurn(4, [ - metricsOf({ step: 1, context: 100, cacheHit: 0.2, cacheRead: 10, cacheWrite: 5, wall: 10 }), - metricsOf({ step: 2, context: 200, cacheHit: 0.5, cacheRead: 20, cacheWrite: 6, wall: 20 }), + metricsOf({ step: 1, context: 100, cacheHit: 0.2, cacheRead: 20, cacheWrite: 5, wall: 10 }), + metricsOf({ step: 2, context: 200, cacheHit: 0.5, cacheRead: 100, cacheWrite: 6, wall: 20 }), metricsOf({ step: 3, context: 300, wall: 30 }), ]) expect(turn.context).toBe(300) - expect(turn.cacheHit).toBe(0.5) - expect(turn.cacheRead).toBe(20) - expect(turn.cacheWrite).toBe(6) + expect(turn.cacheHit).toBeCloseTo(120 / 300) + expect(turn.cacheRead).toBe(120) + expect(turn.cacheWrite).toBe(11) + expect(turn.cacheContext).toBe(300) expect(turn.lastStepWall).toBe(30) const heldBack = aggregateTurn(4, [ - metricsOf({ step: 1, context: 100, cacheHit: 0.2, cacheRead: 10, cacheWrite: 5, wall: 10 }), + metricsOf({ step: 1, context: 100, cacheHit: 0.2, cacheRead: 20, cacheWrite: 5, wall: 10 }), metricsOf({ step: 2, cacheHit: null, cacheRead: 99, cacheWrite: 99, wall: null }), ]) expect(heldBack.context).toBe(100) expect(heldBack.cacheHit).toBe(0.2) - expect(heldBack.cacheRead).toBe(10) + expect(heldBack.cacheRead).toBe(20) expect(heldBack.cacheWrite).toBe(5) + expect(heldBack.cacheContext).toBe(100) expect(heldBack.lastStepWall).toBe(10) const bare = aggregateTurn(4, [metricsOf({ cacheHit: 0.4 })]) - expect(bare.cacheHit).toBe(0.4) + expect(bare.cacheHit).toBeNull() expect(bare.cacheRead).toBeNull() expect(bare.cacheWrite).toBeNull() + expect(bare.cacheContext).toBeNull() + const empty = aggregateTurn(4, [metricsOf({ context: 0, cacheRead: 0 })]) + expect(empty.cacheHit).toBeNull() }) it('sums the sequential step tool spans', () => { 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 3ee80eabc0..ca6818a712 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 @@ -124,6 +124,7 @@ function turn(overrides: Partial = {}): PerfTurnMetrics { cacheHit: null, cacheRead: null, cacheWrite: null, + cacheContext: null, retries: 0, toolErrors: 0, toolOutput: 0, @@ -728,8 +729,9 @@ describe('turn rows', () => { turn({ turn: 2, cacheHit: 0.1, cacheRead: null, cacheWrite: 200, context: 1_000 }), turn({ turn: 3, cacheHit: 0.4, cacheRead: 100, cacheWrite: null, context: null }), turn({ turn: 4, cacheHit: 0.2, cacheRead: null, cacheWrite: null }), + turn({ turn: 5, cacheHit: 0.15, cacheRead: 100, cacheWrite: 200, cacheContext: 2_000 }), ], - stepCount: 4, + stepCount: 5, }) const full = cellsOf(rowWithPrefix(say('turn.label', { turn: 1 }))) expect(full[5]?.getAttribute('title')).toBe(say('cache.detail', { @@ -747,6 +749,10 @@ describe('turn rows', () => { expect(readOnly[3]?.textContent).toBe('—') const absent = cellsOf(rowWithPrefix(say('turn.label', { turn: 4 }))) expect(absent[5]?.getAttribute('title')).toBe(say('column.cache.title')) + const weighted = cellsOf(rowWithPrefix(say('turn.label', { turn: 5 }))) + expect(weighted[5]?.getAttribute('title')).toBe(say('cache.detail', { + read: formatTokens(100), input: formatTokens(1_700), write: formatTokens(200), + })) }) it('scales turn bars against the turns visible on screen', () => {