Files
letta-code/src/utils/timing.ts

193 lines
5.4 KiB
TypeScript

// src/utils/timing.ts
// Debug timing utilities - only active when LETTA_DEBUG_TIMINGS env var is set
/**
* Check if debug timings are enabled via LETTA_DEBUG_TIMINGS env var
* Set LETTA_DEBUG_TIMINGS=1 or LETTA_DEBUG_TIMINGS=true to enable timing logs
*/
export function isTimingsEnabled(): boolean {
const val = process.env.LETTA_DEBUG_TIMINGS;
return val === "1" || val === "true";
}
/**
* Format duration nicely: "245ms" or "1.52s"
*/
export function formatDuration(ms: number): string {
if (ms < 1000) return `${Math.round(ms)}ms`;
return `${(ms / 1000).toFixed(2)}s`;
}
/**
* Format timestamp: "12:34:56.789"
*/
export function formatTimestamp(date: Date): string {
return date.toISOString().slice(11, 23);
}
/**
* Log timing message to stderr (won't interfere with stdout JSON in headless mode)
*/
export function logTiming(message: string): void {
if (isTimingsEnabled()) {
console.error(`[timing] ${message}`);
}
}
// ============================================================================
// Milestone tracking for latency audits
// ============================================================================
// Store milestones with their timestamps (ms since process start via performance.now())
const milestones: Map<string, number> = new Map();
// Reference time for relative measurements (set on first milestone)
let firstMilestoneTime: number | null = null;
/**
* Mark a named milestone in the boot/execution sequence.
* Call this at key points to track where time is spent.
*
* @param name - Descriptive name like "SETTINGS_LOADED" or "AGENT_RESOLVED"
*/
export function markMilestone(name: string): void {
const now = performance.now();
milestones.set(name, now);
if (firstMilestoneTime === null) {
firstMilestoneTime = now;
}
if (isTimingsEnabled()) {
const relative = now - firstMilestoneTime;
console.error(
`[timing] MILESTONE ${name} at +${formatDuration(relative)} (${formatTimestamp(new Date())})`,
);
}
}
/**
* Measure time elapsed since a previous milestone.
*
* @param label - Description of what we're measuring (e.g., "tool loading")
* @param fromMilestone - Name of the starting milestone
*/
export function measureSinceMilestone(
label: string,
fromMilestone: string,
): void {
if (!isTimingsEnabled()) return;
const startTime = milestones.get(fromMilestone);
if (startTime === undefined) {
console.error(
`[timing] WARNING: milestone "${fromMilestone}" not found for measurement "${label}"`,
);
return;
}
const duration = performance.now() - startTime;
console.error(`[timing] ${label}: ${formatDuration(duration)}`);
}
/**
* Get the duration between two milestones in milliseconds.
* Returns null if either milestone doesn't exist.
*/
export function getMilestoneDuration(
fromMilestone: string,
toMilestone: string,
): number | null {
const start = milestones.get(fromMilestone);
const end = milestones.get(toMilestone);
if (start === undefined || end === undefined) return null;
return end - start;
}
/**
* Print a summary of all milestones with relative timestamps.
* Useful at the end of a benchmark run.
*/
export function reportAllMilestones(): void {
if (!isTimingsEnabled() || milestones.size === 0) return;
const first = firstMilestoneTime ?? 0;
console.error(`[timing] ======== MILESTONE SUMMARY ========`);
// Sort by timestamp
const sorted = [...milestones.entries()].sort((a, b) => a[1] - b[1]);
let prevTime = first;
for (const [name, time] of sorted) {
const relativeToStart = time - first;
const delta = time - prevTime;
const deltaStr = prevTime === first ? "" : ` (+${formatDuration(delta)})`;
console.error(
`[timing] +${formatDuration(relativeToStart).padStart(8)} ${name}${deltaStr}`,
);
prevTime = time;
}
console.error(`[timing] =====================================`);
}
/**
* Clear all milestones (useful for running multiple benchmarks in sequence).
*/
export function clearMilestones(): void {
milestones.clear();
firstMilestoneTime = null;
}
// Simple fetch type that matches the SDK's expected signature
type SimpleFetch = (
input: string | URL | Request,
init?: RequestInit,
) => Promise<Response>;
/**
* Create an instrumented fetch that logs timing for every request.
* Logs request start and end (with duration and status) to stderr.
*/
export function createTimingFetch(baseFetch: SimpleFetch): SimpleFetch {
return async (input, init) => {
const start = performance.now();
const startTime = formatTimestamp(new Date());
// Extract method and URL for logging
const url =
typeof input === "string"
? input
: input instanceof URL
? input.href
: input.url;
const method = init?.method || "GET";
// Parse path from URL, handling potential errors
let path: string;
try {
path = new URL(url).pathname;
} catch {
path = url;
}
logTiming(`${method} ${path} started at ${startTime}`);
try {
const response = await baseFetch(input, init);
const duration = performance.now() - start;
logTiming(
`${method} ${path} -> ${formatDuration(duration)} (status: ${response.status})`,
);
return response;
} catch (error) {
const duration = performance.now() - start;
logTiming(
`${method} ${path} -> FAILED after ${formatDuration(duration)}`,
);
throw error;
}
};
}