feat: add web probe performance hotspot analysis

This commit is contained in:
Codex
2026-07-02 07:10:21 +00:00
parent bcaff08ad7
commit 88024dd251
30 changed files with 12674 additions and 11614 deletions
@@ -0,0 +1,595 @@
// SPEC: PJ2026-01040111 long-running Workbench observation.
// Responsibility: Analyzer sample metrics, loading continuity, session rail title, and bounded report rows source fragment.
export function nodeWebObserveAnalyzerSampleMetricsSource(): string {
return String.raw`function isTerminalTraceText(text) {
return /轮次完成|轮次失败|轮次取消|已记录|已完成第\d+轮|final response|sealed final response|turn completed|turn failed|turn canceled|terminal result|\bcompleted\b|\bfailed\b|\bcanceled\b|\bcancelled\b|\bterminal\b|\bdone\b/iu.test(String(text || ""));
}
function isFinalResultText(text) {
return /已完成第\d+轮|已按第\d+轮完成|final response|sealed final response|最终结果|已完成[:]|smoke\s*测试结果|benchmark|PVC\/workspace|修改文件|Results:/iu.test(String(text || ""));
}
function buildSampleMetrics(samples, control) {
const promptCommands = buildSendPromptCommandTimeline(control);
const promptTimes = promptCommands.map((item) => item.tsMs);
const timeline = samples.map((sample) => {
const texts = sampleTexts(sample);
const timingTexts = sampleTurnTimingTexts(sample);
const tsMs = Date.parse(sample.ts);
const promptIndex = Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0;
const totalElapsedValues = timingTexts.flatMap(parseTotalElapsedSeconds).filter(Number.isFinite);
const recentUpdateValues = timingTexts.flatMap(parseRecentUpdateSeconds).filter(Number.isFinite);
const diagnosticTexts = texts.filter(isDiagnosticText).slice(0, 5);
const terminalTexts = texts.filter(isTerminalTraceText).slice(0, 5);
const finalResultTexts = texts.filter(isFinalResultText).slice(0, 5);
const loadings = Array.isArray(sample.loadings) ? sample.loadings : [];
const loadingOwners = uniqueLoadingOwners(loadings);
return {
seq: sample.seq ?? null,
ts: sample.ts ?? null,
routeSessionId: sample.routeSessionId ?? null,
activeSessionId: sample.activeSessionId ?? null,
promptIndex,
messageCount: Array.isArray(sample.messages) ? sample.messages.length : 0,
traceRowCount: Array.isArray(sample.traceRows) ? sample.traceRows.length : 0,
loadingCount: loadings.length,
loadingOwnerCount: loadingOwners.length,
loadingOwners: loadingOwners.map((item) => ({ ownerKey: item.ownerKey, ownerKind: item.ownerKind, ownerLabel: item.ownerLabel, count: item.count })).slice(0, 12),
sessionRailVisibleCount: Number(sample?.sessionRail?.visibleCount ?? 0),
sessionRailFallbackTitleCount: Number(sample?.sessionRail?.fallbackTitleCount ?? 0),
sessionRailFallbackTitleRatio: Number(sample?.sessionRail?.fallbackTitleRatio ?? 0),
totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null,
recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null,
terminalSeen: terminalTexts.length > 0,
finalResultTextSeen: finalResultTexts.length > 0,
diagnosticSeen: diagnosticTexts.length > 0,
diagnosticTextHashes: diagnosticTexts.map(sha256).slice(0, 5),
textDigest: digestSample(sample)
};
});
const turnTiming = buildTurnTimingTable(samples, timeline);
const traceOrder = buildTraceOrderMetrics(samples, timeline);
const codeAgentCardTiming = buildCodeAgentCardTimingMetrics(samples, timeline, turnTiming);
const codeAgentCardDurationUnderreported = buildCodeAgentCardDurationUnderreportedMetrics(samples, timeline);
const codeAgentCardDurationMismatches = buildCodeAgentCardDurationMismatchMetrics(samples, timeline);
if (codeAgentCardTiming && codeAgentCardTiming.summary) {
codeAgentCardTiming.summary.durationUnderreportedCount = codeAgentCardDurationUnderreported.length;
codeAgentCardTiming.summary.durationMismatchCount = codeAgentCardDurationMismatches.length;
codeAgentCardTiming.durationUnderreported = codeAgentCardDurationUnderreported;
codeAgentCardTiming.durationMismatches = codeAgentCardDurationMismatches;
}
const turnCells = turnTiming.rows.flatMap((row) => Object.values(row.cells || {}));
const turnTimingNonMonotonic = Array.isArray(turnTiming.nonMonotonic) ? turnTiming.nonMonotonic : [];
const turnTimingElapsedZeroResets = Array.isArray(turnTiming.elapsedZeroResets) ? turnTiming.elapsedZeroResets : [];
const turnTimingTotalElapsedForwardJumps = Array.isArray(turnTiming.totalElapsedForwardJumps) ? turnTiming.totalElapsedForwardJumps : [];
const turnTimingRecentUpdateSawtoothJumps = turnTimingNonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump");
const turnTimingTerminalElapsedGrowth = Array.isArray(turnTiming.terminalElapsedGrowth) ? turnTiming.terminalElapsedGrowth : [];
const turnTimingRecentUpdateResets = Array.isArray(turnTiming.recentUpdateResets) ? turnTiming.recentUpdateResets : [];
const turnTimingRecentUpdateSteps = Array.isArray(turnTiming.recentUpdateSteps) ? turnTiming.recentUpdateSteps : [];
const turnTimingTerminalElapsedGrowthDeltas = turnTimingTerminalElapsedGrowth
.map((item) => Number(item.delta))
.filter((value) => Number.isFinite(value) && value > 0);
const turnTimingRecentUpdateLargestSteps = turnTimingRecentUpdateSteps
.filter((item) => Number.isFinite(Number(item.delta)))
.slice()
.sort((a, b) => Number(b.delta) - Number(a.delta))
.slice(0, 200);
const turnTimingRecentUpdatePositiveSteps = turnTimingRecentUpdateSteps
.map((item) => Number(item.delta))
.filter((value) => Number.isFinite(value) && value >= 0);
const turnTimingRecentUpdateExcessSteps = turnTimingRecentUpdateSteps
.map((item) => Number(item.excessiveIncreaseSeconds))
.filter((value) => Number.isFinite(value) && value > 0);
const withTotal = timeline.filter((item) => item.totalElapsedSeconds !== null).length;
const withRecent = timeline.filter((item) => item.recentUpdateSeconds !== null).length;
const diagnostics = timeline.filter((item) => item.diagnosticSeen).length;
const loading = buildLoadingMetrics(samples, timeline);
const sessionRailTitles = buildSessionRailTitleMetrics(samples, timeline);
const workbenchTurnStateTriad = buildWorkbenchTurnStateTriadMetrics(samples, timeline);
const reportTurnTimingRows = boundedTurnTimingRowsForReport(turnTiming.rows);
const reportTimeline = boundedRowsForReport(timeline);
const rounds = buildRoundMetricSummaries(timeline, promptCommands, {
columns: turnTiming.columns,
rows: turnTiming.rows,
nonMonotonic: turnTimingNonMonotonic,
elapsedZeroResets: turnTimingElapsedZeroResets,
totalElapsedForwardJumps: turnTimingTotalElapsedForwardJumps,
terminalElapsedGrowth: turnTimingTerminalElapsedGrowth,
recentUpdateResets: turnTimingRecentUpdateResets,
recentUpdateSteps: turnTimingRecentUpdateSteps
});
const recentUpdateJumpCount = turnTimingRecentUpdateSawtoothJumps.length;
return {
summary: {
sampleCount: timeline.length,
withTotalElapsed: withTotal,
withRecentUpdate: withRecent,
diagnostics,
loadingSampleCount: loading.summary.loadingSampleCount,
loadingMaxCount: loading.summary.maxSimultaneousCount,
loadingMaxOwnerCount: loading.summary.maxSimultaneousOwnerCount,
loadingOwnerCount: loading.summary.ownerCount,
loadingConcurrentSampleCount: loading.summary.concurrentLoadingSampleCount,
loadingLongestContinuousSeconds: loading.summary.longestContinuousSeconds,
loadingCurrentContinuousSeconds: loading.summary.currentContinuousSeconds,
loadingOverFiveSecondSegmentCount: loading.summary.overFiveSecondSegmentCount,
loadingOverBudgetSegmentCount: loading.summary.overBudgetSegmentCount,
sessionRailSampleCount: sessionRailTitles.summary.sampleCount,
sessionRailVisibleSampleCount: sessionRailTitles.summary.visibleSampleCount,
sessionRailFallbackMajoritySampleCount: sessionRailTitles.summary.majorityFallbackSampleCount,
sessionRailFallbackMaxRatio: sessionRailTitles.summary.maxFallbackRatio,
sessionRailFallbackMaxVisibleCount: sessionRailTitles.summary.maxVisibleCount,
sessionRailFallbackMaxCount: sessionRailTitles.summary.maxFallbackTitleCount,
workbenchTurnStateTriadRows: workbenchTurnStateTriad.summary.rowCount,
workbenchTurnStateTriadInvalidRows: workbenchTurnStateTriad.summary.invalidRowCount,
workbenchTurnStateTriadFullInvalidRows: workbenchTurnStateTriad.summary.invalidFullTriadCount,
workbenchTurnStateCardFinalResponseMismatchRows: workbenchTurnStateTriad.summary.cardFinalResponseMismatchCount,
workbenchTurnStateCollectorMissingRows: workbenchTurnStateTriad.summary.collectorMissingRowCount,
promptSegments: Math.max(0, promptTimes.length),
rounds: rounds.length,
turnColumns: turnTiming.columns.length,
turnTimingRows: turnTiming.rows.length,
turnCellsWithTotalElapsed: turnCells.filter((item) => item.totalElapsedSeconds !== null).length,
turnCellsWithRecentUpdate: turnCells.filter((item) => item.recentUpdateSeconds !== null).length,
turnTimingNonMonotonicCount: turnTimingNonMonotonic.length,
turnTimingTotalElapsedDecreaseCount: turnTimingNonMonotonic.filter((item) => item.metric === "totalElapsedSeconds").length,
turnTimingTotalElapsedZeroResetCount: turnTimingElapsedZeroResets.length,
turnTimingTotalElapsedForwardJumpCount: turnTimingTotalElapsedForwardJumps.length,
turnTimingTotalElapsedForwardJumpMaxSeconds: maxPositiveDelta(turnTimingTotalElapsedForwardJumps),
turnTimingTerminalElapsedGrowthCount: turnTimingTerminalElapsedGrowth.length,
turnTimingTerminalElapsedGrowthMaxSeconds: turnTimingTerminalElapsedGrowthDeltas.length > 0 ? Math.max(...turnTimingTerminalElapsedGrowthDeltas) : 0,
turnTimingRecentUpdateJumpCount: recentUpdateJumpCount,
turnTimingRecentUpdateSawtoothJumpCount: recentUpdateJumpCount,
turnTimingRecentUpdateStepCount: turnTimingRecentUpdateSteps.length,
turnTimingRecentUpdateMaxIncreaseSeconds: turnTimingRecentUpdatePositiveSteps.length > 0 ? Math.max(...turnTimingRecentUpdatePositiveSteps) : null,
turnTimingRecentUpdateMaxExcessSeconds: turnTimingRecentUpdateExcessSteps.length > 0 ? Math.max(...turnTimingRecentUpdateExcessSteps) : 0,
turnTimingRecentUpdateResetCount: turnTimingRecentUpdateResets.length,
turnTimingRecentUpdateDecreaseCount: turnTimingRecentUpdateResets.length,
codeAgentCardSampleCount: codeAgentCardTiming.summary.cardSampleCount,
codeAgentCardMissingElapsedCount: codeAgentCardTiming.summary.missingElapsedCount,
codeAgentCardMissingRecentUpdateCount: codeAgentCardTiming.summary.missingRecentUpdateCount,
roundCompletionEventCount: codeAgentCardTiming.summary.roundCompletionEventCount,
roundCompletionElapsedMismatchCount: codeAgentCardTiming.summary.roundCompletionElapsedMismatchCount,
roundCompletionFinalResponseMissingCount: codeAgentCardTiming.summary.roundCompletionFinalResponseMissingCount,
roundCompletionPostTimingChangeCount: codeAgentCardTiming.summary.roundCompletionPostTimingChangeCount,
codeAgentCardDurationUnderreportedCount: codeAgentCardTiming.summary.durationUnderreportedCount,
codeAgentCardDurationMismatchCount: codeAgentCardTiming.summary.durationMismatchCount,
traceRowCount: traceOrder.summary.traceRowCount,
traceRowOrderAnomalyCount: traceOrder.summary.orderAnomalyCount,
traceRowCompletionNotLastCount: traceOrder.summary.completionNotLastCount,
roundsWithTurnTimingNonMonotonic: rounds.filter((item) => item.turnTimingNonMonotonicCount > 0).length,
roundsWithTurnTimingTotalElapsedForwardJumps: rounds.filter((item) => item.turnTimingTotalElapsedForwardJumpCount > 0).length,
roundsWithTerminalElapsedGrowth: rounds.filter((item) => item.turnTimingTerminalElapsedGrowthCount > 0).length,
roundsWithRecentUpdateJumps: rounds.filter((item) => item.turnTimingRecentUpdateJumpCount > 0).length
},
loading,
sessionRailTitles,
workbenchTurnStateTriad,
codeAgentCardTiming,
traceOrder,
rounds,
turnColumns: turnTiming.columns,
turnTimingTable: reportTurnTimingRows.rows,
turnTimingTableDisclosure: reportTurnTimingRows.disclosure,
turnTimingNonMonotonic,
turnTimingElapsedZeroResets,
turnTimingTotalElapsedForwardJumps,
turnTimingTerminalElapsedGrowth,
turnTimingRecentUpdateSawtoothJumps,
turnTimingRecentUpdateSteps,
turnTimingRecentUpdateLargestSteps,
turnTimingRecentUpdateResets,
timeline: reportTimeline.rows,
timelineDisclosure: reportTimeline.disclosure
};
}
function boundedRowsForReport(rows) {
const sourceRows = Array.isArray(rows) ? rows : [];
const maxRows = 1200;
const headRows = 120;
if (sourceRows.length <= maxRows) return { rows: sourceRows, disclosure: { truncated: false, totalRows: sourceRows.length, includedRows: sourceRows.length, omittedRows: 0, headRows: sourceRows.length, tailRows: 0 } };
const tailRows = Math.max(0, maxRows - headRows);
return {
rows: [...sourceRows.slice(0, headRows), ...sourceRows.slice(-tailRows)],
disclosure: {
truncated: true,
totalRows: sourceRows.length,
includedRows: maxRows,
omittedRows: Math.max(0, sourceRows.length - maxRows),
headRows,
tailRows,
policy: "report-bounded-head-tail; summary metrics are computed before truncation",
valuesRedacted: true
}
};
}
function buildSendPromptCommandTimeline(control) {
const byCommand = new Map();
let ordinal = 0;
for (const item of Array.isArray(control) ? control : []) {
if (item?.type !== "sendPrompt") continue;
const commandId = item.commandId ? String(item.commandId) : "sendPrompt-" + String(ordinal++);
const existing = byCommand.get(commandId) || {
commandId,
startedAt: null,
startedTsMs: null,
completedAt: null,
completedTsMs: null,
failedAt: null,
failedTsMs: null,
textHash: null,
textBytes: null,
};
if (!existing.textHash && item.input?.textHash) existing.textHash = item.input.textHash;
if (!existing.textBytes && item.input?.textBytes) existing.textBytes = item.input.textBytes;
const tsMs = Date.parse(item.ts);
if (item.phase === "started" && Number.isFinite(tsMs)) {
existing.startedAt = item.ts ?? existing.startedAt;
existing.startedTsMs = tsMs;
} else if (item.phase === "completed" && Number.isFinite(tsMs)) {
existing.completedAt = item.ts ?? existing.completedAt;
existing.completedTsMs = tsMs;
} else if (item.phase === "failed" && Number.isFinite(tsMs)) {
existing.failedAt = item.ts ?? existing.failedAt;
existing.failedTsMs = tsMs;
}
byCommand.set(commandId, existing);
}
return Array.from(byCommand.values())
.map((item) => {
const tsMs = Number.isFinite(item.startedTsMs) ? item.startedTsMs : Number.isFinite(item.completedTsMs) ? item.completedTsMs : item.failedTsMs;
const ts = Number.isFinite(item.startedTsMs) ? item.startedAt : Number.isFinite(item.completedTsMs) ? item.completedAt : item.failedAt;
return { ...item, ts, tsMs };
})
.filter((item) => Number.isFinite(item.tsMs))
.sort((a, b) => a.tsMs - b.tsMs)
.map((item, index) => ({ ...item, promptIndex: index + 1 }));
}
function buildSessionRailTitleMetrics(samples, timeline) {
const rows = [];
const examplesByHash = new Map();
for (let index = 0; index < (Array.isArray(samples) ? samples : []).length; index += 1) {
const sample = samples[index];
const rail = sample?.sessionRail && typeof sample.sessionRail === "object" ? sample.sessionRail : null;
if (!rail) continue;
const visibleCount = Number(rail.visibleCount ?? 0);
const fallbackTitleCount = Number(rail.fallbackTitleCount ?? 0);
const safeVisibleCount = Number.isFinite(visibleCount) && visibleCount > 0 ? visibleCount : 0;
const safeFallbackTitleCount = Number.isFinite(fallbackTitleCount) && fallbackTitleCount > 0 ? fallbackTitleCount : 0;
const fallbackTitleRatio = safeVisibleCount > 0 ? Number((safeFallbackTitleCount / safeVisibleCount).toFixed(4)) : 0;
const fallbackItems = Array.isArray(rail.fallbackItems) ? rail.fallbackItems : [];
for (const item of fallbackItems) {
const hash = String(item?.titleHash || item?.titlePreview || item?.sessionIdPrefix || "").trim();
if (!hash || examplesByHash.has(hash)) continue;
examplesByHash.set(hash, {
titleHash: item?.titleHash ?? null,
titlePreview: limitText(String(item?.titlePreview || ""), 160),
sessionIdPrefix: item?.sessionIdPrefix ?? null,
active: item?.active === true,
firstSeq: sample?.seq ?? null,
firstAt: sample?.ts ?? null,
pageRole: sample?.pageRole ?? null,
});
}
rows.push({
...ref(sample),
promptIndex: timeline[index]?.promptIndex ?? 0,
visibleCount: safeVisibleCount,
fallbackTitleCount: safeFallbackTitleCount,
fallbackTitleRatio,
majorityFallback: safeVisibleCount > 0 && safeFallbackTitleCount > safeVisibleCount / 2,
overThreshold: safeVisibleCount > 0 && fallbackTitleRatio > alertThresholds.sessionRailFallbackRatio,
examples: fallbackItems.slice(0, 5).map((item) => ({
titleHash: item?.titleHash ?? null,
titlePreview: limitText(String(item?.titlePreview || ""), 160),
sessionIdPrefix: item?.sessionIdPrefix ?? null,
active: item?.active === true,
})),
});
}
const visibleRows = rows.filter((item) => item.visibleCount > 0);
const majorityRows = rows.filter((item) => item.majorityFallback);
const overThresholdRows = rows.filter((item) => item.overThreshold);
const fallbackRows = rows.filter((item) => item.fallbackTitleCount > 0);
const maxFallbackRatio = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.fallbackTitleRatio) || 0)) : 0;
const maxVisibleCount = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.visibleCount) || 0)) : 0;
const maxFallbackTitleCount = rows.length > 0 ? Math.max(...rows.map((item) => Number(item.fallbackTitleCount) || 0)) : 0;
return {
summary: {
sampleCount: rows.length,
visibleSampleCount: visibleRows.length,
fallbackSampleCount: fallbackRows.length,
majorityFallbackSampleCount: majorityRows.length,
overThresholdSampleCount: overThresholdRows.length,
thresholdRatio: alertThresholds.sessionRailFallbackRatio,
maxFallbackRatio,
maxVisibleCount,
maxFallbackTitleCount,
},
samples: majorityRows.slice(0, 80),
examples: Array.from(examplesByHash.values()).slice(0, 80),
timeline: rows.slice(-200),
valuesRedacted: true
};
}
function uniqueLoadingOwners(loadings) {
const groups = new Map();
for (let index = 0; index < (Array.isArray(loadings) ? loadings : []).length; index += 1) {
const item = loadings[index];
const ownerKey = loadingOwnerKey(item, index);
const ownerIdentity = loadingOwnerIdentity(item);
const existing = groups.get(ownerKey) || {
ownerKey,
ownerKind: item?.ownerKind ?? "unknown",
ownerLabel: loadingOwnerLabel(item, ownerKey),
...ownerIdentity,
count: 0,
textHashes: []
};
existing.count += 1;
if (item?.textHash && !existing.textHashes.includes(item.textHash)) existing.textHashes.push(item.textHash);
for (const key of ["ownerSessionId", "ownerMessageId", "ownerTraceId"]) {
if (!existing[key] && ownerIdentity[key]) existing[key] = ownerIdentity[key];
}
groups.set(ownerKey, existing);
}
return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.ownerLabel).localeCompare(String(b.ownerLabel)));
}
function loadingOwnerIdentity(item) {
const owner = item?.owner && typeof item.owner === "object" ? item.owner : {};
return {
ownerSessionId: owner.sessionId ?? null,
ownerMessageId: owner.messageId ?? null,
ownerTraceId: owner.traceId ?? null,
};
}
function loadingOwnerKey(item, index = 0) {
const key = String(item?.ownerKey || "").trim();
if (key) return key.slice(0, 240);
const owner = item?.owner && typeof item.owner === "object" ? item.owner : {};
return [
item?.ownerKind || "unknown",
owner.testId || item?.testId || owner.id || owner.role || owner.className || item?.role || item?.tag || "node",
owner.sessionId || owner.messageId || owner.traceId || item?.textHash || String(index)
].filter(Boolean).join(":").slice(0, 240);
}
function loadingOwnerLabel(item, fallback) {
return limitText(String(item?.ownerLabel || item?.owner?.ariaLabel || item?.owner?.testId || item?.owner?.className || fallback || "unknown"), 160);
}
function buildLoadingMetrics(samples, timeline) {
const events = samples.map((sample, index) => {
const tsMs = Date.parse(sample?.ts);
const loadings = Array.isArray(sample?.loadings) ? sample.loadings : [];
const owners = uniqueLoadingOwners(loadings);
return {
seq: sample?.seq ?? null,
ts: sample?.ts ?? null,
tsMs,
promptIndex: timeline[index]?.promptIndex ?? 0,
routeSessionId: sample?.routeSessionId ?? null,
activeSessionId: sample?.activeSessionId ?? null,
loadingCount: loadings.length,
ownerCount: owners.length,
owners,
ownerKeys: owners.map((item) => item.ownerKey),
ownerLabels: owners.map((item) => item.ownerLabel).slice(0, 8)
};
}).filter((item) => Number.isFinite(item.tsMs));
const continuityThresholdMs = loadingContinuityThresholdMs(events);
const segments = buildLoadingSegments(events, continuityThresholdMs, (event) => event.loadingCount, (event) => event.owners)
.sort((a, b) => Number(b.durationSeconds ?? 0) - Number(a.durationSeconds ?? 0) || Number(b.maxCount ?? 0) - Number(a.maxCount ?? 0));
const ownerMap = new Map();
for (const event of events) {
for (const owner of event.owners) {
const existing = ownerMap.get(owner.ownerKey) || {
ownerKey: owner.ownerKey,
ownerKind: owner.ownerKind,
ownerLabel: owner.ownerLabel,
ownerSessionId: owner.ownerSessionId ?? null,
ownerMessageId: owner.ownerMessageId ?? null,
ownerTraceId: owner.ownerTraceId ?? null,
sampleCount: 0,
occurrenceCount: 0,
maxSimultaneousCount: 0,
firstAt: event.ts,
lastAt: event.ts,
firstSeq: event.seq,
lastSeq: event.seq,
promptIndexes: new Set(),
events: []
};
existing.sampleCount += 1;
existing.occurrenceCount += owner.count;
existing.maxSimultaneousCount = Math.max(existing.maxSimultaneousCount, owner.count);
existing.lastAt = event.ts;
existing.lastSeq = event.seq;
for (const key of ["ownerSessionId", "ownerMessageId", "ownerTraceId"]) {
if (!existing[key] && owner[key]) existing[key] = owner[key];
}
if (Number.isFinite(Number(event.promptIndex))) existing.promptIndexes.add(Number(event.promptIndex));
existing.events.push({ ...event, loadingCount: owner.count, owners: [owner] });
ownerMap.set(owner.ownerKey, existing);
}
}
const owners = Array.from(ownerMap.values()).map((owner) => {
const ownerSegments = buildLoadingSegments(owner.events, continuityThresholdMs, (event) => event.loadingCount, (event) => event.owners);
const longest = ownerSegments.reduce((max, item) => Math.max(max, Number(item.durationSeconds ?? 0)), 0);
return {
ownerKey: owner.ownerKey,
ownerKind: owner.ownerKind,
ownerLabel: owner.ownerLabel,
ownerSessionId: owner.ownerSessionId ?? null,
ownerMessageId: owner.ownerMessageId ?? null,
ownerTraceId: owner.ownerTraceId ?? null,
sampleCount: owner.sampleCount,
occurrenceCount: owner.occurrenceCount,
maxSimultaneousCount: owner.maxSimultaneousCount,
longestContinuousSeconds: longest,
firstAt: owner.firstAt,
lastAt: owner.lastAt,
firstSeq: owner.firstSeq,
lastSeq: owner.lastSeq,
promptIndexes: Array.from(owner.promptIndexes).sort((a, b) => a - b),
segments: ownerSegments.sort((a, b) => Number(b.durationSeconds ?? 0) - Number(a.durationSeconds ?? 0)).slice(0, 8),
valuesRedacted: true
};
}).sort((a, b) => Number(b.longestContinuousSeconds ?? 0) - Number(a.longestContinuousSeconds ?? 0) || Number(b.occurrenceCount ?? 0) - Number(a.occurrenceCount ?? 0));
const latest = events[events.length - 1] || null;
const currentSegment = latest && latest.loadingCount > 0
? segments.find((segment) => segment.ongoing === true && segment.lastSeq === latest.seq) || null
: null;
const timelineRows = events
.filter((event, index) => event.loadingCount > 0 || (index > 0 && events[index - 1]?.loadingCount > 0))
.slice(0, 500)
.map((event) => ({
seq: event.seq,
ts: event.ts,
promptIndex: event.promptIndex,
loadingCount: event.loadingCount,
ownerCount: event.ownerCount,
owners: event.owners.map((owner) => ({ ownerKind: owner.ownerKind, ownerLabel: owner.ownerLabel, ownerSessionId: owner.ownerSessionId ?? null, ownerMessageId: owner.ownerMessageId ?? null, ownerTraceId: owner.ownerTraceId ?? null, count: owner.count })).slice(0, 8)
}));
return {
summary: {
sampleCount: events.length,
loadingSampleCount: events.filter((event) => event.loadingCount > 0).length,
maxSimultaneousCount: events.reduce((max, event) => Math.max(max, event.loadingCount), 0),
maxSimultaneousOwnerCount: events.reduce((max, event) => Math.max(max, event.ownerCount), 0),
concurrentLoadingSampleCount: events.filter((event) => event.loadingCount > 1).length,
ownerCount: owners.length,
segmentCount: segments.length,
overFiveSecondSegmentCount: segments.filter((segment) => Number(segment.durationSeconds ?? 0) > 5).length,
overBudgetSegmentCount: segments.filter((segment) => Number(segment.durationSeconds ?? 0) > alertThresholds.visibleLoadingSlowMs / 1000).length,
budgetSeconds: alertThresholds.visibleLoadingSlowMs / 1000,
longestContinuousSeconds: segments.length > 0 ? Number(segments[0].durationSeconds ?? 0) : 0,
currentContinuousSeconds: currentSegment ? Number(currentSegment.durationSeconds ?? 0) : 0,
continuityThresholdMs,
latestLoadingCount: latest?.loadingCount ?? 0,
latestOwnerCount: latest?.ownerCount ?? 0,
valuesRedacted: true
},
segments: segments.slice(0, 80),
owners: owners.slice(0, 80),
timeline: timelineRows,
valuesRedacted: true
};
}
function loadingContinuityThresholdMs(events) {
const deltas = [];
for (let index = 1; index < events.length; index += 1) {
const delta = events[index].tsMs - events[index - 1].tsMs;
if (Number.isFinite(delta) && delta > 0) deltas.push(delta);
}
if (deltas.length === 0) return 5000;
const sorted = deltas.slice().sort((a, b) => a - b);
const median = sorted[Math.floor(sorted.length / 2)];
return Math.min(15000, Math.max(1500, Math.round(median * 2.5)));
}
function buildLoadingSegments(events, continuityThresholdMs, countForEvent, ownersForEvent) {
const segments = [];
let segment = null;
let previousTsMs = null;
for (const event of events) {
const count = Number(countForEvent(event) ?? 0);
const gapOk = previousTsMs === null || !Number.isFinite(event.tsMs) || event.tsMs - previousTsMs <= continuityThresholdMs;
if (count > 0) {
if (!segment || !gapOk) {
if (segment) segments.push(finalizeLoadingSegment(segment, null));
segment = {
firstAt: event.ts,
lastAt: event.ts,
firstSeq: event.seq,
lastSeq: event.seq,
promptIndexes: new Set(),
ownerKeys: new Set(),
ownerLabels: new Map(),
sampleCount: 0,
maxCount: 0,
ongoing: true
};
}
segment.lastAt = event.ts;
segment.lastSeq = event.seq;
segment.sampleCount += 1;
segment.maxCount = Math.max(segment.maxCount, count);
if (Number.isFinite(Number(event.promptIndex))) segment.promptIndexes.add(Number(event.promptIndex));
for (const owner of ownersForEvent(event) || []) {
if (!owner?.ownerKey) continue;
segment.ownerKeys.add(owner.ownerKey);
if (!segment.ownerLabels.has(owner.ownerKey)) segment.ownerLabels.set(owner.ownerKey, { ownerKey: owner.ownerKey, ownerKind: owner.ownerKind, ownerLabel: owner.ownerLabel, count: 0 });
const label = segment.ownerLabels.get(owner.ownerKey);
label.count += owner.count ?? 1;
}
} else if (segment) {
segment.ongoing = false;
segment.endedAt = event.ts;
segment.endSeq = event.seq;
segments.push(finalizeLoadingSegment(segment, event));
segment = null;
}
previousTsMs = event.tsMs;
}
if (segment) segments.push(finalizeLoadingSegment(segment, null));
return segments;
}
function finalizeLoadingSegment(segment, absentEvent) {
const startMs = Date.parse(segment.firstAt || "");
const lastMs = Date.parse(segment.lastAt || "");
const absentMs = Date.parse(absentEvent?.ts || "");
const durationSeconds = Number.isFinite(startMs) && Number.isFinite(lastMs) && lastMs >= startMs ? Number(((lastMs - startMs) / 1000).toFixed(3)) : 0;
const upperBoundSeconds = Number.isFinite(startMs) && Number.isFinite(absentMs) && absentMs >= startMs ? Number(((absentMs - startMs) / 1000).toFixed(3)) : durationSeconds;
const endedGapSeconds = Number.isFinite(lastMs) && Number.isFinite(absentMs) && absentMs >= lastMs ? Number(((absentMs - lastMs) / 1000).toFixed(3)) : null;
return {
firstAt: segment.firstAt,
lastAt: segment.lastAt,
endedAt: absentEvent?.ts ?? null,
firstSeq: segment.firstSeq,
lastSeq: segment.lastSeq,
endSeq: absentEvent?.seq ?? null,
durationSeconds,
upperBoundSeconds,
endedGapSeconds,
sampleCount: segment.sampleCount,
maxCount: segment.maxCount,
ownerCount: segment.ownerKeys.size,
owners: Array.from(segment.ownerLabels.values()).sort((a, b) => b.count - a.count || String(a.ownerLabel).localeCompare(String(b.ownerLabel))).slice(0, 12),
promptIndexes: Array.from(segment.promptIndexes).sort((a, b) => a - b),
ongoing: absentEvent ? false : segment.ongoing === true,
valuesRedacted: true
};
}
function boundedTurnTimingRowsForReport(rows) {
const sourceRows = Array.isArray(rows) ? rows : [];
const maxRows = 1200;
const headRows = 120;
if (sourceRows.length <= maxRows) return { rows: sourceRows, disclosure: { truncated: false, totalRows: sourceRows.length, includedRows: sourceRows.length, omittedRows: 0, headRows: sourceRows.length, tailRows: 0 } };
const tailRows = Math.max(0, maxRows - headRows);
return {
rows: [...sourceRows.slice(0, headRows), ...sourceRows.slice(-tailRows)],
disclosure: {
truncated: true,
totalRows: sourceRows.length,
includedRows: maxRows,
omittedRows: Math.max(0, sourceRows.length - maxRows),
headRows,
tailRows,
policy: "report-bounded-head-tail; full anomaly counters are computed before truncation",
valuesRedacted: true
}
};
}
`;
}