fix(workbench): flush sse and report open latency (#1986)
Co-authored-by: root <root@lyon.remote>
This commit is contained in:
@@ -106,6 +106,7 @@ export async function handleWorkbenchRealtimeHttp(request, response, url, option
|
|||||||
"x-accel-buffering": "no",
|
"x-accel-buffering": "no",
|
||||||
"x-content-type-options": "nosniff"
|
"x-content-type-options": "nosniff"
|
||||||
});
|
});
|
||||||
|
if (typeof response.flushHeaders === "function") response.flushHeaders();
|
||||||
|
|
||||||
const writeEvent = (name, payload = {}) => {
|
const writeEvent = (name, payload = {}) => {
|
||||||
if (closed || response.destroyed) return;
|
if (closed || response.destroyed) return;
|
||||||
@@ -146,7 +147,7 @@ export async function handleWorkbenchRealtimeHttp(request, response, url, option
|
|||||||
await (perf ? perf.measure("workbench_initial_trace", () => writeTraceRealtimeSnapshotSafe({ writeEvent, options, actor: auth.actor, traceId: activeTraceId, reason: "initial" })) : writeTraceRealtimeSnapshotSafe({ writeEvent, options, actor: auth.actor, traceId: activeTraceId, reason: "initial" }));
|
await (perf ? perf.measure("workbench_initial_trace", () => writeTraceRealtimeSnapshotSafe({ writeEvent, options, actor: auth.actor, traceId: activeTraceId, reason: "initial" })) : writeTraceRealtimeSnapshotSafe({ writeEvent, options, actor: auth.actor, traceId: activeTraceId, reason: "initial" }));
|
||||||
}
|
}
|
||||||
const runtimeStore = options.runtimeStore ?? null;
|
const runtimeStore = options.runtimeStore ?? null;
|
||||||
const outboxPollMs = parsePositiveInteger(options.env?.HWLAB_WORKBENCH_SSE_OUTBOX_POLL_MS, 2000);
|
const outboxPollMs = parsePositiveInteger(options.env?.HWLAB_WORKBENCH_SSE_OUTBOX_POLL_MS, 500);
|
||||||
let outboxCursor = 0;
|
let outboxCursor = 0;
|
||||||
const outboxTraceSnapshots = new Set(activeTraceId ? [activeTraceId] : []);
|
const outboxTraceSnapshots = new Set(activeTraceId ? [activeTraceId] : []);
|
||||||
if (typeof runtimeStore?.readWorkbenchProjectionOutbox === "function" && (streamSessionId || activeTraceId)) {
|
if (typeof runtimeStore?.readWorkbenchProjectionOutbox === "function" && (streamSessionId || activeTraceId)) {
|
||||||
|
|||||||
@@ -65,10 +65,12 @@ export function connectWorkbenchEvents(options: WorkbenchEventStreamOptions): Wo
|
|||||||
appendParam(params, "sessionId", options.sessionId);
|
appendParam(params, "sessionId", options.sessionId);
|
||||||
appendParam(params, "traceId", options.traceId);
|
appendParam(params, "traceId", options.traceId);
|
||||||
const eventRoute = `/v1/workbench/events?${params.toString()}`;
|
const eventRoute = `/v1/workbench/events?${params.toString()}`;
|
||||||
|
const connectStartedAt = typeof performance === "undefined" ? Date.now() : performance.now();
|
||||||
recordWorkbenchSseLifecycle({ state: "connect", route: eventRoute, sessionId: options.sessionId, traceId: options.traceId });
|
recordWorkbenchSseLifecycle({ state: "connect", route: eventRoute, sessionId: options.sessionId, traceId: options.traceId });
|
||||||
const source = new EventSource(eventRoute, { withCredentials: true });
|
const source = new EventSource(eventRoute, { withCredentials: true });
|
||||||
source.onopen = () => {
|
source.onopen = () => {
|
||||||
recordWorkbenchSseLifecycle({ state: "open", route: eventRoute, sessionId: options.sessionId, traceId: options.traceId });
|
const openedAt = typeof performance === "undefined" ? Date.now() : performance.now();
|
||||||
|
recordWorkbenchSseLifecycle({ state: "open", route: eventRoute, sessionId: options.sessionId, traceId: options.traceId, valueMs: Math.max(0, Math.round(openedAt - connectStartedAt)) });
|
||||||
options.onOpen?.();
|
options.onOpen?.();
|
||||||
};
|
};
|
||||||
source.onerror = (event) => {
|
source.onerror = (event) => {
|
||||||
|
|||||||
@@ -299,18 +299,20 @@ function serverTimingSummary(entries: readonly PerformanceServerTiming[] | undef
|
|||||||
return summary.slice(0, 240) || undefined;
|
return summary.slice(0, 240) || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function recordWorkbenchSseLifecycle(input: { state: "connect" | "open" | "error" | "close"; route: string; sessionId?: string | null; traceId?: string | null; errorName?: string | null }): void {
|
export function recordWorkbenchSseLifecycle(input: { state: "connect" | "open" | "error" | "close"; route: string; sessionId?: string | null; traceId?: string | null; errorName?: string | null; valueMs?: number | null }): void {
|
||||||
ensureInstalled();
|
ensureInstalled();
|
||||||
const state = input.state;
|
const state = input.state;
|
||||||
|
const endedAt = wallNow();
|
||||||
|
const valueMs = roundedNonNegative(Number(input.valueMs ?? 0)) ?? 0;
|
||||||
enqueueWorkbenchUiEvent({
|
enqueueWorkbenchUiEvent({
|
||||||
eventType: "sse_lifecycle",
|
eventType: "sse_lifecycle",
|
||||||
loadingScope: "sse",
|
loadingScope: "sse",
|
||||||
state,
|
state,
|
||||||
reason: state === "connect" ? "sse_connect" : state === "open" ? "sse_open" : state === "close" ? "sse_close" : "sse_error",
|
reason: state === "connect" ? "sse_connect" : state === "open" ? "sse_open" : state === "close" ? "sse_close" : "sse_error",
|
||||||
route: input.route,
|
route: input.route,
|
||||||
valueMs: 0,
|
valueMs,
|
||||||
startedAtEpochMs: wallNow(),
|
startedAtEpochMs: Math.max(0, endedAt - valueMs),
|
||||||
endedAtEpochMs: wallNow(),
|
endedAtEpochMs: endedAt,
|
||||||
sessionHash: hashIdentifier(input.sessionId, "ses"),
|
sessionHash: hashIdentifier(input.sessionId, "ses"),
|
||||||
traceHash: hashIdentifier(input.traceId, "trc"),
|
traceHash: hashIdentifier(input.traceId, "trc"),
|
||||||
outcome: state === "error" ? "network" : "ok",
|
outcome: state === "error" ? "network" : "ok",
|
||||||
|
|||||||
Reference in New Issue
Block a user