Merge pull request #70 from getpaseo/diagnostics/android-hang-cleanup-policy

Centralize perf diagnostics and activity coalescing
This commit is contained in:
Mohamed Boudra
2026-02-22 11:57:33 +07:00
committed by GitHub
24 changed files with 1119 additions and 81 deletions
@@ -97,6 +97,13 @@ const consoleLogger: Logger = {
error: (obj, msg) => console.error(msg, obj),
}
function getNowMs(): number {
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
return performance.now()
}
return Date.now()
}
export type {
DaemonTransport,
DaemonTransportFactory,
@@ -163,8 +170,26 @@ export type DaemonClientConfig = {
baseDelayMs?: number
maxDelayMs?: number
}
onDiagnosticsEvent?: (event: DaemonClientDiagnosticsEvent) => void
}
export type DaemonClientDiagnosticsEvent =
| {
type: 'transport_message_timing'
messageType: string
payloadBytes: number
parseMs: number
validateMs: number
totalMs: number
outcome: 'ok' | 'parse_error' | 'validation_error'
}
| {
type: 'transport_binary_frame'
channel: BinaryMuxChannel
messageType: number
payloadBytes: number
}
export type SendMessageOptions = {
messageId?: string
images?: Array<{ data: string; mimeType: string }>
@@ -432,6 +457,14 @@ export class DaemonClient {
})
}
private emitDiagnosticsEvent(event: DaemonClientDiagnosticsEvent): void {
try {
this.config.onDiagnosticsEvent?.(event)
} catch {
// Diagnostics hooks must never break daemon message handling.
}
}
// ============================================================================
// Connection
// ============================================================================
@@ -2724,12 +2757,19 @@ export class DaemonClient {
}
private handleTransportMessage(data: unknown): void {
const startedAtMs = getNowMs()
const rawData =
data && typeof data === 'object' && 'data' in data ? (data as { data: unknown }).data : data
const rawBytes = asUint8Array(rawData)
if (rawBytes) {
const frame = decodeBinaryMuxFrame(rawBytes)
if (frame) {
this.emitDiagnosticsEvent({
type: 'transport_binary_frame',
channel: frame.channel,
messageType: frame.messageType,
payloadBytes: frame.payload?.byteLength ?? 0,
})
this.handleBinaryFrame(frame)
return
}
@@ -2740,19 +2780,52 @@ export class DaemonClient {
}
let parsedJson: unknown
let parseMs = 0
try {
const parseStartedAtMs = getNowMs()
parsedJson = JSON.parse(payload)
parseMs = getNowMs() - parseStartedAtMs
} catch {
this.emitDiagnosticsEvent({
type: 'transport_message_timing',
messageType: 'unknown',
payloadBytes: payload.length,
parseMs: 0,
validateMs: 0,
totalMs: getNowMs() - startedAtMs,
outcome: 'parse_error',
})
return
}
const validateStartedAtMs = getNowMs()
const parsed = WSOutboundMessageSchema.safeParse(parsedJson)
const validateMs = getNowMs() - validateStartedAtMs
if (!parsed.success) {
const msgType = (parsedJson as { type?: string })?.type ?? 'unknown'
this.emitDiagnosticsEvent({
type: 'transport_message_timing',
messageType: msgType,
payloadBytes: payload.length,
parseMs,
validateMs,
totalMs: getNowMs() - startedAtMs,
outcome: 'validation_error',
})
this.logger.warn({ msgType, error: parsed.error.message }, 'Message validation failed')
return
}
this.emitDiagnosticsEvent({
type: 'transport_message_timing',
messageType: parsed.data.type,
payloadBytes: payload.length,
parseMs,
validateMs,
totalMs: getNowMs() - startedAtMs,
outcome: 'ok',
})
if (parsed.data.type === 'pong') {
return
}