perf(ui): record pending send paint timing (#88960)

This commit is contained in:
Vincent Koc
2026-06-01 07:42:24 +01:00
committed by GitHub
parent c11ff35841
commit 69b2c8bd15
3 changed files with 85 additions and 0 deletions

View File

@@ -1057,6 +1057,7 @@ describe("handleSendChat", () => {
});
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
@@ -1223,6 +1224,42 @@ describe("handleSendChat", () => {
expect(ack?.requestDurationMs).toEqual(expect.any(Number));
});
it("records pending send paint timing before a delayed chat.send ACK", async () => {
vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
queueMicrotask(() => callback(0));
return 1;
});
const chatSend = createDeferred<{ status: "started" }>();
const request = vi.fn((method: string) => {
if (method === "chat.send") {
return chatSend.promise;
}
throw new Error(`Unexpected request: ${method}`);
});
const host = makeHost({
client: { request } as unknown as ChatHost["client"],
chatMessage: "measure painted pending send",
eventLogBuffer: [],
tab: "debug",
});
const send = handleSendChat(host);
await vi.waitFor(() =>
expect(eventPayloads(host, "control-ui.chat.send").map((payload) => payload.phase)).toEqual(
expect.arrayContaining(["pending-visible", "request-start", "pending-painted"]),
),
);
chatSend.resolve({ status: "started" });
await send;
const phasesAfterAck = eventPayloads(host, "control-ui.chat.send").map(
(payload) => payload.phase,
);
expect(phasesAfterAck).toEqual(expect.arrayContaining(["ack"]));
});
it("waits for an in-flight model picker update before sending chat", async () => {
const switchUpdate = createDeferred<boolean>();
const request = vi.fn(async (method: string) => {

View File

@@ -31,6 +31,7 @@ import {
controlUiNowMs,
recordControlUiPerformanceEvent,
roundedControlUiDurationMs,
scheduleControlUiAfterPaint,
} from "./control-ui-performance.ts";
import { resolveControlUiAuthHeader } from "./control-ui-auth.ts";
import {
@@ -417,6 +418,7 @@ function enqueuePendingSendMessage(
};
host.chatQueue = [...host.chatQueue, pending];
recordChatSendTiming(host, pending, "pending-visible", submittedAtMs);
schedulePendingSendPaintTiming(host, pending, submittedAtMs);
scheduleChatScroll(host as unknown as Parameters<typeof scheduleChatScroll>[0], true);
return pending;
}
@@ -563,6 +565,7 @@ type QueuedChatSendResult = "sent" | "pending" | "failed";
type ChatSendTimingPhase =
| "pending-visible"
| "pending-painted"
| "request-start"
| "ack"
| "queued-busy"
@@ -600,6 +603,42 @@ function recordChatSendTiming(
);
}
function shouldRecordPendingSendPaint(item: ChatQueueItem): boolean {
return (
typeof item.sendSubmittedAtMs === "number" &&
(item.sendState === "waiting-model" ||
item.sendState === "sending" ||
item.sendState === "waiting-reconnect")
);
}
function schedulePendingSendPaintTiming(
host: ChatHost,
item: ChatQueueItem,
startedAtMs = item.sendSubmittedAtMs,
) {
const sessionKey = item.sessionKey ?? host.sessionKey;
const sendRunId = item.sendRunId;
if (!sendRunId || startedAtMs == null) {
return;
}
scheduleControlUiAfterPaint(
host as Parameters<typeof scheduleControlUiAfterPaint>[0],
() => {
if (!visibleSessionMatches(host, sessionKey, item.agentId)) {
return;
}
const queued = readChatQueueForSession(host, sessionKey).find(
(entry) => entry.id === item.id && entry.sendRunId === sendRunId,
);
if (!queued || !shouldRecordPendingSendPaint(queued)) {
return;
}
recordChatSendTiming(host, queued, "pending-painted", startedAtMs);
},
);
}
function ensureQueuedSendState(
host: ChatHost,
item: ChatQueueItem,

View File

@@ -159,6 +159,15 @@ export function scheduleControlUiTabVisibleTiming(
.then(() => runAfterPaint(record));
}
export function scheduleControlUiAfterPaint(
host: Pick<ControlUiPerformanceHost, "updateComplete">,
callback: () => void,
) {
void Promise.resolve(host.updateComplete)
.catch(() => undefined)
.then(() => runAfterPaint(callback));
}
export function beginControlUiRefresh(
host: ControlUiPerformanceHost,
tab: Tab,