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_DISTANCE = 1;
type ErrorInfo = {
readonly message: string;
readonly stack?: string;
};
type State = {
readonly error?: Error;
readonly error?: ErrorInfo;
};
// Root component for all Ink apps
@@ -142,7 +147,7 @@ export default class App extends PureComponent<Props, State> {
static displayName = 'InternalApp';
static getDerivedStateFromError(error: Error) {
return { error };
return { error: { message: error.message, stack: error.stack } };
}
override state = {
@@ -221,7 +226,7 @@ export default class App extends PureComponent<Props, State> {
<TerminalFocusProvider>
<ClockProvider>
<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>
</ClockProvider>
</TerminalFocusProvider>

View File

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

View File

@@ -340,6 +340,15 @@ export async function* query(
terminal?.reason === 'aborted_tools'
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

View File

@@ -1,22 +1,69 @@
/**
* Shared infrastructure for profiler modules (startupProfiler, queryProfiler,
* headlessProfiler). All three use the same perf_hooks timeline and the same
* line format for detailed reports.
* headlessProfiler).
*
* 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'
// Lazy-load performance API only when profiling is enabled.
// Shared across all profilers — perf_hooks.performance is a process-wide singleton.
let performance: typeof PerformanceType | null = null
/** Minimal PerformanceEntry-like object used by profilers */
export interface CheckpointEntry {
readonly name: string
readonly startTime: number
readonly entryType: 'mark'
}
export function getPerformance(): typeof PerformanceType {
if (!performance) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
performance = require('perf_hooks').performance
/**
* Lightweight replacement for perf_hooks.performance that stores marks in a
* plain JavaScript Map instead of JSC's C++ Vector. This avoids the memory
* 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 {