refactor: remove performance monitoring and diagnostics infrastructure
This commit is contained in:
@@ -175,29 +175,6 @@ test("workspace terminal responsiveness benchmark (report-only, single stress pr
|
|||||||
timeout: 120_000,
|
timeout: 120_000,
|
||||||
}).toBe(true);
|
}).toBe(true);
|
||||||
|
|
||||||
const diagnostics = await page.evaluate(async () => {
|
|
||||||
const debug = (
|
|
||||||
window as {
|
|
||||||
__PASEO_PERF_DIAGNOSTICS_DEBUG__?: {
|
|
||||||
consumeReports?: () => Promise<unknown[]>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
).__PASEO_PERF_DIAGNOSTICS_DEBUG__;
|
|
||||||
if (!debug || typeof debug.consumeReports !== "function") {
|
|
||||||
return { available: false, reports: [] as unknown[] };
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const reports = await debug.consumeReports();
|
|
||||||
return { available: true, reports: Array.isArray(reports) ? reports : [] };
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
available: true,
|
|
||||||
reports: [] as unknown[],
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const frameGapsMs = (rafResult.samples ?? []).filter(
|
const frameGapsMs = (rafResult.samples ?? []).filter(
|
||||||
(sample) => Number.isFinite(sample) && sample > 0
|
(sample) => Number.isFinite(sample) && sample > 0
|
||||||
);
|
);
|
||||||
@@ -220,12 +197,6 @@ test("workspace terminal responsiveness benchmark (report-only, single stress pr
|
|||||||
over500Ms: frameGapsMs.filter((gap) => gap > 500).length,
|
over500Ms: frameGapsMs.filter((gap) => gap > 500).length,
|
||||||
},
|
},
|
||||||
explorerToggleLatencyMs: summarize(interactionLatenciesMs),
|
explorerToggleLatencyMs: summarize(interactionLatenciesMs),
|
||||||
diagnostics: {
|
|
||||||
available: diagnostics.available,
|
|
||||||
reportCount: diagnostics.reports.length,
|
|
||||||
reports: diagnostics.reports,
|
|
||||||
error: "error" in diagnostics ? diagnostics.error : undefined,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await testInfo.attach("terminal-responsiveness-report", {
|
await testInfo.attach("terminal-responsiveness-report", {
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ import {
|
|||||||
parseWorkspaceOpenIntent,
|
parseWorkspaceOpenIntent,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
import { getTauri } from "@/utils/tauri";
|
import { getTauri } from "@/utils/tauri";
|
||||||
import { PerfDiagnosticsProvider } from "@/runtime/perf-diagnostics";
|
|
||||||
import { attachConsole } from "@/utils/tauri-attach-console";
|
import { attachConsole } from "@/utils/tauri-attach-console";
|
||||||
|
|
||||||
polyfillCrypto();
|
polyfillCrypto();
|
||||||
@@ -556,55 +555,53 @@ export default function RootLayout() {
|
|||||||
<GestureHandlerRootView
|
<GestureHandlerRootView
|
||||||
style={{ flex: 1, backgroundColor: darkTheme.colors.surface0 }}
|
style={{ flex: 1, backgroundColor: darkTheme.colors.surface0 }}
|
||||||
>
|
>
|
||||||
<PerfDiagnosticsProvider scope="root_layout">
|
<PortalProvider>
|
||||||
<PortalProvider>
|
<SafeAreaProvider>
|
||||||
<SafeAreaProvider>
|
<KeyboardProvider>
|
||||||
<KeyboardProvider>
|
<BottomSheetModalProvider>
|
||||||
<BottomSheetModalProvider>
|
<QueryProvider>
|
||||||
<QueryProvider>
|
<HostRuntimeBootstrapProvider>
|
||||||
<HostRuntimeBootstrapProvider>
|
<PushNotificationRouter />
|
||||||
<PushNotificationRouter />
|
<HostSessionManager />
|
||||||
<HostSessionManager />
|
<ProvidersWrapper>
|
||||||
<ProvidersWrapper>
|
<SidebarAnimationProvider>
|
||||||
<SidebarAnimationProvider>
|
<HorizontalScrollProvider>
|
||||||
<HorizontalScrollProvider>
|
<ToastProvider>
|
||||||
<ToastProvider>
|
<AppWithSidebar>
|
||||||
<AppWithSidebar>
|
<Stack
|
||||||
<Stack
|
screenOptions={{
|
||||||
screenOptions={{
|
headerShown: false,
|
||||||
headerShown: false,
|
animation: "none",
|
||||||
animation: "none",
|
contentStyle: {
|
||||||
contentStyle: {
|
backgroundColor: darkTheme.colors.surface0,
|
||||||
backgroundColor: darkTheme.colors.surface0,
|
},
|
||||||
},
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Stack.Screen name="index" />
|
||||||
<Stack.Screen name="index" />
|
<Stack.Screen name="settings" />
|
||||||
<Stack.Screen name="settings" />
|
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]" />
|
||||||
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]" />
|
<Stack.Screen
|
||||||
<Stack.Screen
|
name="h/[serverId]/agent/[agentId]"
|
||||||
name="h/[serverId]/agent/[agentId]"
|
options={{ gestureEnabled: false }}
|
||||||
options={{ gestureEnabled: false }}
|
/>
|
||||||
/>
|
<Stack.Screen name="h/[serverId]/index" />
|
||||||
<Stack.Screen name="h/[serverId]/index" />
|
<Stack.Screen name="h/[serverId]/agents" />
|
||||||
<Stack.Screen name="h/[serverId]/agents" />
|
<Stack.Screen name="h/[serverId]/new-agent" />
|
||||||
<Stack.Screen name="h/[serverId]/new-agent" />
|
<Stack.Screen name="h/[serverId]/open-project" />
|
||||||
<Stack.Screen name="h/[serverId]/open-project" />
|
<Stack.Screen name="h/[serverId]/settings" />
|
||||||
<Stack.Screen name="h/[serverId]/settings" />
|
<Stack.Screen name="pair-scan" />
|
||||||
<Stack.Screen name="pair-scan" />
|
</Stack>
|
||||||
</Stack>
|
</AppWithSidebar>
|
||||||
</AppWithSidebar>
|
</ToastProvider>
|
||||||
</ToastProvider>
|
</HorizontalScrollProvider>
|
||||||
</HorizontalScrollProvider>
|
</SidebarAnimationProvider>
|
||||||
</SidebarAnimationProvider>
|
</ProvidersWrapper>
|
||||||
</ProvidersWrapper>
|
</HostRuntimeBootstrapProvider>
|
||||||
</HostRuntimeBootstrapProvider>
|
</QueryProvider>
|
||||||
</QueryProvider>
|
</BottomSheetModalProvider>
|
||||||
</BottomSheetModalProvider>
|
</KeyboardProvider>
|
||||||
</KeyboardProvider>
|
</SafeAreaProvider>
|
||||||
</SafeAreaProvider>
|
</PortalProvider>
|
||||||
</PortalProvider>
|
|
||||||
</PerfDiagnosticsProvider>
|
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useSyncExternalStore, useState } from 'react'
|
import { useEffect, useSyncExternalStore, useState } from 'react'
|
||||||
import { usePathname, useRouter } from 'expo-router'
|
import { usePathname, useRouter } from 'expo-router'
|
||||||
import { useHosts } from '@/runtime/host-runtime'
|
import { useHosts } from '@/runtime/host-runtime'
|
||||||
import { shouldUseManagedDesktopDaemon } from '@/desktop/managed-runtime/managed-runtime'
|
import { shouldUseManagedDesktopDaemon } from '@/desktop/managed-runtime/managed-runtime'
|
||||||
@@ -13,8 +13,6 @@ import {
|
|||||||
} from './index-startup'
|
} from './index-startup'
|
||||||
|
|
||||||
const STARTUP_TIMEOUT_MS = 30_000
|
const STARTUP_TIMEOUT_MS = 30_000
|
||||||
const APP_START_TIME = performance.now()
|
|
||||||
|
|
||||||
function useAnyHostOnline(serverIds: string[]): string | null {
|
function useAnyHostOnline(serverIds: string[]): string | null {
|
||||||
const runtime = getHostRuntimeStore()
|
const runtime = getHostRuntimeStore()
|
||||||
return useSyncExternalStore(
|
return useSyncExternalStore(
|
||||||
@@ -61,18 +59,6 @@ export default function Index() {
|
|||||||
const [hasTimedOut, setHasTimedOut] = useState(false)
|
const [hasTimedOut, setHasTimedOut] = useState(false)
|
||||||
const isDesktopStartupRace = shouldUseManagedDesktopDaemon()
|
const isDesktopStartupRace = shouldUseManagedDesktopDaemon()
|
||||||
const onlineServerId = useAnyHostOnline(daemons.map((daemon) => daemon.serverId))
|
const onlineServerId = useAnyHostOnline(daemons.map((daemon) => daemon.serverId))
|
||||||
const startupLogged = useRef(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (onlineServerId && !startupLogged.current) {
|
|
||||||
startupLogged.current = true
|
|
||||||
console.info('[Startup] host online', {
|
|
||||||
serverId: onlineServerId,
|
|
||||||
elapsedMs: Math.round(performance.now() - APP_START_TIME),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [onlineServerId])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setHasTimedOut(true)
|
setHasTimedOut(true)
|
||||||
@@ -89,10 +75,6 @@ export default function Index() {
|
|||||||
if (pathname !== '/' && pathname !== '') {
|
if (pathname !== '/' && pathname !== '') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.info('[Startup] navigating to host route', {
|
|
||||||
serverId: onlineServerId,
|
|
||||||
elapsedMs: Math.round(performance.now() - APP_START_TIME),
|
|
||||||
})
|
|
||||||
router.replace(buildHostRootRoute(onlineServerId) as any)
|
router.replace(buildHostRootRoute(onlineServerId) as any)
|
||||||
}, [onlineServerId, pathname, router])
|
}, [onlineServerId, pathname, router])
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { shortenPath } from '@/utils/shorten-path'
|
|||||||
import { type AggregatedAgent } from '@/hooks/use-aggregated-agents'
|
import { type AggregatedAgent } from '@/hooks/use-aggregated-agents'
|
||||||
import { useSessionStore } from '@/stores/session-store'
|
import { useSessionStore } from '@/stores/session-store'
|
||||||
import { AgentStatusDot } from '@/components/agent-status-dot'
|
import { AgentStatusDot } from '@/components/agent-status-dot'
|
||||||
import { buildAgentNavigationKey, startNavigationTiming } from '@/utils/navigation-timing'
|
|
||||||
import { buildHostWorkspaceAgentRoute } from '@/utils/host-routes'
|
import { buildHostWorkspaceAgentRoute } from '@/utils/host-routes'
|
||||||
|
|
||||||
interface AgentListProps {
|
interface AgentListProps {
|
||||||
@@ -271,13 +270,6 @@ export function AgentList({
|
|||||||
|
|
||||||
const serverId = agent.serverId
|
const serverId = agent.serverId
|
||||||
const agentId = agent.id
|
const agentId = agent.id
|
||||||
const navigationKey = buildAgentNavigationKey(serverId, agentId)
|
|
||||||
startNavigationTiming(navigationKey, {
|
|
||||||
from: 'home',
|
|
||||||
to: 'agent',
|
|
||||||
params: { serverId, agentId },
|
|
||||||
})
|
|
||||||
|
|
||||||
const shouldReplace = pathname.startsWith('/h/')
|
const shouldReplace = pathname.startsWith('/h/')
|
||||||
const navigate = shouldReplace ? router.replace : router.push
|
const navigate = shouldReplace ? router.replace : router.push
|
||||||
|
|
||||||
|
|||||||
@@ -67,17 +67,12 @@ import {
|
|||||||
} from "./use-bottom-anchor-controller";
|
} from "./use-bottom-anchor-controller";
|
||||||
import { createMarkdownStyles } from "@/styles/markdown-styles";
|
import { createMarkdownStyles } from "@/styles/markdown-styles";
|
||||||
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||||
import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
|
|
||||||
import { getMarkdownListMarker } from "@/utils/markdown-list";
|
import { getMarkdownListMarker } from "@/utils/markdown-list";
|
||||||
import { buildHostWorkspaceFileRoute } from "@/utils/host-routes";
|
import { buildHostWorkspaceFileRoute } from "@/utils/host-routes";
|
||||||
|
|
||||||
const isUserMessageItem = (item?: StreamItem) => item?.kind === "user_message";
|
const isUserMessageItem = (item?: StreamItem) => item?.kind === "user_message";
|
||||||
const isToolSequenceItem = (item?: StreamItem) =>
|
const isToolSequenceItem = (item?: StreamItem) =>
|
||||||
item?.kind === "tool_call" || item?.kind === "thought" || item?.kind === "todo_list";
|
item?.kind === "tool_call" || item?.kind === "thought" || item?.kind === "todo_list";
|
||||||
const AGENT_STREAM_LOG_TAG = "[AgentStreamView]";
|
|
||||||
const STREAM_ITEM_LOG_MIN_COUNT = 200;
|
|
||||||
const STREAM_ITEM_LOG_DELTA_THRESHOLD = 50;
|
|
||||||
|
|
||||||
export interface AgentStreamViewHandle {
|
export interface AgentStreamViewHandle {
|
||||||
scrollToBottom(reason?: BottomAnchorLocalRequest["reason"]): void;
|
scrollToBottom(reason?: BottomAnchorLocalRequest["reason"]): void;
|
||||||
prepareForViewportChange(): void;
|
prepareForViewportChange(): void;
|
||||||
@@ -116,7 +111,6 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
|
|||||||
[isMobile]
|
[isMobile]
|
||||||
);
|
);
|
||||||
const [isNearBottom, setIsNearBottom] = useState(true);
|
const [isNearBottom, setIsNearBottom] = useState(true);
|
||||||
const streamItemCountRef = useRef(0);
|
|
||||||
const [expandedInlineToolCallIds, setExpandedInlineToolCallIds] = useState<Set<string>>(new Set());
|
const [expandedInlineToolCallIds, setExpandedInlineToolCallIds] = useState<Set<string>>(new Set());
|
||||||
const openFileExplorer = usePanelStore((state) => state.openFileExplorer);
|
const openFileExplorer = usePanelStore((state) => state.openFileExplorer);
|
||||||
const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout);
|
const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout);
|
||||||
@@ -462,74 +456,6 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
|
|||||||
[pendingPermissions, agentId]
|
[pendingPermissions, agentId]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isPerfLoggingEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const totalCount = streamItems.length;
|
|
||||||
const prevCount = streamItemCountRef.current;
|
|
||||||
if (totalCount === prevCount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const delta = Math.abs(totalCount - prevCount);
|
|
||||||
streamItemCountRef.current = totalCount;
|
|
||||||
if (
|
|
||||||
totalCount < STREAM_ITEM_LOG_MIN_COUNT &&
|
|
||||||
delta < STREAM_ITEM_LOG_DELTA_THRESHOLD
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let userCount = 0;
|
|
||||||
let assistantCount = 0;
|
|
||||||
let toolCallCount = 0;
|
|
||||||
let thoughtCount = 0;
|
|
||||||
let activityCount = 0;
|
|
||||||
let todoCount = 0;
|
|
||||||
for (const item of streamItems) {
|
|
||||||
switch (item.kind) {
|
|
||||||
case "user_message":
|
|
||||||
userCount += 1;
|
|
||||||
break;
|
|
||||||
case "assistant_message":
|
|
||||||
assistantCount += 1;
|
|
||||||
break;
|
|
||||||
case "tool_call":
|
|
||||||
toolCallCount += 1;
|
|
||||||
break;
|
|
||||||
case "thought":
|
|
||||||
thoughtCount += 1;
|
|
||||||
break;
|
|
||||||
case "activity_log":
|
|
||||||
activityCount += 1;
|
|
||||||
break;
|
|
||||||
case "todo_list":
|
|
||||||
todoCount += 1;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const metrics =
|
|
||||||
totalCount >= STREAM_ITEM_LOG_MIN_COUNT
|
|
||||||
? measurePayload(streamItems)
|
|
||||||
: null;
|
|
||||||
perfLog(AGENT_STREAM_LOG_TAG, {
|
|
||||||
event: "stream_items",
|
|
||||||
agentId,
|
|
||||||
totalCount,
|
|
||||||
userCount,
|
|
||||||
assistantCount,
|
|
||||||
toolCallCount,
|
|
||||||
thoughtCount,
|
|
||||||
activityCount,
|
|
||||||
todoCount,
|
|
||||||
pendingPermissionCount: pendingPermissionItems.length,
|
|
||||||
streamHeadCount: streamHead?.length ?? 0,
|
|
||||||
payloadApproxBytes: metrics?.approxBytes ?? 0,
|
|
||||||
payloadFieldCount: metrics?.fieldCount ?? 0,
|
|
||||||
});
|
|
||||||
}, [agentId, pendingPermissionItems.length, streamHead, streamItems]);
|
|
||||||
|
|
||||||
const showWorkingIndicator = agent.status === "running";
|
const showWorkingIndicator = agent.status === "running";
|
||||||
const renderModel = useMemo<AgentStreamRenderModel>(() => {
|
const renderModel = useMemo<AgentStreamRenderModel>(() => {
|
||||||
const pendingPermissionsNode =
|
const pendingPermissionsNode =
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import { useCheckoutPrStatusQuery } from "@/hooks/use-checkout-pr-status-query";
|
|||||||
import { useHorizontalScrollOptional } from "@/contexts/horizontal-scroll-context";
|
import { useHorizontalScrollOptional } from "@/contexts/horizontal-scroll-context";
|
||||||
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
||||||
import { Fonts } from "@/constants/theme";
|
import { Fonts } from "@/constants/theme";
|
||||||
import { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf";
|
|
||||||
import { shouldAnchorHeaderBeforeCollapse } from "@/utils/git-diff-scroll";
|
import { shouldAnchorHeaderBeforeCollapse } from "@/utils/git-diff-scroll";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -66,11 +65,6 @@ function openURLInNewTab(url: string): void {
|
|||||||
void openExternalUrl(url);
|
void openExternalUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DIFF_PANE_LOG_TAG = "[GitDiffPane]";
|
|
||||||
const DIFF_FILE_LOG_TAG = "[DiffFileSection]";
|
|
||||||
const DIFF_FILE_LOG_LINE_THRESHOLD = 500;
|
|
||||||
const DIFF_FILE_LOG_TOKEN_THRESHOLD = 5000;
|
|
||||||
|
|
||||||
type HighlightStyle = NonNullable<HighlightToken["style"]>;
|
type HighlightStyle = NonNullable<HighlightToken["style"]>;
|
||||||
|
|
||||||
interface HighlightedTextProps {
|
interface HighlightedTextProps {
|
||||||
@@ -199,75 +193,14 @@ const DiffFileHeader = memo(function DiffFileHeader({
|
|||||||
onHeaderHeightChange,
|
onHeaderHeightChange,
|
||||||
testID,
|
testID,
|
||||||
}: DiffFileSectionProps) {
|
}: DiffFileSectionProps) {
|
||||||
const expandStartRef = useRef<number | null>(null);
|
|
||||||
const layoutYRef = useRef<number | null>(null);
|
const layoutYRef = useRef<number | null>(null);
|
||||||
const pressHandledRef = useRef(false);
|
const pressHandledRef = useRef(false);
|
||||||
const pressInRef = useRef<{ ts: number; pageX: number; pageY: number } | null>(null);
|
const pressInRef = useRef<{ ts: number; pageX: number; pageY: number } | null>(null);
|
||||||
|
|
||||||
const { hunkCount, lineCount, tokenCount } = useMemo(() => {
|
|
||||||
let totalLines = 0;
|
|
||||||
let totalTokens = 0;
|
|
||||||
for (const hunk of file.hunks) {
|
|
||||||
totalLines += hunk.lines.length;
|
|
||||||
for (const line of hunk.lines) {
|
|
||||||
if (line.tokens) {
|
|
||||||
totalTokens += line.tokens.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
hunkCount: file.hunks.length,
|
|
||||||
lineCount: totalLines,
|
|
||||||
tokenCount: totalTokens,
|
|
||||||
};
|
|
||||||
}, [file]);
|
|
||||||
|
|
||||||
const shouldLogFileMetrics =
|
|
||||||
lineCount >= DIFF_FILE_LOG_LINE_THRESHOLD ||
|
|
||||||
tokenCount >= DIFF_FILE_LOG_TOKEN_THRESHOLD;
|
|
||||||
|
|
||||||
const toggleExpanded = useCallback(() => {
|
const toggleExpanded = useCallback(() => {
|
||||||
pressHandledRef.current = true;
|
pressHandledRef.current = true;
|
||||||
if (isPerfLoggingEnabled() && shouldLogFileMetrics) {
|
|
||||||
expandStartRef.current = getNowMs();
|
|
||||||
perfLog(DIFF_FILE_LOG_TAG, {
|
|
||||||
event: "toggle",
|
|
||||||
path: file.path,
|
|
||||||
nextExpanded: !isExpanded,
|
|
||||||
hunkCount,
|
|
||||||
lineCount,
|
|
||||||
tokenCount,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
onToggle(file.path);
|
onToggle(file.path);
|
||||||
}, [file.path, onToggle, isExpanded, hunkCount, lineCount, tokenCount, shouldLogFileMetrics]);
|
}, [file.path, onToggle]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isPerfLoggingEnabled() || !shouldLogFileMetrics) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const startMs = expandStartRef.current;
|
|
||||||
if (startMs === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
expandStartRef.current = null;
|
|
||||||
const logCommit = () => {
|
|
||||||
const durationMs = getNowMs() - startMs;
|
|
||||||
perfLog(DIFF_FILE_LOG_TAG, {
|
|
||||||
event: isExpanded ? "expand_commit" : "collapse_commit",
|
|
||||||
path: file.path,
|
|
||||||
durationMs: Math.round(durationMs),
|
|
||||||
hunkCount,
|
|
||||||
lineCount,
|
|
||||||
tokenCount,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
if (typeof requestAnimationFrame === "function") {
|
|
||||||
requestAnimationFrame(() => logCommit());
|
|
||||||
} else {
|
|
||||||
logCommit();
|
|
||||||
}
|
|
||||||
}, [isExpanded, file.path, hunkCount, lineCount, tokenCount, shouldLogFileMetrics]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -506,30 +439,6 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi
|
|||||||
const headerHeightByPathRef = useRef<Record<string, number>>({});
|
const headerHeightByPathRef = useRef<Record<string, number>>({});
|
||||||
const bodyHeightByPathRef = useRef<Record<string, number>>({});
|
const bodyHeightByPathRef = useRef<Record<string, number>>({});
|
||||||
const defaultHeaderHeightRef = useRef<number>(44);
|
const defaultHeaderHeightRef = useRef<number>(44);
|
||||||
const diffMetrics = useMemo(() => {
|
|
||||||
let hunkCount = 0;
|
|
||||||
let lineCount = 0;
|
|
||||||
let tokenCount = 0;
|
|
||||||
for (const file of files) {
|
|
||||||
hunkCount += file.hunks.length;
|
|
||||||
for (const hunk of file.hunks) {
|
|
||||||
lineCount += hunk.lines.length;
|
|
||||||
for (const line of hunk.lines) {
|
|
||||||
if (line.tokens) {
|
|
||||||
tokenCount += line.tokens.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
fileCount: files.length,
|
|
||||||
hunkCount,
|
|
||||||
lineCount,
|
|
||||||
tokenCount,
|
|
||||||
};
|
|
||||||
}, [files]);
|
|
||||||
const lastMetricsKeyRef = useRef<string | null>(null);
|
|
||||||
|
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
setIsManualRefresh(true);
|
setIsManualRefresh(true);
|
||||||
void refreshDiff();
|
void refreshDiff();
|
||||||
@@ -712,28 +621,6 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi
|
|||||||
setDiffModeOverride(null);
|
setDiffModeOverride(null);
|
||||||
}, [autoDiffMode]);
|
}, [autoDiffMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isPerfLoggingEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const metricsKey = `${diffMetrics.fileCount}:${diffMetrics.hunkCount}:${diffMetrics.lineCount}:${diffMetrics.tokenCount}`;
|
|
||||||
if (lastMetricsKeyRef.current === metricsKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastMetricsKeyRef.current = metricsKey;
|
|
||||||
perfLog(DIFF_PANE_LOG_TAG, {
|
|
||||||
event: "files_snapshot",
|
|
||||||
serverId,
|
|
||||||
workspaceId: workspaceId ?? cwd,
|
|
||||||
fileCount: diffMetrics.fileCount,
|
|
||||||
hunkCount: diffMetrics.hunkCount,
|
|
||||||
lineCount: diffMetrics.lineCount,
|
|
||||||
tokenCount: diffMetrics.tokenCount,
|
|
||||||
isLoading: isDiffLoading,
|
|
||||||
isFetching: isDiffFetching,
|
|
||||||
});
|
|
||||||
}, [cwd, diffMetrics, isDiffFetching, isDiffLoading, serverId, workspaceId]);
|
|
||||||
|
|
||||||
const commitStatus = useCheckoutGitActionsStore((state) =>
|
const commitStatus = useCheckoutGitActionsStore((state) =>
|
||||||
state.getStatus({ serverId, cwd, actionId: "commit" })
|
state.getStatus({ serverId, cwd, actionId: "commit" })
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ import {
|
|||||||
buildToolCallDisplayModel,
|
buildToolCallDisplayModel,
|
||||||
} from "@/utils/tool-call-display";
|
} from "@/utils/tool-call-display";
|
||||||
import { resolveToolCallIcon } from "@/utils/tool-call-icon";
|
import { resolveToolCallIcon } from "@/utils/tool-call-icon";
|
||||||
import { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf";
|
|
||||||
import { parseInlinePathToken, type InlinePathTarget } from "@/utils/inline-path";
|
import { parseInlinePathToken, type InlinePathTarget } from "@/utils/inline-path";
|
||||||
import { getMarkdownListMarker } from "@/utils/markdown-list";
|
import { getMarkdownListMarker } from "@/utils/markdown-list";
|
||||||
import { openExternalUrl } from "@/utils/open-external-url";
|
import { openExternalUrl } from "@/utils/open-external-url";
|
||||||
@@ -1786,10 +1785,6 @@ interface ToolCallProps {
|
|||||||
onInlineDetailsExpandedChange?: (expanded: boolean) => void;
|
onInlineDetailsExpandedChange?: (expanded: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOOL_CALL_LOG_TAG = "[ToolCall]";
|
|
||||||
const TOOL_CALL_COMMIT_THRESHOLD_MS = 16;
|
|
||||||
|
|
||||||
|
|
||||||
export const ToolCall = memo(function ToolCall({
|
export const ToolCall = memo(function ToolCall({
|
||||||
toolName,
|
toolName,
|
||||||
args,
|
args,
|
||||||
@@ -1806,7 +1801,6 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
}: ToolCallProps) {
|
}: ToolCallProps) {
|
||||||
const { openToolCall } = useToolCallSheet();
|
const { openToolCall } = useToolCallSheet();
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const toggleStartRef = useRef<number | null>(null);
|
|
||||||
|
|
||||||
// Check if we're on mobile (use bottom sheet) or desktop (inline expand)
|
// Check if we're on mobile (use bottom sheet) or desktop (inline expand)
|
||||||
const isMobile =
|
const isMobile =
|
||||||
@@ -1849,7 +1843,6 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
const displayName = displayModel.displayName;
|
const displayName = displayModel.displayName;
|
||||||
const summary = displayModel.summary;
|
const summary = displayModel.summary;
|
||||||
const errorText = displayModel.errorText;
|
const errorText = displayModel.errorText;
|
||||||
const iconCategory = effectiveDetail?.type ?? toolName.trim().toLowerCase();
|
|
||||||
const IconComponent = resolveToolCallIcon(toolName, effectiveDetail);
|
const IconComponent = resolveToolCallIcon(toolName, effectiveDetail);
|
||||||
|
|
||||||
// Check if there's any content to display
|
// Check if there's any content to display
|
||||||
@@ -1862,9 +1855,6 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
: false);
|
: false);
|
||||||
|
|
||||||
const handleToggle = useCallback(() => {
|
const handleToggle = useCallback(() => {
|
||||||
if (!isMobile && isPerfLoggingEnabled()) {
|
|
||||||
toggleStartRef.current = getNowMs();
|
|
||||||
}
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
openToolCall({
|
openToolCall({
|
||||||
toolName,
|
toolName,
|
||||||
@@ -1878,33 +1868,6 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
}
|
}
|
||||||
}, [isMobile, openToolCall, toolName, displayName, summary, effectiveDetail, errorText]);
|
}, [isMobile, openToolCall, toolName, displayName, summary, effectiveDetail, errorText]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isMobile || !isPerfLoggingEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const startMs = toggleStartRef.current;
|
|
||||||
if (startMs === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
toggleStartRef.current = null;
|
|
||||||
const logCommit = () => {
|
|
||||||
const durationMs = getNowMs() - startMs;
|
|
||||||
if (durationMs >= TOOL_CALL_COMMIT_THRESHOLD_MS) {
|
|
||||||
perfLog(TOOL_CALL_LOG_TAG, {
|
|
||||||
event: isExpanded ? "expand_commit" : "collapse_commit",
|
|
||||||
toolName,
|
|
||||||
iconCategory,
|
|
||||||
durationMs: Math.round(durationMs),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (typeof requestAnimationFrame === "function") {
|
|
||||||
requestAnimationFrame(() => logCommit());
|
|
||||||
} else {
|
|
||||||
logCommit();
|
|
||||||
}
|
|
||||||
}, [isExpanded, isMobile, toolName, iconCategory]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!onInlineDetailsHoverChange || isMobile || isExpanded) {
|
if (!onInlineDetailsHoverChange || isMobile || isExpanded) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ class FakeDaemonClient {
|
|||||||
return { rttMs: 0 };
|
return { rttMs: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setReconnectEnabled(_enabled: boolean): void {}
|
||||||
|
|
||||||
setConnectionState(next: ConnectionState): void {
|
setConnectionState(next: ConnectionState): void {
|
||||||
this.state = next;
|
this.state = next;
|
||||||
if (next.status === "disconnected") {
|
if (next.status === "disconnected") {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
|
|||||||
import {
|
import {
|
||||||
DaemonClient,
|
DaemonClient,
|
||||||
type ConnectionState,
|
type ConnectionState,
|
||||||
type DaemonClientDiagnosticsEvent,
|
|
||||||
type FetchAgentsOptions,
|
type FetchAgentsOptions,
|
||||||
} from "@server/client/daemon-client";
|
} from "@server/client/daemon-client";
|
||||||
import {
|
import {
|
||||||
@@ -38,10 +37,6 @@ import {
|
|||||||
import { createTauriWebSocketTransportFactory } from "@/utils/tauri-daemon-transport";
|
import { createTauriWebSocketTransportFactory } from "@/utils/tauri-daemon-transport";
|
||||||
import { applyFetchedAgentDirectory } from "@/utils/agent-directory-sync";
|
import { applyFetchedAgentDirectory } from "@/utils/agent-directory-sync";
|
||||||
import { useSessionStore, type Agent } from "@/stores/session-store";
|
import { useSessionStore, type Agent } from "@/stores/session-store";
|
||||||
import {
|
|
||||||
recordDaemonClientDiagnostics,
|
|
||||||
recordHostRuntimeCreateClient,
|
|
||||||
} from "@/runtime/perf-diagnostics/host-runtime-diagnostics";
|
|
||||||
|
|
||||||
export type HostRuntimeConnectionStatus =
|
export type HostRuntimeConnectionStatus =
|
||||||
| "idle"
|
| "idle"
|
||||||
@@ -461,16 +456,6 @@ function probeIntervalForConnection(
|
|||||||
function createDefaultDeps(): HostRuntimeControllerDeps {
|
function createDefaultDeps(): HostRuntimeControllerDeps {
|
||||||
return {
|
return {
|
||||||
createClient: ({ host, connection, clientId, runtimeGeneration }) => {
|
createClient: ({ host, connection, clientId, runtimeGeneration }) => {
|
||||||
recordHostRuntimeCreateClient({
|
|
||||||
serverId: host.serverId,
|
|
||||||
connectionType: connection.type,
|
|
||||||
endpoint:
|
|
||||||
connection.type === "directTcp"
|
|
||||||
? connection.endpoint
|
|
||||||
: connection.type === "directSocket" || connection.type === "directPipe"
|
|
||||||
? connection.path
|
|
||||||
: connection.relayEndpoint,
|
|
||||||
});
|
|
||||||
const tauriTransportFactory = createTauriWebSocketTransportFactory();
|
const tauriTransportFactory = createTauriWebSocketTransportFactory();
|
||||||
const localTransportFactory = createTauriLocalDaemonTransportFactory();
|
const localTransportFactory = createTauriLocalDaemonTransportFactory();
|
||||||
const base = {
|
const base = {
|
||||||
@@ -478,8 +463,6 @@ function createDefaultDeps(): HostRuntimeControllerDeps {
|
|||||||
clientId,
|
clientId,
|
||||||
clientType: "mobile" as const,
|
clientType: "mobile" as const,
|
||||||
runtimeGeneration,
|
runtimeGeneration,
|
||||||
onDiagnosticsEvent: (event: DaemonClientDiagnosticsEvent) =>
|
|
||||||
recordDaemonClientDiagnostics(host.serverId, event),
|
|
||||||
};
|
};
|
||||||
if (connection.type === "directSocket" || connection.type === "directPipe") {
|
if (connection.type === "directSocket" || connection.type === "directPipe") {
|
||||||
return new DaemonClient({
|
return new DaemonClient({
|
||||||
@@ -1074,6 +1057,9 @@ export class HostRuntimeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextGeneration = this.snapshot.clientGeneration + 1;
|
const nextGeneration = this.snapshot.clientGeneration + 1;
|
||||||
|
if (existingClient) {
|
||||||
|
existingClient.setReconnectEnabled(true);
|
||||||
|
}
|
||||||
const client = existingClient ?? this.deps.createClient({
|
const client = existingClient ?? this.deps.createClient({
|
||||||
host: this.host,
|
host: this.host,
|
||||||
connection,
|
connection,
|
||||||
@@ -1264,15 +1250,10 @@ export class HostRuntimeStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async bootstrapDesktop(): Promise<void> {
|
private async bootstrapDesktop(): Promise<void> {
|
||||||
const t0 = performance.now();
|
|
||||||
const elapsed = () => Math.round(performance.now() - t0);
|
|
||||||
try {
|
try {
|
||||||
console.info("[HostRuntime] desktop bootstrap: starting", { elapsedMs: elapsed() });
|
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
(async () => {
|
(async () => {
|
||||||
console.info("[HostRuntime] desktop bootstrap: startManagedDaemon", { elapsedMs: elapsed() });
|
|
||||||
const daemon = await startManagedDaemon();
|
const daemon = await startManagedDaemon();
|
||||||
console.info("[HostRuntime] desktop bootstrap: managed daemon ready", { elapsedMs: elapsed(), serverId: daemon.serverId });
|
|
||||||
if (!daemon.serverId) {
|
if (!daemon.serverId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1285,12 +1266,10 @@ export class HostRuntimeStore {
|
|||||||
label: daemon.hostname ?? undefined,
|
label: daemon.hostname ?? undefined,
|
||||||
connection,
|
connection,
|
||||||
});
|
});
|
||||||
console.info("[HostRuntime] desktop bootstrap: host connection upserted", { elapsedMs: elapsed() });
|
|
||||||
})().catch((error) => {
|
})().catch((error) => {
|
||||||
console.warn("[HostRuntime] Failed to bootstrap desktop daemon connection", { elapsedMs: elapsed() }, error);
|
console.warn("[HostRuntime] Failed to bootstrap desktop daemon connection", error);
|
||||||
}),
|
}),
|
||||||
(async () => {
|
(async () => {
|
||||||
console.info("[HostRuntime] desktop bootstrap: probing localhost", { elapsedMs: elapsed() });
|
|
||||||
const { client, serverId, hostname } = await connectToDaemon(
|
const { client, serverId, hostname } = await connectToDaemon(
|
||||||
{
|
{
|
||||||
id: `bootstrap:${DEFAULT_LOCALHOST_ENDPOINT}`,
|
id: `bootstrap:${DEFAULT_LOCALHOST_ENDPOINT}`,
|
||||||
@@ -1299,17 +1278,13 @@ export class HostRuntimeStore {
|
|||||||
},
|
},
|
||||||
{ timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS }
|
{ timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS }
|
||||||
);
|
);
|
||||||
console.info("[HostRuntime] desktop bootstrap: localhost probe succeeded", { elapsedMs: elapsed(), serverId });
|
|
||||||
await this.upsertDirectConnection({
|
await this.upsertDirectConnection({
|
||||||
serverId,
|
serverId,
|
||||||
endpoint: DEFAULT_LOCALHOST_ENDPOINT,
|
endpoint: DEFAULT_LOCALHOST_ENDPOINT,
|
||||||
label: hostname ?? undefined,
|
label: hostname ?? undefined,
|
||||||
existingClient: client,
|
existingClient: client,
|
||||||
});
|
});
|
||||||
console.info("[HostRuntime] desktop bootstrap: direct connection upserted", { elapsedMs: elapsed() });
|
})().catch(() => undefined),
|
||||||
})().catch(() => {
|
|
||||||
console.info("[HostRuntime] desktop bootstrap: localhost probe failed", { elapsedMs: elapsed() });
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[HostRuntime] Failed to bootstrap desktop startup host connections", error);
|
console.warn("[HostRuntime] Failed to bootstrap desktop startup host connections", error);
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
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),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
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: "directTcp" | "directSocket" | "directPipe" | "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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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";
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { useContext } from "react";
|
|
||||||
import { PerfDiagnosticsContext } from "./perf-diagnostics-context";
|
|
||||||
import type { PerfDiagnosticsReporter } from "./types";
|
|
||||||
|
|
||||||
export function usePerfDiagnostics(): PerfDiagnosticsReporter {
|
|
||||||
return useContext(PerfDiagnosticsContext);
|
|
||||||
}
|
|
||||||
@@ -30,11 +30,6 @@ import {
|
|||||||
import { useCreateFlowStore } from "@/stores/create-flow-store";
|
import { useCreateFlowStore } from "@/stores/create-flow-store";
|
||||||
import type { Agent } from "@/contexts/session-context";
|
import type { Agent } from "@/contexts/session-context";
|
||||||
import type { StreamItem } from "@/types/stream";
|
import type { StreamItem } from "@/types/stream";
|
||||||
import {
|
|
||||||
buildAgentNavigationKey,
|
|
||||||
endNavigationTiming,
|
|
||||||
} from "@/utils/navigation-timing";
|
|
||||||
import { startPerfMonitor } from "@/utils/perf-monitor";
|
|
||||||
import {
|
import {
|
||||||
checkoutStatusQueryKey,
|
checkoutStatusQueryKey,
|
||||||
type CheckoutStatusPayload,
|
type CheckoutStatusPayload,
|
||||||
@@ -115,27 +110,6 @@ export function AgentReadyScreen({
|
|||||||
const lastConnectionError = runtimeSnapshot?.lastError ?? null;
|
const lastConnectionError = runtimeSnapshot?.lastError ?? null;
|
||||||
const isRuntimeSessionAvailable = Boolean(resolvedServerId && runtimeClient);
|
const isRuntimeSessionAvailable = Boolean(resolvedServerId && runtimeClient);
|
||||||
|
|
||||||
const focusServerId = resolvedServerId;
|
|
||||||
const navigationStatus = isRuntimeSessionAvailable
|
|
||||||
? "ready"
|
|
||||||
: "session_unavailable";
|
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
if (!resolvedAgentId || !focusServerId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const navigationKey = buildAgentNavigationKey(
|
|
||||||
focusServerId,
|
|
||||||
resolvedAgentId
|
|
||||||
);
|
|
||||||
endNavigationTiming(navigationKey, {
|
|
||||||
screen: "agent",
|
|
||||||
status: navigationStatus,
|
|
||||||
});
|
|
||||||
}, [focusServerId, navigationStatus, resolvedAgentId])
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!resolvedServerId || !runtimeClient) {
|
if (!resolvedServerId || !runtimeClient) {
|
||||||
return (
|
return (
|
||||||
<AgentSessionUnavailableState
|
<AgentSessionUnavailableState
|
||||||
@@ -294,15 +268,6 @@ function AgentScreenContent({
|
|||||||
}
|
}
|
||||||
openFileExplorer();
|
openFileExplorer();
|
||||||
}, [activateExplorerTabForCheckout, openFileExplorer, resolveCurrentExplorerCheckout]);
|
}, [activateExplorerTabForCheckout, openFileExplorer, resolveCurrentExplorerCheckout]);
|
||||||
useEffect(() => {
|
|
||||||
if (Platform.OS !== "web") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const scope = `agent:${serverId}:${agentId ?? "unknown"}`;
|
|
||||||
const stop = startPerfMonitor(scope);
|
|
||||||
return stop;
|
|
||||||
}, [serverId, agentId]);
|
|
||||||
|
|
||||||
// Swipe-left gesture to open explorer sidebar on mobile
|
// Swipe-left gesture to open explorer sidebar on mobile
|
||||||
const explorerOpenGesture = useExplorerOpenGesture({
|
const explorerOpenGesture = useExplorerOpenGesture({
|
||||||
enabled: isMobile && mobileView === "agent",
|
enabled: isMobile && mobileView === "agent",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { SidebarMenuToggle } from "@/components/headers/menu-header";
|
|||||||
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import { getIsTauriMac } from "@/constants/layout";
|
import { getIsTauriMac } from "@/constants/layout";
|
||||||
import { useTrafficLightPadding } from "@/utils/tauri-window";
|
import { useTauriDragHandlers, useTrafficLightPadding } from "@/utils/tauri-window";
|
||||||
|
|
||||||
export function OpenProjectScreen({ serverId: _serverId }: { serverId: string }) {
|
export function OpenProjectScreen({ serverId: _serverId }: { serverId: string }) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
@@ -21,6 +21,7 @@ export function OpenProjectScreen({ serverId: _serverId }: { serverId: string })
|
|||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||||
const needsTrafficLightInset = !isMobile && !desktopAgentListOpen && getIsTauriMac();
|
const needsTrafficLightInset = !isMobile && !desktopAgentListOpen && getIsTauriMac();
|
||||||
const trafficLightInset = needsTrafficLightInset ? trafficLightPadding.left : 0;
|
const trafficLightInset = needsTrafficLightInset ? trafficLightPadding.left : 0;
|
||||||
|
const dragHandlers = useTauriDragHandlers();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
@@ -29,7 +30,7 @@ export function OpenProjectScreen({ serverId: _serverId }: { serverId: string })
|
|||||||
}, [isMobile, openAgentList]);
|
}, [isMobile, openAgentList]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container} {...dragHandlers}>
|
||||||
<View style={[styles.menuToggle, { paddingTop: insets.top, paddingLeft: trafficLightInset }]}>
|
<View style={[styles.menuToggle, { paddingTop: insets.top, paddingLeft: trafficLightInset }]}>
|
||||||
<SidebarMenuToggle />
|
<SidebarMenuToggle />
|
||||||
</View>
|
</View>
|
||||||
@@ -56,6 +57,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: theme.colors.surface0,
|
backgroundColor: theme.colors.surface0,
|
||||||
|
userSelect: "none",
|
||||||
},
|
},
|
||||||
menuToggle: {
|
menuToggle: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { AgentInputArea } from '@/components/agent-input-area'
|
|||||||
import { FileDropZone } from '@/components/file-drop-zone'
|
import { FileDropZone } from '@/components/file-drop-zone'
|
||||||
import { AgentStreamView } from '@/components/agent-stream-view'
|
import { AgentStreamView } from '@/components/agent-stream-view'
|
||||||
import type { ImageAttachment } from '@/components/message-input'
|
import type { ImageAttachment } from '@/components/message-input'
|
||||||
import { MAX_CONTENT_WIDTH } from '@/constants/layout'
|
|
||||||
import { useAgentFormState } from '@/hooks/use-agent-form-state'
|
import { useAgentFormState } from '@/hooks/use-agent-form-state'
|
||||||
import { useDraftAgentCreateFlow } from '@/hooks/use-draft-agent-create-flow'
|
import { useDraftAgentCreateFlow } from '@/hooks/use-draft-agent-create-flow'
|
||||||
import { useHostRuntimeSession } from '@/runtime/host-runtime'
|
import { useHostRuntimeSession } from '@/runtime/host-runtime'
|
||||||
@@ -260,8 +259,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
alignSelf: 'center',
|
|
||||||
maxWidth: MAX_CONTENT_WIDTH,
|
|
||||||
backgroundColor: theme.colors.surface0,
|
backgroundColor: theme.colors.surface0,
|
||||||
},
|
},
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import type {
|
|||||||
WorkspaceDescriptorPayload,
|
WorkspaceDescriptorPayload,
|
||||||
} from "@server/shared/messages";
|
} from "@server/shared/messages";
|
||||||
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
|
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
|
||||||
import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
|
|
||||||
import {
|
import {
|
||||||
createAgentLastActivityCoalescer,
|
createAgentLastActivityCoalescer,
|
||||||
type AgentLastActivityCommitter,
|
type AgentLastActivityCommitter,
|
||||||
@@ -357,30 +356,8 @@ interface SessionStoreActions {
|
|||||||
|
|
||||||
type SessionStore = SessionStoreState & SessionStoreActions;
|
type SessionStore = SessionStoreState & SessionStoreActions;
|
||||||
|
|
||||||
const SESSION_STORE_LOG_TAG = "[SessionStore]";
|
|
||||||
let sessionStoreUpdateCount = 0;
|
|
||||||
const agentLastActivityCoalescer = createAgentLastActivityCoalescer();
|
const agentLastActivityCoalescer = createAgentLastActivityCoalescer();
|
||||||
|
|
||||||
function logSessionStoreUpdate(
|
|
||||||
type: string,
|
|
||||||
serverId: string,
|
|
||||||
payload?: unknown
|
|
||||||
) {
|
|
||||||
if (!isPerfLoggingEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sessionStoreUpdateCount += 1;
|
|
||||||
const metrics = payload ? measurePayload(payload) : null;
|
|
||||||
perfLog(SESSION_STORE_LOG_TAG, {
|
|
||||||
event: type,
|
|
||||||
serverId,
|
|
||||||
updateCount: sessionStoreUpdateCount,
|
|
||||||
payloadApproxBytes: metrics?.approxBytes ?? 0,
|
|
||||||
payloadFieldCount: metrics?.fieldCount ?? 0,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to create initial session state
|
// Helper to create initial session state
|
||||||
function createInitialSessionState(serverId: string, client: DaemonClient, audioPlayer: ReturnType<typeof useAudioPlayer>): SessionState {
|
function createInitialSessionState(serverId: string, client: DaemonClient, audioPlayer: ReturnType<typeof useAudioPlayer>): SessionState {
|
||||||
return {
|
return {
|
||||||
@@ -452,7 +429,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (prev.sessions[serverId]) {
|
if (prev.sessions[serverId]) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("initializeSession", serverId);
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -470,7 +446,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (!session) {
|
if (!session) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("clearSession", serverId);
|
|
||||||
const nextSessions = { ...prev.sessions };
|
const nextSessions = { ...prev.sessions };
|
||||||
delete nextSessions[serverId];
|
delete nextSessions[serverId];
|
||||||
let nextActivity = prev.agentLastActivity;
|
let nextActivity = prev.agentLastActivity;
|
||||||
@@ -507,12 +482,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
logSessionStoreUpdate("updateSessionClient", serverId, {
|
|
||||||
wasNull: session.client === null,
|
|
||||||
isNowConnected: client.isConnected,
|
|
||||||
isNowConnecting: client.isConnecting,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -549,12 +518,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
logSessionStoreUpdate("updateSessionServerInfo", serverId, {
|
|
||||||
serverId: info.serverId,
|
|
||||||
hostname: nextHostname,
|
|
||||||
version: nextVersion,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -584,7 +547,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (!session || session.isPlayingAudio === playing) {
|
if (!session || session.isPlayingAudio === playing) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("setIsPlayingAudio", serverId, { playing });
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -602,8 +564,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (!session || session.focusedAgentId === agentId) {
|
if (!session || session.focusedAgentId === agentId) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("setFocusedAgentId", serverId, { agentId });
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -629,7 +589,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (session.messages === nextMessages) {
|
if (session.messages === nextMessages) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("setMessages", serverId, { count: nextMessages.length });
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -650,7 +609,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (session.currentAssistantMessage === nextMessage) {
|
if (session.currentAssistantMessage === nextMessage) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("setCurrentAssistantMessage", serverId, { length: nextMessage.length });
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -672,7 +630,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (session.agentStreamTail === nextState) {
|
if (session.agentStreamTail === nextState) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("setAgentStreamTail", serverId, { agentCount: nextState.size });
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -693,7 +650,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (session.agentStreamHead === nextState) {
|
if (session.agentStreamHead === nextState) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("setAgentStreamHead", serverId, { agentCount: nextState.size });
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -745,12 +701,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
logSessionStoreUpdate("setAgentStreamState", serverId, {
|
|
||||||
agentId,
|
|
||||||
changedTail,
|
|
||||||
changedHead,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -776,7 +726,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
}
|
}
|
||||||
const nextHead = new Map(session.agentStreamHead);
|
const nextHead = new Map(session.agentStreamHead);
|
||||||
nextHead.delete(agentId);
|
nextHead.delete(agentId);
|
||||||
logSessionStoreUpdate("clearAgentStreamHead", serverId, { agentId });
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -798,9 +747,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (session.agentTimelineCursor === nextState) {
|
if (session.agentTimelineCursor === nextState) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("setAgentTimelineCursor", serverId, {
|
|
||||||
agentCount: nextState.size,
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -818,9 +764,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
const nextGeneration = session.historySyncGeneration + 1;
|
const nextGeneration = session.historySyncGeneration + 1;
|
||||||
logSessionStoreUpdate("bumpHistorySyncGeneration", serverId, {
|
|
||||||
generation: nextGeneration,
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -847,10 +790,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
}
|
}
|
||||||
const nextMap = new Map(session.agentHistorySyncGeneration);
|
const nextMap = new Map(session.agentHistorySyncGeneration);
|
||||||
nextMap.set(agentId, currentGeneration);
|
nextMap.set(agentId, currentGeneration);
|
||||||
logSessionStoreUpdate("markAgentHistorySynchronized", serverId, {
|
|
||||||
agentId,
|
|
||||||
generation: currentGeneration,
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -884,11 +823,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
nextApplied.delete(agentId);
|
nextApplied.delete(agentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
logSessionStoreUpdate("setAgentAuthoritativeHistoryApplied", serverId, {
|
|
||||||
agentId,
|
|
||||||
applied,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -913,7 +847,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (session.initializingAgents === nextState) {
|
if (session.initializingAgents === nextState) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("setInitializingAgents", serverId, { count: nextState.size });
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -935,7 +868,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (session.agents === nextAgents) {
|
if (session.agents === nextAgents) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("setAgents", serverId, { count: nextAgents.size });
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -957,7 +889,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (session.workspaces === nextWorkspaces) {
|
if (session.workspaces === nextWorkspaces) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("setWorkspaces", serverId, { count: nextWorkspaces.size });
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -988,7 +919,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (!changed) {
|
if (!changed) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("mergeWorkspaces", serverId, { count: nextEntries.length });
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -1007,7 +937,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
}
|
}
|
||||||
const next = new Map(session.workspaces);
|
const next = new Map(session.workspaces);
|
||||||
next.delete(workspaceId);
|
next.delete(workspaceId);
|
||||||
logSessionStoreUpdate("removeWorkspace", serverId, { workspaceId });
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -1074,7 +1003,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (session.pendingPermissions === nextPerms) {
|
if (session.pendingPermissions === nextPerms) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("setPendingPermissions", serverId, { count: nextPerms.size });
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -1096,7 +1024,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (session.fileExplorer === nextState) {
|
if (session.fileExplorer === nextState) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("setFileExplorer", serverId, { agentCount: nextState.size });
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -1118,7 +1045,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (session.queuedMessages === nextValue) {
|
if (session.queuedMessages === nextValue) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("setQueuedMessages", serverId, { agentCount: nextValue.size });
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -1136,7 +1062,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (!session || session.hasHydratedAgents === hydrated) {
|
if (!session || session.hasHydratedAgents === hydrated) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("setHasHydratedAgents", serverId, { hydrated });
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
@@ -1153,7 +1078,6 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
if (!session || session.hasHydratedWorkspaces === hydrated) {
|
if (!session || session.hasHydratedWorkspaces === hydrated) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
logSessionStoreUpdate("setHasHydratedWorkspaces", serverId, { hydrated });
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
sessions: {
|
sessions: {
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf";
|
|
||||||
|
|
||||||
type NavigationTimingDetails = {
|
|
||||||
from: string;
|
|
||||||
to: string;
|
|
||||||
params?: Record<string, unknown>;
|
|
||||||
targetMs?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type NavigationTimingEntry = NavigationTimingDetails & {
|
|
||||||
startedAt: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const NAVIGATION_TAG = "[NavigationTiming]";
|
|
||||||
const pendingNavigations = new Map<string, NavigationTimingEntry>();
|
|
||||||
|
|
||||||
export const HOME_NAVIGATION_KEY = "home";
|
|
||||||
|
|
||||||
export const buildAgentNavigationKey = (serverId: string, agentId: string) =>
|
|
||||||
`agent:${serverId}:${agentId}`;
|
|
||||||
|
|
||||||
export const startNavigationTiming = (key: string, details: NavigationTimingDetails): void => {
|
|
||||||
if (!isPerfLoggingEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingNavigations.set(key, {
|
|
||||||
...details,
|
|
||||||
startedAt: getNowMs(),
|
|
||||||
});
|
|
||||||
|
|
||||||
perfLog(NAVIGATION_TAG, {
|
|
||||||
phase: "start",
|
|
||||||
key,
|
|
||||||
from: details.from,
|
|
||||||
to: details.to,
|
|
||||||
targetMs: details.targetMs ?? null,
|
|
||||||
params: details.params ?? null,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const endNavigationTiming = (key: string, extra?: Record<string, unknown>): void => {
|
|
||||||
if (!isPerfLoggingEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = pendingNavigations.get(key);
|
|
||||||
if (!entry) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pendingNavigations.delete(key);
|
|
||||||
|
|
||||||
const durationMs = getNowMs() - entry.startedAt;
|
|
||||||
perfLog(NAVIGATION_TAG, {
|
|
||||||
phase: "complete",
|
|
||||||
key,
|
|
||||||
from: entry.from,
|
|
||||||
to: entry.to,
|
|
||||||
durationMs: Number(durationMs.toFixed(2)),
|
|
||||||
targetMs: entry.targetMs ?? null,
|
|
||||||
params: entry.params ?? null,
|
|
||||||
extra: extra ?? null,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cancelNavigationTiming = (key: string, reason?: string): void => {
|
|
||||||
if (!isPerfLoggingEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pendingNavigations.has(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pendingNavigations.delete(key);
|
|
||||||
perfLog(NAVIGATION_TAG, {
|
|
||||||
phase: "cancelled",
|
|
||||||
key,
|
|
||||||
reason: reason ?? null,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// 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,113 +0,0 @@
|
|||||||
import { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf";
|
|
||||||
|
|
||||||
const PERF_MONITOR_LOG_TAG = "[PerfMonitor]";
|
|
||||||
const FRAME_GAP_THRESHOLD_MS = 100;
|
|
||||||
const EVENT_LOOP_STALL_THRESHOLD_MS = 100;
|
|
||||||
const EVENT_LOOP_TICK_MS = 100;
|
|
||||||
const LONG_TASK_THRESHOLD_MS = 100;
|
|
||||||
|
|
||||||
type StopPerfMonitor = () => void;
|
|
||||||
|
|
||||||
type GlobalScope = typeof globalThis & {
|
|
||||||
requestAnimationFrame?: (callback: (timestamp: number) => void) => number;
|
|
||||||
cancelAnimationFrame?: (handle: number) => void;
|
|
||||||
PerformanceObserver?: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PerfObserverEntry = {
|
|
||||||
duration: number;
|
|
||||||
startTime: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PerfObserverLike = {
|
|
||||||
observe: (options: { entryTypes: string[] }) => void;
|
|
||||||
disconnect: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PerfObserverConstructor = new (
|
|
||||||
callback: (list: { getEntries(): PerfObserverEntry[] }) => void
|
|
||||||
) => PerfObserverLike;
|
|
||||||
|
|
||||||
export function startPerfMonitor(scope: string): StopPerfMonitor {
|
|
||||||
if (!isPerfLoggingEnabled()) {
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalScope = globalThis as GlobalScope;
|
|
||||||
if (!globalScope) {
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const startMs = getNowMs();
|
|
||||||
perfLog(PERF_MONITOR_LOG_TAG, { event: "monitor_start", scope });
|
|
||||||
|
|
||||||
let rafHandle: number | null = null;
|
|
||||||
let lastFrameMs = getNowMs();
|
|
||||||
|
|
||||||
if (typeof globalScope.requestAnimationFrame === "function") {
|
|
||||||
const onFrame = (now: number) => {
|
|
||||||
const delta = now - lastFrameMs;
|
|
||||||
if (delta >= FRAME_GAP_THRESHOLD_MS) {
|
|
||||||
perfLog(PERF_MONITOR_LOG_TAG, {
|
|
||||||
event: "frame_gap",
|
|
||||||
scope,
|
|
||||||
deltaMs: Math.round(delta),
|
|
||||||
sinceStartMs: Math.round(now - startMs),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
lastFrameMs = now;
|
|
||||||
rafHandle = globalScope.requestAnimationFrame?.(onFrame) ?? null;
|
|
||||||
};
|
|
||||||
rafHandle = globalScope.requestAnimationFrame(onFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastTickMs = getNowMs();
|
|
||||||
const intervalHandle = setInterval(() => {
|
|
||||||
const now = getNowMs();
|
|
||||||
const drift = now - lastTickMs - EVENT_LOOP_TICK_MS;
|
|
||||||
if (drift >= EVENT_LOOP_STALL_THRESHOLD_MS) {
|
|
||||||
perfLog(PERF_MONITOR_LOG_TAG, {
|
|
||||||
event: "event_loop_stall",
|
|
||||||
scope,
|
|
||||||
driftMs: Math.round(drift),
|
|
||||||
sinceStartMs: Math.round(now - startMs),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
lastTickMs = now;
|
|
||||||
}, EVENT_LOOP_TICK_MS);
|
|
||||||
|
|
||||||
let observer: PerfObserverLike | null = null;
|
|
||||||
const ObserverCtor = globalScope.PerformanceObserver as PerfObserverConstructor | undefined;
|
|
||||||
if (typeof ObserverCtor === "function") {
|
|
||||||
try {
|
|
||||||
observer = new ObserverCtor((list) => {
|
|
||||||
for (const entry of list.getEntries()) {
|
|
||||||
if (entry.duration >= LONG_TASK_THRESHOLD_MS) {
|
|
||||||
perfLog(PERF_MONITOR_LOG_TAG, {
|
|
||||||
event: "longtask",
|
|
||||||
scope,
|
|
||||||
durationMs: Math.round(entry.duration),
|
|
||||||
startMs: Math.round(entry.startTime),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
observer.observe({ entryTypes: ["longtask"] });
|
|
||||||
} catch (error) {
|
|
||||||
perfLog(PERF_MONITOR_LOG_TAG, {
|
|
||||||
event: "longtask_observer_error",
|
|
||||||
scope,
|
|
||||||
message: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (rafHandle !== null && typeof globalScope.cancelAnimationFrame === "function") {
|
|
||||||
globalScope.cancelAnimationFrame(rafHandle);
|
|
||||||
}
|
|
||||||
clearInterval(intervalHandle);
|
|
||||||
observer?.disconnect();
|
|
||||||
perfLog(PERF_MONITOR_LOG_TAG, { event: "monitor_stop", scope });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
const PERF_LOGGING_ENABLED = false;
|
|
||||||
|
|
||||||
export const isPerfLoggingEnabled = (): boolean => PERF_LOGGING_ENABLED;
|
|
||||||
|
|
||||||
export const perfLog = (tag: string, details: Record<string, unknown>): void => {
|
|
||||||
if (!PERF_LOGGING_ENABLED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.info(tag, details);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getNowMs = (): number => {
|
|
||||||
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
||||||
return performance.now();
|
|
||||||
}
|
|
||||||
return Date.now();
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface PayloadMetrics {
|
|
||||||
approxBytes: number;
|
|
||||||
fieldCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MEASUREMENT_DEPTH = 2;
|
|
||||||
|
|
||||||
export const measurePayload = (payload: unknown): PayloadMetrics => {
|
|
||||||
if (!payload || typeof payload !== "object") {
|
|
||||||
return {
|
|
||||||
approxBytes: 0,
|
|
||||||
fieldCount: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldCount = Object.keys(payload as Record<string, unknown>).length;
|
|
||||||
const approxBytes = estimateSize(payload, MEASUREMENT_DEPTH, new WeakSet());
|
|
||||||
|
|
||||||
return {
|
|
||||||
approxBytes,
|
|
||||||
fieldCount,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const estimateSize = (value: unknown, depth: number, seen: WeakSet<object>): number => {
|
|
||||||
if (depth <= 0 || value === null || value === undefined) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "number") {
|
|
||||||
return 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "boolean") {
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "bigint") {
|
|
||||||
return value.toString().length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "symbol" || typeof value === "function") {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value !== "object") {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const objectValue = value as object;
|
|
||||||
if (seen.has(objectValue)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
seen.add(objectValue);
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.reduce((total, item) => total + estimateSize(item, depth - 1, seen), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value instanceof Map) {
|
|
||||||
let total = 0;
|
|
||||||
for (const [mapKey, mapValue] of value.entries()) {
|
|
||||||
total += estimateSize(mapKey, depth - 1, seen);
|
|
||||||
total += estimateSize(mapValue, depth - 1, seen);
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value instanceof Set) {
|
|
||||||
let total = 0;
|
|
||||||
for (const setValue of value.values()) {
|
|
||||||
total += estimateSize(setValue, depth - 1, seen);
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
let total = 0;
|
|
||||||
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
||||||
total += key.length;
|
|
||||||
total += estimateSize(child, depth - 1, seen);
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
};
|
|
||||||
@@ -56,6 +56,7 @@ export async function buildClientConfig(
|
|||||||
clientId,
|
clientId,
|
||||||
clientType: "mobile" as const,
|
clientType: "mobile" as const,
|
||||||
suppressSendErrors: true,
|
suppressSendErrors: true,
|
||||||
|
reconnect: { enabled: false },
|
||||||
...(connection.type === "directSocket" || connection.type === "directPipe"
|
...(connection.type === "directSocket" || connection.type === "directPipe"
|
||||||
? localTransportFactory
|
? localTransportFactory
|
||||||
? { transportFactory: localTransportFactory }
|
? { transportFactory: localTransportFactory }
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getNowMs, isPerfLoggingEnabled, perfLog } from "./perf";
|
|
||||||
|
|
||||||
const TOOL_CALL_DIFF_LOG_TAG = "[ToolCallDiff]";
|
|
||||||
const LINE_DIFF_DURATION_THRESHOLD_MS = 16;
|
|
||||||
const WORD_DIFF_DURATION_THRESHOLD_MS = 8;
|
|
||||||
const LINE_DIFF_MATRIX_THRESHOLD = 200000;
|
|
||||||
const WORD_DIFF_MATRIX_THRESHOLD = 50000;
|
|
||||||
|
|
||||||
export type DiffSegment = {
|
export type DiffSegment = {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -56,14 +49,11 @@ function splitIntoWords(text: string): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function computeWordLevelDiff(oldLine: string, newLine: string): { oldSegments: DiffSegment[]; newSegments: DiffSegment[] } {
|
function computeWordLevelDiff(oldLine: string, newLine: string): { oldSegments: DiffSegment[]; newSegments: DiffSegment[] } {
|
||||||
const shouldLog = isPerfLoggingEnabled();
|
|
||||||
const startMs = shouldLog ? getNowMs() : 0;
|
|
||||||
const oldWords = splitIntoWords(oldLine);
|
const oldWords = splitIntoWords(oldLine);
|
||||||
const newWords = splitIntoWords(newLine);
|
const newWords = splitIntoWords(newLine);
|
||||||
|
|
||||||
const m = oldWords.length;
|
const m = oldWords.length;
|
||||||
const n = newWords.length;
|
const n = newWords.length;
|
||||||
const matrixSize = m * n;
|
|
||||||
|
|
||||||
// LCS to find common words
|
// LCS to find common words
|
||||||
const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
||||||
@@ -131,22 +121,6 @@ function computeWordLevelDiff(oldLine: string, newLine: string): { oldSegments:
|
|||||||
const oldSegments = buildSegments(oldWords, oldInLCS);
|
const oldSegments = buildSegments(oldWords, oldInLCS);
|
||||||
const newSegments = buildSegments(newWords, newInLCS);
|
const newSegments = buildSegments(newWords, newInLCS);
|
||||||
|
|
||||||
if (shouldLog) {
|
|
||||||
const durationMs = getNowMs() - startMs;
|
|
||||||
if (
|
|
||||||
durationMs >= WORD_DIFF_DURATION_THRESHOLD_MS ||
|
|
||||||
matrixSize >= WORD_DIFF_MATRIX_THRESHOLD
|
|
||||||
) {
|
|
||||||
perfLog(TOOL_CALL_DIFF_LOG_TAG, {
|
|
||||||
event: "word_diff",
|
|
||||||
durationMs: Math.round(durationMs),
|
|
||||||
oldWordCount: m,
|
|
||||||
newWordCount: n,
|
|
||||||
matrixSize,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
oldSegments,
|
oldSegments,
|
||||||
newSegments,
|
newSegments,
|
||||||
@@ -154,8 +128,6 @@ function computeWordLevelDiff(oldLine: string, newLine: string): { oldSegments:
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildLineDiff(originalText: string, updatedText: string): DiffLine[] {
|
export function buildLineDiff(originalText: string, updatedText: string): DiffLine[] {
|
||||||
const shouldLog = isPerfLoggingEnabled();
|
|
||||||
const startMs = shouldLog ? getNowMs() : 0;
|
|
||||||
const originalLines = splitIntoLines(originalText);
|
const originalLines = splitIntoLines(originalText);
|
||||||
const updatedLines = splitIntoLines(updatedText);
|
const updatedLines = splitIntoLines(updatedText);
|
||||||
|
|
||||||
@@ -166,7 +138,6 @@ export function buildLineDiff(originalText: string, updatedText: string): DiffLi
|
|||||||
|
|
||||||
const m = originalLines.length;
|
const m = originalLines.length;
|
||||||
const n = updatedLines.length;
|
const n = updatedLines.length;
|
||||||
const matrixSize = m * n;
|
|
||||||
const dp: number[][] = Array.from({ length: m + 1 }, () =>
|
const dp: number[][] = Array.from({ length: m + 1 }, () =>
|
||||||
Array(n + 1).fill(0)
|
Array(n + 1).fill(0)
|
||||||
);
|
);
|
||||||
@@ -225,23 +196,6 @@ export function buildLineDiff(originalText: string, updatedText: string): DiffLi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldLog) {
|
|
||||||
const durationMs = getNowMs() - startMs;
|
|
||||||
if (
|
|
||||||
durationMs >= LINE_DIFF_DURATION_THRESHOLD_MS ||
|
|
||||||
matrixSize >= LINE_DIFF_MATRIX_THRESHOLD
|
|
||||||
) {
|
|
||||||
perfLog(TOOL_CALL_DIFF_LOG_TAG, {
|
|
||||||
event: "line_diff",
|
|
||||||
durationMs: Math.round(durationMs),
|
|
||||||
originalLineCount: m,
|
|
||||||
updatedLineCount: n,
|
|
||||||
diffLineCount: diff.length,
|
|
||||||
matrixSize,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return diff;
|
return diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,13 +102,6 @@ const consoleLogger: Logger = {
|
|||||||
error: (obj, msg) => console.error(msg, obj),
|
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 {
|
export type {
|
||||||
DaemonTransport,
|
DaemonTransport,
|
||||||
DaemonTransportFactory,
|
DaemonTransportFactory,
|
||||||
@@ -180,26 +173,8 @@ export type DaemonClientConfig = {
|
|||||||
baseDelayMs?: number
|
baseDelayMs?: number
|
||||||
maxDelayMs?: 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 = {
|
export type SendMessageOptions = {
|
||||||
messageId?: string
|
messageId?: string
|
||||||
images?: Array<{ data: string; mimeType: string }>
|
images?: Array<{ data: string; mimeType: string }>
|
||||||
@@ -489,14 +464,6 @@ export class DaemonClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitDiagnosticsEvent(event: DaemonClientDiagnosticsEvent): void {
|
|
||||||
try {
|
|
||||||
this.config.onDiagnosticsEvent?.(event)
|
|
||||||
} catch {
|
|
||||||
// Diagnostics hooks must never break daemon message handling.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Connection
|
// Connection
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -2873,7 +2840,6 @@ export class DaemonClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleTransportMessage(data: unknown): void {
|
private handleTransportMessage(data: unknown): void {
|
||||||
const startedAtMs = getNowMs()
|
|
||||||
const rawData =
|
const rawData =
|
||||||
data && typeof data === 'object' && 'data' in data ? (data as { data: unknown }).data : data
|
data && typeof data === 'object' && 'data' in data ? (data as { data: unknown }).data : data
|
||||||
|
|
||||||
@@ -2897,12 +2863,6 @@ export class DaemonClient {
|
|||||||
if (rawBytes) {
|
if (rawBytes) {
|
||||||
const frame = decodeBinaryMuxFrame(rawBytes)
|
const frame = decodeBinaryMuxFrame(rawBytes)
|
||||||
if (frame) {
|
if (frame) {
|
||||||
this.emitDiagnosticsEvent({
|
|
||||||
type: 'transport_binary_frame',
|
|
||||||
channel: frame.channel,
|
|
||||||
messageType: frame.messageType,
|
|
||||||
payloadBytes: frame.payload?.byteLength ?? 0,
|
|
||||||
})
|
|
||||||
this.handleBinaryFrame(frame)
|
this.handleBinaryFrame(frame)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2913,52 +2873,19 @@ export class DaemonClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let parsedJson: unknown
|
let parsedJson: unknown
|
||||||
let parseMs = 0
|
|
||||||
try {
|
try {
|
||||||
const parseStartedAtMs = getNowMs()
|
|
||||||
parsedJson = JSON.parse(payload)
|
parsedJson = JSON.parse(payload)
|
||||||
parseMs = getNowMs() - parseStartedAtMs
|
|
||||||
} catch {
|
} catch {
|
||||||
this.emitDiagnosticsEvent({
|
|
||||||
type: 'transport_message_timing',
|
|
||||||
messageType: 'unknown',
|
|
||||||
payloadBytes: payload.length,
|
|
||||||
parseMs: 0,
|
|
||||||
validateMs: 0,
|
|
||||||
totalMs: getNowMs() - startedAtMs,
|
|
||||||
outcome: 'parse_error',
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateStartedAtMs = getNowMs()
|
|
||||||
const parsed = WSOutboundMessageSchema.safeParse(parsedJson)
|
const parsed = WSOutboundMessageSchema.safeParse(parsedJson)
|
||||||
const validateMs = getNowMs() - validateStartedAtMs
|
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
const msgType = (parsedJson as { type?: string })?.type ?? 'unknown'
|
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')
|
this.logger.warn({ msgType, error: parsed.error.message }, 'Message validation failed')
|
||||||
return
|
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') {
|
if (parsed.data.type === 'pong') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -3018,6 +2945,10 @@ export class DaemonClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setReconnectEnabled(enabled: boolean): void {
|
||||||
|
this.config = { ...this.config, reconnect: { ...this.config.reconnect, enabled } }
|
||||||
|
}
|
||||||
|
|
||||||
private scheduleReconnect(input?: {
|
private scheduleReconnect(input?: {
|
||||||
reason?: string
|
reason?: string
|
||||||
event?: string
|
event?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user