import { feature } from 'bun:bundle' import type { WriteFileOptions } from 'fs' import { closeSync, writeFileSync as fsWriteFileSync, fsyncSync, openSync, } from 'fs' // biome-ignore lint: This file IS the cloneDeep wrapper - it must import the original import lodashCloneDeep from 'lodash-es/cloneDeep.js' import { addSlowOperation } from '../bootstrap/state.js' import { logForDebugging } from './debug.js' // Extended WriteFileOptions to include 'flush' which is available in Node.js 20.1.0+ // but not yet in @types/node type WriteFileOptionsWithFlush = | WriteFileOptions | (WriteFileOptions & { flush?: boolean }) // --- Slow operation logging infrastructure --- /** * Threshold in milliseconds for logging slow JSON/clone operations. * Operations taking longer than this will be logged for debugging. * - Override: set CLAUDE_CODE_SLOW_OPERATION_THRESHOLD_MS to a number * - Dev builds: 20ms (lower threshold for development) * - Ants: 300ms (enabled for all internal users) */ const SLOW_OPERATION_THRESHOLD_MS = (() => { const envValue = process.env.CLAUDE_CODE_SLOW_OPERATION_THRESHOLD_MS if (envValue !== undefined) { const parsed = Number(envValue) if (!Number.isNaN(parsed) && parsed >= 0) { return parsed } } if (process.env.NODE_ENV === 'development') { return 20 } if (process.env.USER_TYPE === 'ant') { return 300 } return Infinity })() // Re-export for callers that still need the threshold value directly export { SLOW_OPERATION_THRESHOLD_MS } // Module-level re-entrancy guard. logForDebugging writes to a debug file via // appendFileSync, which goes through slowLogging again. Without this guard, // a slow appendFileSync → dispose → logForDebugging → appendFileSync → dispose → ... let isLogging = false /** * Extract the first stack frame outside this file, so the DevBar warning * points at the actual caller instead of a useless `Object{N keys}`. * Only called when an operation was actually slow — never on the fast path. */ export function callerFrame(stack: string | undefined): string { if (!stack) return '' for (const line of stack.split('\n')) { if (line.includes('slowOperations')) continue const m = line.match(/([^/\\]+?):(\d+):\d+\)?$/) if (m) return ` @ ${m[1]}:${m[2]}` } return '' } /** * Builds a human-readable description from tagged template arguments. * Only called when an operation was actually slow — never on the fast path. * * args[0] = TemplateStringsArray, args[1..n] = interpolated values */ function buildDescription(args: IArguments): string { const strings = args[0] as TemplateStringsArray let result = '' for (let i = 0; i < strings.length; i++) { result += strings[i] if (i + 1 < args.length) { const v = args[i + 1] if (Array.isArray(v)) { result += `Array[${(v as unknown[]).length}]` } else if (v !== null && typeof v === 'object') { result += `Object{${Object.keys(v as Record).length} keys}` } else if (typeof v === 'string') { result += v.length > 80 ? `${v.slice(0, 80)}…` : v } else { result += String(v) } } } return result } class AntSlowLogger { startTime: number args: IArguments err: Error constructor(args: IArguments) { this.startTime = performance.now() this.args = args // V8/JSC capture the stack at construction but defer the expensive string // formatting until .stack is read — so this stays off the fast path. this.err = new Error() } [Symbol.dispose](): void { const duration = performance.now() - this.startTime if (duration > SLOW_OPERATION_THRESHOLD_MS && !isLogging) { isLogging = true try { const description = buildDescription(this.args) + callerFrame(this.err.stack) logForDebugging( `[SLOW OPERATION DETECTED] ${description} (${duration.toFixed(1)}ms)`, ) addSlowOperation(description, duration) } finally { isLogging = false } } } } const NOOP_LOGGER: Disposable = { [Symbol.dispose]() {} } // Must be regular functions (not arrows) to access `arguments` function slowLoggingAnt( _strings: TemplateStringsArray, ..._values: unknown[] ): AntSlowLogger { // eslint-disable-next-line prefer-rest-params return new AntSlowLogger(arguments) } function slowLoggingExternal(): Disposable { return NOOP_LOGGER } /** * Tagged template for slow operation logging. * * In ANT builds: creates an AntSlowLogger that times the operation and logs * if it exceeds the threshold. Description is built lazily only when slow. * * In external builds: returns a singleton no-op disposable. Zero allocations, * zero timing. AntSlowLogger and buildDescription are dead-code-eliminated. * * @example * using _ = slowLogging`structuredClone(${value})` * const result = structuredClone(value) */ export const slowLogging: { (strings: TemplateStringsArray, ...values: unknown[]): Disposable } = feature('SLOW_OPERATION_LOGGING') ? slowLoggingAnt : slowLoggingExternal // --- Wrapped operations --- /** * Wrapped JSON.stringify with slow operation logging. * Use this instead of JSON.stringify directly to detect performance issues. * * @example * import { jsonStringify } from './slowOperations.js' * const json = jsonStringify(data) * const prettyJson = jsonStringify(data, null, 2) */ export function jsonStringify( value: unknown, replacer?: (this: unknown, key: string, value: unknown) => unknown, space?: string | number, ): string export function jsonStringify( value: unknown, replacer?: (number | string)[] | null, space?: string | number, ): string export function jsonStringify( value: unknown, replacer?: | ((this: unknown, key: string, value: unknown) => unknown) | (number | string)[] | null, space?: string | number, ): string { using _ = slowLogging`JSON.stringify(${value})` return JSON.stringify( value, replacer as Parameters[1], space, ) } /** * Wrapped JSON.parse with slow operation logging. * Use this instead of JSON.parse directly to detect performance issues. * * @example * import { jsonParse } from './slowOperations.js' * const data = jsonParse(jsonString) */ export const jsonParse: typeof JSON.parse = (text, reviver) => { using _ = slowLogging`JSON.parse(${text})` // V8 de-opts JSON.parse when a second argument is passed, even if undefined. // Branch explicitly so the common (no-reviver) path stays on the fast path. return typeof reviver === 'undefined' ? JSON.parse(text) : JSON.parse(text, reviver) } /** * Wrapped structuredClone with slow operation logging. * Use this instead of structuredClone directly to detect performance issues. * * @example * import { clone } from './slowOperations.js' * const copy = clone(originalObject) */ export function clone(value: T, options?: StructuredSerializeOptions): T { using _ = slowLogging`structuredClone(${value})` return structuredClone(value, options) } /** * Wrapped cloneDeep with slow operation logging. * Use this instead of lodash cloneDeep directly to detect performance issues. * * @example * import { cloneDeep } from './slowOperations.js' * const copy = cloneDeep(originalObject) */ export function cloneDeep(value: T): T { using _ = slowLogging`cloneDeep(${value})` return lodashCloneDeep(value) } /** * Wrapper around fs.writeFileSync with slow operation logging. * Supports flush option to ensure data is written to disk before returning. * @param filePath The path to the file to write to * @param data The data to write (string or Buffer) * @param options Optional write options (encoding, mode, flag, flush) * @deprecated Use `fs.promises.writeFile` instead for non-blocking writes. * Sync file writes block the event loop and cause performance issues. */ export function writeFileSync_DEPRECATED( filePath: string, data: string | NodeJS.ArrayBufferView, options?: WriteFileOptionsWithFlush, ): void { using _ = slowLogging`fs.writeFileSync(${filePath}, ${data})` // Check if flush is requested (for object-style options) const needsFlush = options !== null && typeof options === 'object' && 'flush' in options && options.flush === true if (needsFlush) { // Manual flush: open file, write, fsync, close const encoding = typeof options === 'object' && 'encoding' in options ? options.encoding : undefined const mode = typeof options === 'object' && 'mode' in options ? options.mode : undefined let fd: number | undefined try { fd = openSync(filePath, 'w', mode) fsWriteFileSync(fd, data, { encoding: encoding ?? undefined }) fsyncSync(fd) } finally { if (fd !== undefined) { closeSync(fd) } } } else { // No flush needed, use standard writeFileSync fsWriteFileSync(filePath, data, options as WriteFileOptions) } }