Skip to content

Commit 385baf5

Browse files
Merge pull request claude-code-best#402 from claude-code-best/fixture/memory-peak
fixture: 修复内存高峰问题
2 parents 96f1700 + 0977b05 commit 385baf5

8 files changed

Lines changed: 215 additions & 188 deletions

File tree

docs/memory-peak-analysis.md

Lines changed: 158 additions & 175 deletions
Large diffs are not rendered by default.

src/bootstrap/state.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1463,6 +1463,16 @@ export function getPlanSlugCache(): Map<string, string> {
14631463
return STATE.planSlugCache
14641464
}
14651465

1466+
export function setPlanSlugCacheEntry(sessionId: string, slug: string): void {
1467+
if (STATE.planSlugCache.size >= 50) {
1468+
const firstKey = STATE.planSlugCache.keys().next().value
1469+
if (firstKey !== undefined) {
1470+
STATE.planSlugCache.delete(firstKey)
1471+
}
1472+
}
1473+
STATE.planSlugCache.set(sessionId, slug)
1474+
}
1475+
14661476
export function getSessionCreatedTeams(): Set<string> {
14671477
return STATE.sessionCreatedTeams
14681478
}
@@ -1640,6 +1650,12 @@ export function setSystemPromptSectionCacheEntry(
16401650
name: string,
16411651
value: string | null,
16421652
): void {
1653+
if (STATE.systemPromptSectionCache.size >= 100) {
1654+
const firstKey = STATE.systemPromptSectionCache.keys().next().value
1655+
if (firstKey !== undefined) {
1656+
STATE.systemPromptSectionCache.delete(firstKey)
1657+
}
1658+
}
16431659
STATE.systemPromptSectionCache.set(name, value)
16441660
}
16451661

src/cli/print.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -551,9 +551,19 @@ export async function runHeadless(
551551
proactiveModule.activateProactive('command')
552552
}
553553

554-
// Periodically force a full GC to keep memory usage in check
554+
// Periodically run GC to keep memory usage in check.
555+
// Uses a memory threshold to trigger a forced (major) GC when RSS grows
556+
// beyond 350MB — the incremental GC may not reclaim enough during peaks
557+
// (compact, long sessions with many mounted DOM nodes).
555558
if (typeof Bun !== 'undefined') {
556-
const gcTimer = setInterval(Bun.gc, 1000)
559+
const gcTimer = setInterval(() => {
560+
const rss = process.memoryUsage.rss()
561+
if (rss > 350 * 1024 * 1024) {
562+
Bun.gc(true)
563+
} else {
564+
Bun.gc(false)
565+
}
566+
}, 1000)
557567
gcTimer.unref()
558568
}
559569

