feat: 尝试改进 Error 处理以提升内存管理效率

This commit is contained in:
claude-code-best
2026-05-05 18:18:13 +08:00
parent d0915fc880
commit 18d6656a6a
4 changed files with 81 additions and 15 deletions

View File

@@ -131,8 +131,13 @@ type Props = {
const MULTI_CLICK_TIMEOUT_MS = 500; const MULTI_CLICK_TIMEOUT_MS = 500;
const MULTI_CLICK_DISTANCE = 1; const MULTI_CLICK_DISTANCE = 1;
type ErrorInfo = {
readonly message: string;
readonly stack?: string;
};
type State = { type State = {
readonly error?: Error; readonly error?: ErrorInfo;
}; };
// Root component for all Ink apps // Root component for all Ink apps
@@ -142,7 +147,7 @@ export default class App extends PureComponent<Props, State> {
static displayName = 'InternalApp'; static displayName = 'InternalApp';
static getDerivedStateFromError(error: Error) { static getDerivedStateFromError(error: Error) {
return { error }; return { error: { message: error.message, stack: error.stack } };
} }
override state = { override state = {
@@ -221,7 +226,7 @@ export default class App extends PureComponent<Props, State> {
<TerminalFocusProvider> <TerminalFocusProvider>
<ClockProvider> <ClockProvider>
<CursorDeclarationContext.Provider value={this.props.onCursorDeclaration ?? (() => {})}> <CursorDeclarationContext.Provider value={this.props.onCursorDeclaration ?? (() => {})}>
{this.state.error ? <ErrorOverview error={this.state.error as Error} /> : this.props.children} {this.state.error ? <ErrorOverview error={this.state.error} /> : this.props.children}
</CursorDeclarationContext.Provider> </CursorDeclarationContext.Provider>
</ClockProvider> </ClockProvider>
</TerminalFocusProvider> </TerminalFocusProvider>

View File

@@ -23,8 +23,13 @@ function getStackUtils(): StackUtils {
/* eslint-enable custom-rules/no-process-cwd */ /* eslint-enable custom-rules/no-process-cwd */
type ErrorLike = {
readonly message: string;
readonly stack?: string;
};
type Props = { type Props = {
readonly error: Error; readonly error: ErrorLike;
}; };
export default function ErrorOverview({ error }: Props) { export default function ErrorOverview({ error }: Props) {

View File

@@ -340,6 +340,15 @@ export async function* query(
terminal?.reason === 'aborted_tools' terminal?.reason === 'aborted_tools'
endTrace(langfuseTrace, undefined, isAborted ? 'interrupted' : undefined) endTrace(langfuseTrace, undefined, isAborted ? 'interrupted' : undefined)
} }
// Break the closure chain: toolUseContext captures langfuseTrace which
// holds SpanImpl → otperformance (the 571MB Performance object). Nulling
// these after endTrace allows GC to reclaim the span tree.
if (paramsWithTrace !== params) {
paramsWithTrace.toolUseContext.langfuseTrace = null
paramsWithTrace.toolUseContext.langfuseRootTrace = null
paramsWithTrace.toolUseContext.langfuseBatchSpan = null
}
} }
// Only reached if queryLoop returned normally. Skipped on throw (error // Only reached if queryLoop returned normally. Skipped on throw (error

View File

@@ -1,22 +1,69 @@
/** /**
* Shared infrastructure for profiler modules (startupProfiler, queryProfiler, * Shared infrastructure for profiler modules (startupProfiler, queryProfiler,
* headlessProfiler). All three use the same perf_hooks timeline and the same * headlessProfiler).
* line format for detailed reports. *
* Uses process.hrtime.bigint() for timing instead of perf_hooks.performance
* to avoid a Bun/JSC memory leak: JSC's Performance object stores marks in a
* C++ Vector that never shrinks even after clearMarks(). Long-running sessions
* (daemon, /loop) accumulate hundreds of MB of dead capacity.
*
* The LightweightPerf class provides the same interface the profilers need
* (mark, getEntriesByType, clearMarks, now) backed by a plain JS Map.
*/ */
import type { performance as PerformanceType } from 'perf_hooks'
import { formatFileSize } from './format.js' import { formatFileSize } from './format.js'
// Lazy-load performance API only when profiling is enabled. /** Minimal PerformanceEntry-like object used by profilers */
// Shared across all profilers — perf_hooks.performance is a process-wide singleton. export interface CheckpointEntry {
let performance: typeof PerformanceType | null = null readonly name: string
readonly startTime: number
readonly entryType: 'mark'
}
export function getPerformance(): typeof PerformanceType { /**
if (!performance) { * Lightweight replacement for perf_hooks.performance that stores marks in a
// eslint-disable-next-line @typescript-eslint/no-require-imports * plain JavaScript Map instead of JSC's C++ Vector. This avoids the memory
performance = require('perf_hooks').performance * leak where clearMarks() sets the count to 0 but never frees Vector capacity.
*/
class LightweightPerf {
private marks = new Map<string, number>()
private _origin: number
constructor() {
this._origin = Number(process.hrtime.bigint() / 1000n) / 1000
} }
return performance!
mark(name: string): void {
this.marks.set(name, this.now())
}
getEntriesByType(type: 'mark'): CheckpointEntry[] {
if (type !== 'mark') return []
const entries: CheckpointEntry[] = []
for (const [name, startTime] of this.marks) {
entries.push({ name, startTime, entryType: 'mark' })
}
return entries
}
clearMarks(name?: string): void {
if (name !== undefined) {
this.marks.delete(name)
} else {
this.marks.clear()
}
}
now(): number {
return Number(process.hrtime.bigint() / 1000n) / 1000 - this._origin
}
}
// Singleton — shared across all profilers (same as the old perf_hooks singleton)
const perf = new LightweightPerf()
export function getPerformance(): LightweightPerf {
return perf
} }
export function formatMs(ms: number): string { export function formatMs(ms: number): string {