|
|
|
@@ -893,6 +893,91 @@ async function samplePage(reason) {
|
|
|
|
|
return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
|
|
|
|
};
|
|
|
|
|
const textHashInput = (element) => trim(element.textContent || "", 800);
|
|
|
|
|
const loadingTextPattern = /加载中|Loading/iu;
|
|
|
|
|
const elementTextForLoading = (element) => [
|
|
|
|
|
element.textContent || "",
|
|
|
|
|
element.getAttribute("aria-label") || "",
|
|
|
|
|
element.getAttribute("title") || "",
|
|
|
|
|
element.getAttribute("data-testid") || ""
|
|
|
|
|
].map((value) => trim(value, 240)).filter(Boolean).join(" ");
|
|
|
|
|
const hasLoadingText = (element) => loadingTextPattern.test(elementTextForLoading(element));
|
|
|
|
|
const elementDescriptor = (element) => {
|
|
|
|
|
if (!element) return null;
|
|
|
|
|
const className = String(element.className || "").replace(/\s+/g, " ").trim().split(" ").slice(0, 6).join(" ");
|
|
|
|
|
return {
|
|
|
|
|
tag: element.tagName.toLowerCase(),
|
|
|
|
|
testId: element.getAttribute("data-testid") || null,
|
|
|
|
|
role: element.getAttribute("role") || null,
|
|
|
|
|
id: element.getAttribute("id") || null,
|
|
|
|
|
className: className || null,
|
|
|
|
|
status: element.getAttribute("data-status") || element.getAttribute("aria-busy") || null,
|
|
|
|
|
sessionId: element.getAttribute("data-session-id") || null,
|
|
|
|
|
messageId: element.getAttribute("data-message-id") || null,
|
|
|
|
|
traceId: element.getAttribute("data-trace-id") || null,
|
|
|
|
|
ariaLabel: element.getAttribute("aria-label") || null
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
const ownerKindFor = (element) => {
|
|
|
|
|
const value = [
|
|
|
|
|
element.getAttribute("data-testid") || "",
|
|
|
|
|
element.getAttribute("class") || "",
|
|
|
|
|
element.getAttribute("role") || "",
|
|
|
|
|
element.tagName || ""
|
|
|
|
|
].join(" ").toLowerCase();
|
|
|
|
|
if (/message|turn|agent|assistant/.test(value)) return "turn";
|
|
|
|
|
if (/session|rail|sidebar|nav/.test(value)) return "session-nav";
|
|
|
|
|
if (/composer|prompt|input|textarea/.test(value)) return "composer";
|
|
|
|
|
if (/diagnostic|alert|error|warning/.test(value)) return "diagnostic";
|
|
|
|
|
if (/trace|event|terminal|log/.test(value)) return "trace";
|
|
|
|
|
if (/performance|metric|chart|table/.test(value)) return "performance";
|
|
|
|
|
if (/main|workspace|workbench|root/.test(value)) return "workbench";
|
|
|
|
|
return "unknown";
|
|
|
|
|
};
|
|
|
|
|
const ownerLabelFor = (element) => {
|
|
|
|
|
const heading = element.querySelector("h1,h2,h3,h4,[data-testid*='title' i],[class*='title' i],[class*='header' i]");
|
|
|
|
|
return trim(
|
|
|
|
|
element.getAttribute("aria-label")
|
|
|
|
|
|| element.getAttribute("data-testid")
|
|
|
|
|
|| (heading ? heading.textContent || "" : "")
|
|
|
|
|
|| element.getAttribute("class")
|
|
|
|
|
|| element.tagName,
|
|
|
|
|
160
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
const ownerKeyFor = (element) => {
|
|
|
|
|
const descriptor = elementDescriptor(element) || {};
|
|
|
|
|
return [
|
|
|
|
|
ownerKindFor(element),
|
|
|
|
|
descriptor.testId || descriptor.id || descriptor.role || descriptor.className || descriptor.tag || "unknown",
|
|
|
|
|
descriptor.sessionId || descriptor.messageId || descriptor.traceId || ""
|
|
|
|
|
].filter(Boolean).join(":").slice(0, 240);
|
|
|
|
|
};
|
|
|
|
|
const collectLoadingNodes = () => {
|
|
|
|
|
const candidates = Array.from(document.querySelectorAll("body *"))
|
|
|
|
|
.filter(visible)
|
|
|
|
|
.filter(hasLoadingText)
|
|
|
|
|
.filter((element) => !Array.from(element.children).some((child) => visible(child) && hasLoadingText(child)))
|
|
|
|
|
.slice(-80);
|
|
|
|
|
return candidates.map((element, index) => {
|
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
|
const owner = element.closest('[data-message-id], [data-trace-id], article.message-card, .message-card, [data-testid*="message" i], [data-testid*="turn" i], [data-testid*="composer" i], [data-testid*="session" i], [class*="composer" i], [class*="session" i], [class*="trace" i], [class*="diagnostic" i], article, section, aside, main, form, [role="status"], [role="alert"], [role="article"], [role="navigation"]') || element;
|
|
|
|
|
const ownerDescriptor = elementDescriptor(owner);
|
|
|
|
|
return {
|
|
|
|
|
index,
|
|
|
|
|
tag: element.tagName.toLowerCase(),
|
|
|
|
|
className: String(element.className || "").slice(0, 180),
|
|
|
|
|
testId: element.getAttribute("data-testid"),
|
|
|
|
|
role: element.getAttribute("role"),
|
|
|
|
|
text: trim(elementTextForLoading(element), 240),
|
|
|
|
|
ownerKind: ownerKindFor(owner),
|
|
|
|
|
ownerKey: ownerKeyFor(owner),
|
|
|
|
|
ownerLabel: ownerLabelFor(owner),
|
|
|
|
|
owner: ownerDescriptor,
|
|
|
|
|
ownerText: trim(owner.textContent || "", 300),
|
|
|
|
|
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
const diagnosticSummaryText = (element) => {
|
|
|
|
|
const summarySelectors = [
|
|
|
|
|
".api-error-diagnostic-summary-text p",
|
|
|
|
@@ -950,6 +1035,7 @@ async function samplePage(reason) {
|
|
|
|
|
const diagnosticSelector = '.api-error-diagnostic, [class*="api-error-diagnostic" i], [class*="message-diagnostic" i], [class*="projection-diagnostic" i], [data-testid="api-error-diagnostic" i], [data-testid="error-diagnostic" i], [data-testid*="diagnostic" i], [role="alert"], [aria-live="assertive"]';
|
|
|
|
|
const messages = summarize(messageSelector, 20);
|
|
|
|
|
const traceRows = summarize(traceSelector, 30);
|
|
|
|
|
const loadings = collectLoadingNodes();
|
|
|
|
|
const diagnostics = Array.from(document.querySelectorAll(diagnosticSelector)).filter(visible).slice(-40).map((element, index) => {
|
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
|
const text = diagnosticSummaryText(element);
|
|
|
|
@@ -1013,6 +1099,7 @@ async function samplePage(reason) {
|
|
|
|
|
scroll: { x: Math.round(window.scrollX), y: Math.round(window.scrollY), height: Math.round(document.documentElement.scrollHeight), width: Math.round(document.documentElement.scrollWidth) },
|
|
|
|
|
messages,
|
|
|
|
|
traceRows,
|
|
|
|
|
loadings,
|
|
|
|
|
diagnostics,
|
|
|
|
|
turns,
|
|
|
|
|
pageProvenance: {
|
|
|
|
@@ -1069,11 +1156,12 @@ function digestDom(dom) {
|
|
|
|
|
if (dom && dom.error) return dom;
|
|
|
|
|
const messages = Array.isArray(dom.messages) ? dom.messages.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || "") })) : [];
|
|
|
|
|
const traceRows = Array.isArray(dom.traceRows) ? dom.traceRows.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || "") })) : [];
|
|
|
|
|
const loadings = Array.isArray(dom.loadings) ? dom.loadings.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || ""), ownerTextHash: sha256Text(item.ownerText || ""), ownerTextPreview: truncate(item.ownerText || "", 160) })) : [];
|
|
|
|
|
const diagnostics = Array.isArray(dom.diagnostics) ? dom.diagnostics.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 260), textBytes: Buffer.byteLength(item.text || "") })) : [];
|
|
|
|
|
const turns = Array.isArray(dom.turns) ? dom.turns.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 200), textBytes: Buffer.byteLength(item.text || "") })) : [];
|
|
|
|
|
const pageProvenance = normalizePageProvenance(dom.pageProvenance, { reason: "sample", pageLoadSeq: currentPageProvenance?.pageLoadSeq ?? pageLoadSeq });
|
|
|
|
|
currentPageProvenance = pageProvenance;
|
|
|
|
|
return { ...dom, messages, traceRows, diagnostics, turns, pageProvenance: compactPageProvenance(pageProvenance) };
|
|
|
|
|
return { ...dom, messages, traceRows, loadings, diagnostics, turns, pageProvenance: compactPageProvenance(pageProvenance) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function captureScreenshot(reason, imageType = "png") {
|
|
|
|
@@ -1308,8 +1396,9 @@ console.log(JSON.stringify({
|
|
|
|
|
readIssues: jsonlReadIssues.slice(0, 3).map((item) => ({ file: item.file, line: item.line ?? null, error: String(item.error ?? "").slice(0, 160) })),
|
|
|
|
|
sampleMetrics: {
|
|
|
|
|
...recentWindow.sampleMetrics.summary,
|
|
|
|
|
rounds: recentWindow.sampleMetrics.rounds.slice(-8).map((item) => ({ promptIndex: item.promptIndex, promptTextHash: item.promptTextHash, sampleCount: item.sampleCount, firstSeq: item.firstSeq, lastSeq: item.lastSeq, lastTotalElapsedSeconds: item.lastTotalElapsedSeconds, lastRecentUpdateSeconds: item.lastRecentUpdateSeconds, diagnosticSamples: item.diagnosticSamples, terminalSamples: item.terminalSamples, finalTextSamples: item.finalTextSamples, turnTimingTotalElapsedZeroResetCount: item.turnTimingTotalElapsedZeroResetCount, turnTimingTotalElapsedForwardJumpCount: item.turnTimingTotalElapsedForwardJumpCount, turnTimingTotalElapsedForwardJumpMaxSeconds: item.turnTimingTotalElapsedForwardJumpMaxSeconds, turnTimingRecentUpdateJumpCount: item.turnTimingRecentUpdateJumpCount, turnTimingRecentUpdateMaxIncreaseSeconds: item.turnTimingRecentUpdateMaxIncreaseSeconds })),
|
|
|
|
|
rounds: recentWindow.sampleMetrics.rounds.slice(-8).map((item) => ({ promptIndex: item.promptIndex, promptTextHash: item.promptTextHash, sampleCount: item.sampleCount, firstSeq: item.firstSeq, lastSeq: item.lastSeq, lastTotalElapsedSeconds: item.lastTotalElapsedSeconds, lastRecentUpdateSeconds: item.lastRecentUpdateSeconds, loadingSamples: item.loadingSamples, maxLoadingCount: item.maxLoadingCount, loadingOwnerCount: item.loadingOwnerCount, diagnosticSamples: item.diagnosticSamples, terminalSamples: item.terminalSamples, finalTextSamples: item.finalTextSamples, turnTimingTotalElapsedZeroResetCount: item.turnTimingTotalElapsedZeroResetCount, turnTimingTotalElapsedForwardJumpCount: item.turnTimingTotalElapsedForwardJumpCount, turnTimingTotalElapsedForwardJumpMaxSeconds: item.turnTimingTotalElapsedForwardJumpMaxSeconds, turnTimingRecentUpdateJumpCount: item.turnTimingRecentUpdateJumpCount, turnTimingRecentUpdateMaxIncreaseSeconds: item.turnTimingRecentUpdateMaxIncreaseSeconds })),
|
|
|
|
|
turnColumns: recentWindow.sampleMetrics.turnColumns.slice(-12).map((item) => ({ label: item.label, source: item.source, promptIndex: item.promptIndex, lastPromptIndex: item.lastPromptIndex, firstSeq: item.firstSeq, lastSeq: item.lastSeq, traceId: item.traceId, messageId: item.messageId })),
|
|
|
|
|
loading: compactLoadingMetricsForOutput(recentWindow.sampleMetrics.loading),
|
|
|
|
|
},
|
|
|
|
|
pageProvenance: recentWindow.pageProvenance.summary,
|
|
|
|
|
pagePerformance: recentWindow.pagePerformance.summary,
|
|
|
|
@@ -1472,6 +1561,7 @@ function compactSampleForAnalysis(sample) {
|
|
|
|
|
activeSessionId: sample.activeSessionId ?? null,
|
|
|
|
|
messages: compactDomItems(sample.messages),
|
|
|
|
|
traceRows: compactDomItems(sample.traceRows),
|
|
|
|
|
loadings: compactLoadingItems(sample.loadings),
|
|
|
|
|
turns: compactDomItems(sample.turns),
|
|
|
|
|
diagnostics: compactDomItems(sample.diagnostics),
|
|
|
|
|
pageProvenance: compactSamplePageProvenance(sample.pageProvenance),
|
|
|
|
@@ -1479,6 +1569,41 @@ function compactSampleForAnalysis(sample) {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function compactLoadingItems(items) {
|
|
|
|
|
if (!Array.isArray(items)) return [];
|
|
|
|
|
return items.map((item) => {
|
|
|
|
|
if (!item || typeof item !== "object") return item;
|
|
|
|
|
const rawText = String(item.text ?? item.textPreview ?? "");
|
|
|
|
|
return {
|
|
|
|
|
index: item.index ?? null,
|
|
|
|
|
tag: item.tag ?? null,
|
|
|
|
|
testId: item.testId ?? null,
|
|
|
|
|
role: item.role ?? null,
|
|
|
|
|
ownerKind: item.ownerKind ?? null,
|
|
|
|
|
ownerKey: item.ownerKey ?? null,
|
|
|
|
|
ownerLabel: item.ownerLabel ?? null,
|
|
|
|
|
owner: item.owner && typeof item.owner === "object" ? {
|
|
|
|
|
tag: item.owner.tag ?? null,
|
|
|
|
|
testId: item.owner.testId ?? null,
|
|
|
|
|
role: item.owner.role ?? null,
|
|
|
|
|
id: item.owner.id ?? null,
|
|
|
|
|
className: item.owner.className ?? null,
|
|
|
|
|
status: item.owner.status ?? null,
|
|
|
|
|
sessionId: item.owner.sessionId ?? null,
|
|
|
|
|
messageId: item.owner.messageId ?? null,
|
|
|
|
|
traceId: item.owner.traceId ?? null,
|
|
|
|
|
ariaLabel: item.owner.ariaLabel ?? null,
|
|
|
|
|
} : null,
|
|
|
|
|
text: limitText(rawText, 400),
|
|
|
|
|
textPreview: limitText(String(item.textPreview ?? rawText), 240),
|
|
|
|
|
textHash: item.textHash ?? sha256(rawText),
|
|
|
|
|
textBytes: item.textBytes ?? Buffer.byteLength(rawText),
|
|
|
|
|
ownerTextHash: item.ownerTextHash ?? null,
|
|
|
|
|
ownerTextPreview: item.ownerTextPreview ? limitText(item.ownerTextPreview, 240) : null,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function compactDomItems(items) {
|
|
|
|
|
if (!Array.isArray(items)) return [];
|
|
|
|
|
return items.map(compactDomItem);
|
|
|
|
@@ -1537,6 +1662,17 @@ function compactPerformanceItems(items) {
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function compactLoadingMetricsForOutput(value) {
|
|
|
|
|
if (!value || typeof value !== "object") return null;
|
|
|
|
|
return {
|
|
|
|
|
summary: value.summary ?? null,
|
|
|
|
|
longestSegments: Array.isArray(value.segments) ? value.segments.slice(0, 8) : [],
|
|
|
|
|
owners: Array.isArray(value.owners) ? value.owners.slice(0, 8) : [],
|
|
|
|
|
timeline: Array.isArray(value.timeline) ? value.timeline.slice(-12) : [],
|
|
|
|
|
valuesRedacted: true
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function compactSamplePageProvenance(value) {
|
|
|
|
|
if (!value || typeof value !== "object") return null;
|
|
|
|
|
return {
|
|
|
|
@@ -1599,6 +1735,9 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN
|
|
|
|
|
? sampleMetrics.turnTimingNonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump")
|
|
|
|
|
: [];
|
|
|
|
|
if (recentUpdateSawtoothJumps.length > 0) findings.push({ id: "turn-timing-recent-update-sawtooth-jump", severity: "amber", summary: "最近更新 value jumped faster than sample interval; expected sawtooth increase-or-reset", count: recentUpdateSawtoothJumps.length, samples: recentUpdateSawtoothJumps.slice(0, 20) });
|
|
|
|
|
const loadingSummary = sampleMetrics?.loading?.summary || {};
|
|
|
|
|
if (Number(loadingSummary.longestContinuousSeconds ?? 0) > 5) findings.push({ id: "page-loading-visible-over-5s", severity: "red", summary: "visible 加载中 stayed on screen longer than 5s; fix real loading latency instead of revealing incomplete content early", count: loadingSummary.overFiveSecondSegmentCount ?? 1, longestContinuousSeconds: loadingSummary.longestContinuousSeconds, segments: sampleMetrics.loading.segments.slice(0, 20), owners: sampleMetrics.loading.owners.slice(0, 20) });
|
|
|
|
|
if (Number(loadingSummary.maxSimultaneousCount ?? 0) > 1) findings.push({ id: "page-loading-concurrent", severity: "info", summary: "multiple 加载中 indicators were visible in the same sampled DOM point", count: loadingSummary.concurrentLoadingSampleCount ?? 0, maxSimultaneousCount: loadingSummary.maxSimultaneousCount, owners: sampleMetrics.loading.owners.slice(0, 20) });
|
|
|
|
|
if ((runtimeAlerts?.summary?.httpErrorCount ?? 0) > 0) findings.push({ id: "runtime-http-errors", severity: "amber", summary: "natural page requests returned HTTP error status during observation", count: runtimeAlerts.summary.httpErrorCount, groups: runtimeAlerts.networkHttpErrorsByPath.slice(0, 12) });
|
|
|
|
|
if ((runtimeAlerts?.summary?.requestFailedCount ?? 0) > 0) findings.push({ id: "runtime-requestfailed", severity: "amber", summary: "browser requestfailed events were captured during observation", count: runtimeAlerts.summary.requestFailedCount, groups: runtimeAlerts.networkRequestFailedByPath.slice(0, 12) });
|
|
|
|
|
if ((runtimeAlerts?.summary?.domDiagnosticSampleCount ?? 0) > 0) findings.push({ id: "runtime-dom-diagnostics", severity: "amber", summary: "diagnostic/error/warning-like text was visible in sampled DOM", count: runtimeAlerts.summary.domDiagnosticSampleCount, groupCount: runtimeAlerts.summary.domDiagnosticGroupCount ?? 0, groups: runtimeAlerts.domDiagnosticsByText.slice(0, 12), samples: runtimeAlerts.domDiagnostics.slice(0, 12) });
|
|
|
|
@@ -2562,6 +2701,8 @@ function buildSampleMetrics(samples, control) {
|
|
|
|
|
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,
|
|
|
|
@@ -2570,6 +2711,9 @@ function buildSampleMetrics(samples, control) {
|
|
|
|
|
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),
|
|
|
|
|
totalElapsedSeconds: totalElapsedValues.length > 0 ? Math.max(...totalElapsedValues) : null,
|
|
|
|
|
recentUpdateSeconds: recentUpdateValues.length > 0 ? Math.max(...recentUpdateValues) : null,
|
|
|
|
|
terminalSeen: terminalTexts.length > 0,
|
|
|
|
@@ -2605,6 +2749,7 @@ function buildSampleMetrics(samples, control) {
|
|
|
|
|
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 rounds = buildRoundMetricSummaries(timeline, promptCommands, {
|
|
|
|
|
nonMonotonic: turnTimingNonMonotonic,
|
|
|
|
|
elapsedZeroResets: turnTimingElapsedZeroResets,
|
|
|
|
@@ -2620,6 +2765,14 @@ function buildSampleMetrics(samples, control) {
|
|
|
|
|
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,
|
|
|
|
|
promptSegments: Math.max(0, promptTimes.length),
|
|
|
|
|
rounds: rounds.length,
|
|
|
|
|
turnColumns: turnTiming.columns.length,
|
|
|
|
@@ -2645,6 +2798,7 @@ function buildSampleMetrics(samples, control) {
|
|
|
|
|
roundsWithTerminalElapsedGrowth: rounds.filter((item) => item.turnTimingTerminalElapsedGrowthCount > 0).length,
|
|
|
|
|
roundsWithRecentUpdateJumps: rounds.filter((item) => item.turnTimingRecentUpdateJumpCount > 0).length
|
|
|
|
|
},
|
|
|
|
|
loading,
|
|
|
|
|
rounds,
|
|
|
|
|
turnColumns: turnTiming.columns,
|
|
|
|
|
turnTimingTable: turnTiming.rows,
|
|
|
|
@@ -2660,6 +2814,231 @@ function buildSampleMetrics(samples, control) {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 existing = groups.get(ownerKey) || {
|
|
|
|
|
ownerKey,
|
|
|
|
|
ownerKind: item?.ownerKind ?? "unknown",
|
|
|
|
|
ownerLabel: loadingOwnerLabel(item, ownerKey),
|
|
|
|
|
count: 0,
|
|
|
|
|
textHashes: []
|
|
|
|
|
};
|
|
|
|
|
existing.count += 1;
|
|
|
|
|
if (item?.textHash && !existing.textHashes.includes(item.textHash)) existing.textHashes.push(item.textHash);
|
|
|
|
|
groups.set(ownerKey, existing);
|
|
|
|
|
}
|
|
|
|
|
return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.ownerLabel).localeCompare(String(b.ownerLabel)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
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;
|
|
|
|
|
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,
|
|
|
|
|
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, 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,
|
|
|
|
|
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 15000;
|
|
|
|
|
const sorted = deltas.slice().sort((a, b) => a - b);
|
|
|
|
|
const median = sorted[Math.floor(sorted.length / 2)];
|
|
|
|
|
return Math.max(15000, 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 endAnchor = absentEvent?.ts || segment.lastAt;
|
|
|
|
|
const endMs = Date.parse(endAnchor || "");
|
|
|
|
|
const durationSeconds = Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs ? Number(((endMs - startMs) / 1000).toFixed(3)) : 0;
|
|
|
|
|
return {
|
|
|
|
|
firstAt: segment.firstAt,
|
|
|
|
|
lastAt: segment.lastAt,
|
|
|
|
|
endedAt: absentEvent?.ts ?? null,
|
|
|
|
|
firstSeq: segment.firstSeq,
|
|
|
|
|
lastSeq: segment.lastSeq,
|
|
|
|
|
endSeq: absentEvent?.seq ?? null,
|
|
|
|
|
durationSeconds,
|
|
|
|
|
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 buildTurnTimingTable(samples, timeline) {
|
|
|
|
|
const columns = [];
|
|
|
|
|
const registry = new Map();
|
|
|
|
@@ -3034,6 +3413,13 @@ function buildRoundMetricSummaries(timeline, promptCommands, timing = {}) {
|
|
|
|
|
const items = timeline.filter((item) => item.promptIndex === promptIndex);
|
|
|
|
|
const totalElapsed = items.map((item) => item.totalElapsedSeconds).filter((value) => value !== null);
|
|
|
|
|
const recentUpdate = items.map((item) => item.recentUpdateSeconds).filter((value) => value !== null);
|
|
|
|
|
const loadingCounts = items.map((item) => Number(item.loadingCount ?? 0)).filter(Number.isFinite);
|
|
|
|
|
const loadingOwners = new Set();
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
for (const owner of Array.isArray(item.loadingOwners) ? item.loadingOwners : []) {
|
|
|
|
|
if (owner?.ownerKey) loadingOwners.add(owner.ownerKey);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const timingAnomalies = nonMonotonic.filter((item) => item.promptIndex === promptIndex);
|
|
|
|
|
const timingForwardJumps = totalElapsedForwardJumps.filter((item) => item.promptIndex === promptIndex);
|
|
|
|
|
const timingZeroResets = elapsedZeroResets.filter((item) => item.promptIndex === promptIndex);
|
|
|
|
@@ -3056,6 +3442,9 @@ function buildRoundMetricSummaries(timeline, promptCommands, timing = {}) {
|
|
|
|
|
lastSampleAt: items[items.length - 1]?.ts ?? null,
|
|
|
|
|
withTotalElapsed: totalElapsed.length,
|
|
|
|
|
withRecentUpdate: recentUpdate.length,
|
|
|
|
|
loadingSamples: loadingCounts.filter((value) => value > 0).length,
|
|
|
|
|
maxLoadingCount: loadingCounts.length > 0 ? Math.max(...loadingCounts) : 0,
|
|
|
|
|
loadingOwnerCount: loadingOwners.size,
|
|
|
|
|
maxTotalElapsedSeconds: totalElapsed.length > 0 ? Math.max(...totalElapsed) : null,
|
|
|
|
|
lastTotalElapsedSeconds: lastNonNull(items.map((item) => item.totalElapsedSeconds)),
|
|
|
|
|
maxRecentUpdateSeconds: recentUpdate.length > 0 ? Math.max(...recentUpdate) : null,
|
|
|
|
@@ -3287,6 +3676,8 @@ function renderMarkdown(report) {
|
|
|
|
|
const commandLines = report.commandTimeline.length === 0 ? "- 无控制命令。" : report.commandTimeline.map((item) => "- " + item.ts + " " + item.phase + " " + item.type + " " + item.commandId + " " + (item.afterUrl || "")).join("\n");
|
|
|
|
|
const transitionLines = report.transitions.length === 0 ? "- 无状态变化。" : report.transitions.slice(0, 80).map((item) => "- #" + item.seq + " " + item.ts + " messages=" + item.messageCount + " traceRows=" + item.traceRowCount + " route=" + (item.routeSessionId || "-") + " active=" + (item.activeSessionId || "-")).join("\n");
|
|
|
|
|
const metricSummary = report.sampleMetrics?.summary || {};
|
|
|
|
|
const loading = report.sampleMetrics?.loading || {};
|
|
|
|
|
const loadingSummary = loading.summary || {};
|
|
|
|
|
const alertSummary = report.runtimeAlerts?.summary || {};
|
|
|
|
|
const httpAlertLines = Array.isArray(report.runtimeAlerts?.networkHttpErrorsByPath) && report.runtimeAlerts.networkHttpErrorsByPath.length > 0
|
|
|
|
|
? report.runtimeAlerts.networkHttpErrorsByPath.slice(0, 40).map((item) => "- HTTP " + (item.status ?? "-") + " " + item.method + " " + item.urlPath + " count=" + item.count + " prompts=" + (item.promptIndexes?.join(",") || "-") + " first=" + (item.firstAt || "-") + " last=" + (item.lastAt || "-")).join("\n")
|
|
|
|
@@ -3307,8 +3698,17 @@ function renderMarkdown(report) {
|
|
|
|
|
? report.promptNetwork.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " chatPostOk=" + String(item.chatPostOk) + " failure=" + (item.failureKind || "-") + " statuses=" + (Array.isArray(item.responseStatuses) && item.responseStatuses.length > 0 ? item.responseStatuses.join(",") : "-") + " firstChat=" + (item.firstChatEventAt || "-") + " lastChat=" + (item.lastChatEventAt || "-")).join("\n")
|
|
|
|
|
: "- 无 prompt 网络记录。";
|
|
|
|
|
const roundLines = Array.isArray(report.sampleMetrics?.rounds) && report.sampleMetrics.rounds.length > 0
|
|
|
|
|
? report.sampleMetrics.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " samples=" + item.sampleCount + " totalMax=" + (item.maxTotalElapsedSeconds ?? "-") + " totalLast=" + (item.lastTotalElapsedSeconds ?? "-") + " recentMax=" + (item.maxRecentUpdateSeconds ?? "-") + " recentLast=" + (item.lastRecentUpdateSeconds ?? "-") + " totalDecrease=" + (item.turnTimingTotalElapsedDecreaseCount ?? 0) + " totalForwardJump=" + (item.turnTimingTotalElapsedForwardJumpCount ?? 0) + " totalForwardJumpMax=" + (item.turnTimingTotalElapsedForwardJumpMaxSeconds ?? 0) + " terminalGrowth=" + (item.turnTimingTerminalElapsedGrowthCount ?? 0) + " terminalGrowthMax=" + (item.turnTimingTerminalElapsedGrowthMaxSeconds ?? 0) + " recentJump=" + (item.turnTimingRecentUpdateJumpCount ?? 0) + " recentSawtoothJump=" + (item.turnTimingRecentUpdateSawtoothJumpCount ?? item.turnTimingRecentUpdateJumpCount ?? 0) + " recentStep=" + (item.turnTimingRecentUpdateStepCount ?? 0) + " recentMaxIncrease=" + (item.turnTimingRecentUpdateMaxIncreaseSeconds ?? "-") + " recentMaxExcess=" + (item.turnTimingRecentUpdateMaxExcessSeconds ?? 0) + " recentReset=" + (item.turnTimingRecentUpdateResetCount ?? 0) + " diagnostics=" + item.diagnosticSamples + " terminal=" + item.terminalSamples + " finalText=" + item.finalTextSamples).join("\n")
|
|
|
|
|
? report.sampleMetrics.rounds.map((item) => "- round " + item.promptIndex + " promptHash=" + (item.promptTextHash || "-") + " samples=" + item.sampleCount + " loadingSamples=" + (item.loadingSamples ?? 0) + " maxLoading=" + (item.maxLoadingCount ?? 0) + " loadingOwners=" + (item.loadingOwnerCount ?? 0) + " totalMax=" + (item.maxTotalElapsedSeconds ?? "-") + " totalLast=" + (item.lastTotalElapsedSeconds ?? "-") + " recentMax=" + (item.maxRecentUpdateSeconds ?? "-") + " recentLast=" + (item.lastRecentUpdateSeconds ?? "-") + " totalDecrease=" + (item.turnTimingTotalElapsedDecreaseCount ?? 0) + " totalForwardJump=" + (item.turnTimingTotalElapsedForwardJumpCount ?? 0) + " totalForwardJumpMax=" + (item.turnTimingTotalElapsedForwardJumpMaxSeconds ?? 0) + " terminalGrowth=" + (item.turnTimingTerminalElapsedGrowthCount ?? 0) + " terminalGrowthMax=" + (item.turnTimingTerminalElapsedGrowthMaxSeconds ?? 0) + " recentJump=" + (item.turnTimingRecentUpdateJumpCount ?? 0) + " recentSawtoothJump=" + (item.turnTimingRecentUpdateSawtoothJumpCount ?? item.turnTimingRecentUpdateJumpCount ?? 0) + " recentStep=" + (item.turnTimingRecentUpdateStepCount ?? 0) + " recentMaxIncrease=" + (item.turnTimingRecentUpdateMaxIncreaseSeconds ?? "-") + " recentMaxExcess=" + (item.turnTimingRecentUpdateMaxExcessSeconds ?? 0) + " recentReset=" + (item.turnTimingRecentUpdateResetCount ?? 0) + " diagnostics=" + item.diagnosticSamples + " terminal=" + item.terminalSamples + " finalText=" + item.finalTextSamples).join("\n")
|
|
|
|
|
: "- 无轮次指标。";
|
|
|
|
|
const loadingSegmentLines = Array.isArray(loading.segments) && loading.segments.length > 0
|
|
|
|
|
? loading.segments.slice(0, 80).map((item) => "- duration=" + (item.durationSeconds ?? 0) + "s countMax=" + (item.maxCount ?? 0) + " owners=" + (item.ownerCount ?? 0) + " seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " ts=" + (item.firstAt || "-") + ".." + (item.lastAt || "-") + " endedAt=" + (item.endedAt || (item.ongoing ? "ongoing" : "-")) + " ownerLabels=" + ((Array.isArray(item.owners) ? item.owners : []).slice(0, 6).map((owner) => (owner.ownerKind || "-") + ":" + (owner.ownerLabel || "-") + "x" + (owner.count ?? 0)).join(",") || "-")).join("\n")
|
|
|
|
|
: "- 未观察到“加载中”可见区间。";
|
|
|
|
|
const loadingOwnerLines = Array.isArray(loading.owners) && loading.owners.length > 0
|
|
|
|
|
? loading.owners.slice(0, 80).map((item) => "- " + (item.ownerKind || "-") + " " + escapeMarkdownCell(item.ownerLabel || item.ownerKey || "-") + " samples=" + (item.sampleCount ?? 0) + " occurrences=" + (item.occurrenceCount ?? 0) + " maxCount=" + (item.maxSimultaneousCount ?? 0) + " longest=" + (item.longestContinuousSeconds ?? 0) + "s seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " prompts=" + (Array.isArray(item.promptIndexes) ? item.promptIndexes.join(",") : "-")).join("\n")
|
|
|
|
|
: "- 未观察到“加载中”归属。";
|
|
|
|
|
const loadingTimelineLines = Array.isArray(loading.timeline) && loading.timeline.length > 0
|
|
|
|
|
? loading.timeline.slice(0, 160).map((item) => "- #" + (item.seq ?? "-") + " " + (item.ts || "-") + " prompt=" + (item.promptIndex ?? "-") + " loadingCount=" + (item.loadingCount ?? 0) + " ownerCount=" + (item.ownerCount ?? 0) + " owners=" + ((Array.isArray(item.owners) ? item.owners : []).slice(0, 6).map((owner) => (owner.ownerKind || "-") + ":" + (owner.ownerLabel || "-") + "x" + (owner.count ?? 0)).join(",") || "-")).join("\n")
|
|
|
|
|
: "- 未观察到“加载中”采样点。";
|
|
|
|
|
const provenanceLines = Array.isArray(report.pageProvenance?.segments) && report.pageProvenance.segments.length > 0
|
|
|
|
|
? report.pageProvenance.segments.slice(0, 40).map((item) => "- fingerprint=" + (item.assetFingerprint || "-") + " samples=" + item.sampleCount + " seq=" + (item.firstSeq ?? "-") + ".." + (item.lastSeq ?? "-") + " ts=" + (item.firstAt || "-") + ".." + (item.lastAt || "-") + " scripts=" + (item.scriptCount ?? "-") + " styles=" + (item.stylesheetCount ?? "-") + " urlPaths=" + (Array.isArray(item.urlPaths) ? item.urlPaths.slice(0, 4).join(",") : "-")).join("\n")
|
|
|
|
|
: "- 无页面 provenance segment。";
|
|
|
|
@@ -3316,7 +3716,7 @@ function renderMarkdown(report) {
|
|
|
|
|
? report.pagePerformance.sameOriginApiByPath.slice(0, 80).map((item) => "- " + item.path + " kind=" + (item.routeKind || "same-origin-api") + " budgetMetric=" + (item.budgetMetric || "durationMs") + " samples=" + item.sampleCount + " p50=" + (item.p50Ms ?? "-") + "ms p75=" + (item.p75Ms ?? "-") + "ms p95=" + (item.p95Ms ?? "-") + "ms max=" + (item.maxMs ?? "-") + "ms >5s=" + (item.overFiveSecondCount ?? 0) + " streamOpenP95=" + (item.streamOpenP95Ms ?? "-") + "ms streamLifetime>5s=" + (item.streamLifetimeOverFiveSecondCount ?? 0) + " window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n")
|
|
|
|
|
: "- 无同源 API Resource Timing 样本。";
|
|
|
|
|
const metricLines = Array.isArray(report.sampleMetrics?.timeline) && report.sampleMetrics.timeline.length > 0
|
|
|
|
|
? report.sampleMetrics.timeline.slice(0, 120).map((item) => "- #" + item.seq + " " + item.ts + " prompt=" + item.promptIndex + " totalElapsedSeconds=" + (item.totalElapsedSeconds ?? "-") + " recentUpdateSeconds=" + (item.recentUpdateSeconds ?? "-") + " terminal=" + item.terminalSeen + " finalText=" + item.finalResultTextSeen + " diagnostic=" + item.diagnosticSeen).join("\n")
|
|
|
|
|
? report.sampleMetrics.timeline.slice(0, 120).map((item) => "- #" + item.seq + " " + item.ts + " prompt=" + item.promptIndex + " loadingCount=" + (item.loadingCount ?? 0) + " loadingOwners=" + (item.loadingOwnerCount ?? 0) + " totalElapsedSeconds=" + (item.totalElapsedSeconds ?? "-") + " recentUpdateSeconds=" + (item.recentUpdateSeconds ?? "-") + " terminal=" + item.terminalSeen + " finalText=" + item.finalResultTextSeen + " diagnostic=" + item.diagnosticSeen).join("\n")
|
|
|
|
|
: "- 无采样指标。";
|
|
|
|
|
const turnTimingTable = renderTurnTimingTable(report.sampleMetrics);
|
|
|
|
|
return "# web-probe observe analysis\n\n"
|
|
|
|
@@ -3333,6 +3733,13 @@ function renderMarkdown(report) {
|
|
|
|
|
+ "- withTotalElapsed: " + (metricSummary.withTotalElapsed ?? 0) + "\n"
|
|
|
|
|
+ "- withRecentUpdate: " + (metricSummary.withRecentUpdate ?? 0) + "\n"
|
|
|
|
|
+ "- diagnostics: " + (metricSummary.diagnostics ?? 0) + "\n"
|
|
|
|
|
+ "- loadingSamples: " + (metricSummary.loadingSampleCount ?? 0) + "\n"
|
|
|
|
|
+ "- loadingMaxCount: " + (metricSummary.loadingMaxCount ?? 0) + "\n"
|
|
|
|
|
+ "- loadingMaxOwnerCount: " + (metricSummary.loadingMaxOwnerCount ?? 0) + "\n"
|
|
|
|
|
+ "- loadingOwnerCount: " + (metricSummary.loadingOwnerCount ?? 0) + "\n"
|
|
|
|
|
+ "- loadingLongestContinuousSeconds: " + (metricSummary.loadingLongestContinuousSeconds ?? 0) + "\n"
|
|
|
|
|
+ "- loadingCurrentContinuousSeconds: " + (metricSummary.loadingCurrentContinuousSeconds ?? 0) + "\n"
|
|
|
|
|
+ "- loadingOverFiveSecondSegmentCount: " + (metricSummary.loadingOverFiveSecondSegmentCount ?? 0) + "\n"
|
|
|
|
|
+ "- promptSegments: " + (metricSummary.promptSegments ?? 0) + "\n\n"
|
|
|
|
|
+ "- turnColumns: " + (metricSummary.turnColumns ?? 0) + "\n"
|
|
|
|
|
+ "- turnTimingRows: " + (metricSummary.turnTimingRows ?? 0) + "\n"
|
|
|
|
@@ -3349,6 +3756,21 @@ function renderMarkdown(report) {
|
|
|
|
|
+ "- turnTimingRecentUpdateMaxExcessSeconds: " + (metricSummary.turnTimingRecentUpdateMaxExcessSeconds ?? 0) + "\n"
|
|
|
|
|
+ "- turnTimingRecentUpdateResetCount: " + (metricSummary.turnTimingRecentUpdateResetCount ?? 0) + "\n\n"
|
|
|
|
|
+ "### Rounds\n\n" + roundLines + "\n\n"
|
|
|
|
|
+ "### Loading visibility: visible 加载中\n\n"
|
|
|
|
|
+ "- sampleCount: " + (loadingSummary.sampleCount ?? 0) + "\n"
|
|
|
|
|
+ "- loadingSampleCount: " + (loadingSummary.loadingSampleCount ?? 0) + "\n"
|
|
|
|
|
+ "- maxSimultaneousCount: " + (loadingSummary.maxSimultaneousCount ?? 0) + "\n"
|
|
|
|
|
+ "- maxSimultaneousOwnerCount: " + (loadingSummary.maxSimultaneousOwnerCount ?? 0) + "\n"
|
|
|
|
|
+ "- concurrentLoadingSampleCount: " + (loadingSummary.concurrentLoadingSampleCount ?? 0) + "\n"
|
|
|
|
|
+ "- ownerCount: " + (loadingSummary.ownerCount ?? 0) + "\n"
|
|
|
|
|
+ "- segmentCount: " + (loadingSummary.segmentCount ?? 0) + "\n"
|
|
|
|
|
+ "- overFiveSecondSegmentCount: " + (loadingSummary.overFiveSecondSegmentCount ?? 0) + "\n"
|
|
|
|
|
+ "- longestContinuousSeconds: " + (loadingSummary.longestContinuousSeconds ?? 0) + "\n"
|
|
|
|
|
+ "- currentContinuousSeconds: " + (loadingSummary.currentContinuousSeconds ?? 0) + "\n"
|
|
|
|
|
+ "- policy: 该指标只能证明用户真实看到“加载中”的持续时间;修复必须降低真实请求/投影/渲染耗时,禁止提前展示未加载完内容来压低该指标。\n\n"
|
|
|
|
|
+ "#### Loading segments\n\n" + loadingSegmentLines + "\n\n"
|
|
|
|
|
+ "#### Loading owners\n\n" + loadingOwnerLines + "\n\n"
|
|
|
|
|
+ "#### Loading sample timeline\n\n" + loadingTimelineLines + "\n\n"
|
|
|
|
|
+ "### Page provenance\n\n"
|
|
|
|
|
+ "- segmentCount: " + (report.pageProvenance?.summary?.segmentCount ?? 0) + "\n"
|
|
|
|
|
+ "- controlSegmentCount: " + (report.pageProvenance?.summary?.controlSegmentCount ?? 0) + "\n\n"
|
|
|
|
|