Skip to content

Commit 18d6656

Browse files
feat: 尝试改进 Error 处理以提升内存管理效率
1 parent d0915fc commit 18d6656

4 files changed

Lines changed: 81 additions & 15 deletions

File tree

packages/@ant/ink/src/components/App.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,13 @@ type Props = {
131131
const MULTI_CLICK_TIMEOUT_MS = 500;
132132
const MULTI_CLICK_DISTANCE = 1;
133133

134+
type ErrorInfo = {
135+
readonly message: string;
136+
readonly stack?: string;
137+
};
138+
134139
type State = {
135-
readonly error?: Error;
140+
readonly error?: ErrorInfo;
136141
};
137142

138143
// Root component for all Ink apps
@@ -142,7 +147,7 @@ export default class App extends PureComponent<Props, State> {
142147
static displayName = 'InternalApp';
143148

144149
static getDerivedStateFromError(error: Error) {
145-
return { error };
150+
return { error: { message: error.message, stack: error.stack } };
146151
}
147152

148153
override state = {
@@ -221,7 +226,7 @@ export default class App extends PureComponent<Props, State> {
221226
<TerminalFocusProvider>
222227
<ClockProvider>
223228
<CursorDeclarationContext.Provider value={this.props.onCursorDeclaration ?? (() => {})}>
224-
{this.state.error ? <ErrorOverview error={this.state.error as Error} /> : this.props.children}
229+
{this.state.error ? <ErrorOverview error={this.state.error} /> : this.props.children}
225230
</CursorDeclarationContext.Provider>
226231
</ClockProvider>
227232
</TerminalFocusProvider>

packages/@ant/ink/src/components/ErrorOverview.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@ function getStackUtils(): StackUtils {
2323

2424
/* eslint-enable custom-rules/no-process-cwd */
2525

26+
type ErrorLike = {
27+
readonly message: string;
28+
readonly stack?: string;
29+
};
30+
2631
type Props = {
27-
readonly error: Error;
32+
readonly error: ErrorLike;
2833
};
2934

3035
export default function ErrorOverview({ error }: Props) {

src/query.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,15 @@ export async function* query(
340340
terminal?.reason === 'aborted_tools'
341341
endTrace(langfuseTrace, undefined, isAborted ? 'interrupted' : undefined)
342342
}
343+
344+
// Break the closure chain: toolUseContext captures langfuseTrace which
345+
// holds SpanImpl → otperformance (the 571MB Performance object). Nulling
346+
// these after endTrace allows GC to reclaim the span tree.
347+
if (paramsWithTrace !== params) {
348+
paramsWithTrace.toolUseContext.langfuseTrace = null
349+
paramsWithTrace.toolUseContext.langfuseRootTrace = null
350+
paramsWithTrace.toolUseContext.langfuseBatchSpan = null
351+
}
343352
}
344353

345354
// Only reached if queryLoop returned normally. Skipped on throw (error

src/utils/profilerBase.ts

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,69 @@
11
/**
22
* Shared infrastructure for profiler modules (startupProfiler, queryProfiler,
3-
* headlessProfiler). All three use the same perf_hooks timeline and the same
4-
* line format for detailed reports.
3+
* headlessProfiler).
4+
*
5+
* Uses process.hrtime.bigint() for timing instead of perf_hooks.performance
6+
* to avoid a Bun/JSC memory leak: JSC's Performance object stores marks in a
7+
* C++ Vector that never shrinks even after clearMarks(). Long-running sessions
8+
* (daemon, /loop) accumulate hundreds of MB of dead capacity.
9+
*
10+
* The LightweightPerf class provides the same interface the profilers need
11+
* (mark, getEntriesByType, clearMarks, now) backed by a plain JS Map.
512
*/
613

7-
import type { performance as PerformanceType } from 'perf_hooks'
814
import { formatFileSize } from './format.js'
915

10-
// Lazy-load performance API only when profiling is enabled.
11-
// Shared across all profilers — perf_hooks.performance is a process-wide singleton.
12-
let performance: typeof PerformanceType | null = null
16+
/** Minimal PerformanceEntry-like object used by profilers */
17+
export interface CheckpointEntry {
18+
readonly name: string
19+
readonly startTime: number
20+
readonly entryType: 'mark'
21+
}
1322

14-
export function getPerformance(): typeof PerformanceType {
15-
if (!performance) {
16-
// eslint-disable-next-line @typescript-eslint/no-require-imports
17-
performance = require('perf_hooks').performance
23+
/**
24+
* Lightweight replacement for perf_hooks.performance that stores marks in a
25+
* plain JavaScript Map instead of JSC's C++ Vector. This avoids the memory
26+
* leak where clearMarks() sets the count to 0 but never frees Vector capacity.
27+
*/
28+
class LightweightPerf {
29+
private marks = new Map<string, number>()
30+
private _origin: number
31+
32+
constructor() {
33+
this._origin = Number(process.hrtime.bigint() / 1000n) / 1000
34+
}
35+
36+
mark(name: string): void {
37+
this.marks.set(name, this.now())
1838
}
19-
return performance!
39+
40+
getEntriesByType(type: 'mark'): CheckpointEntry[] {
41+
if (type !== 'mark') return []
42+
const entries: CheckpointEntry[] = []
43+
for (const [name, startTime] of this.marks) {
44+
entries.push({ name, startTime, entryType: 'mark' })
45+
}
46+
return entries
47+
}
48+
49+
clearMarks(name?: string): void {
50+
if (name !== undefined) {
51+
this.marks.delete(name)
52+
} else {
53+
this.marks.clear()
54+
}
55+
}
56+
57+
now(): number {
58+
return Number(process.hrtime.bigint() / 1000n) / 1000 - this._origin
59+
}
60+
}
61+
62+
// Singleton — shared across all profilers (same as the old perf_hooks singleton)
63+
const perf = new LightweightPerf()
64+
65+
export function getPerformance(): LightweightPerf {
66+
return perf
2067
}
2168

2269
export function formatMs(ms: number): string {

0 commit comments

Comments
 (0)