mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 13:55:50 +00:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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+
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user