src/cli/transports/HybridTransport.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export class HybridTransport extends WebSocketTransport {
8383
// SerialBatchEventUploader backpressure check). So set it high enough
8484
// to be a memory bound only. Wire real backpressure in a follow-up
8585
// once callers await.
86-
maxQueueSize: 100_000,
86+
maxQueueSize: 10_000,
8787
baseDelayMs: 500,
8888
maxDelayMs: 8000,
8989
jitterMs: 1000,

src/hooks/useVirtualScroll.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const DEFAULT_ESTIMATE = 3
2020
* Extra rows rendered above and below the viewport. Generous because real
2121
* heights can be 10x the estimate for long tool results.
2222
*/
23-
const OVERSCAN_ROWS = 80
23+
const OVERSCAN_ROWS = 40
2424
/** Items rendered before the ScrollBox has laid out (viewportHeight=0). */
2525
const COLD_START_COUNT = 30
2626
/**
@@ -43,7 +43,7 @@ const SCROLL_QUANTUM = OVERSCAN_ROWS >> 1
4343
*/
4444
const PESSIMISTIC_HEIGHT = 1
4545
/** Cap on mounted items to bound fiber allocation even in degenerate cases. */
46-
const MAX_MOUNTED_ITEMS = 300
46+
const MAX_MOUNTED_ITEMS = 200
4747
/**
4848
* Max NEW items to mount in a single commit. Scrolling into a fresh range
4949
* with PESSIMISTIC_HEIGHT=1 would mount 194 items at once (OVERSCAN_ROWS*2+

src/services/compact/compact.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ export async function compactConversation(
521521
}
522522

523523
// Store the current file state before clearing
524-
const preCompactReadFileState = cacheToObject(context.readFileState)
524+
let preCompactReadFileState = cacheToObject(context.readFileState)
525525

526526
// Clear the cache
527527
context.readFileState.clear()
@@ -543,6 +543,9 @@ export async function compactConversation(
543543
),
544544
createAsyncAgentAttachmentsIfNeeded(context),
545545
])
546+
// Release the readFileState snapshot — it can hold 25+ MB of file content
547+
preCompactReadFileState =
548+
undefined as unknown as typeof preCompactReadFileState
546549

547550
const postCompactFileAttachments: AttachmentMessage[] = [
548551
...fileAttachments,
@@ -649,6 +652,8 @@ export async function compactConversation(
649652

650653
// Extract compaction API usage metrics
651654
const compactionUsage = getTokenUsage(summaryResponse)
655+
// Release the full API response — it holds content blocks + usage metadata
656+
summaryResponse = undefined as unknown as typeof summaryResponse
652657

653658
const querySourceForEvent =
654659
recompactionInfo?.querySource ?? context.options.querySource ?? 'unknown'
@@ -922,7 +927,7 @@ export async function partialCompactConversation(
922927
}
923928

924929
// Store the current file state before clearing
925-
const preCompactReadFileState = cacheToObject(context.readFileState)
930+
let preCompactReadFileState = cacheToObject(context.readFileState)
926931
context.readFileState.clear()
927932
context.loadedNestedMemoryPaths?.clear()
928933
// Intentionally NOT resetting sentSkillNames — see compactConversation()
@@ -937,6 +942,9 @@ export async function partialCompactConversation(
937942
),
938943
createAsyncAgentAttachmentsIfNeeded(context),
939944
])
945+
// Release the readFileState snapshot — it can hold 25+ MB of file content
946+
preCompactReadFileState =
947+
undefined as unknown as typeof preCompactReadFileState
940948

941949
const postCompactFileAttachments: AttachmentMessage[] = [
942950
...fileAttachments,
@@ -992,6 +1000,8 @@ export async function partialCompactConversation(
9921000
summaryResponse,
9931001
])
9941002
const compactionUsage = getTokenUsage(summaryResponse)
1003+
// Release the full API response — it holds content blocks + usage metadata
1004+
summaryResponse = undefined as unknown as typeof summaryResponse
9951005

9961006
logEvent('tengu_partial_compact', {
9971007
preCompactTokenCount,

src/utils/plans.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import type {
1010
SystemFileSnapshotMessage,
1111
UserMessage,
1212
} from 'src/types/message.js'
13-
import { getPlanSlugCache, getSessionId } from '../bootstrap/state.js'
13+
import {
14+
getPlanSlugCache,
15+
getSessionId,
16+
setPlanSlugCacheEntry,
17+
} from '../bootstrap/state.js'
1418
import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/ExitPlanModeTool/constants.js'
1519
import { getCwd } from './cwd.js'
1620
import { logForDebugging } from './debug.js'
@@ -43,7 +47,7 @@ export function getPlanSlug(sessionId?: SessionId): string {
4347
break
4448
}
4549
}
46-
cache.set(id, slug!)
50+
setPlanSlugCacheEntry(id, slug!)
4751
}
4852
return slug!
4953
}
@@ -52,7 +56,7 @@ export function getPlanSlug(sessionId?: SessionId): string {
5256
* Set a specific plan slug for a session (used when resuming a session)
5357
*/
5458
export function setPlanSlug(sessionId: SessionId, slug: string): void {
55-
getPlanSlugCache().set(sessionId, slug)
59+
setPlanSlugCacheEntry(sessionId, slug)
5660
}
5761

5862
/**

src/utils/taskSummary.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,13 @@ export function maybeGenerateTaskSummary(
4444
if (!messages || messages.length === 0) return
4545

4646
// Extract a short status from the most recent assistant message
47-
const lastAssistant = [...messages]
48-
.reverse()
49-
.find(m => m.type === 'assistant')
47+
let lastAssistant: (typeof messages)[0] | undefined
48+
for (let i = messages.length - 1; i >= 0; i--) {
49+
if (messages[i]!.type === 'assistant') {
50+
lastAssistant = messages[i]
51+
break
52+
}
53+
}
5054

5155
let status: 'busy' | 'idle' = 'busy'
5256
let waitingFor: string | undefined

0 commit comments

Comments
 (0)