Files
pikasTech-unidesk/scripts/src/hwlab-node-web-observe-analyzer-sample-metrics-source.ts
T

596 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}
};
}
`;
}