perf: 会话切换去卡顿——历史渐进渲染 + 消息树 memo + 主进程减负
This commit is contained in:
@@ -308,40 +308,17 @@ export class AgentService {
|
||||
|
||||
private emitSwitched(session: AgentSession): void {
|
||||
const state = this.buildState(session)
|
||||
// 工具状态不再随事件重发:渲染层用 historyToTools 从 messages 自行重建,
|
||||
// 重复克隆一遍全量工具参数/结果是长会话切换卡顿的大头(历史被序列化两次)
|
||||
this.emit({
|
||||
type: 'session_switched',
|
||||
state,
|
||||
messages: session.messages.map((m) => toCloneable(m)),
|
||||
activeTools: this.collectToolStates(session)
|
||||
messages: session.messages.map((m) => toCloneable(m))
|
||||
})
|
||||
// 切换/回溯后立刻带上一次统计(上下文容量、缓存命中率),Usage Card 无需等第一回合
|
||||
void this.emitStats()
|
||||
}
|
||||
|
||||
/** 从历史消息中恢复工具状态(用于切换会话后 ToolCard 的参数/结果展示) */
|
||||
private collectToolStates(session: AgentSession): Record<string, unknown> {
|
||||
const tools: Record<string, unknown> = {}
|
||||
for (const message of session.messages) {
|
||||
const m = message as { role?: string; content?: unknown; toolCallId?: unknown; toolName?: unknown; isError?: unknown }
|
||||
if (m.role !== 'assistant' || !Array.isArray(m.content)) continue
|
||||
for (const block of m.content as Array<{ type?: string; id?: string; name?: string; arguments?: unknown }>) {
|
||||
if (block.type === 'toolCall' && block.id) {
|
||||
tools[block.id] = { toolName: block.name ?? 'tool', args: toCloneable(block.arguments), status: 'done' }
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const message of session.messages) {
|
||||
const m = message as { role?: string; toolCallId?: unknown; toolName?: unknown; content?: unknown; isError?: unknown }
|
||||
if (m.role !== 'toolResult' || typeof m.toolCallId !== 'string') continue
|
||||
tools[m.toolCallId] = {
|
||||
...((tools[m.toolCallId] as Record<string, unknown>) ?? { toolName: m.toolName ?? 'tool' }),
|
||||
result: toCloneable(m),
|
||||
status: m.isError ? 'error' : 'done'
|
||||
}
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
async ensureReady(): Promise<SessionState> {
|
||||
try {
|
||||
const runtime = await this.getRuntime()
|
||||
@@ -498,12 +475,12 @@ export class AgentService {
|
||||
switch (m.role) {
|
||||
case 'user':
|
||||
case 'assistant':
|
||||
byKey.get('messages')!.tokens += estimateTokens(message as Parameters<typeof estimateTokens>[0])
|
||||
byKey.get('messages')!.tokens += estimateMessageTokens(message)
|
||||
break
|
||||
case 'toolResult': {
|
||||
const key =
|
||||
typeof m.toolName === 'string' && CUSTOM_TOOL_NAMES.has(m.toolName) ? 'customTools' : 'builtinTools'
|
||||
byKey.get(key)!.tokens += estimateTokens(message as Parameters<typeof estimateTokens>[0])
|
||||
byKey.get(key)!.tokens += estimateMessageTokens(message)
|
||||
break
|
||||
}
|
||||
// bash 执行、扩展注入、压缩/分支摘要等注入上下文的消息
|
||||
@@ -511,7 +488,7 @@ export class AgentService {
|
||||
case 'custom':
|
||||
case 'compactionSummary':
|
||||
case 'branchSummary':
|
||||
byKey.get('other')!.tokens += estimateTokens(message as Parameters<typeof estimateTokens>[0])
|
||||
byKey.get('other')!.tokens += estimateMessageTokens(message)
|
||||
break
|
||||
default:
|
||||
break
|
||||
@@ -1154,15 +1131,35 @@ function samePath(a: string, b: string): boolean {
|
||||
return a.replace(/\\/g, '/').toLowerCase() === b.replace(/\\/g, '/').toLowerCase()
|
||||
}
|
||||
|
||||
/** 事件消息对象转为可结构化克隆的普通对象(剥离 undefined/函数/循环引用) */
|
||||
/** 事件消息对象转为可结构化克隆的普通对象(剥离函数/循环引用等不可克隆值)。
|
||||
* structuredClone 远快于 JSON 往返——切换会话时整段历史都要过一遍这里,
|
||||
* 长上下文下 JSON 序列化+反序列化是主进程最重的同步开销。 */
|
||||
function toCloneable(value: unknown): unknown {
|
||||
if (value === null || typeof value !== 'object') return value ?? null
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value ?? null))
|
||||
return structuredClone(value)
|
||||
} catch {
|
||||
return null
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 消息级 token 估算缓存:消息对象落定后不可变(pi 只追加),按引用缓存。
|
||||
* emitStats 在每条 assistant 消息后都会全量重扫,不缓存的话整个会话是 O(n²) 的重复估算。 */
|
||||
const messageTokenCache = new WeakMap<object, number>()
|
||||
|
||||
function estimateMessageTokens(message: object): number {
|
||||
let tokens = messageTokenCache.get(message)
|
||||
if (tokens === undefined) {
|
||||
tokens = estimateTokens(message as Parameters<typeof estimateTokens>[0])
|
||||
messageTokenCache.set(message, tokens)
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
function readRole(message: unknown): string {
|
||||
if (message && typeof message === 'object' && 'role' in message) {
|
||||
return String((message as { role: unknown }).role)
|
||||
|
||||
Reference in New Issue
Block a user