refactor(app): centralize perf diagnostics and activity coalescing
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
export type {
|
||||
ActivityFlushHandle,
|
||||
AgentLastActivityCoalescer,
|
||||
AgentLastActivityCommitter,
|
||||
AgentLastActivityUpdates,
|
||||
} from "./types";
|
||||
|
||||
export { scheduleAgentLastActivityFlush } from "./last-activity-scheduler";
|
||||
export { createAgentLastActivityCoalescer } from "./last-activity-coalescer";
|
||||
@@ -0,0 +1,67 @@
|
||||
import { scheduleAgentLastActivityFlush } from "./last-activity-scheduler";
|
||||
import type {
|
||||
ActivityFlushHandle,
|
||||
AgentLastActivityCoalescer,
|
||||
AgentLastActivityCommitter,
|
||||
} from "./types";
|
||||
|
||||
export function createAgentLastActivityCoalescer(): AgentLastActivityCoalescer {
|
||||
let pending = new Map<string, Date>();
|
||||
let scheduledFlush: ActivityFlushHandle | null = null;
|
||||
let committer: AgentLastActivityCommitter | null = null;
|
||||
|
||||
const cancelScheduledFlush = () => {
|
||||
if (!scheduledFlush) {
|
||||
return;
|
||||
}
|
||||
scheduledFlush.cancel();
|
||||
scheduledFlush = null;
|
||||
};
|
||||
|
||||
const flushNow = () => {
|
||||
cancelScheduledFlush();
|
||||
if (pending.size === 0) {
|
||||
return;
|
||||
}
|
||||
const updates = pending;
|
||||
pending = new Map();
|
||||
committer?.(updates);
|
||||
};
|
||||
|
||||
const scheduleFlush = () => {
|
||||
if (scheduledFlush) {
|
||||
return;
|
||||
}
|
||||
scheduledFlush = scheduleAgentLastActivityFlush(() => {
|
||||
scheduledFlush = null;
|
||||
flushNow();
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
setCommitter(nextCommitter) {
|
||||
committer = nextCommitter;
|
||||
},
|
||||
|
||||
enqueue(agentId, timestamp) {
|
||||
const current = pending.get(agentId);
|
||||
if (current && current.getTime() >= timestamp.getTime()) {
|
||||
return;
|
||||
}
|
||||
pending.set(agentId, timestamp);
|
||||
scheduleFlush();
|
||||
},
|
||||
|
||||
flushNow,
|
||||
|
||||
deletePending(agentId) {
|
||||
pending.delete(agentId);
|
||||
},
|
||||
|
||||
dispose() {
|
||||
cancelScheduledFlush();
|
||||
pending.clear();
|
||||
committer = null;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { ActivityFlushHandle } from "./types";
|
||||
|
||||
const DEFAULT_ACTIVITY_FLUSH_INTERVAL_MS = 16;
|
||||
|
||||
export function scheduleAgentLastActivityFlush(
|
||||
callback: () => void,
|
||||
fallbackIntervalMs = DEFAULT_ACTIVITY_FLUSH_INTERVAL_MS
|
||||
): ActivityFlushHandle {
|
||||
if (typeof requestAnimationFrame === "function") {
|
||||
const handle = requestAnimationFrame(callback);
|
||||
return {
|
||||
cancel: () => {
|
||||
if (typeof cancelAnimationFrame === "function") {
|
||||
cancelAnimationFrame(handle);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const handle = setTimeout(callback, fallbackIntervalMs);
|
||||
return {
|
||||
cancel: () => {
|
||||
clearTimeout(handle);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export type AgentLastActivityUpdates = Map<string, Date>;
|
||||
|
||||
export type AgentLastActivityCommitter = (
|
||||
updates: AgentLastActivityUpdates
|
||||
) => void;
|
||||
|
||||
export interface ActivityFlushHandle {
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
export interface AgentLastActivityCoalescer {
|
||||
setCommitter: (committer: AgentLastActivityCommitter | null) => void;
|
||||
enqueue: (agentId: string, timestamp: Date) => void;
|
||||
flushNow: () => void;
|
||||
deletePending: (agentId: string) => void;
|
||||
dispose: () => void;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useSyncExternalStore } from "react";
|
||||
import {
|
||||
DaemonClient,
|
||||
type ConnectionState,
|
||||
type DaemonClientDiagnosticsEvent,
|
||||
type FetchAgentsOptions,
|
||||
} from "@server/client/daemon-client";
|
||||
import type { HostConnection, HostProfile } from "@/contexts/daemon-registry-context";
|
||||
@@ -19,6 +20,10 @@ import {
|
||||
import { createTauriWebSocketTransportFactory } from "@/utils/tauri-daemon-transport";
|
||||
import { applyFetchedAgentDirectory } from "@/utils/agent-directory-sync";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import {
|
||||
recordDaemonClientDiagnostics,
|
||||
recordHostRuntimeCreateClient,
|
||||
} from "@/runtime/perf-diagnostics/host-runtime-diagnostics";
|
||||
|
||||
export type HostRuntimeConnectionStatus =
|
||||
| "idle"
|
||||
@@ -375,12 +380,22 @@ function selectInitialConnectionId(host: HostProfile): string | null {
|
||||
function createDefaultDeps(): HostRuntimeControllerDeps {
|
||||
return {
|
||||
createClient: ({ host, connection, clientId, runtimeGeneration }) => {
|
||||
recordHostRuntimeCreateClient({
|
||||
serverId: host.serverId,
|
||||
connectionType: connection.type,
|
||||
endpoint:
|
||||
connection.type === "direct"
|
||||
? connection.endpoint
|
||||
: connection.relayEndpoint,
|
||||
});
|
||||
const tauriTransportFactory = createTauriWebSocketTransportFactory();
|
||||
const base = {
|
||||
suppressSendErrors: true,
|
||||
clientId,
|
||||
clientType: "mobile" as const,
|
||||
runtimeGeneration,
|
||||
onDiagnosticsEvent: (event: DaemonClientDiagnosticsEvent) =>
|
||||
recordDaemonClientDiagnostics(host.serverId, event),
|
||||
...(tauriTransportFactory
|
||||
? { transportFactory: tauriTransportFactory }
|
||||
: {}),
|
||||
@@ -1099,10 +1114,10 @@ export class HostRuntimeStore {
|
||||
filter: input.filter ?? { labels: { ui: "true" } },
|
||||
...(input.subscribe ? { subscribe: input.subscribe } : {}),
|
||||
});
|
||||
const { agents } = applyFetchedAgentDirectory({
|
||||
const agents = applyFetchedAgentDirectory({
|
||||
serverId: input.serverId,
|
||||
entries: payload.entries,
|
||||
});
|
||||
}).agents;
|
||||
controller.markAgentDirectorySyncReady();
|
||||
return {
|
||||
agents,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
consumePersistedPerfDiagnosticReports,
|
||||
getPerfDiagnosticsSnapshot,
|
||||
isPerfDiagnosticsEnabled,
|
||||
peekPersistedPerfDiagnosticReports,
|
||||
} from "./engine";
|
||||
|
||||
export function installPerfDiagnosticsDebugTools(): () => void {
|
||||
if (!isPerfDiagnosticsEnabled()) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
(
|
||||
globalThis as {
|
||||
__PASEO_PERF_DIAGNOSTICS_DEBUG__?: {
|
||||
snapshot: (limit?: number) => ReturnType<typeof getPerfDiagnosticsSnapshot>;
|
||||
consumeReports: () => Promise<
|
||||
Awaited<ReturnType<typeof consumePersistedPerfDiagnosticReports>>
|
||||
>;
|
||||
peekReports: () => Promise<
|
||||
Awaited<ReturnType<typeof peekPersistedPerfDiagnosticReports>>
|
||||
>;
|
||||
};
|
||||
}
|
||||
).__PASEO_PERF_DIAGNOSTICS_DEBUG__ = {
|
||||
snapshot: (limit?: number) => getPerfDiagnosticsSnapshot(limit ?? 120),
|
||||
consumeReports: () => consumePersistedPerfDiagnosticReports(),
|
||||
peekReports: () => peekPersistedPerfDiagnosticReports(),
|
||||
};
|
||||
|
||||
return () => {
|
||||
(
|
||||
globalThis as {
|
||||
__PASEO_PERF_DIAGNOSTICS_DEBUG__?: unknown;
|
||||
}
|
||||
).__PASEO_PERF_DIAGNOSTICS_DEBUG__ = undefined;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { Platform } from "react-native";
|
||||
import { getNowMs } from "@/utils/perf";
|
||||
import type {
|
||||
PerfDiagnosticBreadcrumb,
|
||||
PerfDiagnosticFields,
|
||||
PerfDiagnosticsReport,
|
||||
PerfDiagnosticsSnapshot,
|
||||
RecordPerfDiagnosticMarkOptions,
|
||||
} from "./types";
|
||||
|
||||
const STORAGE_KEY = "paseo:perf-diagnostics:v1";
|
||||
const LEGACY_STORAGE_KEY = "paseo:js-hang-diagnostics:v1";
|
||||
const MONITOR_INTERVAL_MS = 100;
|
||||
const STALL_THRESHOLD_MS = 250;
|
||||
const SAMPLE_EVERY_N = 40;
|
||||
const MAX_BREADCRUMBS = 600;
|
||||
const BREADCRUMBS_PER_REPORT = 220;
|
||||
const MAX_STORED_REPORTS = 20;
|
||||
|
||||
const state = {
|
||||
breadcrumbs: [] as PerfDiagnosticBreadcrumb[],
|
||||
sampleCursor: 0,
|
||||
monitorHandle: null as ReturnType<typeof setInterval> | null,
|
||||
monitorLastTickMs: 0,
|
||||
monitorScope: null as string | null,
|
||||
monitorRefCount: 0,
|
||||
loadLogged: false,
|
||||
persistQueue: Promise.resolve(),
|
||||
};
|
||||
|
||||
function isPrimitive(value: unknown): value is string | number | boolean | null {
|
||||
return (
|
||||
value === null ||
|
||||
typeof value === "string" ||
|
||||
typeof value === "number" ||
|
||||
typeof value === "boolean"
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeFieldValue(value: unknown): unknown {
|
||||
if (isPrimitive(value)) {
|
||||
if (typeof value === "string" && value.length > 180) {
|
||||
return `${value.slice(0, 180)}...`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return `[array:${value.length}]`;
|
||||
}
|
||||
if (value instanceof Map) {
|
||||
return `[map:${value.size}]`;
|
||||
}
|
||||
if (value instanceof Set) {
|
||||
return `[set:${value.size}]`;
|
||||
}
|
||||
if (typeof value === "bigint") {
|
||||
return value.toString();
|
||||
}
|
||||
if (value === undefined) {
|
||||
return "[undefined]";
|
||||
}
|
||||
return "[object]";
|
||||
}
|
||||
|
||||
function sanitizeFields(
|
||||
fields?: PerfDiagnosticFields
|
||||
): PerfDiagnosticFields | undefined {
|
||||
if (!fields) {
|
||||
return undefined;
|
||||
}
|
||||
const next: PerfDiagnosticFields = {};
|
||||
let count = 0;
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
if (count >= 16) {
|
||||
next.__truncated__ = true;
|
||||
break;
|
||||
}
|
||||
next[key] = sanitizeFieldValue(value);
|
||||
count += 1;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function shouldEnableDiagnostics(): boolean {
|
||||
const globalFlag = (
|
||||
globalThis as {
|
||||
__PASEO_PERF_DIAGNOSTICS__?: unknown;
|
||||
__PASEO_JS_HANG_DIAGNOSTICS__?: unknown;
|
||||
}
|
||||
).__PASEO_PERF_DIAGNOSTICS__;
|
||||
const legacyFlag = (
|
||||
globalThis as {
|
||||
__PASEO_PERF_DIAGNOSTICS__?: unknown;
|
||||
__PASEO_JS_HANG_DIAGNOSTICS__?: unknown;
|
||||
}
|
||||
).__PASEO_JS_HANG_DIAGNOSTICS__;
|
||||
if (typeof globalFlag === "boolean") {
|
||||
return globalFlag;
|
||||
}
|
||||
if (typeof legacyFlag === "boolean") {
|
||||
return legacyFlag;
|
||||
}
|
||||
const isDev = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
||||
return Platform.OS === "android" || isDev;
|
||||
}
|
||||
|
||||
function shouldSample(): boolean {
|
||||
state.sampleCursor += 1;
|
||||
if (state.sampleCursor >= SAMPLE_EVERY_N) {
|
||||
state.sampleCursor = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function pushBreadcrumb(item: PerfDiagnosticBreadcrumb): void {
|
||||
state.breadcrumbs.push(item);
|
||||
if (state.breadcrumbs.length > MAX_BREADCRUMBS) {
|
||||
state.breadcrumbs.splice(0, state.breadcrumbs.length - MAX_BREADCRUMBS);
|
||||
}
|
||||
}
|
||||
|
||||
function safeParseReports(raw: string | null): PerfDiagnosticsReport[] {
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
return parsed.filter(
|
||||
(entry) => entry && typeof entry === "object"
|
||||
) as PerfDiagnosticsReport[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function readPersistedReports(): Promise<PerfDiagnosticsReport[]> {
|
||||
const stored = safeParseReports(await AsyncStorage.getItem(STORAGE_KEY));
|
||||
if (stored.length > 0) {
|
||||
return stored;
|
||||
}
|
||||
const legacy = safeParseReports(await AsyncStorage.getItem(LEGACY_STORAGE_KEY));
|
||||
if (legacy.length > 0) {
|
||||
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(legacy));
|
||||
await AsyncStorage.removeItem(LEGACY_STORAGE_KEY);
|
||||
}
|
||||
return legacy;
|
||||
}
|
||||
|
||||
async function persistReport(report: PerfDiagnosticsReport): Promise<void> {
|
||||
state.persistQueue = state.persistQueue
|
||||
.then(async () => {
|
||||
const existing = await readPersistedReports();
|
||||
existing.push(report);
|
||||
if (existing.length > MAX_STORED_REPORTS) {
|
||||
existing.splice(0, existing.length - MAX_STORED_REPORTS);
|
||||
}
|
||||
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(existing));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn("[PerfDiagnostics] Failed to persist report", {
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
});
|
||||
await state.persistQueue;
|
||||
}
|
||||
|
||||
function buildReport(
|
||||
scope: string,
|
||||
atMs: number,
|
||||
lagMs: number
|
||||
): PerfDiagnosticsReport {
|
||||
const id = `${Math.round(atMs)}-${Math.floor(Math.random() * 1_000_000)}`;
|
||||
const breadcrumbs = state.breadcrumbs.slice(-BREADCRUMBS_PER_REPORT);
|
||||
const wallTimeMs = Date.now();
|
||||
return {
|
||||
id,
|
||||
scope,
|
||||
atMs,
|
||||
wallTimeMs,
|
||||
wallTimeIso: new Date(wallTimeMs).toISOString(),
|
||||
lagMs,
|
||||
platform: Platform.OS,
|
||||
breadcrumbs,
|
||||
};
|
||||
}
|
||||
|
||||
export function recordPerfDiagnosticMark(
|
||||
name: string,
|
||||
fields?: PerfDiagnosticFields,
|
||||
options?: RecordPerfDiagnosticMarkOptions
|
||||
): void {
|
||||
if (!shouldEnableDiagnostics()) {
|
||||
return;
|
||||
}
|
||||
const force = options?.force === true;
|
||||
if (!force && !shouldSample()) {
|
||||
return;
|
||||
}
|
||||
pushBreadcrumb({
|
||||
atMs: getNowMs(),
|
||||
kind: "mark",
|
||||
name,
|
||||
fields: sanitizeFields(fields),
|
||||
});
|
||||
}
|
||||
|
||||
export function installPerfDiagnosticsMonitor(scope: string): () => void {
|
||||
if (!shouldEnableDiagnostics()) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
if (state.monitorRefCount === 0) {
|
||||
state.monitorScope = scope;
|
||||
state.monitorLastTickMs = getNowMs();
|
||||
state.monitorHandle = setInterval(() => {
|
||||
const nowMs = getNowMs();
|
||||
const lagMs = nowMs - state.monitorLastTickMs - MONITOR_INTERVAL_MS;
|
||||
if (lagMs >= STALL_THRESHOLD_MS) {
|
||||
const report = buildReport(state.monitorScope ?? scope, nowMs, lagMs);
|
||||
recordPerfDiagnosticMark(
|
||||
"perf.stall_detected",
|
||||
{
|
||||
scope: report.scope,
|
||||
lagMs: Math.round(lagMs),
|
||||
reportId: report.id,
|
||||
breadcrumbs: report.breadcrumbs.length,
|
||||
},
|
||||
{ force: true }
|
||||
);
|
||||
console.warn("[PerfDiagnostics] JS stall detected", {
|
||||
scope: report.scope,
|
||||
lagMs: Math.round(lagMs),
|
||||
reportId: report.id,
|
||||
breadcrumbs: report.breadcrumbs.length,
|
||||
});
|
||||
void persistReport(report);
|
||||
}
|
||||
state.monitorLastTickMs = nowMs;
|
||||
}, MONITOR_INTERVAL_MS);
|
||||
}
|
||||
|
||||
state.monitorRefCount += 1;
|
||||
|
||||
if (!state.loadLogged) {
|
||||
state.loadLogged = true;
|
||||
void readPersistedReports()
|
||||
.then((reports) => {
|
||||
if (reports.length > 0) {
|
||||
console.warn("[PerfDiagnostics] Recovered persisted stall reports", {
|
||||
count: reports.length,
|
||||
latestReportId: reports[reports.length - 1]?.id ?? null,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
return () => {
|
||||
state.monitorRefCount = Math.max(0, state.monitorRefCount - 1);
|
||||
if (state.monitorRefCount === 0 && state.monitorHandle) {
|
||||
clearInterval(state.monitorHandle);
|
||||
state.monitorHandle = null;
|
||||
state.monitorScope = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function consumePersistedPerfDiagnosticReports(): Promise<
|
||||
PerfDiagnosticsReport[]
|
||||
> {
|
||||
const reports = await readPersistedReports();
|
||||
await AsyncStorage.removeItem(STORAGE_KEY);
|
||||
await AsyncStorage.removeItem(LEGACY_STORAGE_KEY);
|
||||
return reports;
|
||||
}
|
||||
|
||||
export async function peekPersistedPerfDiagnosticReports(): Promise<
|
||||
PerfDiagnosticsReport[]
|
||||
> {
|
||||
return readPersistedReports();
|
||||
}
|
||||
|
||||
export function isPerfDiagnosticsEnabled(): boolean {
|
||||
return shouldEnableDiagnostics();
|
||||
}
|
||||
|
||||
export function getPerfDiagnosticBreadcrumbs(
|
||||
limit = 120
|
||||
): PerfDiagnosticBreadcrumb[] {
|
||||
if (limit <= 0) {
|
||||
return [];
|
||||
}
|
||||
return state.breadcrumbs.slice(-Math.floor(limit));
|
||||
}
|
||||
|
||||
export function getPerfDiagnosticsSnapshot(limit = 120): PerfDiagnosticsSnapshot {
|
||||
return {
|
||||
enabled: shouldEnableDiagnostics(),
|
||||
monitorScope: state.monitorScope,
|
||||
breadcrumbCount: state.breadcrumbs.length,
|
||||
breadcrumbs: getPerfDiagnosticBreadcrumbs(limit),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { DaemonClientDiagnosticsEvent } from "@server/client/daemon-client";
|
||||
import { recordPerfDiagnosticMark } from "./engine";
|
||||
|
||||
let fastTransportSampleCursor = 0;
|
||||
const transportRateByServer = new Map<
|
||||
string,
|
||||
{
|
||||
startedAtMs: number;
|
||||
messageCount: number;
|
||||
bytes: number;
|
||||
errorCount: number;
|
||||
slowCount: number;
|
||||
}
|
||||
>();
|
||||
|
||||
function shouldSampleFastTransportEvent(): boolean {
|
||||
fastTransportSampleCursor += 1;
|
||||
if (fastTransportSampleCursor >= 50) {
|
||||
fastTransportSampleCursor = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function recordHostRuntimeCreateClient(params: {
|
||||
serverId: string;
|
||||
connectionType: "direct" | "relay";
|
||||
endpoint: string;
|
||||
}): void {
|
||||
recordPerfDiagnosticMark(
|
||||
"host_runtime.create_client",
|
||||
{
|
||||
serverId: params.serverId,
|
||||
connectionType: params.connectionType,
|
||||
endpoint: params.endpoint,
|
||||
},
|
||||
{ force: true }
|
||||
);
|
||||
}
|
||||
|
||||
export function recordDaemonClientDiagnostics(
|
||||
serverId: string,
|
||||
event: DaemonClientDiagnosticsEvent
|
||||
): void {
|
||||
if (event.type === "transport_message_timing") {
|
||||
const nowWallTimeMs = Date.now();
|
||||
const current =
|
||||
transportRateByServer.get(serverId) ?? {
|
||||
startedAtMs: nowWallTimeMs,
|
||||
messageCount: 0,
|
||||
bytes: 0,
|
||||
errorCount: 0,
|
||||
slowCount: 0,
|
||||
};
|
||||
current.messageCount += 1;
|
||||
current.bytes += event.payloadBytes;
|
||||
if (event.outcome !== "ok") {
|
||||
current.errorCount += 1;
|
||||
}
|
||||
if (event.totalMs >= 8) {
|
||||
current.slowCount += 1;
|
||||
}
|
||||
if (nowWallTimeMs - current.startedAtMs >= 1000) {
|
||||
recordPerfDiagnosticMark(
|
||||
"daemon_client.message_rate",
|
||||
{
|
||||
serverId,
|
||||
windowMs: nowWallTimeMs - current.startedAtMs,
|
||||
messageCount: current.messageCount,
|
||||
bytes: current.bytes,
|
||||
errorCount: current.errorCount,
|
||||
slowCount: current.slowCount,
|
||||
},
|
||||
{ force: current.errorCount > 0 || current.slowCount > 0 }
|
||||
);
|
||||
current.startedAtMs = nowWallTimeMs;
|
||||
current.messageCount = 0;
|
||||
current.bytes = 0;
|
||||
current.errorCount = 0;
|
||||
current.slowCount = 0;
|
||||
}
|
||||
transportRateByServer.set(serverId, current);
|
||||
}
|
||||
|
||||
if (event.type === "transport_binary_frame") {
|
||||
if (event.payloadBytes < 16_384) {
|
||||
return;
|
||||
}
|
||||
recordPerfDiagnosticMark(
|
||||
"daemon_client.binary_frame",
|
||||
{
|
||||
serverId,
|
||||
channel: event.channel,
|
||||
messageType: event.messageType,
|
||||
payloadBytes: event.payloadBytes,
|
||||
},
|
||||
{ force: true }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const isSlow =
|
||||
event.totalMs >= 8 || event.parseMs >= 4 || event.validateMs >= 4;
|
||||
const isError = event.outcome !== "ok";
|
||||
if (!isSlow && !isError && !shouldSampleFastTransportEvent()) {
|
||||
return;
|
||||
}
|
||||
recordPerfDiagnosticMark(
|
||||
"daemon_client.transport_message",
|
||||
{
|
||||
serverId,
|
||||
messageType: event.messageType,
|
||||
outcome: event.outcome,
|
||||
payloadBytes: event.payloadBytes,
|
||||
parseMs: Math.round(event.parseMs * 100) / 100,
|
||||
validateMs: Math.round(event.validateMs * 100) / 100,
|
||||
totalMs: Math.round(event.totalMs * 100) / 100,
|
||||
},
|
||||
{ force: isSlow || isError }
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export type {
|
||||
PerfDiagnosticBreadcrumb,
|
||||
PerfDiagnosticFields,
|
||||
PerfDiagnosticsReport,
|
||||
PerfDiagnosticsSnapshot,
|
||||
PerfDiagnosticsReporter,
|
||||
RecordPerfDiagnosticMarkOptions,
|
||||
} from "./types";
|
||||
|
||||
export {
|
||||
consumePersistedPerfDiagnosticReports,
|
||||
getPerfDiagnosticBreadcrumbs,
|
||||
getPerfDiagnosticsSnapshot,
|
||||
installPerfDiagnosticsMonitor,
|
||||
isPerfDiagnosticsEnabled,
|
||||
peekPersistedPerfDiagnosticReports,
|
||||
recordPerfDiagnosticMark,
|
||||
} from "./engine";
|
||||
|
||||
export { getPerfDiagnosticsReporter } from "./reporter";
|
||||
export { PerfDiagnosticsProvider } from "./perf-diagnostics-provider";
|
||||
export { usePerfDiagnostics } from "./use-perf-diagnostics";
|
||||
@@ -0,0 +1,21 @@
|
||||
import { createContext } from "react";
|
||||
import type { PerfDiagnosticsReporter, PerfDiagnosticsSnapshot } from "./types";
|
||||
|
||||
const EMPTY_SNAPSHOT: PerfDiagnosticsSnapshot = {
|
||||
enabled: false,
|
||||
monitorScope: null,
|
||||
breadcrumbCount: 0,
|
||||
breadcrumbs: [],
|
||||
};
|
||||
|
||||
const NOOP_REPORTER: PerfDiagnosticsReporter = {
|
||||
mark: () => {},
|
||||
installMonitor: () => () => {},
|
||||
consumeReports: async () => [],
|
||||
peekReports: async () => [],
|
||||
isEnabled: () => false,
|
||||
getSnapshot: () => EMPTY_SNAPSHOT,
|
||||
};
|
||||
|
||||
export const PerfDiagnosticsContext =
|
||||
createContext<PerfDiagnosticsReporter>(NOOP_REPORTER);
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useEffect, useMemo, type ReactNode } from "react";
|
||||
import { installPerfDiagnosticsDebugTools } from "./debug-tools";
|
||||
import { PerfDiagnosticsContext } from "./perf-diagnostics-context";
|
||||
import { getPerfDiagnosticsReporter } from "./reporter";
|
||||
|
||||
interface PerfDiagnosticsProviderProps {
|
||||
children: ReactNode;
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
export function PerfDiagnosticsProvider({
|
||||
children,
|
||||
scope = "root_layout",
|
||||
}: PerfDiagnosticsProviderProps) {
|
||||
const reporter = useMemo(() => getPerfDiagnosticsReporter(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!reporter.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const removeDebugTools = installPerfDiagnosticsDebugTools();
|
||||
const stopMonitor = reporter.installMonitor(scope);
|
||||
|
||||
void reporter
|
||||
.peekReports()
|
||||
.then((reports) => {
|
||||
if (reports.length === 0) {
|
||||
return;
|
||||
}
|
||||
const latest = reports[reports.length - 1];
|
||||
console.warn("[PerfDiagnostics] Loaded persisted stall reports", {
|
||||
count: reports.length,
|
||||
latestReportId: latest?.id ?? null,
|
||||
latestWallTime: latest?.wallTimeIso ?? null,
|
||||
latestLagMs: latest ? Math.round(latest.lagMs) : null,
|
||||
latestBreadcrumbs: latest?.breadcrumbs.length ?? null,
|
||||
});
|
||||
})
|
||||
.catch(() => undefined);
|
||||
|
||||
return () => {
|
||||
removeDebugTools();
|
||||
stopMonitor();
|
||||
};
|
||||
}, [scope, reporter]);
|
||||
|
||||
return (
|
||||
<PerfDiagnosticsContext.Provider value={reporter}>
|
||||
{children}
|
||||
</PerfDiagnosticsContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
consumePersistedPerfDiagnosticReports,
|
||||
getPerfDiagnosticsSnapshot,
|
||||
installPerfDiagnosticsMonitor,
|
||||
isPerfDiagnosticsEnabled,
|
||||
peekPersistedPerfDiagnosticReports,
|
||||
recordPerfDiagnosticMark,
|
||||
} from "./engine";
|
||||
import type { PerfDiagnosticsReporter } from "./types";
|
||||
|
||||
const reporter: PerfDiagnosticsReporter = {
|
||||
mark: recordPerfDiagnosticMark,
|
||||
installMonitor: installPerfDiagnosticsMonitor,
|
||||
consumeReports: consumePersistedPerfDiagnosticReports,
|
||||
peekReports: peekPersistedPerfDiagnosticReports,
|
||||
isEnabled: isPerfDiagnosticsEnabled,
|
||||
getSnapshot: getPerfDiagnosticsSnapshot,
|
||||
};
|
||||
|
||||
export function getPerfDiagnosticsReporter(): PerfDiagnosticsReporter {
|
||||
return reporter;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export type PerfDiagnosticFields = Record<string, unknown>;
|
||||
|
||||
export interface PerfDiagnosticBreadcrumb {
|
||||
atMs: number;
|
||||
kind: "mark" | "span";
|
||||
name: string;
|
||||
durationMs?: number;
|
||||
fields?: PerfDiagnosticFields;
|
||||
}
|
||||
|
||||
export interface PerfDiagnosticsReport {
|
||||
id: string;
|
||||
scope: string;
|
||||
atMs: number;
|
||||
wallTimeMs: number;
|
||||
wallTimeIso: string;
|
||||
lagMs: number;
|
||||
platform: string;
|
||||
breadcrumbs: PerfDiagnosticBreadcrumb[];
|
||||
}
|
||||
|
||||
export interface RecordPerfDiagnosticMarkOptions {
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
export interface PerfDiagnosticsSnapshot {
|
||||
enabled: boolean;
|
||||
monitorScope: string | null;
|
||||
breadcrumbCount: number;
|
||||
breadcrumbs: PerfDiagnosticBreadcrumb[];
|
||||
}
|
||||
|
||||
export interface PerfDiagnosticsReporter {
|
||||
mark: (
|
||||
name: string,
|
||||
fields?: PerfDiagnosticFields,
|
||||
options?: RecordPerfDiagnosticMarkOptions
|
||||
) => void;
|
||||
installMonitor: (scope: string) => () => void;
|
||||
consumeReports: () => Promise<PerfDiagnosticsReport[]>;
|
||||
peekReports: () => Promise<PerfDiagnosticsReport[]>;
|
||||
isEnabled: () => boolean;
|
||||
getSnapshot: (limit?: number) => PerfDiagnosticsSnapshot;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { useContext } from "react";
|
||||
import { PerfDiagnosticsContext } from "./perf-diagnostics-context";
|
||||
import type { PerfDiagnosticsReporter } from "./types";
|
||||
|
||||
export function usePerfDiagnostics(): PerfDiagnosticsReporter {
|
||||
return useContext(PerfDiagnosticsContext);
|
||||
}
|
||||
Reference in New Issue
Block a user