Merge pull request #2358 from pikasTech/fix/2356-webperf-diagnostics-session-cost

fix(workbench): surface SSE flush diagnostics
This commit is contained in:
Lyon
2026-07-03 00:23:28 +08:00
committed by GitHub
4 changed files with 120 additions and 10 deletions
@@ -142,6 +142,56 @@ test("Workbench SSE flush diagnostics expose bounded queue cost fields", () => {
}
});
test("Workbench SSE flush diagnostics request prompt web-performance delivery", async () => {
resetWorkbenchPerformanceForTest();
const timers: { delay: number; handler: () => void }[] = [];
const posts: { url: string; body: string }[] = [];
const restoreBrowserRuntime = installBrowserProbeRuntime({
setTimeout: (handler, timeout) => {
timers.push({ delay: Number(timeout ?? 0), handler: () => invokeTimerHandler(handler) });
return timers.length;
},
clearTimeout: () => undefined,
fetch: (input, init) => {
posts.push({ url: String(input), body: String(init?.body ?? "") });
return Promise.resolve(new Response(null, { status: 204 }));
}
});
try {
recordWorkbenchRuntimeDiagnostic({
module: "workbench-events",
sessionId: "ses_secret",
traceId: "trc_secret",
outcome: "ok",
diagnostic: {
code: "workbench_sse_flush",
eventCount: 3,
deliveredCount: 2,
dropped: 1,
chunkCount: 2,
flushDurationMs: 9,
maxItemsPerChunk: 4,
maxChunkMs: 8
}
});
const priorityTimer = timers.find((timer) => timer.delay === 250);
assert.ok(priorityTimer);
priorityTimer.handler();
await Promise.resolve();
assert.equal(posts.length, 1);
assert.equal(posts[0]?.url, "/v1/web-performance");
assert.match(posts[0]?.body ?? "", /workbench_sse_flush/u);
assert.match(posts[0]?.body ?? "", /"eventCount":3/u);
assert.match(posts[0]?.body ?? "", /"maxChunkMs":8/u);
assert.equal(drainWorkbenchPerformanceEventsForTest().length, 0);
} finally {
resetWorkbenchPerformanceForTest();
restoreBrowserRuntime();
}
});
test("Workbench API timing enriches from ResourceTiming and clears the browser buffer", () => {
resetWorkbenchPerformanceForTest();
let clearCount = 0;
@@ -250,18 +300,19 @@ function traceOnlyAgentMessage(sessionId: string, traceId: string, events: Trace
};
}
function installBrowserProbeRuntime(options: { performance?: FakePerformance } = {}): () => void {
function installBrowserProbeRuntime(options: { performance?: FakePerformance; setTimeout?: BrowserSetTimeout; clearTimeout?: BrowserClearTimeout; fetch?: BrowserFetch } = {}): () => void {
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
const previousDocument = Object.getOwnPropertyDescriptor(globalThis, "document");
const previousPerformance = Object.getOwnPropertyDescriptor(globalThis, "performance");
const previousFetch = Object.getOwnPropertyDescriptor(globalThis, "fetch");
Object.defineProperty(globalThis, "window", {
configurable: true,
value: {
location: { href: "https://hwlab.example.test/workbench", pathname: "/workbench", hash: "" },
crypto: globalThis.crypto,
addEventListener: () => undefined,
setTimeout: globalThis.setTimeout,
clearTimeout: globalThis.clearTimeout
setTimeout: options.setTimeout ?? globalThis.setTimeout,
clearTimeout: options.clearTimeout ?? globalThis.clearTimeout
}
});
Object.defineProperty(globalThis, "document", {
@@ -277,13 +328,24 @@ function installBrowserProbeRuntime(options: { performance?: FakePerformance } =
value: options.performance
});
}
if (options.fetch) {
Object.defineProperty(globalThis, "fetch", {
configurable: true,
value: options.fetch
});
}
return () => {
restoreGlobalProperty("window", previousWindow);
restoreGlobalProperty("document", previousDocument);
restoreGlobalProperty("performance", previousPerformance);
restoreGlobalProperty("fetch", previousFetch);
};
}
type BrowserSetTimeout = (handler: TimerHandler, timeout?: number, ...arguments_: unknown[]) => number;
type BrowserClearTimeout = (handle?: number) => void;
type BrowserFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
interface FakePerformance {
timeOrigin: number;
now: () => number;
@@ -292,6 +354,14 @@ interface FakePerformance {
clearResourceTimings?: () => void;
}
function invokeTimerHandler(handler: TimerHandler): void {
if (typeof handler === "function") {
handler();
return;
}
throw new TypeError("string timer handlers are not supported by this test runtime");
}
function resourceTimingEntry(input: { name: string; startTime: number; duration: number; responseStart: number; responseEnd: number }): PerformanceResourceTiming {
return {
name: input.name,
@@ -310,7 +380,7 @@ function resourceTimingEntry(input: { name: string; startTime: number; duration:
} as unknown as PerformanceResourceTiming;
}
function restoreGlobalProperty(key: "window" | "document" | "performance", descriptor: PropertyDescriptor | undefined): void {
function restoreGlobalProperty(key: "window" | "document" | "performance" | "fetch", descriptor: PropertyDescriptor | undefined): void {
if (descriptor) {
Object.defineProperty(globalThis, key, descriptor);
return;
@@ -138,6 +138,7 @@ test("workbench active terminal paths seal final response from turn authority",
const realtimeTurnBlock = source.slice(source.indexOf("function applyRealtimeTurnSnapshot"), source.indexOf("async function refreshTerminalTraceFromRest"));
const terminalRestBlock = source.slice(source.indexOf("async function refreshTerminalTraceFromRest"), source.indexOf("function installRealtimeVisibilityHandler"));
const completeBlock = source.slice(source.indexOf("function completeTrace"), source.indexOf("async function hydrateTerminalMessageDiagnostics"));
const realtimeSessionDetailBlock = source.slice(source.indexOf("async function refreshRealtimeSessionFromRest"), source.indexOf("function completeTrace"));
const loadBlock = source.slice(source.indexOf("async function loadWorkbenchSession"), source.indexOf("async function sealRestoredActiveTurnMessages"));
const restoreSealBlock = source.slice(source.indexOf("async function sealRestoredActiveTurnMessages"), source.indexOf("function reattachRestoredActiveTrace"));
@@ -150,7 +151,12 @@ test("workbench active terminal paths seal final response from turn authority",
assert.match(terminalRestBlock, /traceProjectionIsTerminalSealed\(id, ownerMessagesBeforeProjection\)[\s\S]*return/u);
assert.match(completeBlock, /projectTurnAuthorityToMessages\(traceId, result, "complete-trace"\)/u);
assert.match(completeBlock, /traceProjectionIsTerminalSealed\(traceId, serverState\.value\.messagesBySessionId\[ownerSessionId\] \?\? \[\]\)[\s\S]*scheduleSessionListRefresh/u);
assert.match(realtimeSessionDetailBlock, /traceIdFromRealtimeRefreshReason\(reason\)/u);
assert.match(realtimeSessionDetailBlock, /traceTerminalBodyIsVisible\(traceId, id\)[\s\S]*terminal_low_priority_session_detail_skip[\s\S]*return/u);
assert.match(realtimeSessionDetailBlock, /traceTerminalBodyIsVisible\(traceId, id\)[\s\S]*terminal_low_priority_session_detail_apply_skip[\s\S]*return/u);
assert.match(loadBlock, /const messageLimit = sessionMessageProjectionWindowLimit\(\);/u);
assert.match(loadBlock, /sessionFromWorkbenchSession\(detail\.data\?\.session, \{ includeMessages: false \}\)/u);
assert.match(loadBlock, /const fallbackMessages = seed\?\.sessionId === id \? seed\.messages \?\? \[\] : \[\];/u);
assert.doesNotMatch(loadBlock, /limit:\s*100/u);
assert.doesNotMatch(restoreSealBlock, /Promise\.all/u);
assert.doesNotMatch(restoreSealBlock, /force:\s*true/u);
+23 -6
View File
@@ -1353,10 +1353,21 @@ export const useWorkbenchStore = defineStore("workbench", () => {
async function refreshRealtimeSessionFromRest(sessionId: string, reason: string): Promise<void> {
const id = normalizeWorkbenchSessionId(sessionId);
if (!id || id !== activeSessionId.value) return;
const traceId = traceIdFromRealtimeRefreshReason(reason);
if (traceTerminalBodyIsVisible(traceId, id)) {
recordWorkbenchRuntimeDiagnostic({ module: "workbench-terminal-priority", sessionId: id, traceId, outcome: "ok", diagnostic: { code: "terminal_low_priority_session_detail_skip", reason, source: "realtime-session-detail", valuesRedacted: true } });
return;
}
recordActivity(reason);
const existing = sessions.value.find((item) => item.sessionId === id) ?? null;
const selected = await loadWorkbenchSession(id, existing);
if (selected && activeSessionId.value === id) applySelectedSessionDetail(selected, "system");
if (selected && activeSessionId.value === id) {
if (traceTerminalBodyIsVisible(traceId, id)) {
recordWorkbenchRuntimeDiagnostic({ module: "workbench-terminal-priority", sessionId: id, traceId, outcome: "ok", diagnostic: { code: "terminal_low_priority_session_detail_apply_skip", reason, source: "realtime-session-detail", valuesRedacted: true } });
return;
}
applySelectedSessionDetail(selected, "system");
}
}
function completeTrace(traceId: string, result: AgentChatResultResponse, options: { forceRead?: boolean } = {}): void {
@@ -1695,14 +1706,15 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const eagerMessages = normalizedRequestId ? workbenchColadaQueries.fetchSessionMessages(normalizedRequestId, { limit: messageLimit, force: true, minIntervalMs: runtimePolicy.workbenchSessionMessagesMinRefreshMs }) : null;
const detail = await workbenchColadaQueries.fetchSession(requestId, { force: true, minIntervalMs: runtimePolicy.workbenchSessionDetailMinRefreshMs });
if (!detail.ok) return null;
const detailSession = sessionFromWorkbenchSession(detail.data?.session);
const detailSession = sessionFromWorkbenchSession(detail.data?.session, { includeMessages: false });
const id = detailSession?.sessionId ?? normalizedRequestId ?? seed?.sessionId;
if (!id) return null;
const messages = eagerMessages && id === normalizedRequestId ? await eagerMessages : await workbenchColadaQueries.fetchSessionMessages(id, { limit: messageLimit, force: true, minIntervalMs: runtimePolicy.workbenchSessionMessagesMinRefreshMs });
const base = detailSession ?? seed;
const fallbackMessages = seed?.sessionId === id ? seed.messages ?? [] : [];
const base = detailSession ? { ...detailSession, messages: fallbackMessages } : seed;
if (!base) return null;
const page = messages.ok ? messages.data : null;
const pageMessages = Array.isArray(page?.messages) ? await sealRestoredActiveTurnMessages(page.messages.map((message) => normalizeChatMessage(message as ChatMessage))) : base.messages;
const pageMessages = Array.isArray(page?.messages) ? await sealRestoredActiveTurnMessages(page.messages.map((message) => normalizeChatMessage(message as ChatMessage))) : fallbackMessages;
return { ...base, sessionId: id, messages: pageMessages, messageCount: page?.total ?? pageMessages?.length ?? base.messageCount };
}
@@ -1744,7 +1756,7 @@ function workbenchSessionsFromPayload(payload: unknown): WorkbenchSessionRecord[
return sessions.map((item) => sessionFromWorkbenchSession(item)).filter((item): item is WorkbenchSessionRecord => Boolean(item));
}
function sessionFromWorkbenchSession(value: unknown): WorkbenchSessionRecord | null {
function sessionFromWorkbenchSession(value: unknown, options: { includeMessages?: boolean } = {}): WorkbenchSessionRecord | null {
const record = recordValue(value);
const sessionId = normalizeWorkbenchSessionId(record?.sessionId ?? record?.id);
if (!sessionId) return null;
@@ -1758,10 +1770,15 @@ function sessionFromWorkbenchSession(value: unknown): WorkbenchSessionRecord | n
lastTraceId: firstNonEmptyString(record?.lastTraceId),
messageCount: firstFiniteNumber(record?.messageCount) ?? undefined,
firstUserMessagePreview: firstNonEmptyString(record?.firstUserMessagePreview),
messages: Array.isArray(record?.messages) ? record.messages.map((message) => normalizeChatMessage(message as ChatMessage)) : undefined
messages: options.includeMessages === false ? undefined : Array.isArray(record?.messages) ? record.messages.map((message) => normalizeChatMessage(message as ChatMessage)) : undefined
};
}
function traceIdFromRealtimeRefreshReason(reason: string): string | null {
const match = /(?:^|[:\s])((?:trc|trace)[_-][A-Za-z0-9_-]+)/u.exec(reason);
return firstNonEmptyString(match?.[1]);
}
function routeSelectedSessionRecord(sessionId: string | null | undefined, loadingSessionId: string | null | undefined, errorText: string | null | undefined, projected: WorkbenchSessionRecord | null): WorkbenchSessionRecord | null {
if (projected) return null;
const id = normalizeWorkbenchSessionId(sessionId);
@@ -146,6 +146,7 @@ interface LoadingStateRecord {
const SCHEMA_VERSION = "hwlab-web-performance-v2";
const FLUSH_INTERVAL_MS = 10_000;
const PRIORITY_FLUSH_DELAY_MS = 250;
const FLUSH_BATCH_THRESHOLD = 120;
const MAX_QUEUE = 300;
const submitJourneys = new Map<string, SubmitJourneyState>();
@@ -157,6 +158,7 @@ let queue: WorkbenchPerformanceEvent[] = [];
let installed = false;
let resourceTimingBufferConfigured = false;
let flushTimer: number | null = null;
let priorityFlushTimer: number | null = null;
let uiTrace: WorkbenchUiTrace | null = null;
export function currentWorkbenchUiTrace(): WorkbenchUiTrace | null {
@@ -388,6 +390,7 @@ export function recordWorkbenchRuntimeDiagnostic(input: { module?: string | null
maxItemsPerChunk: diagnosticNumber(diagnostic, "maxItemsPerChunk"),
maxChunkMs: diagnosticNumber(diagnostic, "maxChunkMs")
});
if (isSseFlush) schedulePriorityFlush();
}
export function startWorkbenchOpenJourney(input: { route?: string; cache?: "warm" | "cold" | "unknown"; authState?: "warm" | "login_redirect" | "unknown" } = {}): void {
@@ -578,6 +581,8 @@ export function resetWorkbenchPerformanceForTest(): void {
uiTrace = null;
if (flushTimer !== null && hasBrowserTimerRuntime()) window.clearTimeout(flushTimer);
flushTimer = null;
if (priorityFlushTimer !== null && hasBrowserTimerRuntime()) window.clearTimeout(priorityFlushTimer);
priorityFlushTimer = null;
installed = false;
resourceTimingBufferConfigured = false;
}
@@ -617,11 +622,23 @@ function scheduleFlush(): void {
flushTimer = window.setTimeout(() => flushWorkbenchPerformance(false), FLUSH_INTERVAL_MS);
}
function schedulePriorityFlush(): void {
if (!hasBrowserTimerRuntime() || priorityFlushTimer !== null) return;
priorityFlushTimer = window.setTimeout(() => {
priorityFlushTimer = null;
flushWorkbenchPerformance(false);
}, PRIORITY_FLUSH_DELAY_MS);
}
function flushWorkbenchPerformance(useBeacon: boolean): void {
if (flushTimer !== null && hasBrowserTimerRuntime()) {
window.clearTimeout(flushTimer);
flushTimer = null;
}
if (priorityFlushTimer !== null && hasBrowserTimerRuntime()) {
window.clearTimeout(priorityFlushTimer);
priorityFlushTimer = null;
}
if (!queue.length || typeof window === "undefined") return;
const events = queue.splice(0, queue.length).map(enrichWorkbenchEventForFlush);
const body = JSON.stringify({ schemaVersion: SCHEMA_VERSION, serviceId: "hwlab-cloud-web", page: pageRoute(), events });