perf: gate perf logs and dedup build_config_update on first message (#8627)
Signed-off-by: Bradley Axen <baxen@squareup.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
clearActiveMessageId,
|
||||
} from "./acpNotificationHandler";
|
||||
import { searchSessionsViaExports } from "./sessionSearch";
|
||||
import { perfLog } from "@/shared/lib/perfLog";
|
||||
|
||||
export interface AcpProvider {
|
||||
id: string;
|
||||
@@ -36,6 +37,8 @@ export async function acpSendMessage(
|
||||
options: AcpSendMessageOptions = {},
|
||||
): Promise<void> {
|
||||
const { systemPrompt, personaId, images } = options;
|
||||
const sid = sessionId.slice(0, 8);
|
||||
const tStart = performance.now();
|
||||
|
||||
const gooseSessionId = sessionTracker.getGooseSessionId(sessionId, personaId);
|
||||
if (!gooseSessionId) {
|
||||
@@ -57,7 +60,15 @@ export async function acpSendMessage(
|
||||
const messageId = crypto.randomUUID();
|
||||
setActiveMessageId(gooseSessionId, messageId);
|
||||
|
||||
perfLog(
|
||||
`[perf:send] ${sid} acpSendMessage → prompt(len=${prompt.length}, imgs=${images?.length ?? 0})`,
|
||||
);
|
||||
const tPrompt = performance.now();
|
||||
await directAcp.prompt(gooseSessionId, content);
|
||||
const tDone = performance.now();
|
||||
perfLog(
|
||||
`[perf:send] ${sid} prompt() resolved in ${(tDone - tPrompt).toFixed(1)}ms (total acpSendMessage ${(tDone - tStart).toFixed(1)}ms)`,
|
||||
);
|
||||
|
||||
clearActiveMessageId(gooseSessionId);
|
||||
}
|
||||
@@ -69,12 +80,20 @@ export async function acpPrepareSession(
|
||||
workingDir: string,
|
||||
options: AcpPrepareSessionOptions = {},
|
||||
): Promise<void> {
|
||||
const sid = sessionId.slice(0, 8);
|
||||
const t0 = performance.now();
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} acpPrepareSession start (provider=${providerId})`,
|
||||
);
|
||||
await sessionTracker.prepareSession(
|
||||
sessionId,
|
||||
providerId,
|
||||
workingDir,
|
||||
options.personaId,
|
||||
);
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} acpPrepareSession done in ${(performance.now() - t0).toFixed(1)}ms`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function acpSetModel(
|
||||
@@ -125,7 +144,13 @@ export async function acpLoadSession(
|
||||
workingDir?: string,
|
||||
): Promise<void> {
|
||||
const effectiveWorkingDir = workingDir ?? "~/.goose/artifacts";
|
||||
const sid = sessionId.slice(0, 8);
|
||||
const t0 = performance.now();
|
||||
perfLog(`[perf:load] ${sid} acpLoadSession → client.loadSession`);
|
||||
await directAcp.loadSession(gooseSessionId, effectiveWorkingDir);
|
||||
perfLog(
|
||||
`[perf:load] ${sid} client.loadSession resolved in ${(performance.now() - t0).toFixed(1)}ms`,
|
||||
);
|
||||
sessionTracker.registerSession(
|
||||
sessionId,
|
||||
gooseSessionId,
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
PromptResponse,
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import { getClient } from "./acpConnection";
|
||||
import { perfLog } from "@/shared/lib/perfLog";
|
||||
|
||||
export interface AcpProvider {
|
||||
id: string;
|
||||
@@ -84,24 +85,36 @@ export async function setModel(
|
||||
sessionId: string,
|
||||
modelId: string,
|
||||
): Promise<void> {
|
||||
const sid = sessionId.slice(0, 8);
|
||||
const tClient = performance.now();
|
||||
const client = await getClient();
|
||||
const tCall = performance.now();
|
||||
await client.setSessionConfigOption({
|
||||
sessionId,
|
||||
configId: "model",
|
||||
value: modelId,
|
||||
});
|
||||
perfLog(
|
||||
`[perf:api] ${sid} setModel(${modelId}) getClient=${(tCall - tClient).toFixed(1)}ms wire=${(performance.now() - tCall).toFixed(1)}ms`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function setProvider(
|
||||
sessionId: string,
|
||||
providerId: string,
|
||||
): Promise<void> {
|
||||
const sid = sessionId.slice(0, 8);
|
||||
const tClient = performance.now();
|
||||
const client = await getClient();
|
||||
const tCall = performance.now();
|
||||
await client.setSessionConfigOption({
|
||||
sessionId,
|
||||
configId: "provider",
|
||||
value: providerId,
|
||||
});
|
||||
perfLog(
|
||||
`[perf:api] ${sid} setProvider(${providerId}) getClient=${(tCall - tClient).toFixed(1)}ms wire=${(performance.now() - tCall).toFixed(1)}ms`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateWorkingDir(
|
||||
@@ -120,16 +133,34 @@ export async function cancelSession(sessionId: string): Promise<void> {
|
||||
export async function newSession(
|
||||
workingDir: string,
|
||||
): Promise<NewSessionResponse> {
|
||||
const tClient = performance.now();
|
||||
const client = await getClient();
|
||||
return client.newSession({ cwd: workingDir, mcpServers: [] });
|
||||
const tCall = performance.now();
|
||||
const response = await client.newSession({ cwd: workingDir, mcpServers: [] });
|
||||
const sid = response.sessionId.slice(0, 8);
|
||||
perfLog(
|
||||
`[perf:api] ${sid} newSession getClient=${(tCall - tClient).toFixed(1)}ms wire=${(performance.now() - tCall).toFixed(1)}ms`,
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function loadSession(
|
||||
sessionId: string,
|
||||
workingDir: string,
|
||||
): Promise<LoadSessionResponse> {
|
||||
const sid = sessionId.slice(0, 8);
|
||||
const tClient = performance.now();
|
||||
const client = await getClient();
|
||||
return client.loadSession({ sessionId, cwd: workingDir, mcpServers: [] });
|
||||
const tCall = performance.now();
|
||||
const response = await client.loadSession({
|
||||
sessionId,
|
||||
cwd: workingDir,
|
||||
mcpServers: [],
|
||||
});
|
||||
perfLog(
|
||||
`[perf:api] ${sid} loadSession getClient=${(tCall - tClient).toFixed(1)}ms wire=${(performance.now() - tCall).toFixed(1)}ms`,
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function prompt(
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type RequestPermissionResponse,
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import { createWebSocketStream } from "./createWebSocketStream";
|
||||
import { perfLog } from "@/shared/lib/perfLog";
|
||||
|
||||
let notificationHandler: AcpNotificationHandler | null = null;
|
||||
|
||||
@@ -63,12 +64,21 @@ function monitorConnection(client: GooseClient): void {
|
||||
}
|
||||
|
||||
async function initializeConnection(): Promise<GooseClient> {
|
||||
const tStart = performance.now();
|
||||
const wsUrl: string = await invoke("get_goose_serve_url");
|
||||
perfLog(
|
||||
`[perf:conn] get_goose_serve_url in ${(performance.now() - tStart).toFixed(1)}ms`,
|
||||
);
|
||||
|
||||
const tStream = performance.now();
|
||||
const stream = createWebSocketStream(wsUrl);
|
||||
|
||||
const client = new GooseClient(createClientCallbacks(), stream);
|
||||
perfLog(
|
||||
`[perf:conn] ws stream + client created in ${(performance.now() - tStream).toFixed(1)}ms`,
|
||||
);
|
||||
|
||||
const tInit = performance.now();
|
||||
await client.initialize({
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
clientCapabilities: {},
|
||||
@@ -77,6 +87,9 @@ async function initializeConnection(): Promise<GooseClient> {
|
||||
version: "0.1.0",
|
||||
},
|
||||
});
|
||||
perfLog(
|
||||
`[perf:conn] client.initialize in ${(performance.now() - tInit).toFixed(1)}ms (total ${(performance.now() - tStart).toFixed(1)}ms)`,
|
||||
);
|
||||
|
||||
monitorConnection(client);
|
||||
|
||||
@@ -89,6 +102,7 @@ export async function getClient(): Promise<GooseClient> {
|
||||
}
|
||||
|
||||
if (!clientPromise) {
|
||||
perfLog("[perf:conn] getClient() → initializing new ACP connection");
|
||||
clientPromise = initializeConnection()
|
||||
.then((client) => {
|
||||
resolvedClient = client;
|
||||
@@ -98,6 +112,8 @@ export async function getClient(): Promise<GooseClient> {
|
||||
clientPromise = null;
|
||||
throw error;
|
||||
});
|
||||
} else {
|
||||
perfLog("[perf:conn] getClient() awaiting in-flight initializeConnection");
|
||||
}
|
||||
|
||||
return clientPromise;
|
||||
|
||||
@@ -15,19 +15,52 @@ import type {
|
||||
} from "@/shared/types/messages";
|
||||
import type { AcpNotificationHandler } from "./acpConnection";
|
||||
import { getLocalSessionId } from "./acpSessionTracker";
|
||||
import { perfLog } from "@/shared/lib/perfLog";
|
||||
|
||||
// Pre-set message ID for the next live stream per goose session
|
||||
const presetMessageIds = new Map<string, string>();
|
||||
|
||||
// Per-session perf counters for replay/live streaming.
|
||||
interface ReplayPerf {
|
||||
firstAt: number;
|
||||
lastAt: number;
|
||||
count: number;
|
||||
}
|
||||
const replayPerf = new Map<string, ReplayPerf>();
|
||||
interface LivePerf {
|
||||
sendStartedAt: number;
|
||||
firstChunkAt: number | null;
|
||||
chunkCount: number;
|
||||
}
|
||||
const livePerf = new Map<string, LivePerf>();
|
||||
|
||||
export function setActiveMessageId(
|
||||
gooseSessionId: string,
|
||||
messageId: string,
|
||||
): void {
|
||||
presetMessageIds.set(gooseSessionId, messageId);
|
||||
livePerf.set(gooseSessionId, {
|
||||
sendStartedAt: performance.now(),
|
||||
firstChunkAt: null,
|
||||
chunkCount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
export function clearActiveMessageId(gooseSessionId: string): void {
|
||||
presetMessageIds.delete(gooseSessionId);
|
||||
const perf = livePerf.get(gooseSessionId);
|
||||
if (perf) {
|
||||
const sid = gooseSessionId.slice(0, 8);
|
||||
const total = performance.now() - perf.sendStartedAt;
|
||||
const ttft =
|
||||
perf.firstChunkAt !== null
|
||||
? (perf.firstChunkAt - perf.sendStartedAt).toFixed(1)
|
||||
: "n/a";
|
||||
perfLog(
|
||||
`[perf:stream] ${sid} stream ended — ttft=${ttft}ms total=${total.toFixed(1)}ms chunks=${perf.chunkCount}`,
|
||||
);
|
||||
livePerf.delete(gooseSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleSessionNotification(
|
||||
@@ -39,12 +72,45 @@ export async function handleSessionNotification(
|
||||
const isReplay = useChatStore.getState().loadingSessionIds.has(sessionId);
|
||||
|
||||
if (isReplay) {
|
||||
const sid = sessionId.slice(0, 8);
|
||||
let perf = replayPerf.get(sessionId);
|
||||
const now = performance.now();
|
||||
if (!perf) {
|
||||
perf = { firstAt: now, lastAt: now, count: 0 };
|
||||
replayPerf.set(sessionId, perf);
|
||||
perfLog(`[perf:replay] ${sid} first notification received`);
|
||||
}
|
||||
perf.lastAt = now;
|
||||
perf.count += 1;
|
||||
handleReplay(sessionId, update);
|
||||
} else {
|
||||
const perf = livePerf.get(gooseSessionId);
|
||||
if (perf && update.sessionUpdate === "agent_message_chunk") {
|
||||
perf.chunkCount += 1;
|
||||
if (perf.firstChunkAt === null) {
|
||||
perf.firstChunkAt = performance.now();
|
||||
const sid = gooseSessionId.slice(0, 8);
|
||||
perfLog(
|
||||
`[perf:stream] ${sid} first agent_message_chunk at ttft=${(perf.firstChunkAt - perf.sendStartedAt).toFixed(1)}ms`,
|
||||
);
|
||||
}
|
||||
}
|
||||
handleLive(sessionId, gooseSessionId, update);
|
||||
}
|
||||
}
|
||||
|
||||
export function getReplayPerf(
|
||||
sessionId: string,
|
||||
): { count: number; spanMs: number } | null {
|
||||
const perf = replayPerf.get(sessionId);
|
||||
if (!perf) return null;
|
||||
return { count: perf.count, spanMs: perf.lastAt - perf.firstAt };
|
||||
}
|
||||
|
||||
export function clearReplayPerf(sessionId: string): void {
|
||||
replayPerf.delete(sessionId);
|
||||
}
|
||||
|
||||
function handleReplay(sessionId: string, update: SessionUpdate): void {
|
||||
switch (update.sessionUpdate) {
|
||||
case "agent_message_chunk": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as acpApi from "./acpApi";
|
||||
import { perfLog } from "@/shared/lib/perfLog";
|
||||
|
||||
interface PreparedSession {
|
||||
gooseSessionId: string;
|
||||
@@ -22,34 +23,63 @@ export async function prepareSession(
|
||||
workingDir: string,
|
||||
personaId?: string,
|
||||
): Promise<string> {
|
||||
const sid = sessionId.slice(0, 8);
|
||||
const key = makeKey(sessionId, personaId);
|
||||
|
||||
const existing = prepared.get(key) ?? prepared.get(sessionId);
|
||||
if (existing) {
|
||||
const tReuse = performance.now();
|
||||
let changed = false;
|
||||
if (existing.workingDir !== workingDir) {
|
||||
await acpApi.updateWorkingDir(existing.gooseSessionId, workingDir);
|
||||
existing.workingDir = workingDir;
|
||||
changed = true;
|
||||
}
|
||||
if (existing.providerId !== providerId) {
|
||||
const tProv = performance.now();
|
||||
await acpApi.setProvider(existing.gooseSessionId, providerId);
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} reuse setProvider(${providerId}) in ${(performance.now() - tProv).toFixed(1)}ms (goose_sid=${existing.gooseSessionId.slice(0, 8)})`,
|
||||
);
|
||||
existing.providerId = providerId;
|
||||
changed = true;
|
||||
}
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} reuse existing session (updates=${changed}) in ${(performance.now() - tReuse).toFixed(1)}ms`,
|
||||
);
|
||||
return existing.gooseSessionId;
|
||||
}
|
||||
|
||||
let gooseSessionId: string | null = null;
|
||||
|
||||
const tLoad = performance.now();
|
||||
try {
|
||||
await acpApi.loadSession(sessionId, workingDir);
|
||||
gooseSessionId = sessionId;
|
||||
} catch {}
|
||||
|
||||
if (!gooseSessionId) {
|
||||
const response = await acpApi.newSession(workingDir);
|
||||
gooseSessionId = response.sessionId;
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} tracker loadSession ok in ${(performance.now() - tLoad).toFixed(1)}ms`,
|
||||
);
|
||||
} catch {
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} tracker loadSession failed in ${(performance.now() - tLoad).toFixed(1)}ms → newSession`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!gooseSessionId) {
|
||||
const tNew = performance.now();
|
||||
const response = await acpApi.newSession(workingDir);
|
||||
gooseSessionId = response.sessionId;
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} tracker newSession done in ${(performance.now() - tNew).toFixed(1)}ms (goose_sid=${gooseSessionId.slice(0, 8)})`,
|
||||
);
|
||||
}
|
||||
|
||||
const gooseSid = gooseSessionId.slice(0, 8);
|
||||
const tProv = performance.now();
|
||||
await acpApi.setProvider(gooseSessionId, providerId);
|
||||
perfLog(
|
||||
`[perf:prepare] ${sid} tracker setProvider(${providerId}) in ${(performance.now() - tProv).toFixed(1)}ms (goose_sid=${gooseSid})`,
|
||||
);
|
||||
|
||||
prepared.set(key, { gooseSessionId, providerId, workingDir });
|
||||
prepared.set(sessionId, { gooseSessionId, providerId, workingDir });
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Gated performance logger for frontend timing instrumentation.
|
||||
*
|
||||
* Enabled when any of the following is true:
|
||||
* - Running under Vite dev (`import.meta.env.DEV`)
|
||||
* - `localStorage.getItem("goose.perf") === "1"`
|
||||
*
|
||||
* Otherwise a no-op, so perf call sites add zero runtime cost in release
|
||||
* builds for users who have not opted in.
|
||||
*
|
||||
* Messages are prefixed with `[perf:<channel>]` by callers; this helper
|
||||
* is intentionally dumb and forwards the already-formatted string.
|
||||
*/
|
||||
function isEnabled(): boolean {
|
||||
try {
|
||||
if (import.meta.env?.DEV) return true;
|
||||
} catch {
|
||||
// import.meta may be unavailable in some test contexts
|
||||
}
|
||||
try {
|
||||
if (
|
||||
typeof localStorage !== "undefined" &&
|
||||
localStorage.getItem("goose.perf") === "1"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// localStorage can throw in restricted contexts
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const enabled = isEnabled();
|
||||
|
||||
export function perfLog(message: string): void {
|
||||
if (!enabled) return;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(message);
|
||||
}
|
||||
Reference in New Issue
Block a user