Replace Date.now() with performance.now() for duration measurements (#258)

* Replace Date.now() with performance.now() for duration measurements

- Import performance from node:perf_hooks in all affected files
- Update Timer and ThinkingTimer classes to use performance.now()
- Update activity tracking (markActivity, getIdleMs) to use performance.now()
- Update cache duration measurements to use performance.now()
- Update agent execution timing (cursor, opencode) to use performance.now()
- Update subprocess execution timing to use performance.now()
- Update API performance monitoring to use performance.now()
- Update prep phase timing to use performance.now()
- Update timer.test.ts to mock performance.now() instead of Date.now()

Benefits:
- Monotonic clock immune to system clock adjustments
- Higher precision (microsecond vs millisecond resolution)
- Purpose-built for performance measurement

Fixes #245

* fix lint.

* Round float durations to integers in logging

Preserve original behavior by rounding performance.now() float values
to integers when displaying/logging millisecond durations.

* fix lint.

---------

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
Co-authored-by: Robin Tail <robin_tail@me.com>
This commit is contained in:
pullfrog[bot]
2026-02-12 15:26:28 +00:00
committed by pullfrog[bot]
parent a442f766aa
commit b6e6a8976c
8 changed files with 125 additions and 111 deletions
+8 -7
View File
@@ -1,4 +1,5 @@
import { type ChildProcess, spawn as nodeSpawn } from "node:child_process";
import { performance } from "node:perf_hooks";
import { DEFAULT_ACTIVITY_CHECK_INTERVAL_MS, DEFAULT_ACTIVITY_TIMEOUT_MS } from "./activity.ts";
import { log } from "./cli.ts";
@@ -107,7 +108,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
installSignalHandlers();
const startTime = Date.now();
const startTime = performance.now();
let stdoutBuffer = "";
let stderrBuffer = "";
@@ -129,7 +130,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
let activityCheckIntervalId: NodeJS.Timeout | undefined;
let isTimedOut = false;
let isActivityTimedOut = false;
let lastActivityTime = Date.now();
let lastActivityTime = performance.now();
// overall timeout
if (timeout) {
@@ -148,7 +149,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
// activity timeout: kill if no output for too long
if (activityTimeoutMs > 0) {
activityCheckIntervalId = setInterval(() => {
const idleMs = Date.now() - lastActivityTime;
const idleMs = performance.now() - lastActivityTime;
if (idleMs > activityTimeoutMs) {
isActivityTimedOut = true;
const idleSec = Math.round(idleMs / 1000);
@@ -160,7 +161,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
}
function updateActivity(): void {
lastActivityTime = Date.now();
lastActivityTime = performance.now();
}
if (child.stdout) {
@@ -182,7 +183,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
}
child.on("close", (exitCode) => {
const durationMs = Date.now() - startTime;
const durationMs = performance.now() - startTime;
untrackChild(child);
if (timeoutId) clearTimeout(timeoutId);
@@ -194,7 +195,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
}
if (isActivityTimedOut) {
const idleSec = Math.round((Date.now() - lastActivityTime) / 1000);
const idleSec = Math.round((performance.now() - lastActivityTime) / 1000);
reject(new Error(`activity timeout: no output for ${idleSec}s`));
return;
}
@@ -208,7 +209,7 @@ export async function spawn(options: SpawnOptions): Promise<SpawnResult> {
});
child.on("error", (error) => {
const durationMs = Date.now() - startTime;
const durationMs = performance.now() - startTime;
untrackChild(child);
if (timeoutId) clearTimeout(timeoutId);