headlessProfiler.ts
utils/headlessProfiler.ts
179
Lines
6058
Bytes
3
Exports
6
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 file-tools. It contains 179 lines, 6 detected imports, and 3 detected exports.
Important relationships
Detected exports
headlessProfilerStartTurnheadlessProfilerCheckpointlogHeadlessProfilerTurn
Keywords
markturnonlyperfnamemetadataheadlessmodetimemarks
Detected imports
../bootstrap/state.js../services/analytics/index.js./debug.js./envUtils.js./profilerBase.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
/**
* Headless mode profiling utility for measuring per-turn latency in -p (print) mode.
*
* Tracks key timing phases per turn:
* - Time to system message output (turn 0 only)
* - Time to first query started
* - Time to first API response (TTFT)
*
* Uses Node.js built-in performance hooks API for standard timing measurement.
* Sampled logging: 100% of ant users, 5% of external users.
*
* Set CLAUDE_CODE_PROFILE_STARTUP=1 for detailed logging output.
*/
import { getIsNonInteractiveSession } from '../bootstrap/state.js'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../services/analytics/index.js'
import { logForDebugging } from './debug.js'
import { isEnvTruthy } from './envUtils.js'
import { getPerformance } from './profilerBase.js'
import { jsonStringify } from './slowOperations.js'
// Detailed profiling mode - same env var as startupProfiler
// eslint-disable-next-line custom-rules/no-process-env-top-level
const DETAILED_PROFILING = isEnvTruthy(process.env.CLAUDE_CODE_PROFILE_STARTUP)
// Sampling for Statsig logging: 100% ant, 5% external
// Decision made once at module load - non-sampled users pay no profiling cost
const STATSIG_SAMPLE_RATE = 0.05
// eslint-disable-next-line custom-rules/no-process-env-top-level
const STATSIG_LOGGING_SAMPLED =
process.env.USER_TYPE === 'ant' || Math.random() < STATSIG_SAMPLE_RATE
// Enable profiling if either detailed mode OR sampled for Statsig
const SHOULD_PROFILE = DETAILED_PROFILING || STATSIG_LOGGING_SAMPLED
// Use a unique prefix to avoid conflicts with other profiler marks
const MARK_PREFIX = 'headless_'
// Track current turn number (auto-incremented by headlessProfilerStartTurn)
let currentTurnNumber = -1
/**
* Clear all headless profiler marks from performance timeline
*/
function clearHeadlessMarks(): void {
const perf = getPerformance()
const allMarks = perf.getEntriesByType('mark')
for (const mark of allMarks) {
if (mark.name.startsWith(MARK_PREFIX)) {
perf.clearMarks(mark.name)
}
}
}
/**
* Start a new turn for profiling. Clears previous marks, increments turn number,
* and records turn_start. Call this at the beginning of each user message processing.
*/
export function headlessProfilerStartTurn(): void {
// Only profile in headless/non-interactive mode
if (!getIsNonInteractiveSession()) return
// Only profile if enabled
if (!SHOULD_PROFILE) return
currentTurnNumber++
clearHeadlessMarks()
const perf = getPerformance()
perf.mark(`${MARK_PREFIX}turn_start`)
if (DETAILED_PROFILING) {
logForDebugging(`[headlessProfiler] Started turn ${currentTurnNumber}`)
}
}
/**
* Record a checkpoint with the given name.
* Only records if in headless mode and profiling is enabled.
*/
export function headlessProfilerCheckpoint(name: string): void {
// Only profile in headless/non-interactive mode
if (!getIsNonInteractiveSession()) return
// Only profile if enabled
if (!SHOULD_PROFILE) return
const perf = getPerformance()
perf.mark(`${MARK_PREFIX}${name}`)
if (DETAILED_PROFILING) {
logForDebugging(
`[headlessProfiler] Checkpoint: ${name} at ${perf.now().toFixed(1)}ms`,
)
}
}
/**
* Log headless latency metrics for the current turn to Statsig.
* Call this at the end of each turn (before processing next user message).
*/
export function logHeadlessProfilerTurn(): void {
// Only log in headless mode
if (!getIsNonInteractiveSession()) return
// Only log if enabled
if (!SHOULD_PROFILE) return
const perf = getPerformance()
const allMarks = perf.getEntriesByType('mark')
// Filter to only our headless marks
const marks = allMarks.filter(mark => mark.name.startsWith(MARK_PREFIX))
if (marks.length === 0) return
// Build checkpoint lookup (strip prefix for easier access)
const checkpointTimes = new Map<string, number>()
for (const mark of marks) {
const name = mark.name.slice(MARK_PREFIX.length)
checkpointTimes.set(name, mark.startTime)
}
const turnStart = checkpointTimes.get('turn_start')
if (turnStart === undefined) return
// Compute phase durations relative to turn_start
const metadata: Record<string, number | string | undefined> = {
turn_number: currentTurnNumber,
}
// Time to system message from process start (only meaningful for turn 0)
// Use absolute time since perf_hooks startTime is relative to process start
const systemMessageTime = checkpointTimes.get('system_message_yielded')
if (systemMessageTime !== undefined && currentTurnNumber === 0) {
metadata.time_to_system_message_ms = Math.round(systemMessageTime)
}
// Time to query start
const queryStartTime = checkpointTimes.get('query_started')
if (queryStartTime !== undefined) {
metadata.time_to_query_start_ms = Math.round(queryStartTime - turnStart)
}
// Time to first response (first chunk from API)
const firstChunkTime = checkpointTimes.get('first_chunk')
if (firstChunkTime !== undefined) {
metadata.time_to_first_response_ms = Math.round(firstChunkTime - turnStart)
}
// Query overhead (time between query start and API request sent)
const apiRequestTime = checkpointTimes.get('api_request_sent')
if (queryStartTime !== undefined && apiRequestTime !== undefined) {
metadata.query_overhead_ms = Math.round(apiRequestTime - queryStartTime)
}
// Add checkpoint count for debugging
metadata.checkpoint_count = marks.length
// Add entrypoint for segmentation (sdk-ts, sdk-py, sdk-cli, or undefined)
if (process.env.CLAUDE_CODE_ENTRYPOINT) {
metadata.entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT
}
// Log to Statsig if sampled
if (STATSIG_LOGGING_SAMPLED) {
logEvent(
'tengu_headless_latency',
metadata as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
)
}
// Log detailed output if CLAUDE_CODE_PROFILE_STARTUP=1
if (DETAILED_PROFILING) {
logForDebugging(
`[headlessProfiler] Turn ${currentTurnNumber} metrics: ${jsonStringify(metadata)}`,
)
}
}