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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user