refactor(app): centralize perf diagnostics and activity coalescing

This commit is contained in:
Mohamed Boudra
2026-02-22 11:52:08 +07:00
parent d7e2ab4437
commit d6b7c2ed2d
24 changed files with 1119 additions and 81 deletions
+44 -41
View File
@@ -49,6 +49,7 @@ import {
parseHostAgentRouteFromPathname,
} from "@/utils/host-routes";
import { getTauri } from "@/utils/tauri";
import { PerfDiagnosticsProvider } from "@/runtime/perf-diagnostics";
polyfillCrypto();
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
@@ -442,47 +443,49 @@ function MissingDaemonView() {
export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<PortalProvider>
<SafeAreaProvider>
<KeyboardProvider>
<BottomSheetModalProvider>
<QueryProvider>
<DaemonRegistryProvider>
<PushNotificationRouter />
<MultiDaemonSessionHost />
<ProvidersWrapper>
<SidebarAnimationProvider>
<HorizontalScrollProvider>
<ToastProvider>
<AppWithSidebar>
<Stack
screenOptions={{
headerShown: false,
animation: "none",
}}
>
<Stack.Screen name="index" />
<Stack.Screen
name="h/[serverId]/agent/[agentId]"
options={{ gestureEnabled: false }}
/>
<Stack.Screen name="h/[serverId]/index" />
<Stack.Screen name="h/[serverId]/agent/index" />
<Stack.Screen name="h/[serverId]/agents" />
<Stack.Screen name="h/[serverId]/settings" />
<Stack.Screen name="pair-scan" />
</Stack>
</AppWithSidebar>
</ToastProvider>
</HorizontalScrollProvider>
</SidebarAnimationProvider>
</ProvidersWrapper>
</DaemonRegistryProvider>
</QueryProvider>
</BottomSheetModalProvider>
</KeyboardProvider>
</SafeAreaProvider>
</PortalProvider>
<PerfDiagnosticsProvider scope="root_layout">
<PortalProvider>
<SafeAreaProvider>
<KeyboardProvider>
<BottomSheetModalProvider>
<QueryProvider>
<DaemonRegistryProvider>
<PushNotificationRouter />
<MultiDaemonSessionHost />
<ProvidersWrapper>
<SidebarAnimationProvider>
<HorizontalScrollProvider>
<ToastProvider>
<AppWithSidebar>
<Stack
screenOptions={{
headerShown: false,
animation: "none",
}}
>
<Stack.Screen name="index" />
<Stack.Screen
name="h/[serverId]/agent/[agentId]"
options={{ gestureEnabled: false }}
/>
<Stack.Screen name="h/[serverId]/index" />
<Stack.Screen name="h/[serverId]/agent/index" />
<Stack.Screen name="h/[serverId]/agents" />
<Stack.Screen name="h/[serverId]/settings" />
<Stack.Screen name="pair-scan" />
</Stack>
</AppWithSidebar>
</ToastProvider>
</HorizontalScrollProvider>
</SidebarAnimationProvider>
</ProvidersWrapper>
</DaemonRegistryProvider>
</QueryProvider>
</BottomSheetModalProvider>
</KeyboardProvider>
</SafeAreaProvider>
</PortalProvider>
</PerfDiagnosticsProvider>
</GestureHandlerRootView>
);
}
+22 -4
View File
@@ -65,10 +65,26 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
const pathname = usePathname();
const { daemons } = useDaemonRegistry();
const runtime = getHostRuntimeStore();
const runtimeVersion = useSyncExternalStore(
const runtimeConnectionStatusSignature = useSyncExternalStore(
(onStoreChange) => runtime.subscribeAll(onStoreChange),
() => runtime.getVersion(),
() => runtime.getVersion()
() =>
daemons
.map(
(daemon) =>
`${daemon.serverId}:${
runtime.getSnapshot(daemon.serverId)?.connectionStatus ?? "connecting"
}`
)
.join("|"),
() =>
daemons
.map(
(daemon) =>
`${daemon.serverId}:${
runtime.getSnapshot(daemon.serverId)?.connectionStatus ?? "connecting"
}`
)
.join("|")
);
const activeServerIdFromPath = useMemo(
() => parseServerIdFromPathname(pathname),
@@ -99,7 +115,7 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
runtime.getSnapshot(daemon.serverId)?.connectionStatus ?? "connecting"
),
})),
[daemons, runtime, runtimeVersion]
[daemons, runtime, runtimeConnectionStatusSignature]
);
const hostTriggerRef = useRef<View>(null);
const [isHostPickerOpen, setIsHostPickerOpen] = useState(false);
@@ -386,6 +402,7 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
<SidebarAgentListSkeleton />
) : (
<SidebarAgentList
isOpen={isOpen}
entries={entries}
projectFilterOptions={projectFilterOptions}
selectedProjectFilterKeys={selectedProjectFilterKeys}
@@ -512,6 +529,7 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
<SidebarAgentListSkeleton />
) : (
<SidebarAgentList
isOpen={isOpen}
entries={entries}
projectFilterOptions={projectFilterOptions}
selectedProjectFilterKeys={selectedProjectFilterKeys}
@@ -45,6 +45,7 @@ import {
type SidebarAgentListEntry,
type SidebarProjectFilterOption,
} from "@/hooks/use-sidebar-agents-list";
import type { ProjectIcon } from "@server/shared/messages";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { getIsTauri } from "@/constants/layout";
import { AgentStatusDot } from "@/components/agent-status-dot";
@@ -57,7 +58,15 @@ import { useArchiveAgent } from "@/hooks/use-archive-agent";
type EntryData = SidebarAgentListEntry;
function toProjectIconDataUri(icon: ProjectIcon | null): string | null {
if (!icon) {
return null;
}
return `data:${icon.mimeType};base64,${icon.data}`;
}
interface SidebarAgentListProps {
isOpen?: boolean;
entries: SidebarAgentListEntry[];
projectFilterOptions: SidebarProjectFilterOption[];
selectedProjectFilterKeys: string[];
@@ -443,6 +452,7 @@ function deriveProjectDisplayName(input: {
}
export function SidebarAgentList({
isOpen = true,
entries,
projectFilterOptions,
selectedProjectFilterKeys,
@@ -494,6 +504,9 @@ export function SidebarAgentList({
const showProjectFilters = projectFilterOptions.length > 0;
const projectIconRequests = useMemo(() => {
if (!isOpen) {
return [];
}
const unique = new Map<string, { serverId: string; cwd: string }>();
for (const option of projectFilterOptions) {
if (!option.serverId || !option.workingDir) {
@@ -513,7 +526,7 @@ export function SidebarAgentList({
unique.set(`${serverId}:${cwd}`, { serverId, cwd });
}
return Array.from(unique.values());
}, [entries, projectFilterOptions]);
}, [entries, isOpen, projectFilterOptions]);
const projectIconQueries = useQueries({
queries: projectIconRequests.map((request) => ({
@@ -526,8 +539,10 @@ export function SidebarAgentList({
const result = await client.requestProjectIcon(request.cwd);
return result.icon;
},
select: toProjectIconDataUri,
enabled: Boolean(
getHostRuntimeStore().getClient(request.serverId) &&
isOpen &&
getHostRuntimeStore().getClient(request.serverId) &&
isHostRuntimeConnected(
getHostRuntimeStore().getSnapshot(request.serverId)
) &&
@@ -548,10 +563,7 @@ export function SidebarAgentList({
if (!request) {
continue;
}
const icon = projectIconQueries[i]?.data ?? null;
const dataUri = icon
? `data:${icon.mimeType};base64,${icon.data}`
: null;
const dataUri = projectIconQueries[i]?.data ?? null;
map.set(`${request.serverId}:${request.cwd}`, dataUri);
}
return map;
@@ -205,6 +205,9 @@ function SessionProviderInternal({
const setAgentLastActivity = useSessionStore(
(state) => state.setAgentLastActivity
);
const flushAgentLastActivity = useSessionStore(
(state) => state.flushAgentLastActivity
);
const setPendingPermissions = useSessionStore(
(state) => state.setPendingPermissions
);
@@ -376,10 +379,11 @@ function SessionProviderInternal({
// If the client drops mid-initialization, clear pending flags
useEffect(() => {
if (!isConnected) {
flushAgentLastActivity();
pendingAgentUpdatesRef.current.clear();
setInitializingAgents(serverId, new Map());
}
}, [serverId, isConnected, setInitializingAgents]);
}, [flushAgentLastActivity, serverId, isConnected, setInitializingAgents]);
const applyAgentUpdatePayload = useCallback(
(update: AgentUpdatePayload) => {
@@ -430,7 +434,6 @@ function SessionProviderInternal({
next.delete(agentId);
return next;
});
return;
}
@@ -1,10 +1,8 @@
import { useCallback, useMemo } from "react";
import { useCallback, useMemo, useSyncExternalStore } from "react";
import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
import { useSessionStore, type Agent } from "@/stores/session-store";
import {
getHostRuntimeStore,
isHostRuntimeDirectoryLoading,
useHostRuntimeSession,
} from "@/runtime/host-runtime";
import type { AggregatedAgent } from "@/hooks/use-aggregated-agents";
import {
@@ -124,6 +122,7 @@ function toAggregatedAgent(params: {
source: Agent;
serverId: string;
serverLabel: string;
lastActivityAt: Date;
}): AggregatedAgent & { createdAt: Date } {
const source = params.source;
return {
@@ -133,7 +132,7 @@ function toAggregatedAgent(params: {
title: source.title ?? null,
status: source.status,
createdAt: source.createdAt,
lastActivityAt: source.lastActivityAt,
lastActivityAt: params.lastActivityAt,
cwd: source.cwd,
provider: source.provider,
requiresAttention: source.requiresAttention,
@@ -149,14 +148,28 @@ export function useSidebarAgentsList(options?: {
serverId?: string | null;
selectedProjectFilterKeys?: string[];
}): SidebarAgentsListResult {
const isSidebarOpen = options?.isOpen ?? true;
const { daemons } = useDaemonRegistry();
const runtime = getHostRuntimeStore();
const daemonLabelSignature = useMemo(
() =>
daemons
.map((daemon) => `${daemon.serverId}:${daemon.label ?? ""}`)
.join("|"),
[daemons]
);
const serverId = useMemo(() => {
const value = options?.serverId;
return typeof value === "string" && value.trim().length > 0
? value.trim()
: null;
}, [options?.serverId]);
const serverLabel = useMemo(() => {
if (!serverId) {
return "";
}
return daemons.find((daemon) => daemon.serverId === serverId)?.label ?? serverId;
}, [daemonLabelSignature, serverId]);
const selectedProjectFilterKeys = useMemo(
() =>
@@ -168,13 +181,40 @@ export function useSidebarAgentsList(options?: {
[options?.selectedProjectFilterKeys]
);
const isActive = Boolean(serverId && isSidebarOpen);
const liveAgents = useSessionStore((state) =>
serverId ? state.sessions[serverId]?.agents ?? null : null
isActive && serverId ? state.sessions[serverId]?.agents ?? null : null
);
const { snapshot } = useHostRuntimeSession(serverId ?? "");
const agentLastActivity = useSessionStore((state) =>
isActive ? state.agentLastActivity : null
);
const runtimeStatusSignature = useSyncExternalStore(
(onStoreChange) =>
isActive && serverId ? runtime.subscribe(serverId, onStoreChange) : () => {},
() => {
if (!isActive || !serverId) {
return "idle:idle";
}
const snapshot = runtime.getSnapshot(serverId);
const connectionStatus = snapshot?.connectionStatus ?? "idle";
const directoryStatus = snapshot?.agentDirectoryStatus ?? "idle";
return `${connectionStatus}:${directoryStatus}`;
},
() => {
if (!isActive || !serverId) {
return "idle:idle";
}
const snapshot = runtime.getSnapshot(serverId);
const connectionStatus = snapshot?.connectionStatus ?? "idle";
const directoryStatus = snapshot?.agentDirectoryStatus ?? "idle";
return `${connectionStatus}:${directoryStatus}`;
}
);
const [connectionStatus = "idle", directoryStatus = "idle"] =
runtimeStatusSignature.split(":", 2);
const { entries, projectFilterOptions, hasAnyData, hasMoreEntries } = useMemo(() => {
if (!serverId || !liveAgents) {
if (!isActive || !serverId || !liveAgents) {
return {
entries: [] as SidebarAgentListEntry[],
projectFilterOptions: [] as SidebarProjectFilterOption[],
@@ -183,8 +223,6 @@ export function useSidebarAgentsList(options?: {
};
}
const serverLabel =
daemons.find((daemon) => daemon.serverId === serverId)?.label ?? serverId;
const seenAgentIds = new Set<string>();
const byProject = new Map<string, SidebarProjectFilterOption>();
const mergedEntries: SidebarAgentListEntry[] = [];
@@ -232,10 +270,13 @@ export function useSidebarAgentsList(options?: {
projectPlacement: live.projectPlacement ?? null,
cwd: live.cwd,
});
const effectiveLastActivity =
agentLastActivity?.get(live.id) ?? live.lastActivityAt;
const agent = toAggregatedAgent({
source: live,
serverId,
serverLabel,
lastActivityAt: effectiveLastActivity,
});
pushEntry({ agent, project });
}
@@ -255,22 +296,26 @@ export function useSidebarAgentsList(options?: {
return left.projectName.localeCompare(right.projectName);
});
return {
const result = {
entries: ordered.entries,
projectFilterOptions: options,
hasAnyData: ordered.entries.length > 0,
hasMoreEntries: ordered.hasMore,
};
}, [daemons, liveAgents, selectedProjectFilterKeys, serverId]);
return result;
}, [agentLastActivity, isActive, liveAgents, selectedProjectFilterKeys, serverId, serverLabel]);
const refreshAll = useCallback(() => {
if (!serverId || snapshot?.connectionStatus !== "online") {
if (!isActive || !serverId || connectionStatus !== "online") {
return;
}
void runtime.refreshAgentDirectory({ serverId }).catch(() => undefined);
}, [runtime, serverId, snapshot?.connectionStatus]);
}, [connectionStatus, isActive, runtime, serverId]);
const isDirectoryLoading = Boolean(serverId && isHostRuntimeDirectoryLoading(snapshot));
const isDirectoryLoading =
isActive &&
Boolean(serverId) &&
(directoryStatus === "initial_loading" || directoryStatus === "revalidating");
const isInitialLoad = isDirectoryLoading && !hasAnyData;
const isRevalidating = isDirectoryLoading && hasAnyData;
@@ -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;
}
+17 -2
View File
@@ -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);
}
+95 -11
View File
@@ -24,6 +24,10 @@ import type {
ServerCapabilities,
} from "@server/shared/messages";
import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
import {
createAgentLastActivityCoalescer,
type AgentLastActivityCommitter,
} from "@/runtime/activity";
// Re-export types that were in session-context
export type MessageEntry =
@@ -257,6 +261,10 @@ interface SessionStoreActions {
// Agent activity timestamps
setAgentLastActivity: (agentId: string, timestamp: Date) => void;
setAgentLastActivityBatch: (
updates: Map<string, Date> | ((prev: Map<string, Date>) => Map<string, Date>)
) => void;
flushAgentLastActivity: () => void;
// Permissions
setPendingPermissions: (serverId: string, perms: Map<string, PendingPermission> | ((prev: Map<string, PendingPermission>) => Map<string, PendingPermission>)) => void;
@@ -278,6 +286,7 @@ type SessionStore = SessionStoreState & SessionStoreActions;
const SESSION_STORE_LOG_TAG = "[SessionStore]";
let sessionStoreUpdateCount = 0;
const agentLastActivityCoalescer = createAgentLastActivityCoalescer();
function logSessionStoreUpdate(
type: string,
@@ -332,9 +341,34 @@ function areServerCapabilitiesEqual(
}
export const useSessionStore = create<SessionStore>()(
subscribeWithSelector((set, get) => ({
sessions: {},
agentLastActivity: new Map(),
subscribeWithSelector((set, get) => {
const commitActivityUpdates: AgentLastActivityCommitter = (updates) => {
set((prev) => {
let nextActivity: Map<string, Date> | null = null;
for (const [agentId, timestamp] of updates.entries()) {
const current = prev.agentLastActivity.get(agentId);
if (current && current.getTime() >= timestamp.getTime()) {
continue;
}
if (!nextActivity) {
nextActivity = new Map(prev.agentLastActivity);
}
nextActivity.set(agentId, timestamp);
}
if (!nextActivity) {
return prev;
}
return {
...prev,
agentLastActivity: nextActivity,
};
});
};
agentLastActivityCoalescer.setCommitter(commitActivityUpdates);
return {
sessions: {},
agentLastActivity: new Map(),
// Session management
initializeSession: (serverId, client, audioPlayer) => {
@@ -354,14 +388,34 @@ export const useSessionStore = create<SessionStore>()(
},
clearSession: (serverId) => {
agentLastActivityCoalescer.flushNow();
set((prev) => {
if (!(serverId in prev.sessions)) {
const session = prev.sessions[serverId];
if (!session) {
return prev;
}
logSessionStoreUpdate("clearSession", serverId);
const nextSessions = { ...prev.sessions };
delete nextSessions[serverId];
return { ...prev, sessions: nextSessions };
let nextActivity = prev.agentLastActivity;
if (session.agents.size > 0) {
const candidate = new Map(prev.agentLastActivity);
let changed = false;
for (const agentId of session.agents.keys()) {
if (candidate.delete(agentId)) {
changed = true;
}
agentLastActivityCoalescer.deletePending(agentId);
}
if (changed) {
nextActivity = candidate;
}
}
return {
...prev,
sessions: nextSessions,
agentLastActivity: nextActivity,
};
});
},
@@ -714,20 +768,49 @@ export const useSessionStore = create<SessionStore>()(
// Agent activity timestamps (top-level, does NOT mutate session object)
setAgentLastActivity: (agentId, timestamp) => {
agentLastActivityCoalescer.enqueue(agentId, timestamp);
},
setAgentLastActivityBatch: (updates) => {
set((prev) => {
const currentTimestamp = prev.agentLastActivity.get(agentId);
if (currentTimestamp && currentTimestamp.getTime() === timestamp.getTime()) {
const nextActivity =
typeof updates === "function"
? updates(prev.agentLastActivity)
: updates;
if (nextActivity === prev.agentLastActivity) {
return prev;
}
if (nextActivity.size === 0) {
if (prev.agentLastActivity.size === 0) {
return prev;
}
return {
...prev,
agentLastActivity: new Map(),
};
}
let changed = false;
for (const [agentId, timestamp] of nextActivity.entries()) {
const currentTimestamp = prev.agentLastActivity.get(agentId);
if (!currentTimestamp || currentTimestamp.getTime() !== timestamp.getTime()) {
changed = true;
break;
}
}
if (!changed && nextActivity.size === prev.agentLastActivity.size) {
return prev;
}
const nextActivity = new Map(prev.agentLastActivity);
nextActivity.set(agentId, timestamp);
return {
...prev,
agentLastActivity: nextActivity,
agentLastActivity: new Map(nextActivity),
};
});
},
flushAgentLastActivity: () => {
agentLastActivityCoalescer.flushNow();
},
// Permissions
setPendingPermissions: (serverId, perms) => {
set((prev) => {
@@ -840,5 +923,6 @@ export const useSessionStore = create<SessionStore>()(
}
return entries;
},
}))
};
})
);
@@ -48,11 +48,17 @@ export function applyFetchedAgentDirectory(input: {
entries: FetchAgentsEntry[];
}): { agents: Map<string, Agent> } {
const { agents, pendingPermissions } = buildAgentDirectoryState(input);
const store = useSessionStore.getState();
store.setAgents(input.serverId, agents);
const lastActivityByAgentId = new Map<string, Date>();
for (const agent of agents.values()) {
store.setAgentLastActivity(agent.id, agent.lastActivityAt);
lastActivityByAgentId.set(agent.id, agent.lastActivityAt);
}
store.setAgentLastActivityBatch(lastActivityByAgentId);
store.setPendingPermissions(input.serverId, pendingPermissions);
store.setInitializingAgents(input.serverId, new Map());
store.setHasHydratedAgents(input.serverId, true);
@@ -0,0 +1,20 @@
// Compatibility shim while callers move to `@/runtime/perf-diagnostics`.
// Keep this file side-effect free.
export {
consumePersistedPerfDiagnosticReports,
getPerfDiagnosticBreadcrumbs,
getPerfDiagnosticsSnapshot,
installPerfDiagnosticsMonitor,
isPerfDiagnosticsEnabled,
peekPersistedPerfDiagnosticReports,
recordPerfDiagnosticMark,
} from "@/runtime/perf-diagnostics";
export type {
PerfDiagnosticBreadcrumb,
PerfDiagnosticFields,
PerfDiagnosticsReport,
PerfDiagnosticsSnapshot,
PerfDiagnosticsReporter,
RecordPerfDiagnosticMarkOptions,
} from "@/runtime/perf-diagnostics";
+1
View File
@@ -17,6 +17,7 @@ export default defineConfig({
alias: {
"@": path.resolve(__dirname, "src"),
"@server": path.resolve(__dirname, "../server/src"),
"react-native": "react-native-web",
},
},
});
@@ -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
// ============================================================================
@@ -2638,12 +2671,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
}
@@ -2654,19 +2694,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
}