feat(experimental): show compaction summary calls in the perf table
This commit is contained in:
1 parent
8dec878ecf
commit
a885fc7d5d
14 files changed
+500
-9
No files matched your search
@@ -24,8 +24,8 @@
|
||||
en: e001601f2f9779b1
|
||||
zh: 077d3703b8c82774
|
||||
/deepseek-ai-dsh-experimental-client-ui-perf/understand-the-implementation:
|
||||
en: c68d4c69e3820208
|
||||
zh: 58b9c35c9d49b512
|
||||
en: 072702cff9f7f016
|
||||
zh: cdee020ed84d2e9d
|
||||
/deepseek-ai-dsh-experimental-client-ui-perf/further-exploration:
|
||||
en: 285cfe1d7a99af34
|
||||
zh: fbcee76e0c2f20fb
|
||||
|
||||
@@ -61,6 +61,8 @@ The footer switches control the totals row and the context basis, and the footer
|
||||
|
||||
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.
|
||||
|
||||
Compaction is a log-only transaction, not a step. The table inserts a synthetic `Compaction` row between the steps it ran between; the row shows the summary call's context, output and cache-hit cells, the shadowed token count, the summary size, and its wall time. These rows are excluded from the turn and totals step sums, so the step columns keep their step-only meaning.
|
||||
|
||||
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.
|
||||
|
||||
@@ -61,6 +61,8 @@ kind: "package-reference"
|
||||
|
||||
页脚的开关控制合计行与上下文口径。「上下文口径」只改变轮次行对其各步的聚合方式:「最新」显示该轮最后一步的提示词大小,「累计」显示该轮各步提示词大小之和。合计行不受这个开关影响:上下文列始终是已加载轮次的步骤累计值,其余数值列是求和(缓存仍按 token 加权,速度仍按输出 ÷ 生成时长)。token 超过一千万时从 K 切换为 M。
|
||||
|
||||
压缩是 log-only 事务,不是 step。表格会在压缩发生的相邻步骤之间插入一条合成的「压缩」行,显示摘要调用的上下文、输出与缓存命中、被压缩的 token 数、摘要大小和墙钟时间;这些行不计入轮次与合计的步骤求和,步骤列仍只表示步骤。
|
||||
|
||||
轮次的缓存命中率按报了 usage 的各步做 token 加权(缓存读之和 ÷ 提示词 token 之和),不会因为相邻步命中高就把某一次冷启动盖掉;tooltip 展示汇总后的读/输入/写明细。步骤行仍显示该步自己的实测比例。
|
||||
|
||||
会话窗口是有界的:某一步的 `step/start` 不在已加载窗口里时,与起点有关的列显示 `—` 并标「起止未知」,不估算数值。滚到顶部会向 Session 请求更早的一页;视图按新增内容的高度补偿滚动位置,正在读的行不会跳走。
|
||||
|
||||
@@ -64,7 +64,8 @@
|
||||
"@deepseek-ai/dsh-tools": "workspace:*",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"@deepseek-ai/dsh-compaction": "workspace:*"
|
||||
},
|
||||
"files": [
|
||||
"locale/*.json",
|
||||
|
||||
@@ -13,7 +13,7 @@ import { HoverCard, ImageLightbox, SegmentedControl, Switch } from '@deepseek-ai
|
||||
import type { ConvViewProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { InjectFace, PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type {
|
||||
PerfCall, PerfSnapshot, PerfStepMetrics, PerfToolCount, PerfTurnMetrics,
|
||||
PerfCall, PerfCompactionMetrics, PerfSnapshot, PerfStepMetrics, PerfToolCount, PerfTurnMetrics,
|
||||
} from './perf-contract.ts'
|
||||
import {
|
||||
DASH, barShare, formatChars, formatClock, formatDateTime, formatDateTimeShort, formatMs, formatPercent,
|
||||
@@ -1395,6 +1395,77 @@ function moreRows(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* One synthetic compaction row: the summary model call's cost, shown between
|
||||
* the steps it ran between but excluded from the turn's step totals.
|
||||
* @param compaction - folded compaction metrics.
|
||||
* @param t - dictionary.
|
||||
* @param now - current time for a running row.
|
||||
* @param scale - step bar scale, widened to this call's wall.
|
||||
* @returns the row.
|
||||
*/
|
||||
function compactionRow(
|
||||
compaction: PerfCompactionMetrics,
|
||||
t: PerfTranslate,
|
||||
now: number,
|
||||
scale: number,
|
||||
): ReactElement {
|
||||
const metrics: MetricsValues = {
|
||||
context: compaction.context,
|
||||
output: compaction.output,
|
||||
cacheHit: compaction.cacheHit,
|
||||
ttft: null,
|
||||
model: null,
|
||||
tps: null,
|
||||
tool: null,
|
||||
cacheRead: compaction.cacheRead,
|
||||
cacheWrite: compaction.cacheWrite,
|
||||
}
|
||||
const cells = metricsCells(metrics, t)
|
||||
const value: TimeState = {
|
||||
running: compaction.running,
|
||||
startTime: compaction.startTime,
|
||||
wall: compaction.wall,
|
||||
ttft: null,
|
||||
model: null,
|
||||
tool: null,
|
||||
toolWall: null,
|
||||
}
|
||||
const range = `${formatClock(compaction.startTime)}${compaction.endTime === null ? '' : `–${formatClock(compaction.endTime)}`}`
|
||||
const parts: string[] = []
|
||||
if (compaction.shadowedTokens !== null) {
|
||||
parts.push(t('compaction.shadowed', { tokens: formatTokens(compaction.shadowedTokens) }))
|
||||
}
|
||||
if (compaction.summaryChars !== null) {
|
||||
parts.push(t('compaction.summary', { chars: formatChars(compaction.summaryChars) }))
|
||||
}
|
||||
if (parts.length === 0) parts.push(t('compaction.running'))
|
||||
const model = compaction.provider !== null && compaction.model !== null
|
||||
? `${compaction.provider} / ${compaction.model}`
|
||||
: t('compaction.running')
|
||||
return (
|
||||
<tr className={css.compactionRow} title={t('compaction.title')}>
|
||||
<td className={css.stepCell}>
|
||||
<span className={css.compactionTag}>{t('compaction.label')}</span>
|
||||
<span className={css.stepClock}>{range}</span>
|
||||
</td>
|
||||
<td className={cx(css.promptCell, css.stepWhen)} title={model}>{model}</td>
|
||||
<td className={css.stepDid}>{parts.join(' · ')}</td>
|
||||
<ContextCell value={compaction.context} className={css.stepMetric} />
|
||||
<MetricCells cells={cells} className={css.stepMetric} />
|
||||
<TimeCell
|
||||
value={value}
|
||||
scale={Math.max(scale, compaction.wall ?? 0)}
|
||||
gap={undefined}
|
||||
className={css.stepMetric}
|
||||
trackClass={css.barStep}
|
||||
now={now}
|
||||
t={t}
|
||||
/>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
/** Rendered rows of one turn: header, step-list controls, and step rows. */
|
||||
function stepRows(options: StepRowsOptions): ReactNode[] {
|
||||
const { turn, t, now, state, onToggleDetail, onControl } = options
|
||||
@@ -1406,8 +1477,19 @@ function stepRows(options: StepRowsOptions): ReactNode[] {
|
||||
return span !== null && span > max ? span : max
|
||||
}, 0)
|
||||
const { shown } = stepsOf(turn, state)
|
||||
const compactions = turn.compactions.slice()
|
||||
let nextCompaction = 0
|
||||
const rows: ReactNode[] = []
|
||||
const emitCompactionsUntil = (startTime: number | null): void => {
|
||||
while (nextCompaction < compactions.length) {
|
||||
const compaction = compactions[nextCompaction]
|
||||
if (compaction === undefined || compaction.startTime > (startTime ?? Number.POSITIVE_INFINITY)) break
|
||||
rows.push(compactionRow(compaction, t, now, stepScale))
|
||||
nextCompaction += 1
|
||||
}
|
||||
}
|
||||
for (const step of shown) {
|
||||
emitCompactionsUntil(step.startTime)
|
||||
const cells = metricsCells(step, t)
|
||||
const gap = gaps.get(step.step)
|
||||
const expanded = open[step.step] === true
|
||||
@@ -1494,6 +1576,7 @@ function stepRows(options: StepRowsOptions): ReactNode[] {
|
||||
)
|
||||
if (expanded) rows.push(stepDetailRow(step, t, state, onControl))
|
||||
}
|
||||
emitCompactionsUntil(null)
|
||||
return rows
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,12 @@ export const zh = {
|
||||
'step.toggle.aria': '展开或折叠第 {step} 步的工具详情',
|
||||
'step.partial': '起止未知',
|
||||
'step.started.title': '步骤开始于 {time}',
|
||||
'compaction.label': '压缩',
|
||||
'compaction.running': '压缩中…',
|
||||
'compaction.shadowed': '压缩 {tokens} tokens 历史',
|
||||
'compaction.summary': '摘要 {chars} 字符',
|
||||
'compaction.title': '压缩调用单独展示,不并入轮次的步骤合计',
|
||||
|
||||
'step.partial.title': '这一步的开始与结束事件不在当前事件窗口里(页签重连或窗口重建时被截断),首 token、模型用时、总用时都没有起点可以相减;重新加载页签会从完整窗口重新装配',
|
||||
'state.running': '进行中',
|
||||
'state.interrupted': '中断',
|
||||
@@ -188,6 +194,12 @@ export const en: Record<PerfKey, string> = {
|
||||
'step.toggle.aria': 'Expand or collapse the tool details of step {step}',
|
||||
'step.partial': 'start unknown',
|
||||
'step.started.title': 'Step started at {time}',
|
||||
'compaction.label': 'Compaction',
|
||||
'compaction.running': 'Compacting…',
|
||||
'compaction.shadowed': 'Pruned {tokens} tokens of history',
|
||||
'compaction.summary': 'Summary {chars} chars',
|
||||
'compaction.title': "Compaction runs are shown separately and excluded from the turn's step totals",
|
||||
|
||||
'step.partial.title': 'This step\'s start and end events are not in the current event window (trimmed while the tab reconnected or the window was rebuilt), so first-token, model time and total have no start to subtract from; reloading the tab reassembles from the full window',
|
||||
'state.running': 'running',
|
||||
'state.interrupted': 'interrupted',
|
||||
@@ -319,6 +331,12 @@ export const ja: Record<PerfKey, string> = {
|
||||
'step.toggle.aria': 'ステップ {step} のツール詳細を展開または折りたたみます',
|
||||
'step.partial': '開始時刻不明',
|
||||
'step.started.title': 'ステップ開始 {time}',
|
||||
'compaction.label': '圧縮',
|
||||
'compaction.running': '圧縮中…',
|
||||
'compaction.shadowed': '履歴 {tokens} tokens を圧縮',
|
||||
'compaction.summary': '要約 {chars} 文字',
|
||||
'compaction.title': '圧縮呼び出しはターンのステップ合計とは別に表示されます',
|
||||
|
||||
'step.partial.title': 'このステップの開始・終了イベントが現在のイベントウィンドウにありません(タブ再接続やウィンドウ再構築で切り詰められました)。最初のトークン、モデル時間、合計時間には差し引く開始時刻がありません。タブを再読み込みすると完全なウィンドウから再構築されます',
|
||||
'state.running': '実行中',
|
||||
'state.interrupted': '中断',
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
||||
import type {
|
||||
PerfCall, PerfContribution, PerfStepMetrics, PerfToolCount, PerfTurnMetrics, PerfViewNode,
|
||||
PerfCall, PerfCompactionMetrics, PerfContribution, PerfStepMetrics, PerfToolCount, PerfTurnMetrics,
|
||||
PerfViewNode,
|
||||
} from './perf-contract.ts'
|
||||
import { CALL_LIMIT, isCodeMode, toolSpanUnion } from './perf-fold.ts'
|
||||
import { isNumber } from './perf-usage.ts'
|
||||
@@ -13,6 +14,9 @@ type InnerContribution = Extract<PerfContribution, { kind: 'inner' }>
|
||||
/** Empty image list shared by turns whose trigger attached no image. */
|
||||
const NO_IMAGES: readonly ImageAttachmentRef[] = []
|
||||
|
||||
/** Empty compaction list shared by turns without one. */
|
||||
const NO_COMPACTIONS: readonly PerfCompactionMetrics[] = []
|
||||
|
||||
/** The trigger that opened one turn, kept from the highest-priority source. */
|
||||
interface TurnTrigger {
|
||||
readonly text: string
|
||||
@@ -200,6 +204,7 @@ export function aggregateTurn(turn: number, steps: readonly PerfStepMetrics[]):
|
||||
interrupted: steps.some(step => step.interrupted),
|
||||
aborted: steps.some(step => step.aborted),
|
||||
wall,
|
||||
compactions: [],
|
||||
prompt: null,
|
||||
promptImages: NO_IMAGES,
|
||||
promptSource: null,
|
||||
@@ -232,6 +237,8 @@ export function buildTurns(nodes: Iterable<PerfViewNode>): PerfTurnMetrics[] {
|
||||
const prompts = new Map<number, TurnTrigger>()
|
||||
// Code-Mode dispatches per step, merged after the steps are ordered.
|
||||
const inner = new Map<string, InnerContribution[]>()
|
||||
// Compaction transactions per turn, ordered by start time after grouping.
|
||||
const compactions = new Map<number, PerfCompactionMetrics[]>()
|
||||
for (const node of nodes) {
|
||||
const data = node.data
|
||||
if (data.kind === 'prompt') {
|
||||
@@ -254,6 +261,13 @@ export function buildTurns(nodes: Iterable<PerfViewNode>): PerfTurnMetrics[] {
|
||||
if (data.closeKind !== null) closedKind.set(data.turn, data.closeKind)
|
||||
continue
|
||||
}
|
||||
if (data.kind === 'compaction') {
|
||||
if (!isNumber(data.metrics.turn)) continue
|
||||
const list = compactions.get(data.metrics.turn)
|
||||
if (list === undefined) compactions.set(data.metrics.turn, [data.metrics])
|
||||
else list.push(data.metrics)
|
||||
continue
|
||||
}
|
||||
if (data.kind === 'inner') {
|
||||
const key = `${data.turn}:${data.step}`
|
||||
const list = inner.get(key)
|
||||
@@ -306,6 +320,8 @@ export function buildTurns(nodes: Iterable<PerfViewNode>): PerfTurnMetrics[] {
|
||||
const prompt = prompts.get(turn)
|
||||
turns.push({
|
||||
...aggregate,
|
||||
compactions: (compactions.get(turn) ?? NO_COMPACTIONS).slice()
|
||||
.sort((left, right) => left.startTime - right.startTime),
|
||||
// A turn followed by another turn cannot still be running.
|
||||
running: (index < order.length - 1 || at !== undefined) ? false : aggregate.running,
|
||||
interrupted: closedKind.get(turn) === 'interrupted' ? true : aggregate.interrupted,
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/** Compaction transaction state and its presentation as a synthetic step row. */
|
||||
|
||||
import type { ConversationNodeDefinition } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type {} from '@deepseek-ai/dsh-compaction/types'
|
||||
import type { PerfCompactionMetrics, PerfCompactionState } from './perf-contract.ts'
|
||||
import {
|
||||
cacheHitRate, cacheReadTokens, cacheWriteTokens, contextTokens, isNumber, outputTokens,
|
||||
} from './perf-usage.ts'
|
||||
|
||||
/** Event shape a Conversation Definition receives. */
|
||||
type MatchEvent = Parameters<ConversationNodeDefinition['match']>[0]
|
||||
|
||||
/**
|
||||
* Compaction id an event names.
|
||||
* @param event - event seen by the Definition match.
|
||||
* @returns the nonempty compaction id, or null for events outside a transaction.
|
||||
*/
|
||||
export function compactionEventId(event: MatchEvent): string | null {
|
||||
if (event.type !== 'compaction/start'
|
||||
&& event.type !== 'compaction/summary'
|
||||
&& event.type !== 'compaction/end') return null
|
||||
const value: unknown = event.data.compactionId
|
||||
return typeof value === 'string' && value !== '' ? value : null
|
||||
}
|
||||
|
||||
/** Text characters of a content-block list, for the summary size note. */
|
||||
function textChars(blocks: readonly { readonly type: string; readonly text?: string }[]): number {
|
||||
let chars = 0
|
||||
for (const block of blocks) {
|
||||
if (block.type === 'text' && typeof block.text === 'string') chars += block.text.length
|
||||
}
|
||||
return chars
|
||||
}
|
||||
|
||||
/**
|
||||
* Start state for one `compaction/start`.
|
||||
* @param event - the start event.
|
||||
* @returns state that later summary and end events refine.
|
||||
*/
|
||||
export function newCompactionState(event: MatchEvent): PerfCompactionState {
|
||||
/* v8 ignore next 3 -- the Definition only calls start for a compaction/start match. */
|
||||
if (event.type !== 'compaction/start') {
|
||||
throw new Error('perf-compaction start requires compaction/start')
|
||||
}
|
||||
return {
|
||||
compactionId: event.data.compactionId,
|
||||
turn: event.data.turn,
|
||||
startSeq: event.seq,
|
||||
startTime: event.time,
|
||||
summarySeq: null,
|
||||
endSeq: null,
|
||||
endTime: null,
|
||||
error: null,
|
||||
provider: null,
|
||||
model: null,
|
||||
usage: null,
|
||||
shadowedTokens: null,
|
||||
summaryChars: null,
|
||||
checkpointSeq: null,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fold the summary or end of a compaction into its state.
|
||||
* @param state - state opened by compaction/start.
|
||||
* @param event - a matching compaction event.
|
||||
* @returns state after the event, or the same state for unrelated events.
|
||||
*/
|
||||
export function foldCompaction(state: PerfCompactionState, event: MatchEvent): PerfCompactionState {
|
||||
if (event.type === 'compaction/summary') {
|
||||
return {
|
||||
...state,
|
||||
summarySeq: event.seq,
|
||||
provider: event.data.provider,
|
||||
model: event.data.model,
|
||||
usage: event.data.usage ?? null,
|
||||
shadowedTokens: event.data.shadowedTokenCount,
|
||||
summaryChars: textChars(event.data.summary),
|
||||
}
|
||||
}
|
||||
/* v8 ignore next -- a summary returned above; only compaction/end reaches this branch. */
|
||||
if (event.type === 'compaction/end') {
|
||||
return {
|
||||
...state,
|
||||
turn: state.turn ?? event.data.turn,
|
||||
endSeq: event.seq,
|
||||
endTime: event.time,
|
||||
error: event.data.error ?? null,
|
||||
}
|
||||
}
|
||||
/* v8 ignore next -- the Definition routes only summary and end updates here. */
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* View metrics of one compaction transaction.
|
||||
* @param state - folded compaction state.
|
||||
* @returns the row values; unavailable measurements stay null.
|
||||
*/
|
||||
export function compactionMetrics(state: PerfCompactionState): PerfCompactionMetrics {
|
||||
const usage = state.usage
|
||||
const endTime = state.endTime
|
||||
return {
|
||||
compactionId: state.compactionId,
|
||||
turn: state.turn,
|
||||
startTime: state.startTime,
|
||||
endTime,
|
||||
wall: isNumber(endTime) ? Math.max(0, endTime - state.startTime) : null,
|
||||
running: state.endSeq === null,
|
||||
context: contextTokens(usage),
|
||||
output: outputTokens(usage),
|
||||
cacheHit: cacheHitRate(usage),
|
||||
cacheRead: cacheReadTokens(usage),
|
||||
cacheWrite: cacheWriteTokens(usage),
|
||||
provider: state.provider,
|
||||
model: state.model,
|
||||
shadowedTokens: state.shadowedTokens,
|
||||
summaryChars: state.summaryChars,
|
||||
error: state.error,
|
||||
}
|
||||
}
|
||||
@@ -155,6 +155,8 @@ export interface PerfTurnMetrics {
|
||||
readonly interrupted: boolean
|
||||
readonly aborted: boolean
|
||||
readonly wall: number | null
|
||||
/** Compaction transactions recorded in this turn, in start order. */
|
||||
readonly compactions: readonly PerfCompactionMetrics[]
|
||||
readonly prompt: string | null
|
||||
/** Durable image references the prompt attached, in message order. */
|
||||
readonly promptImages: readonly ImageAttachmentRef[]
|
||||
@@ -166,6 +168,44 @@ export interface PerfTurnMetrics {
|
||||
readonly answer: string | null
|
||||
}
|
||||
|
||||
/** One completed or running compaction transaction with its model-call facts. */
|
||||
export interface PerfCompactionMetrics {
|
||||
readonly compactionId: string
|
||||
readonly turn: number | null
|
||||
readonly startTime: number
|
||||
readonly endTime: number | null
|
||||
readonly wall: number | null
|
||||
readonly running: boolean
|
||||
readonly context: number | null
|
||||
readonly output: number | null
|
||||
readonly cacheHit: number | null
|
||||
readonly cacheRead: number | null
|
||||
readonly cacheWrite: number | null
|
||||
readonly provider: string | null
|
||||
readonly model: string | null
|
||||
readonly shadowedTokens: number | null
|
||||
readonly summaryChars: number | null
|
||||
readonly error: string | null
|
||||
}
|
||||
|
||||
/** Per-turn compaction rows shown between the step rows. */
|
||||
export interface PerfCompactionState {
|
||||
readonly compactionId: string
|
||||
readonly turn: number | null
|
||||
readonly startSeq: number
|
||||
readonly startTime: number
|
||||
readonly summarySeq: number | null
|
||||
readonly endSeq: number | null
|
||||
readonly endTime: number | null
|
||||
readonly error: string | null
|
||||
readonly provider: string | null
|
||||
readonly model: string | null
|
||||
readonly usage: PerfUsage | null
|
||||
readonly shadowedTokens: number | null
|
||||
readonly summaryChars: number | null
|
||||
readonly checkpointSeq: number | null
|
||||
}
|
||||
|
||||
/** One published node: a step's metrics or a turn-level marker. */
|
||||
export type PerfContribution =
|
||||
| { readonly kind: 'step'; readonly metrics: PerfStepMetrics }
|
||||
@@ -187,6 +227,10 @@ export type PerfContribution =
|
||||
/** Sender agent name of a team/agent message, or null. */
|
||||
readonly sender: string | null
|
||||
}
|
||||
| {
|
||||
readonly kind: 'compaction'
|
||||
readonly metrics: PerfCompactionMetrics
|
||||
}
|
||||
| {
|
||||
readonly kind: 'end'
|
||||
readonly turn: number
|
||||
|
||||
@@ -13,10 +13,13 @@ import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {
|
||||
ConversationLocation, ConversationNodeContext, ConversationNodeDefinition,
|
||||
} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { PerfStepState, PerfViewNode } from './perf-contract.ts'
|
||||
import type { PerfCompactionState, PerfStepState, PerfViewNode } from './perf-contract.ts'
|
||||
import {
|
||||
CALL_LIMIT, fallbackState, foldEvent, isCodeMode, newState, promptOf, stepMetrics, triggerSenderOf,
|
||||
} from './perf-fold.ts'
|
||||
import {
|
||||
compactionEventId, compactionMetrics, foldCompaction, newCompactionState,
|
||||
} from './perf-compaction.ts'
|
||||
|
||||
/** Target name shared by every Definition and the view builder. */
|
||||
export const TARGET = 'perf'
|
||||
@@ -33,6 +36,9 @@ const END_KIND = 'perf-turn-end'
|
||||
/** Definition kind of one Code-Mode root call's inner dispatches. */
|
||||
const CODE_MODE_KIND = 'perf-code-mode'
|
||||
|
||||
/** Definition kind of one compaction transaction. */
|
||||
const COMPACTION_KIND = 'perf-compaction'
|
||||
|
||||
/** Marker ids never collide with a step number. */
|
||||
const PROMPT_STEP = 0
|
||||
const END_STEP = 100_000
|
||||
@@ -232,8 +238,41 @@ const codeModeDefinition: ConversationNodeDefinition<PerfStepState> = {
|
||||
},
|
||||
}
|
||||
|
||||
/** One synthetic row per compaction transaction, including its summary model call. */
|
||||
const compactionDefinition: ConversationNodeDefinition<PerfCompactionState> = {
|
||||
kind: COMPACTION_KIND,
|
||||
target: TARGET,
|
||||
match: (event) => {
|
||||
const id = compactionEventId(event)
|
||||
if (id === null) return null
|
||||
return { id, role: event.type === 'compaction/start' ? 'start' : 'update' }
|
||||
},
|
||||
start: (_context, match) => {
|
||||
if (match.event.type !== 'compaction/start') {
|
||||
throw new Error('perf-compaction start requires compaction/start')
|
||||
}
|
||||
return newCompactionState(match.event)
|
||||
},
|
||||
update: (context, match) => foldCompaction(context.state, match.event),
|
||||
publication: () => 'immediate',
|
||||
buildViewNode: (context) => {
|
||||
const state = context.state
|
||||
if (state === undefined) return null
|
||||
return {
|
||||
key: context.key,
|
||||
kind: context.kind,
|
||||
id: context.id,
|
||||
target: TARGET,
|
||||
anchorSeq: state.startSeq,
|
||||
/* v8 ignore next -- a live start match always carries a Location. */
|
||||
location: context.start?.location ?? { kind: 'unresolved' },
|
||||
data: { kind: 'compaction', metrics: compactionMetrics(state) },
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the four Definitions. Each registration rides the caller's fiber
|
||||
* Register the five Definitions. Each registration rides the caller's fiber
|
||||
* effect, so unloading the plugin removes the nodes with it.
|
||||
* @param ctx - client root context.
|
||||
*/
|
||||
@@ -242,4 +281,5 @@ export function registerPerfDefinitions(ctx: Context): void {
|
||||
ctx.uiConversation.events.register(promptDefinition)
|
||||
ctx.uiConversation.events.register(endDefinition)
|
||||
ctx.uiConversation.events.register(codeModeDefinition)
|
||||
ctx.uiConversation.events.register(compactionDefinition)
|
||||
}
|
||||
@@ -196,6 +196,21 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.compactionRow td {
|
||||
border-bottom: 0.5px solid var(--dsw-alias-border-l1);
|
||||
background: var(--dsw-alias-bg-layer-2);
|
||||
}
|
||||
|
||||
.compactionTag {
|
||||
display: inline-block;
|
||||
margin-right: 6px;
|
||||
padding: 0 4px;
|
||||
border: 0.5px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 4px;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
}
|
||||
|
||||
.stepWhen {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
@@ -380,7 +380,7 @@ describe('browser plugin registration', () => {
|
||||
const bench = await mountPlugin()
|
||||
expect(bench.localeRegister).toHaveBeenCalledWith(NS, { zh, en, ja })
|
||||
expect(bench.events.map(definition => definition.kind)).toEqual([
|
||||
'perf-step', 'perf-prompt', 'perf-turn-end', 'perf-code-mode',
|
||||
'perf-step', 'perf-prompt', 'perf-turn-end', 'perf-code-mode', 'perf-compaction',
|
||||
])
|
||||
expect(clientInject).toEqual(['slots', 'sessions', 'uiConversation', 'locale'])
|
||||
expect(bench.views).toEqual([perfViewDefinition])
|
||||
@@ -636,6 +636,96 @@ describe('conversation Definitions', () => {
|
||||
expect(snapshot!.turns[0]!.prompt).toBeNull()
|
||||
})
|
||||
|
||||
it('publishes a compaction row with its summary call facts', async () => {
|
||||
const definitions = await registeredDefinitions()
|
||||
const snapshot = assemble(definitions, [
|
||||
entry(event('turn/start', { turn: 6 })),
|
||||
entry(event('step/start', { turn: 6, step: 1 }, 5_000)),
|
||||
entry(event('user/message', {
|
||||
content: [{ type: 'text', text: 'go' }],
|
||||
source: { kind: 'user' },
|
||||
}, 5_100)),
|
||||
entry(event('compaction/start', { compactionId: '', turn: 6 }, 4_800)),
|
||||
entry(event('compaction/start', { compactionId: 'cmp-0', turn: null }, 4_900)),
|
||||
entry(event('compaction/summary', {
|
||||
compactionId: 'cmp-0',
|
||||
turn: 6,
|
||||
summary: [{ type: 'text', text: 'older summary' }],
|
||||
shadowedRange: { start: 1 as SessionSeq, end: 2 as SessionSeq },
|
||||
shadowedSeqs: [],
|
||||
shadowedTokenCount: 1_000,
|
||||
provider: 'p',
|
||||
model: 'm',
|
||||
llmStreamCall: false,
|
||||
}, 5_000)),
|
||||
entry(event('compaction/end', { compactionId: 'cmp-0', turn: 6 }, 5_100)),
|
||||
entry(event('compaction/start', { compactionId: 'another', turn: null }, 5_150)),
|
||||
entry(event('compaction/end', { compactionId: 'another', turn: null }, 5_160)),
|
||||
entry(event('compaction/start', { compactionId: 'cmp-1', turn: 6 }, 5_200)),
|
||||
entry(event('compaction/summary', {
|
||||
compactionId: 'cmp-1',
|
||||
turn: 6,
|
||||
summary: [{ type: 'text', text: 'sum' }, { type: 'reasoning', text: 'why' }],
|
||||
shadowedRange: { start: 1 as SessionSeq, end: 2 as SessionSeq },
|
||||
shadowedSeqs: [],
|
||||
shadowedTokenCount: 4_000,
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-flash',
|
||||
usage: { inputTokens: 1_000, cacheReadTokens: 100, outputTokens: 50 },
|
||||
llmStreamCall: false,
|
||||
}, 5_300)),
|
||||
entry(event('compaction/end', { compactionId: 'cmp-1', turn: 6 }, 5_400)),
|
||||
entry(event('step/end', { turn: 6, step: 1 }, 5_500)),
|
||||
entry(event('turn/end', { turn: 6, reason: { kind: 'completed' } }, 5_600)),
|
||||
])
|
||||
expect(snapshot?.turns).toHaveLength(1)
|
||||
expect(snapshot?.turns[0]?.compactions).toHaveLength(2)
|
||||
expect(snapshot?.turns[0]?.compactions.find(entry => entry.compactionId === 'cmp-0')).toMatchObject({
|
||||
turn: 6, context: null, output: null, cacheHit: null, summaryChars: 13,
|
||||
})
|
||||
expect(snapshot?.turns[0]?.compactions[1]).toEqual({
|
||||
compactionId: 'cmp-1',
|
||||
turn: 6,
|
||||
startTime: 5_200,
|
||||
endTime: 5_400,
|
||||
wall: 200,
|
||||
running: false,
|
||||
context: 1_100,
|
||||
output: 50,
|
||||
cacheHit: 100 / 1_100,
|
||||
cacheRead: 100,
|
||||
cacheWrite: null,
|
||||
provider: 'deepseek-official',
|
||||
model: 'deepseek-flash',
|
||||
shadowedTokens: 4_000,
|
||||
summaryChars: 3,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const interrupted = assemble(definitions, [
|
||||
entry(event('turn/start', { turn: 7 })),
|
||||
entry(event('step/start', { turn: 7, step: 1 }, 6_000)),
|
||||
entry(event('user/message', { content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }, 6_010)),
|
||||
entry(event('compaction/start', { compactionId: 'cmp-2', turn: 7 }, 6_100)),
|
||||
entry(event('compaction/end', { compactionId: 'cmp-2', turn: 7, error: 'aborted' }, 6_200)),
|
||||
entry(event('turn/end', { turn: 7, reason: { kind: 'completed' } }, 6_300)),
|
||||
])
|
||||
expect(interrupted?.turns[0]?.compactions[0]?.error).toBe('aborted')
|
||||
expect(interrupted?.turns[0]?.compactions[0]?.running).toBe(false)
|
||||
|
||||
const running = assemble(definitions, [
|
||||
entry(event('turn/start', { turn: 8 })),
|
||||
entry(event('step/start', { turn: 8, step: 1 }, 6_400)),
|
||||
entry(event('user/message', { content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }, 6_410)),
|
||||
entry(event('compaction/start', { compactionId: 'cmp-3', turn: 8 }, 6_500)),
|
||||
entry(event('step/end', { turn: 8, step: 1 }, 6_600)),
|
||||
entry(event('turn/end', { turn: 8, reason: { kind: 'interrupted' } }, 6_700)),
|
||||
])
|
||||
expect(running?.turns[0]?.compactions[0]).toMatchObject({
|
||||
running: true, wall: null, context: null, provider: null, shadowedTokens: null, summaryChars: null, error: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('attributes Code-Mode dispatch events through the root call table', async () => {
|
||||
const definitions = await registeredDefinitions()
|
||||
const snapshot = assemble(definitions, [
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
formatTokensShort, formatTps,
|
||||
} from '../src/client/perf-format.ts'
|
||||
import type {
|
||||
PerfCall, PerfSnapshot, PerfStepMetrics, PerfToolCount, PerfTurnMetrics,
|
||||
PerfCall, PerfCompactionMetrics, PerfSnapshot, PerfStepMetrics, PerfToolCount, PerfTurnMetrics,
|
||||
} from '../src/client/perf-contract.ts'
|
||||
|
||||
const t = makeTranslate(zh)
|
||||
@@ -135,6 +135,7 @@ function turn(overrides: Partial<PerfTurnMetrics> = {}): PerfTurnMetrics {
|
||||
aborted: false,
|
||||
wall: 1_000,
|
||||
prompt: null,
|
||||
compactions: [],
|
||||
promptImages: [],
|
||||
promptSource: null,
|
||||
promptSender: null,
|
||||
@@ -142,6 +143,29 @@ function turn(overrides: Partial<PerfTurnMetrics> = {}): PerfTurnMetrics {
|
||||
}
|
||||
}
|
||||
|
||||
/** One compaction row with every unmeasured field blank. */
|
||||
function compaction(overrides: Partial<PerfCompactionMetrics> = {}): PerfCompactionMetrics {
|
||||
return {
|
||||
compactionId: 'cmp-1',
|
||||
turn: 1,
|
||||
startTime: BASE,
|
||||
endTime: BASE + 1_000,
|
||||
wall: 1_000,
|
||||
running: false,
|
||||
context: null,
|
||||
output: null,
|
||||
cacheHit: null,
|
||||
cacheRead: null,
|
||||
cacheWrite: null,
|
||||
provider: null,
|
||||
model: null,
|
||||
shadowedTokens: null,
|
||||
summaryChars: null,
|
||||
error: null,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
/** A one-turn snapshot carrying the given steps. */
|
||||
function turnSnapshot(
|
||||
steps: readonly PerfStepMetrics[],
|
||||
@@ -825,6 +849,38 @@ describe('turn rows', () => {
|
||||
expect(partial[1]?.getAttribute('title')).toBeNull()
|
||||
})
|
||||
|
||||
it('renders compaction rows between the steps they ran between', () => {
|
||||
mount(turnSnapshot([
|
||||
step({ step: 1, startTime: BASE, endTime: BASE + 1_000 }),
|
||||
step({ step: 2, startTime: BASE + 5_000, endTime: BASE + 6_000 }),
|
||||
], {
|
||||
compactions: [
|
||||
compaction({
|
||||
startTime: BASE + 2_000, endTime: BASE + 3_000, wall: 1_000,
|
||||
context: 1_100, output: 50, cacheHit: 100 / 1_100, cacheRead: 100, cacheWrite: 20,
|
||||
provider: 'deepseek-official', model: 'deepseek-flash',
|
||||
shadowedTokens: 4_000, summaryChars: 30,
|
||||
}),
|
||||
compaction({
|
||||
compactionId: 'cmp-2', startTime: BASE + 7_000, endTime: null, wall: null, running: true,
|
||||
}),
|
||||
],
|
||||
}))
|
||||
const rows = bodyRows()
|
||||
const first = rows.findIndex(row => (row.textContent ?? '').startsWith(say('step.label', { step: 1 })))
|
||||
const marker = rows.findIndex(row => (row.textContent ?? '').startsWith(say('compaction.label')))
|
||||
const second = rows.findIndex(row => (row.textContent ?? '').startsWith(say('step.label', { step: 2 })))
|
||||
expect(first).toBeGreaterThanOrEqual(0)
|
||||
expect(marker).toBeGreaterThan(first)
|
||||
expect(marker).toBeLessThan(second)
|
||||
const completed = cellsOf(rows[marker]!)
|
||||
expect(completed[1]?.textContent).toBe('deepseek-official / deepseek-flash')
|
||||
expect(rows[marker]?.textContent).toContain(say('compaction.shadowed', { tokens: formatTokens(4_000) }))
|
||||
expect(completed[3]?.textContent).toBe(formatTokensShort(1_100))
|
||||
const running = rows.filter(row => (row.textContent ?? '').startsWith(say('compaction.label'))).at(-1)!
|
||||
expect(running.textContent).toContain(say('compaction.running'))
|
||||
})
|
||||
|
||||
it('scales turn bars against the turns visible on screen', () => {
|
||||
interface FakeEntry { readonly target: Element; readonly isIntersecting: boolean }
|
||||
class FakeObserver {
|
||||
|
||||
Generated
+3
@@ -9001,6 +9001,9 @@ importers:
|
||||
'@deepseek-ai/dsh-client-ui-slots':
|
||||
specifier: workspace:*
|
||||
version: link:../../client/ui-slots
|
||||
'@deepseek-ai/dsh-compaction':
|
||||
specifier: workspace:*
|
||||
version: link:../../compaction/compaction
|
||||
'@deepseek-ai/dsh-llm':
|
||||
specifier: workspace:*
|
||||
version: link:../../llm/llm
|
||||
|
||||
Reference in new issue
Block a user