mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
Node.js v22 undici internal calls performance.markResourceTiming() after every fetch. The performance shim was missing this method, causing TypeError crashes in ACP mode when running with Node.js.
169 lines
5.0 KiB
TypeScript
169 lines
5.0 KiB
TypeScript
/**
|
|
* Performance shim — replaces globalThis.performance to prevent JSC's C++ Vector
|
|
* from growing without bound.
|
|
*
|
|
* In Bun, globalThis.performance is JSC's native Performance object. It stores
|
|
* marks, measures, and resource timings in a C++ Vector that never shrinks even
|
|
* after clearMarks(). Long-running sessions (daemon, /loop) accumulate hundreds
|
|
* of MB of dead capacity.
|
|
*
|
|
* This shim keeps performance.now() on the native object (fast, no memory cost)
|
|
* but redirects mark/measure/getEntries operations to a plain JS Map that the GC
|
|
* can reclaim. Third-party code (React reconciler, OTel/Langfuse) uses
|
|
* performance.now() for timing — that stays native. The accumulating operations
|
|
* go to GC-able JS memory instead.
|
|
*
|
|
* MUST be installed before React/OTel import — see cli.tsx first import.
|
|
*/
|
|
|
|
const original = globalThis.performance
|
|
|
|
// JS-backed storage — fully GC-able
|
|
const marks = new Map<string, number>()
|
|
const measures = new Map<
|
|
string,
|
|
{ name: string; startTime: number; duration: number }
|
|
>()
|
|
|
|
function now(): number {
|
|
return original.now()
|
|
}
|
|
|
|
function mark(name: string): PerformanceMark {
|
|
marks.set(name, now())
|
|
// Return a minimal PerformanceMark-like object to satisfy the interface.
|
|
// React/OTel only use mark() for side effects, not the return value.
|
|
return {
|
|
name,
|
|
entryType: 'mark',
|
|
startTime: marks.get(name)!,
|
|
duration: 0,
|
|
} as PerformanceMark
|
|
}
|
|
|
|
function measure(
|
|
name: string,
|
|
startMarkOrOptions?: string | MeasureOptions,
|
|
endMark?: string,
|
|
): void {
|
|
let startTime: number
|
|
let duration: number
|
|
|
|
if (typeof startMarkOrOptions === 'string') {
|
|
const start = marks.get(startMarkOrOptions)
|
|
const end = endMark ? marks.get(endMark) : now()
|
|
startTime = start ?? now()
|
|
duration = (end ?? now()) - startTime
|
|
} else if (startMarkOrOptions && typeof startMarkOrOptions === 'object') {
|
|
startTime = startMarkOrOptions.start ?? 0
|
|
duration = (startMarkOrOptions.end ?? now()) - startTime
|
|
} else {
|
|
startTime = 0
|
|
duration = now()
|
|
}
|
|
|
|
measures.set(name, { name, startTime, duration })
|
|
}
|
|
|
|
interface MeasureOptions {
|
|
start?: number
|
|
end?: number
|
|
detail?: unknown
|
|
}
|
|
|
|
interface PerformanceEntryLike {
|
|
readonly name: string
|
|
readonly entryType: string
|
|
readonly startTime: number
|
|
readonly duration: number
|
|
}
|
|
|
|
function getEntriesByType(type: string): PerformanceEntryLike[] {
|
|
if (type === 'mark') {
|
|
return [...marks.entries()].map(([name, startTime]) => ({
|
|
name,
|
|
entryType: 'mark',
|
|
startTime,
|
|
duration: 0,
|
|
}))
|
|
}
|
|
if (type === 'measure') {
|
|
return [...measures.values()].map(m => ({
|
|
name: m.name,
|
|
entryType: 'measure',
|
|
startTime: m.startTime,
|
|
duration: m.duration,
|
|
}))
|
|
}
|
|
return []
|
|
}
|
|
|
|
function getEntriesByName(name: string, type?: string): PerformanceEntryLike[] {
|
|
const entries = getEntriesByType(type ?? 'mark').concat(
|
|
type === undefined ? getEntriesByType('measure') : [],
|
|
)
|
|
return entries.filter(e => e.name === name)
|
|
}
|
|
|
|
function clearMarks(name?: string): void {
|
|
if (name !== undefined) {
|
|
marks.delete(name)
|
|
} else {
|
|
marks.clear()
|
|
}
|
|
}
|
|
|
|
function clearMeasures(name?: string): void {
|
|
if (name !== undefined) {
|
|
measures.delete(name)
|
|
} else {
|
|
measures.clear()
|
|
}
|
|
}
|
|
|
|
// Plain object shim — must NOT inherit from Performance.prototype because
|
|
// native getters (onresourcetimingbufferfull, timeOrigin, toJSON) check
|
|
// that `this` is an actual JSC Performance instance and throw otherwise.
|
|
const shim = {
|
|
now,
|
|
mark,
|
|
measure: measure as typeof performance.measure,
|
|
getEntriesByType: getEntriesByType as typeof performance.getEntriesByType,
|
|
getEntriesByName: getEntriesByName as typeof performance.getEntriesByName,
|
|
clearMarks: clearMarks as typeof performance.clearMarks,
|
|
clearMeasures: clearMeasures as typeof performance.clearMeasures,
|
|
clearResourceTimings: (() => {}) as typeof performance.clearResourceTimings,
|
|
setResourceTimingBufferSize:
|
|
(() => {}) as typeof performance.setResourceTimingBufferSize,
|
|
// Node.js v22 undici internal calls this after every fetch — must exist to
|
|
// avoid TypeError: markResourceTiming is not a function
|
|
markResourceTiming: (() => {}) as any,
|
|
// Delegate read-only properties to the original
|
|
get timeOrigin() {
|
|
return original.timeOrigin
|
|
},
|
|
get onresourcetimingbufferfull() {
|
|
return (original as any).onresourcetimingbufferfull
|
|
},
|
|
set onresourcetimingbufferfull(_v: any) {
|
|
// no-op — prevent accumulation
|
|
},
|
|
toJSON() {
|
|
return original.toJSON()
|
|
},
|
|
} as unknown as typeof performance
|
|
|
|
/**
|
|
* Install the shim onto globalThis.performance. Safe to call multiple times.
|
|
* Must run before React and OTel import to prevent them from capturing the
|
|
* native Performance reference.
|
|
*/
|
|
export function installPerformanceShim(): void {
|
|
if ((globalThis as any).__performanceShimInstalled) return
|
|
;(globalThis as any).__performanceShimInstalled = true
|
|
globalThis.performance = shim
|
|
}
|
|
|
|
// Auto-install on import
|
|
installPerformanceShim()
|