Files
fusion/packages/dashboard/app/utils/taskTiming.ts
Fusion b9b6d06cad feat(FN-3622): add canonical task-detail timing calculations and stats pane
The merge lands three commits for FN-3622's canonical task timing calculations, adding `taskTiming.ts` logic and stats timing semantics to `TaskDetailModal` and `TaskTokenStatsPanel`, backed by regression tests across those panels and the root `test-changed.mjs` script. The remaining commits introdu

Fusion-Task-Id: FN-3622
2026-05-06 19:55:55 -07:00

95 lines
3.0 KiB
TypeScript

import type { TaskLogEntry, WorkflowStepResult } from "@fusion/core";
export interface TimingEvent {
timestamp: string;
durationMs?: number;
summary: string;
}
function summarizeTimingLabel(entry: TaskLogEntry): string {
const timingText = entry.action || entry.outcome || "";
const stripped = timingText
.replace(/^\[timing\]\s*/i, "")
.replace(/^\[[^\]]+\]\s*/i, "")
.replace(/\s+in\s+\d+(?:\.\d+)?ms\b/i, "")
.replace(/\s+after\s+\d+(?:\.\d+)?ms\b/i, "")
.trim();
return stripped || "Timing event";
}
export function extractTimingEvents(logEntries: TaskLogEntry[]): TimingEvent[] {
return logEntries
.filter((entry) => {
const actionText = typeof entry.action === "string" ? entry.action : "";
const outcomeText = typeof entry.outcome === "string" ? entry.outcome : "";
return actionText.includes("[timing]") || outcomeText.includes("[timing]");
})
.map((entry) => {
const haystack = `${entry.action ?? ""}\n${entry.outcome ?? ""}`;
const durationMatch = haystack.match(/(\d+(?:\.\d+)?)ms\b/i);
const durationMs = durationMatch ? Number(durationMatch[1]) : undefined;
return {
timestamp: entry.timestamp,
durationMs: Number.isFinite(durationMs) ? durationMs : undefined,
summary: summarizeTimingLabel(entry),
};
});
}
export function getTimedDurationMs(logEntries: TaskLogEntry[] | undefined): number | null {
if (!logEntries || logEntries.length === 0) return null;
let total = 0;
let counted = 0;
for (const event of extractTimingEvents(logEntries)) {
if (typeof event.durationMs !== "number") continue;
total += event.durationMs;
counted += 1;
}
return counted > 0 ? total : null;
}
export function parseTimestampToMs(value?: string): number | null {
if (!value) return null;
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : null;
}
export function getWorkflowRuntimeMs(results: WorkflowStepResult[] | undefined, nowMs: number): number | null {
if (!results || results.length === 0) return null;
let total = 0;
let counted = 0;
for (const step of results) {
if (!step.startedAt) continue;
const startedMs = parseTimestampToMs(step.startedAt);
if (startedMs == null) continue;
let endMs: number;
if (step.completedAt) {
const completedMs = parseTimestampToMs(step.completedAt);
if (completedMs == null || completedMs < startedMs) continue;
endMs = completedMs;
} else {
endMs = Math.max(startedMs, nowMs);
}
total += endMs - startedMs;
counted += 1;
}
return counted > 0 ? total : null;
}
export function getEndToEndDurationMs(
executionStartedAt: string | undefined,
executionCompletedAt: string | undefined,
nowMs: number,
): number | null {
const startedMs = parseTimestampToMs(executionStartedAt);
if (startedMs == null) return null;
const completedMs = parseTimestampToMs(executionCompletedAt);
const endMs = completedMs != null && completedMs >= startedMs ? completedMs : nowMs;
return Math.max(0, endMs - startedMs);
}