docs: 合并性能分析报告并优化内存管理

将 performance-reporter.md 合入 memory-peak-analysis.md,统一分析文档。
代码优化包括:compact 峰值释放、GC 阈值触发、虚拟滚动参数调优、
HybridTransport 队列缩减、无界缓存加 LRU 淘汰、taskSummary 避免数组拷贝。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-05-02 09:11:12 +08:00
parent ef10ad2839
commit 0977b0520e
8 changed files with 193 additions and 166 deletions

View File

@@ -1463,6 +1463,16 @@ export function getPlanSlugCache(): Map<string, string> {
return STATE.planSlugCache
}
export function setPlanSlugCacheEntry(sessionId: string, slug: string): void {
if (STATE.planSlugCache.size >= 50) {
const firstKey = STATE.planSlugCache.keys().next().value
if (firstKey !== undefined) {
STATE.planSlugCache.delete(firstKey)
}
}
STATE.planSlugCache.set(sessionId, slug)
}
export function getSessionCreatedTeams(): Set<string> {
return STATE.sessionCreatedTeams
}
@@ -1640,6 +1650,12 @@ export function setSystemPromptSectionCacheEntry(
name: string,
value: string | null,
): void {
if (STATE.systemPromptSectionCache.size >= 100) {
const firstKey = STATE.systemPromptSectionCache.keys().next().value
if (firstKey !== undefined) {
STATE.systemPromptSectionCache.delete(firstKey)
}
}
STATE.systemPromptSectionCache.set(name, value)
}

View File

@@ -551,9 +551,19 @@ export async function runHeadless(
proactiveModule.activateProactive('command')
}
// Periodically force a full GC to keep memory usage in check
// Periodically run GC to keep memory usage in check.
// Uses a memory threshold to trigger a forced (major) GC when RSS grows
// beyond 350MB — the incremental GC may not reclaim enough during peaks
// (compact, long sessions with many mounted DOM nodes).
if (typeof Bun !== 'undefined') {
const gcTimer = setInterval(Bun.gc, 1000)
const gcTimer = setInterval(() => {
const rss = process.memoryUsage.rss()
if (rss > 350 * 1024 * 1024) {
Bun.gc(true)
} else {
Bun.gc(false)
}
}, 1000)
gcTimer.unref()
}

View File

@@ -83,7 +83,7 @@ export class HybridTransport extends WebSocketTransport {
// SerialBatchEventUploader backpressure check). So set it high enough
// to be a memory bound only. Wire real backpressure in a follow-up
// once callers await.
maxQueueSize: 100_000,
maxQueueSize: 10_000,
baseDelayMs: 500,
maxDelayMs: 8000,
jitterMs: 1000,

View File

@@ -20,7 +20,7 @@ const DEFAULT_ESTIMATE = 3
* Extra rows rendered above and below the viewport. Generous because real
* heights can be 10x the estimate for long tool results.
*/
const OVERSCAN_ROWS = 80
const OVERSCAN_ROWS = 40
/** Items rendered before the ScrollBox has laid out (viewportHeight=0). */
const COLD_START_COUNT = 30
/**
@@ -43,7 +43,7 @@ const SCROLL_QUANTUM = OVERSCAN_ROWS >> 1
*/
const PESSIMISTIC_HEIGHT = 1
/** Cap on mounted items to bound fiber allocation even in degenerate cases. */
const MAX_MOUNTED_ITEMS = 300
const MAX_MOUNTED_ITEMS = 200
/**
* Max NEW items to mount in a single commit. Scrolling into a fresh range
* with PESSIMISTIC_HEIGHT=1 would mount 194 items at once (OVERSCAN_ROWS*2+

View File

@@ -521,7 +521,7 @@ export async function compactConversation(
}
// Store the current file state before clearing
const preCompactReadFileState = cacheToObject(context.readFileState)
let preCompactReadFileState = cacheToObject(context.readFileState)
// Clear the cache
context.readFileState.clear()
@@ -543,6 +543,9 @@ export async function compactConversation(
),
createAsyncAgentAttachmentsIfNeeded(context),
])
// Release the readFileState snapshot — it can hold 25+ MB of file content
preCompactReadFileState =
undefined as unknown as typeof preCompactReadFileState
const postCompactFileAttachments: AttachmentMessage[] = [
...fileAttachments,
@@ -649,6 +652,8 @@ export async function compactConversation(
// Extract compaction API usage metrics
const compactionUsage = getTokenUsage(summaryResponse)
// Release the full API response — it holds content blocks + usage metadata
summaryResponse = undefined as unknown as typeof summaryResponse
const querySourceForEvent =
recompactionInfo?.querySource ?? context.options.querySource ?? 'unknown'
@@ -922,7 +927,7 @@ export async function partialCompactConversation(
}
// Store the current file state before clearing
const preCompactReadFileState = cacheToObject(context.readFileState)
let preCompactReadFileState = cacheToObject(context.readFileState)
context.readFileState.clear()
context.loadedNestedMemoryPaths?.clear()
// Intentionally NOT resetting sentSkillNames — see compactConversation()
@@ -937,6 +942,9 @@ export async function partialCompactConversation(
),
createAsyncAgentAttachmentsIfNeeded(context),
])
// Release the readFileState snapshot — it can hold 25+ MB of file content
preCompactReadFileState =
undefined as unknown as typeof preCompactReadFileState
const postCompactFileAttachments: AttachmentMessage[] = [
...fileAttachments,
@@ -992,6 +1000,8 @@ export async function partialCompactConversation(
summaryResponse,
])
const compactionUsage = getTokenUsage(summaryResponse)
// Release the full API response — it holds content blocks + usage metadata
summaryResponse = undefined as unknown as typeof summaryResponse
logEvent('tengu_partial_compact', {
preCompactTokenCount,

View File

@@ -10,7 +10,11 @@ import type {
SystemFileSnapshotMessage,
UserMessage,
} from 'src/types/message.js'
import { getPlanSlugCache, getSessionId } from '../bootstrap/state.js'
import {
getPlanSlugCache,
getSessionId,
setPlanSlugCacheEntry,
} from '../bootstrap/state.js'
import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/ExitPlanModeTool/constants.js'
import { getCwd } from './cwd.js'
import { logForDebugging } from './debug.js'
@@ -43,7 +47,7 @@ export function getPlanSlug(sessionId?: SessionId): string {
break
}
}
cache.set(id, slug!)
setPlanSlugCacheEntry(id, slug!)
}
return slug!
}
@@ -52,7 +56,7 @@ export function getPlanSlug(sessionId?: SessionId): string {
* Set a specific plan slug for a session (used when resuming a session)
*/
export function setPlanSlug(sessionId: SessionId, slug: string): void {
getPlanSlugCache().set(sessionId, slug)
setPlanSlugCacheEntry(sessionId, slug)
}
/**

View File

@@ -44,9 +44,13 @@ export function maybeGenerateTaskSummary(
if (!messages || messages.length === 0) return
// Extract a short status from the most recent assistant message
const lastAssistant = [...messages]
.reverse()
.find(m => m.type === 'assistant')
let lastAssistant: (typeof messages)[0] | undefined
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i]!.type === 'assistant') {
lastAssistant = messages[i]
break
}
}
let status: 'busy' | 'idle' = 'busy'
let waitingFor: string | undefined