perf(app): reduce startup websocket work

Make provider catalogs compact and content-addressed so repeat launches can reuse the persisted snapshot instead of transferring it again. Remove eager global catalog loading and the stopped-agent defensive tail fetch, while keeping sequence-gap and reconnect recovery as the correctness paths. Add an opt-in production-ID Android profiling build for local startup traces.
This commit is contained in:
Mohamed Boudra
2026-08-04 15:45:55 +02:00
parent 8e5b6876b4
commit 7dbeff8290
36 changed files with 1209 additions and 171 deletions
+136 -30
View File
@@ -317,9 +317,16 @@ export interface DaemonClientConfig {
};
runtimeMetricsIntervalMs?: number;
runtimeMetricsWindowMs?: number;
trace?: DaemonClientTrace;
capabilities?: Partial<Record<ClientCapability, unknown>>;
}
export interface DaemonClientTrace {
isEnabled(): boolean;
beginSection(name: string, args?: Record<string, string>): void;
endSection(): void;
}
export interface SendMessageOptions {
messageId?: string;
images?: Array<{ data: string; mimeType: string }>;
@@ -1035,6 +1042,26 @@ function concatByteChunks(chunks: Uint8Array[], size: number): Uint8Array {
return bytes;
}
function getTransportFrameSize(frame: string | Uint8Array | ArrayBuffer): number {
if (typeof frame === "string") {
return frame.length;
}
return frame.byteLength;
}
function describeInboundTransportFrame(
frame: unknown,
rawBytes: Uint8Array | null,
): Record<string, string> {
if (typeof frame === "string") {
return { kind: "text", size: String(frame.length) };
}
if (rawBytes) {
return { kind: "binary", size: String(rawBytes.byteLength) };
}
return { kind: "unknown", size: "0" };
}
function hashForLog(value: string): string {
let hash = 0;
for (let index = 0; index < value.length; index += 1) {
@@ -1535,6 +1562,49 @@ export class DaemonClient {
// Core Send Helpers
// ============================================================================
private beginTraceSection(name: string, args?: Record<string, string>): boolean {
const trace = this.config.trace;
if (!trace?.isEnabled()) {
return false;
}
trace.beginSection(name, args);
return true;
}
private endTraceSection(isOpen: boolean): void {
if (isOpen) {
this.config.trace?.endSection();
}
}
private traceInstant(name: string, args?: Record<string, string>): void {
const isOpen = this.beginTraceSection(name, args);
this.endTraceSection(isOpen);
}
private sendJsonMessage(envelopeType: string, messageType: string, message: unknown): void {
this.traceInstant("paseo.ws.message.outbound", {
envelopeType,
messageType,
});
this.sendTransportFrame(JSON.stringify(message));
}
private sendTransportFrame(frame: string | Uint8Array | ArrayBuffer): void {
if (!this.transport) {
throw new Error("Transport not connected");
}
const isOpen = this.beginTraceSection("paseo.ws.frame.outbound", {
kind: typeof frame === "string" ? "text" : "binary",
size: String(getTransportFrameSize(frame)),
});
try {
this.transport.send(frame);
} finally {
this.endTraceSection(isOpen);
}
}
/**
* Send a session message. For fire-and-forget messages (heartbeats, etc.),
* failures are suppressed if `suppressSendErrors` is configured.
@@ -1549,7 +1619,7 @@ export class DaemonClient {
}
const payload = SessionInboundMessageSchema.parse(message);
try {
this.transport.send(JSON.stringify({ type: "session", message: payload }));
this.sendJsonMessage("session", payload.type, { type: "session", message: payload });
} catch (error) {
if (this.config.suppressSendErrors) {
return;
@@ -1566,7 +1636,11 @@ export class DaemonClient {
throw new Error(`Transport not connected (status: ${this.connectionState.status})`);
}
try {
this.transport.send(frame);
this.traceInstant("paseo.ws.message.outbound", {
envelopeType: "binary",
messageType: "binary",
});
this.sendTransportFrame(frame);
} catch (error) {
if (this.config.suppressSendErrors) {
return;
@@ -1587,7 +1661,7 @@ export class DaemonClient {
// If connected, send immediately
if (this.transport && status === "connected") {
const payload = SessionInboundMessageSchema.parse(message);
this.transport.send(JSON.stringify({ type: "session", message: payload }));
this.sendJsonMessage("session", payload.type, { type: "session", message: payload });
return Promise.resolve();
}
@@ -1623,7 +1697,7 @@ export class DaemonClient {
try {
if (this.transport && this.connectionState.status === "connected") {
const payload = SessionInboundMessageSchema.parse(pending.message);
this.transport.send(JSON.stringify({ type: "session", message: payload }));
this.sendJsonMessage("session", payload.type, { type: "session", message: payload });
pending.resolve();
} else {
pending.reject(new Error("Connection lost before message could be sent"));
@@ -1777,7 +1851,7 @@ export class DaemonClient {
}
const payload = SessionInboundMessageSchema.parse(message);
try {
this.transport.send(JSON.stringify({ type: "session", message: payload }));
this.sendJsonMessage("session", payload.type, { type: "session", message: payload });
} catch (error) {
throw error instanceof Error ? error : new Error(String(error));
}
@@ -1952,7 +2026,7 @@ export class DaemonClient {
this.pingProbe = probe;
try {
this.transport.send(JSON.stringify({ type: "ping" }));
this.sendJsonMessage("ping", "ping", { type: "ping" });
} catch (error) {
this.clearPingProbe();
const sendError = error instanceof Error ? error : new Error(String(error));
@@ -4421,6 +4495,7 @@ export class DaemonClient {
async getProvidersSnapshot(options?: {
cwd?: string;
ifNoneMatch?: string;
requestId?: string;
}): Promise<GetProvidersSnapshotPayload> {
const payload = await this.sendCorrelatedSessionRequest({
@@ -4428,6 +4503,7 @@ export class DaemonClient {
message: {
type: "get_providers_snapshot_request",
cwd: options?.cwd,
ifNoneMatch: options?.ifNoneMatch,
},
responseType: "get_providers_snapshot_response",
});
@@ -5326,23 +5402,22 @@ export class DaemonClient {
}
try {
this.transport.send(
JSON.stringify({
type: "hello",
clientId: this.config.clientId,
clientType: this.config.clientType ?? "cli",
protocolVersion: 1,
capabilities: {
[CLIENT_CAPS.customModeIcons]: true,
[CLIENT_CAPS.reasoningMergeEnum]: true,
[CLIENT_CAPS.terminalReflowableSnapshot]: true,
[CLIENT_CAPS.providerSubagents]: true,
[CLIENT_CAPS.projectUpdates]: true,
...this.config.capabilities,
},
...(this.config.appVersion ? { appVersion: this.config.appVersion } : {}),
}),
);
this.sendJsonMessage("hello", "hello", {
type: "hello",
clientId: this.config.clientId,
clientType: this.config.clientType ?? "cli",
protocolVersion: 1,
capabilities: {
[CLIENT_CAPS.customModeIcons]: true,
[CLIENT_CAPS.reasoningMergeEnum]: true,
[CLIENT_CAPS.terminalReflowableSnapshot]: true,
[CLIENT_CAPS.providerSubagents]: true,
[CLIENT_CAPS.projectUpdates]: true,
[CLIENT_CAPS.compactProviderSnapshots]: true,
...this.config.capabilities,
},
...(this.config.appVersion ? { appVersion: this.config.appVersion } : {}),
});
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to send hello message";
this.lastErrorValue = message;
@@ -5413,24 +5488,37 @@ export class DaemonClient {
}
const rawBytes = asUint8Array(rawData);
if (rawBytes && this.tryHandleBinaryFrame(rawBytes)) {
return;
const isOpen = this.beginTraceSection(
"paseo.ws.frame.inbound",
describeInboundTransportFrame(rawData, rawBytes),
);
try {
if (rawBytes && this.tryHandleBinaryFrame(rawBytes)) {
return;
}
const payload = decodeMessageData(rawData);
if (!payload) {
return;
}
this.handleJsonPayload(payload, rawBytes?.byteLength);
} finally {
this.endTraceSection(isOpen);
}
const payload = decodeMessageData(rawData);
if (!payload) {
return;
}
this.handleJsonPayload(payload, rawBytes?.byteLength);
}
private handleJsonPayload(payload: string, rawBytesLength: number | undefined): void {
const bytes = rawBytesLength ?? payload.length;
const startMs = perfNow();
let parsedJson: unknown;
const parseTraceOpen = this.beginTraceSection("paseo.ws.json.parse", {
size: String(bytes),
});
try {
parsedJson = JSON.parse(payload);
} catch {
return;
} finally {
this.endTraceSection(parseTraceOpen);
}
const parsed = validateWSOutboundMessage(parsedJson);
@@ -5457,11 +5545,19 @@ export class DaemonClient {
this.consecutiveLivenessFailures = 0;
if (parsed.data.type === "pong") {
this.traceInstant("paseo.ws.message.inbound", {
envelopeType: "pong",
messageType: "pong",
});
this.resolvePingProbe();
this.runtimeMetrics?.recordMessage("pong", bytes, perfNow() - startMs);
return;
}
this.traceInstant("paseo.ws.message.inbound", {
envelopeType: "session",
messageType: parsed.data.message.type,
});
this.handleSessionMessage(parsed.data.message);
const msgType = parsed.data.message.type;
this.runtimeMetrics?.recordMessage(msgType, bytes, perfNow() - startMs);
@@ -5473,6 +5569,11 @@ export class DaemonClient {
private tryHandleBinaryFrame(rawBytes: Uint8Array): boolean {
const fileFrame = decodeFileTransferFrame(rawBytes);
if (fileFrame) {
this.traceInstant("paseo.ws.message.inbound", {
envelopeType: "binary",
messageType: "file",
opcode: String(fileFrame.opcode),
});
this.consecutiveLivenessFailures = 0;
this.handleFileTransferFrame(fileFrame);
this.runtimeMetrics?.recordBinaryFrame("other", rawBytes.byteLength, 0);
@@ -5483,6 +5584,11 @@ export class DaemonClient {
if (!frame) {
return false;
}
this.traceInstant("paseo.ws.message.inbound", {
envelopeType: "binary",
messageType: "terminal",
opcode: String(frame.opcode),
});
this.consecutiveLivenessFailures = 0;
const binaryStartMs = perfNow();
this.terminalStreams.handleFrame(frame);