Merge pull request #1424 from pikasTech/fix/1422-workbench-performance
修正 Workbench 首可见性能指标口径
This commit is contained in:
@@ -80,6 +80,18 @@ test("web performance store accepts v2 Workbench journey and phase metrics witho
|
||||
valueMs: 42,
|
||||
commandId: "cmd_secret"
|
||||
},
|
||||
{
|
||||
kind: "workbench_journey",
|
||||
journey: "submit_to_failure",
|
||||
route: "/workbench/sessions/ses_secret",
|
||||
entry: "new",
|
||||
backend: "agentrun-v01/codex",
|
||||
transport: "sse",
|
||||
visibility: "foreground",
|
||||
outcome: "network",
|
||||
valueMs: 530,
|
||||
traceId: "trc_secret"
|
||||
},
|
||||
{
|
||||
kind: "workbench_backend_event_visible",
|
||||
eventType: "tool",
|
||||
@@ -93,8 +105,9 @@ test("web performance store accepts v2 Workbench journey and phase metrics witho
|
||||
} as Record<string, unknown>);
|
||||
const text = store.metricsText();
|
||||
|
||||
assert.deepEqual(result, { accepted: 3, dropped: 0, received: 3 });
|
||||
assert.deepEqual(result, { accepted: 4, dropped: 0, received: 4 });
|
||||
assert.match(text, /hwlab_workbench_journey_total\{[^}]*journey="submit_to_first_visible"[^}]*route="\/workbench\/sessions\/:id"[^}]*backend="agentrun-v01\/codex"/u);
|
||||
assert.match(text, /hwlab_workbench_journey_total\{[^}]*journey="submit_to_failure"[^}]*outcome="network"/u);
|
||||
assert.match(text, /hwlab_workbench_event_phase_duration_seconds_bucket\{[^}]*phase="created_to_append"[^}]*event_type="assistant"[^}]*le="0\.05"\} 1/u);
|
||||
assert.match(text, /hwlab_workbench_backend_event_visible_latency_seconds_bucket\{[^}]*event_type="tool_call"[^}]*backend="agentrun-v01\/codex"[^}]*le="1"\} 1/u);
|
||||
assert.doesNotMatch(text, /trc_secret|ses_secret|run_secret|cmd_secret|cnv_secret|traceId|sessionId|runId|commandId|conversationId|prompt|api key/iu);
|
||||
|
||||
@@ -20,6 +20,7 @@ const DURATION_METRICS = new Set([
|
||||
const WORKBENCH_SCHEMA_VERSION = "hwlab-web-performance-v2";
|
||||
const WORKBENCH_JOURNEYS = new Set([
|
||||
"submit_to_first_visible",
|
||||
"submit_to_failure",
|
||||
"backend_event_to_visible",
|
||||
"session_switch_first_visible",
|
||||
"session_switch_full_load",
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { ChatMessage, TraceEvent } from "../src/types/index.ts";
|
||||
import {
|
||||
acknowledgeWorkbenchVisible,
|
||||
drainWorkbenchPerformanceEventsForTest,
|
||||
failWorkbenchSubmitJourney,
|
||||
finishWorkbenchOpenFullLoad,
|
||||
finishWorkbenchSessionSwitchFullLoad,
|
||||
markWorkbenchSubmitApiAccepted,
|
||||
@@ -53,6 +54,37 @@ test("Workbench performance probe records journeys and strips high-cardinality i
|
||||
assert.doesNotMatch(payload, /trc_secret|cnv_secret|sessionId|conversationId|runId|commandId/u);
|
||||
});
|
||||
|
||||
test("Workbench submit failures do not pollute first visible output SLI", () => {
|
||||
resetWorkbenchPerformanceForTest();
|
||||
startWorkbenchSubmitJourney({ traceId: "trc_network", conversationId: "cnv_secret", entry: "new", backend: "codex", transport: "sse" });
|
||||
failWorkbenchSubmitJourney("trc_network", "network");
|
||||
|
||||
const events = drainWorkbenchPerformanceEventsForTest();
|
||||
assert.equal(events.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_first_visible"), false);
|
||||
assert.ok(events.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_failure" && event.outcome === "network"));
|
||||
});
|
||||
|
||||
test("Workbench submit first visible waits for assistant or tool output", () => {
|
||||
resetWorkbenchPerformanceForTest();
|
||||
const wallBase = Date.now();
|
||||
const createdAt = new Date(wallBase - 12_000).toISOString();
|
||||
const appendedAt = new Date(wallBase - 11_500).toISOString();
|
||||
const backendEvent = { label: "agentrun:run:createdAgentRun", backend: "agentrun-v01/codex", createdAt, appendedAt, message: "AgentRun created." } as TraceEvent;
|
||||
startWorkbenchSubmitJourney({ traceId: "trc_secret", conversationId: "cnv_secret", entry: "existing", backend: "codex", transport: "sse" });
|
||||
markWorkbenchTraceEventsReceived({ traceId: "trc_secret", transport: "rest_gap", events: [backendEvent] });
|
||||
markWorkbenchTraceProjected("trc_secret");
|
||||
acknowledgeWorkbenchVisible({ messages: [traceOnlyAgentMessage("cnv_secret", "trc_secret", [backendEvent])], activeConversationId: "cnv_secret", detailLoading: false });
|
||||
|
||||
const backendOnlyEvents = drainWorkbenchPerformanceEventsForTest();
|
||||
assert.equal(backendOnlyEvents.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_first_visible"), false);
|
||||
assert.equal(backendOnlyEvents.some((event) => event.kind === "workbench_event_phase" && event.phase === "sse_to_receive"), false);
|
||||
assert.ok(backendOnlyEvents.some((event) => event.kind === "workbench_backend_event_visible" && event.eventType === "backend" && event.outcome === "stale"));
|
||||
|
||||
acknowledgeWorkbenchVisible({ messages: [traceOnlyAgentMessage("cnv_secret", "trc_secret", [{ type: "tool_call", command: "pwd" } as TraceEvent])], activeConversationId: "cnv_secret", detailLoading: false });
|
||||
const toolEvents = drainWorkbenchPerformanceEventsForTest();
|
||||
assert.ok(toolEvents.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_first_visible" && event.outcome === "ok"));
|
||||
});
|
||||
|
||||
function userMessage(conversationId: string): ChatMessage {
|
||||
return { id: "msg_user", role: "user", title: "User", text: "hello", status: "sent", createdAt: new Date().toISOString(), conversationId };
|
||||
}
|
||||
@@ -70,3 +102,17 @@ function agentMessage(conversationId: string, traceId: string): ChatMessage {
|
||||
runnerTrace: { traceId, eventCount: 1, events: [{ type: "assistant_message" }] }
|
||||
};
|
||||
}
|
||||
|
||||
function traceOnlyAgentMessage(conversationId: string, traceId: string, events: TraceEvent[]): ChatMessage {
|
||||
return {
|
||||
id: "msg_agent_trace",
|
||||
role: "agent",
|
||||
title: "Code Agent",
|
||||
text: "",
|
||||
status: "running",
|
||||
createdAt: new Date().toISOString(),
|
||||
conversationId,
|
||||
traceId,
|
||||
runnerTrace: { traceId, eventCount: events.length, events }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ interface TraceEventState {
|
||||
eventType: string;
|
||||
backend: string;
|
||||
transport: string;
|
||||
visibleOutcome: WorkbenchOutcome;
|
||||
visibleReported: boolean;
|
||||
}
|
||||
|
||||
@@ -191,7 +192,7 @@ export function failWorkbenchSubmitJourney(traceId: string | null | undefined, o
|
||||
state.firstVisibleReported = true;
|
||||
enqueue({
|
||||
kind: "workbench_journey",
|
||||
journey: "submit_to_first_visible",
|
||||
journey: "submit_to_failure",
|
||||
route: state.route,
|
||||
entry: state.entry,
|
||||
backend: state.backend,
|
||||
@@ -216,11 +217,11 @@ export function markWorkbenchTraceEventsReceived(input: TraceEventTimingInput):
|
||||
const receivedAt = monotonicNow();
|
||||
const receivedWallAt = wallNow();
|
||||
const traceSeq = Number(input.traceSeq ?? event.seq ?? event.sourceSeq ?? NaN);
|
||||
traceEvents.set(traceId, { traceId, receivedAt, projectedAt: null, eventCreatedAt, appendedAt, serverSentAt, traceSeq: Number.isFinite(traceSeq) ? Math.trunc(traceSeq) : null, eventType, backend, transport: input.transport, visibleReported: false });
|
||||
const visibleOutcome: WorkbenchOutcome = input.transport === "sse" && serverSentAt !== null ? "ok" : "stale";
|
||||
traceEvents.set(traceId, { traceId, receivedAt, projectedAt: null, eventCreatedAt, appendedAt, serverSentAt, traceSeq: Number.isFinite(traceSeq) ? Math.trunc(traceSeq) : null, eventType, backend, transport: input.transport, visibleOutcome, visibleReported: false });
|
||||
if (eventCreatedAt !== null && appendedAt !== null) enqueue({ kind: "workbench_event_phase", phase: "created_to_append", eventType, backend, transport: input.transport, outcome: "ok", valueMs: Math.max(0, appendedAt - eventCreatedAt) });
|
||||
if (appendedAt !== null && serverSentAt !== null) enqueue({ kind: "workbench_event_phase", phase: "append_to_sse", eventType, backend, transport: input.transport, outcome: "ok", valueMs: Math.max(0, serverSentAt - appendedAt) });
|
||||
if (serverSentAt !== null) enqueue({ kind: "workbench_event_phase", phase: "sse_to_receive", eventType, backend, transport: input.transport, outcome: "ok", valueMs: Math.max(0, receivedWallAt - serverSentAt) });
|
||||
else if (eventCreatedAt !== null) enqueue({ kind: "workbench_event_phase", phase: "sse_to_receive", eventType, backend, transport: input.transport, outcome: "ok", valueMs: Math.max(0, receivedWallAt - eventCreatedAt) });
|
||||
}
|
||||
|
||||
export function markWorkbenchTraceProjected(traceId: string | null | undefined): void {
|
||||
@@ -250,18 +251,19 @@ export function acknowledgeWorkbenchVisible(input: { messages: ChatMessage[]; ac
|
||||
}
|
||||
for (const message of input.messages) {
|
||||
const traceId = safeText(message.traceId ?? message.runnerTrace?.traceId);
|
||||
if (!traceId || message.role !== "agent" || !isVisibleAgentMessage(message)) continue;
|
||||
if (!traceId || message.role !== "agent") continue;
|
||||
const submit = submitJourneys.get(traceId);
|
||||
if (submit && !submit.firstVisibleReported) {
|
||||
const outputVisible = isVisibleAgentOutputMessage(message);
|
||||
if (submit && !submit.firstVisibleReported && outputVisible) {
|
||||
submit.firstVisibleReported = true;
|
||||
enqueue({ kind: "workbench_journey", journey: "submit_to_first_visible", route: submit.route, entry: submit.entry, backend: submit.backend, transport: submit.transport, visibility: visibilityState(), outcome: "ok", valueMs: Math.max(0, now - submit.startAt) });
|
||||
submitJourneys.delete(traceId);
|
||||
}
|
||||
const event = traceEvents.get(traceId);
|
||||
if (event && !event.visibleReported) {
|
||||
if (event && !event.visibleReported && isVisibleTraceEventMessage(message)) {
|
||||
event.visibleReported = true;
|
||||
if (event.projectedAt !== null) enqueue({ kind: "workbench_event_phase", phase: "project_to_paint", eventType: event.eventType, backend: event.backend, transport: event.transport, outcome: "ok", valueMs: Math.max(0, now - event.projectedAt) });
|
||||
if (event.eventCreatedAt !== null) enqueue({ kind: "workbench_backend_event_visible", eventType: event.eventType, backend: event.backend, transport: event.transport, outcome: "ok", valueMs: Math.max(0, wallNow() - event.eventCreatedAt) });
|
||||
if (event.eventCreatedAt !== null) enqueue({ kind: "workbench_backend_event_visible", eventType: event.eventType, backend: event.backend, transport: event.transport, outcome: event.visibleOutcome, valueMs: Math.max(0, wallNow() - event.eventCreatedAt) });
|
||||
traceEvents.delete(traceId);
|
||||
}
|
||||
}
|
||||
@@ -327,10 +329,17 @@ function afterNextPaint(callback: () => void): void {
|
||||
requestAnimationFrame(() => requestAnimationFrame(callback));
|
||||
}
|
||||
|
||||
function isVisibleAgentMessage(message: ChatMessage): boolean {
|
||||
if (message.runnerTrace && ((message.runnerTrace.events?.length ?? 0) > 0 || message.runnerTrace.eventCount)) return true;
|
||||
function isVisibleAgentOutputMessage(message: ChatMessage): boolean {
|
||||
if (String(message.text ?? "").trim()) return true;
|
||||
return !["pending", "running"].includes(String(message.status ?? "").toLowerCase());
|
||||
return traceEventsFromMessage(message).some(isAssistantOrToolOutputTraceEvent);
|
||||
}
|
||||
|
||||
function isVisibleTraceEventMessage(message: ChatMessage): boolean {
|
||||
return traceEventsFromMessage(message).some(isUserVisibleTraceEvent);
|
||||
}
|
||||
|
||||
function traceEventsFromMessage(message: ChatMessage): TraceEvent[] {
|
||||
return Array.isArray(message.runnerTrace?.events) ? message.runnerTrace.events : [];
|
||||
}
|
||||
|
||||
function isUserVisibleTraceEvent(event: TraceEvent): boolean {
|
||||
@@ -338,6 +347,16 @@ function isUserVisibleTraceEvent(event: TraceEvent): boolean {
|
||||
return type === "assistant" || type === "tool_call" || type === "backend" || type === "terminal" || type === "error";
|
||||
}
|
||||
|
||||
function isAssistantOrToolOutputTraceEvent(event: TraceEvent): boolean {
|
||||
const type = eventTypeFromTraceEvent(event);
|
||||
if (type === "tool_call") return true;
|
||||
return type === "assistant" && traceEventHasDisplayText(event);
|
||||
}
|
||||
|
||||
function traceEventHasDisplayText(event: TraceEvent): boolean {
|
||||
return [event.message, event.text, event.outputSummary, event.stdoutSummary, event.stderrSummary, event.command].some((value) => Boolean(safeText(value)));
|
||||
}
|
||||
|
||||
function eventTypeFromTraceEvent(event: TraceEvent | null | undefined): string {
|
||||
const raw = safeText(event?.eventType ?? event?.type ?? event?.kind ?? event?.label ?? event?.status).toLowerCase();
|
||||
if (/tool/u.test(raw)) return "tool_call";
|
||||
|
||||
Reference in New Issue
Block a user