slowOperations.ts
utils/slowOperations.ts
No strong subsystem tag
287
Lines
9055
Bytes
8
Exports
10
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 287 lines, 10 detected imports, and 8 detected exports.
Important relationships
Detected exports
callerFrameslowLoggingjsonStringifyjsonParseclonecloneDeepwriteFileSync_DEPRECATEDSLOW_OPERATION_THRESHOLD_MS
Keywords
optionsjsonslowunknownoperationargsdataflushclonedeeplogging
Detected imports
bun:bundlefsfslodash-es/cloneDeep.js../bootstrap/state.js./debug.js./slowOperations.js./slowOperations.js./slowOperations.js./slowOperations.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
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<string, unknown>).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<typeof JSON.stringify>[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<T>(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<T>(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)
}
}