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:
Bradley Axen
2026-04-17 12:43:36 -07:00
committed by GitHub
parent 7fa662bcac
commit 8a51e34891
22 changed files with 544 additions and 285 deletions
+6
View File
@@ -161,6 +161,12 @@ Additional tooling notes:
- Pre-push hooks run `just fmt-check`, `just clippy`, `just check`, `just test`, `just build`, and `just tauri-check`.
- Do not use `--no-verify` to bypass hooks. Fix the underlying issue instead.
## Performance Logging
- Frontend perf logs use `perfLog()` from `@/shared/lib/perfLog`. Messages are tagged `[perf:<channel>]` (startup, conn, load, newtab, prepare, send, api, stream, replay, chatview). Enabled automatically in Vite dev mode, or opt-in via `localStorage.setItem("goose.perf", "1")` in a release build.
- Backend perf logs live in `crates/goose-acp/src/server.rs` under `target: "perf"` at `debug!` level. Off by default; enable with `RUST_LOG=perf=debug,info` on the `goose serve` process.
- `just dev` and `just dev-debug` export `RUST_LOG=perf=debug,info` so the child `goose serve` emits perf logs without extra setup. Override by setting `RUST_LOG` in the environment before invoking `just`.
## Testing & Verification
- Unit/component tests use Vitest and Testing Library via `just test` or `pnpm test`.
+6 -2
View File
@@ -84,6 +84,9 @@ dev:
VITE_PORT={{ vite_port }}
export VITE_PORT
# Enable perf logs in the child `goose serve` process by default.
# Override with e.g. RUST_LOG=info just dev to disable.
export RUST_LOG="${RUST_LOG:-perf=debug,info}"
PROJECT_DIR=$(pwd)
GOOSE_BIN="${PROJECT_DIR}/../../target/debug/goose"
export GOOSE_BIN
@@ -114,8 +117,9 @@ dev-debug:
#!/usr/bin/env bash
set -euo pipefail
VITE_PORT={{ vite_port }}
export VITE_PORT
# Enable perf logs in the child `goose serve` process by default.
# Override with e.g. RUST_LOG=info just dev-debug to disable.
export RUST_LOG="${RUST_LOG:-perf=debug,info}"
PROJECT_DIR=$(pwd)
GOOSE_BIN="${PROJECT_DIR}/../../target/debug/goose"
export GOOSE_BIN
+4 -4
View File
@@ -11,9 +11,9 @@ const EXCEPTIONS = {
"Drag-and-drop handlers for session-to-project moves and project reorder, plus activeProjectId highlight.",
},
"src/features/chat/ui/ChatView.tsx": {
limit: 560,
limit: 570,
justification:
"ACP prewarm guards, project-aware working dir selection, working context sync, and chat bootstrapping still live together here.",
"ACP prewarm guards, project-aware working dir selection, working context sync, and chat bootstrapping still live together here. Includes gated [perf:chatview] logging via perfLog (dev-only by default).",
},
"src/features/chat/ui/__tests__/ContextPanel.test.tsx": {
limit: 550,
@@ -26,9 +26,9 @@ const EXCEPTIONS = {
"Search-as-you-type filtering and draft-aware sidebar highlight logic.",
},
"src/app/AppShell.tsx": {
limit: 650,
limit: 660,
justification:
"Shell still coordinates ACP session loading, replay-buffer cleanup on load failure, project reassignment, and app-level chat routing.",
"Shell still coordinates ACP session loading, replay-buffer cleanup on load failure, project reassignment, and app-level chat routing. Includes gated [perf:load]/[perf:newtab] logging via perfLog (dev-only by default).",
},
"src/features/chat/stores/__tests__/chatSessionStore.test.ts": {
limit: 540,
+30 -23
View File
@@ -22,6 +22,7 @@ import {
getAndDeleteReplayBuffer,
} from "@/features/chat/hooks/replayBuffer";
import { resolveSessionCwd } from "@/features/projects/lib/sessionCwdSelection";
import { perfLog } from "@/shared/lib/perfLog";
export type AppView =
| "home"
@@ -65,43 +66,43 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
);
const loadSessionMessages = useCallback(async (sessionId: string) => {
const existing = useChatStore.getState().messagesBySession[sessionId];
if (existing && existing.length > 0) {
console.log(
`[perf:load] ${sessionId.slice(0, 8)} skip — already has messages`,
);
const sid = sessionId.slice(0, 8);
const existingMsgs = useChatStore.getState().messagesBySession[sessionId];
if ((existingMsgs?.length ?? 0) > 0) {
perfLog(`[perf:load] ${sid} skip — has messages`);
return;
}
const t0 = performance.now();
console.log(`[perf:load] ${sessionId.slice(0, 8)} start`);
const store = useChatStore.getState();
store.setSessionLoading(sessionId, true);
perfLog(`[perf:load] ${sid} start`);
useChatStore.getState().setSessionLoading(sessionId, true);
try {
const [{ acpLoadSession }, { getReplayPerf, clearReplayPerf }] =
await Promise.all([
import("@/shared/api/acp"),
import("@/shared/api/acpNotificationHandler"),
]);
const t1 = performance.now();
const { acpLoadSession } = await import("@/shared/api/acp");
const t2 = performance.now();
console.log(
`[perf:load] ${sessionId.slice(0, 8)} import took ${(t2 - t1).toFixed(1)}ms`,
);
perfLog(`[perf:load] ${sid} import in ${(t1 - t0).toFixed(1)}ms`);
const session = useChatSessionStore.getState().getSession(sessionId);
const gooseSessionId = session?.acpSessionId ?? sessionId;
const project = session?.projectId
? (useProjectStore
.getState()
.projects.find((candidate) => candidate.id === session.projectId) ??
null)
.projects.find((p) => p.id === session.projectId) ?? null)
: null;
const workingDir = await resolveSessionCwd(project);
await acpLoadSession(sessionId, gooseSessionId, workingDir);
const tFlush = performance.now();
useChatStore.getState().setSessionLoading(sessionId, false);
const buffer = getAndDeleteReplayBuffer(sessionId);
const replayStats = getReplayPerf(sessionId);
clearReplayPerf(sessionId);
if (buffer && buffer.length > 0) {
useChatStore.getState().setMessages(sessionId, buffer);
}
const t3 = performance.now();
console.log(
`[perf:load] ${sessionId.slice(0, 8)} acpLoadSession resolved in ${(t3 - t2).toFixed(1)}ms (total ${(t3 - t0).toFixed(1)}ms)`,
const t2 = performance.now();
perfLog(
`[perf:load] ${sid} replay: notifs=${replayStats?.count ?? 0} span=${replayStats?.spanMs.toFixed(1) ?? "0"}ms msgs=${buffer?.length ?? 0} flush=${(t2 - tFlush).toFixed(1)}ms total=${(t2 - t0).toFixed(1)}ms`,
);
} catch (err) {
console.error("Failed to load session messages:", err);
@@ -160,6 +161,10 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
const createNewTab = useCallback(
(title = DEFAULT_CHAT_TITLE, project?: ProjectInfo) => {
const tStart = performance.now();
perfLog(
`[perf:newtab] createNewTab start (project=${project?.id ?? "none"})`,
);
const agentId = agentStore.activeAgentId ?? undefined;
const providerId = project?.preferredProvider ?? homeSelectedProvider;
const personaId = homeSelectedPersonaId;
@@ -186,11 +191,12 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
sessionState.setActiveSession(existingDraft.id);
setActiveView("chat");
chatStore.setActiveSession(existingDraft.id);
perfLog(
`[perf:newtab] ${existingDraft.id.slice(0, 8)} reused draft in ${(performance.now() - tStart).toFixed(1)}ms`,
);
return existingDraft;
}
cleanupEmptyDraft(sessionState.activeSessionId);
const session = sessionStore.createDraftSession({
title,
projectId: project?.id,
@@ -198,11 +204,12 @@ export function AppShell({ children }: { children?: React.ReactNode }) {
providerId,
personaId,
});
sessionStore.setActiveSession(session.id);
setActiveView("chat");
chatStore.setActiveSession(session.id);
perfLog(
`[perf:newtab] ${session.id.slice(0, 8)} created draft in ${(performance.now() - tStart).toFixed(1)}ms`,
);
return session;
},
[
+20 -2
View File
@@ -3,24 +3,35 @@ import { useAgentStore } from "@/features/agents/stores/agentStore";
import { useChatSessionStore } from "@/features/chat/stores/chatSessionStore";
import { setNotificationHandler, getClient } from "@/shared/api/acpConnection";
import notificationHandler from "@/shared/api/acpNotificationHandler";
import { perfLog } from "@/shared/lib/perfLog";
export function useAppStartup() {
useEffect(() => {
(async () => {
const tStartup = performance.now();
perfLog("[perf:startup] useAppStartup begin");
try {
const tConn = performance.now();
setNotificationHandler(notificationHandler);
await getClient();
perfLog(
`[perf:startup] ACP getClient ready in ${(performance.now() - tConn).toFixed(1)}ms`,
);
} catch (err) {
console.error("Failed to initialize ACP connection:", err);
}
const store = useAgentStore.getState();
const loadPersonas = async () => {
const t0 = performance.now();
store.setPersonasLoading(true);
try {
const { listPersonas } = await import("@/shared/api/agents");
const personas = await listPersonas();
store.setPersonas(personas);
perfLog(
`[perf:startup] loadPersonas done in ${(performance.now() - t0).toFixed(1)}ms (n=${personas.length})`,
);
} catch (err) {
console.error("Failed to load personas on startup:", err);
} finally {
@@ -29,11 +40,15 @@ export function useAppStartup() {
};
const loadProviders = async () => {
const t0 = performance.now();
store.setProvidersLoading(true);
try {
const { discoverAcpProviders } = await import("@/shared/api/acp");
const providers = await discoverAcpProviders();
store.setProviders(providers);
perfLog(
`[perf:startup] loadProviders done in ${(performance.now() - t0).toFixed(1)}ms (n=${providers.length})`,
);
} catch (err) {
console.error("Failed to load ACP providers on startup:", err);
} finally {
@@ -43,11 +58,11 @@ export function useAppStartup() {
const loadSessionState = async () => {
const t0 = performance.now();
console.log("[perf:startup] loadSessionState start");
perfLog("[perf:startup] loadSessionState start");
const { loadSessions, setActiveSession } =
useChatSessionStore.getState();
await loadSessions();
console.log(
perfLog(
`[perf:startup] loadSessions done in ${(performance.now() - t0).toFixed(1)}ms`,
);
setActiveSession(null);
@@ -58,6 +73,9 @@ export function useAppStartup() {
loadProviders(),
loadSessionState(),
]);
perfLog(
`[perf:startup] useAppStartup complete in ${(performance.now() - tStartup).toFixed(1)}ms`,
);
})();
}, []);
}
@@ -19,6 +19,7 @@ import {
isDefaultChatTitle,
} from "../lib/sessionTitle";
import { findLastIndex } from "@/shared/lib/arrays";
import { perfLog } from "@/shared/lib/perfLog";
import {
buildAcpImages,
buildAttachmentPromptPreamble,
@@ -129,6 +130,8 @@ export function useChat(
overridePersona?: { id: string; name?: string },
attachments?: ChatAttachmentDraft[],
) => {
const sid = sessionId.slice(0, 8);
const tSendStart = performance.now();
const images = buildAcpImages(attachments);
const hasAttachments = (attachments?.length ?? 0) > 0;
if (
@@ -137,6 +140,9 @@ export function useChat(
chatState === "thinking"
)
return;
perfLog(
`[perf:send] ${sid} useChat.sendMessage start (textLen=${text.length}, attachments=${attachments?.length ?? 0})`,
);
const effectivePersonaInfo = resolvePersonaInfo(
overridePersona?.id,
@@ -222,11 +228,19 @@ export function useChat(
if (!workingDir) {
throw new Error("Missing session working directory");
}
const tPrep = performance.now();
await acpPrepareSession(sessionId, providerId, workingDir, {
personaId: effectivePersonaInfo?.id,
});
perfLog(
`[perf:send] ${sid} acpPrepareSession in ${(performance.now() - tPrep).toFixed(1)}ms (wasDraft=${wasDraft})`,
);
if (selectedModelId) {
const tModel = performance.now();
await acpSetModel(sessionId, selectedModelId);
perfLog(
`[perf:send] ${sid} acpSetModel(${selectedModelId}) in ${(performance.now() - tModel).toFixed(1)}ms`,
);
}
}
@@ -237,6 +251,10 @@ export function useChat(
buildAttachmentPromptPreamble(attachments);
const promptBody = text.trim() || (images?.length ? " " : text);
const acpPrompt = `${attachmentPromptPreamble}${promptBody}`;
const tAcp = performance.now();
perfLog(
`[perf:send] ${sid} → acpSendMessage (setup took ${(tAcp - tSendStart).toFixed(1)}ms)`,
);
await acpSendMessage(sessionId, acpPrompt, {
systemPrompt,
personaId: effectivePersonaInfo?.id,
@@ -245,6 +263,9 @@ export function useChat(
(img) => [img.base64, img.mimeType] as [string, string],
),
});
perfLog(
`[perf:send] ${sid} acpSendMessage returned after ${(performance.now() - tAcp).toFixed(1)}ms (total sendMessage ${(performance.now() - tSendStart).toFixed(1)}ms)`,
);
store.setChatState(sessionId, "idle");
store.setStreamingMessageId(sessionId, null);
@@ -25,6 +25,7 @@ import { resolveSessionCwd } from "@/features/projects/lib/sessionCwdSelection";
import { ArtifactPolicyProvider } from "../hooks/ArtifactPolicyContext";
import type { ModelOption } from "../types";
import { ChatContextPanel } from "./ChatContextPanel";
import { perfLog } from "@/shared/lib/perfLog";
const EMPTY_MODELS: ModelOption[] = [];
@@ -51,6 +52,12 @@ export function ChatView({
}: ChatViewProps) {
const { t } = useTranslation("chat");
const activeSessionId = sessionId;
const mountStart = useRef(performance.now());
// biome-ignore lint/correctness/useExhaustiveDependencies: log once on mount per session
useEffect(() => {
const ms = (performance.now() - mountStart.current).toFixed(1);
perfLog(`[perf:chatview] ${sessionId.slice(0, 8)} mounted in ${ms}ms`);
}, [sessionId]);
const isContextPanelOpen = useChatSessionStore(
(s) => s.contextPanelOpenBySession[activeSessionId] ?? false,
);
+25
View File
@@ -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,
+33 -2
View File
@@ -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(
+16
View File
@@ -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": {
+35 -5
View File
@@ -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 });
+39
View File
@@ -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);
}
-13
View File
@@ -37,8 +37,6 @@ import type {
RemoveExtensionRequest,
RemoveSecretRequest,
UnarchiveSessionRequest,
UpdateProviderRequest,
UpdateProviderResponse,
UpdateWorkingDirRequest,
UpsertConfigRequest,
UpsertSecretRequest,
@@ -55,7 +53,6 @@ import {
zListProvidersResponse,
zReadConfigResponse,
zReadResourceResponse,
zUpdateProviderResponse,
} from './zod.gen.js';
export class GooseExtClient {
@@ -105,16 +102,6 @@ export class GooseExtClient {
) as GetSessionExtensionsResponse;
}
async GooseSessionProviderUpdate(
params: UpdateProviderRequest,
): Promise<UpdateProviderResponse> {
const raw = await this.conn.extMethod(
"_goose/session/provider/update",
params,
);
return zUpdateProviderResponse.parse(raw) as UpdateProviderResponse;
}
async GooseProvidersList(
params: ListProvidersRequest,
): Promise<ListProvidersResponse> {
+1 -6
View File
@@ -1,6 +1,6 @@
// This file is auto-generated by @hey-api/openapi-ts
export type { AddExtensionRequest, ArchiveSessionRequest, CheckSecretRequest, CheckSecretResponse, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetProviderDetailsRequest, GetProviderDetailsResponse, GetProviderModelsRequest, GetProviderModelsResponse, GetSessionExtensionsRequest, GetSessionExtensionsResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ListProvidersRequest, ListProvidersResponse, ModelEntry, ProviderConfigKey, ProviderDetailEntry, ProviderListEntry, ReadConfigRequest, ReadConfigResponse, ReadResourceRequest, ReadResourceResponse, RemoveConfigRequest, RemoveExtensionRequest, RemoveSecretRequest, UnarchiveSessionRequest, UpdateProviderRequest, UpdateProviderResponse, UpdateWorkingDirRequest, UpsertConfigRequest, UpsertSecretRequest } from './types.gen.js';
export type { AddExtensionRequest, ArchiveSessionRequest, CheckSecretRequest, CheckSecretResponse, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetProviderDetailsRequest, GetProviderDetailsResponse, GetProviderModelsRequest, GetProviderModelsResponse, GetSessionExtensionsRequest, GetSessionExtensionsResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ListProvidersRequest, ListProvidersResponse, ModelEntry, ProviderConfigKey, ProviderDetailEntry, ProviderListEntry, ReadConfigRequest, ReadConfigResponse, ReadResourceRequest, ReadResourceResponse, RemoveConfigRequest, RemoveExtensionRequest, RemoveSecretRequest, UnarchiveSessionRequest, UpdateWorkingDirRequest, UpsertConfigRequest, UpsertSecretRequest } from './types.gen.js';
export const GOOSE_EXT_METHODS = [
{
@@ -43,11 +43,6 @@ export const GOOSE_EXT_METHODS = [
requestType: "GetSessionExtensionsRequest",
responseType: "GetSessionExtensionsResponse",
},
{
method: "_goose/session/provider/update",
requestType: "UpdateProviderRequest",
responseType: "UpdateProviderResponse",
},
{
method: "_goose/providers/list",
requestType: "ListProvidersRequest",
+2 -25
View File
@@ -104,29 +104,6 @@ export type GetSessionExtensionsResponse = {
extensions: Array<unknown>;
};
/**
* Atomically update the provider for a live session.
*/
export type UpdateProviderRequest = {
sessionId: string;
provider: string;
model?: string | null;
contextLimit?: number | null;
requestParams?: {
[key: string]: unknown;
} | null;
};
/**
* Provider update response.
*/
export type UpdateProviderResponse = {
/**
* Refreshed session config options after the provider/model change.
*/
configOptions: Array<unknown>;
};
/**
* List providers available through goose, including the config-default sentinel.
*/
@@ -307,14 +284,14 @@ export type UnarchiveSessionRequest = {
export type ExtRequest = {
id: string;
method: string;
params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | DeleteSessionRequest | GetExtensionsRequest | GetSessionExtensionsRequest | UpdateProviderRequest | ListProvidersRequest | GetProviderDetailsRequest | GetProviderModelsRequest | ReadConfigRequest | UpsertConfigRequest | RemoveConfigRequest | CheckSecretRequest | UpsertSecretRequest | RemoveSecretRequest | ExportSessionRequest | ImportSessionRequest | ArchiveSessionRequest | UnarchiveSessionRequest | {
params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | DeleteSessionRequest | GetExtensionsRequest | GetSessionExtensionsRequest | ListProvidersRequest | GetProviderDetailsRequest | GetProviderModelsRequest | ReadConfigRequest | UpsertConfigRequest | RemoveConfigRequest | CheckSecretRequest | UpsertSecretRequest | RemoveSecretRequest | ExportSessionRequest | ImportSessionRequest | ArchiveSessionRequest | UnarchiveSessionRequest | {
[key: string]: unknown;
} | null;
};
export type ExtResponse = {
id: string;
result?: EmptyResponse | GetToolsResponse | ReadResourceResponse | GetExtensionsResponse | GetSessionExtensionsResponse | UpdateProviderResponse | ListProvidersResponse | GetProviderDetailsResponse | GetProviderModelsResponse | ReadConfigResponse | CheckSecretResponse | ExportSessionResponse | ImportSessionResponse | unknown;
result?: EmptyResponse | GetToolsResponse | ReadResourceResponse | GetExtensionsResponse | GetSessionExtensionsResponse | ListProvidersResponse | GetProviderDetailsResponse | GetProviderModelsResponse | ReadConfigResponse | CheckSecretResponse | ExportSessionResponse | ImportSessionResponse | unknown;
} | {
error: {
code: number;
-29
View File
@@ -89,33 +89,6 @@ export const zGetSessionExtensionsResponse = z.object({
extensions: z.array(z.unknown())
});
/**
* Atomically update the provider for a live session.
*/
export const zUpdateProviderRequest = z.object({
sessionId: z.string(),
provider: z.string(),
model: z.union([
z.string(),
z.null()
]).optional(),
contextLimit: z.union([
z.number().int().gte(0),
z.null()
]).optional(),
requestParams: z.union([
z.record(z.unknown()),
z.null()
]).optional()
});
/**
* Provider update response.
*/
export const zUpdateProviderResponse = z.object({
configOptions: z.array(z.unknown())
});
/**
* List providers available through goose, including the config-default sentinel.
*/
@@ -311,7 +284,6 @@ export const zExtRequest = z.object({
zDeleteSessionRequest,
zGetExtensionsRequest,
zGetSessionExtensionsRequest,
zUpdateProviderRequest,
zListProvidersRequest,
zGetProviderDetailsRequest,
zGetProviderModelsRequest,
@@ -343,7 +315,6 @@ export const zExtResponse = z.union([
zReadResourceResponse,
zGetExtensionsResponse,
zGetSessionExtensionsResponse,
zUpdateProviderResponse,
zListProvidersResponse,
zGetProviderDetailsResponse,
zGetProviderModelsResponse,