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,
|
||||
}).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(
|
||||
(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,
|
||||
},
|
||||
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", {
|
||||
|
||||
@@ -64,7 +64,6 @@ import {
|
||||
parseWorkspaceOpenIntent,
|
||||
} from "@/utils/host-routes";
|
||||
import { getTauri } from "@/utils/tauri";
|
||||
import { PerfDiagnosticsProvider } from "@/runtime/perf-diagnostics";
|
||||
import { attachConsole } from "@/utils/tauri-attach-console";
|
||||
|
||||
polyfillCrypto();
|
||||
@@ -556,55 +555,53 @@ export default function RootLayout() {
|
||||
<GestureHandlerRootView
|
||||
style={{ flex: 1, backgroundColor: darkTheme.colors.surface0 }}
|
||||
>
|
||||
<PerfDiagnosticsProvider scope="root_layout">
|
||||
<PortalProvider>
|
||||
<SafeAreaProvider>
|
||||
<KeyboardProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<QueryProvider>
|
||||
<HostRuntimeBootstrapProvider>
|
||||
<PushNotificationRouter />
|
||||
<HostSessionManager />
|
||||
<ProvidersWrapper>
|
||||
<SidebarAnimationProvider>
|
||||
<HorizontalScrollProvider>
|
||||
<ToastProvider>
|
||||
<AppWithSidebar>
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
animation: "none",
|
||||
contentStyle: {
|
||||
backgroundColor: darkTheme.colors.surface0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="index" />
|
||||
<Stack.Screen name="settings" />
|
||||
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]" />
|
||||
<Stack.Screen
|
||||
name="h/[serverId]/agent/[agentId]"
|
||||
options={{ gestureEnabled: false }}
|
||||
/>
|
||||
<Stack.Screen name="h/[serverId]/index" />
|
||||
<Stack.Screen name="h/[serverId]/agents" />
|
||||
<Stack.Screen name="h/[serverId]/new-agent" />
|
||||
<Stack.Screen name="h/[serverId]/open-project" />
|
||||
<Stack.Screen name="h/[serverId]/settings" />
|
||||
<Stack.Screen name="pair-scan" />
|
||||
</Stack>
|
||||
</AppWithSidebar>
|
||||
</ToastProvider>
|
||||
</HorizontalScrollProvider>
|
||||
</SidebarAnimationProvider>
|
||||
</ProvidersWrapper>
|
||||
</HostRuntimeBootstrapProvider>
|
||||
</QueryProvider>
|
||||
</BottomSheetModalProvider>
|
||||
</KeyboardProvider>
|
||||
</SafeAreaProvider>
|
||||
</PortalProvider>
|
||||
</PerfDiagnosticsProvider>
|
||||
<PortalProvider>
|
||||
<SafeAreaProvider>
|
||||
<KeyboardProvider>
|
||||
<BottomSheetModalProvider>
|
||||
<QueryProvider>
|
||||
<HostRuntimeBootstrapProvider>
|
||||
<PushNotificationRouter />
|
||||
<HostSessionManager />
|
||||
<ProvidersWrapper>
|
||||
<SidebarAnimationProvider>
|
||||
<HorizontalScrollProvider>
|
||||
<ToastProvider>
|
||||
<AppWithSidebar>
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
animation: "none",
|
||||
contentStyle: {
|
||||
backgroundColor: darkTheme.colors.surface0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="index" />
|
||||
<Stack.Screen name="settings" />
|
||||
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]" />
|
||||
<Stack.Screen
|
||||
name="h/[serverId]/agent/[agentId]"
|
||||
options={{ gestureEnabled: false }}
|
||||
/>
|
||||
<Stack.Screen name="h/[serverId]/index" />
|
||||
<Stack.Screen name="h/[serverId]/agents" />
|
||||
<Stack.Screen name="h/[serverId]/new-agent" />
|
||||
<Stack.Screen name="h/[serverId]/open-project" />
|
||||
<Stack.Screen name="h/[serverId]/settings" />
|
||||
<Stack.Screen name="pair-scan" />
|
||||
</Stack>
|
||||
</AppWithSidebar>
|
||||
</ToastProvider>
|
||||
</HorizontalScrollProvider>
|
||||
</SidebarAnimationProvider>
|
||||
</ProvidersWrapper>
|
||||
</HostRuntimeBootstrapProvider>
|
||||
</QueryProvider>
|
||||
</BottomSheetModalProvider>
|
||||
</KeyboardProvider>
|
||||
</SafeAreaProvider>
|
||||
</PortalProvider>
|
||||
</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 { useHosts } from '@/runtime/host-runtime'
|
||||
import { shouldUseManagedDesktopDaemon } from '@/desktop/managed-runtime/managed-runtime'
|
||||
@@ -13,8 +13,6 @@ import {
|
||||
} from './index-startup'
|
||||
|
||||
const STARTUP_TIMEOUT_MS = 30_000
|
||||
const APP_START_TIME = performance.now()
|
||||
|
||||
function useAnyHostOnline(serverIds: string[]): string | null {
|
||||
const runtime = getHostRuntimeStore()
|
||||
return useSyncExternalStore(
|
||||
@@ -61,18 +59,6 @@ export default function Index() {
|
||||
const [hasTimedOut, setHasTimedOut] = useState(false)
|
||||
const isDesktopStartupRace = shouldUseManagedDesktopDaemon()
|
||||
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(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setHasTimedOut(true)
|
||||
@@ -89,10 +75,6 @@ export default function Index() {
|
||||
if (pathname !== '/' && pathname !== '') {
|
||||
return
|
||||
}
|
||||
console.info('[Startup] navigating to host route', {
|
||||
serverId: onlineServerId,
|
||||
elapsedMs: Math.round(performance.now() - APP_START_TIME),
|
||||
})
|
||||
router.replace(buildHostRootRoute(onlineServerId) as any)
|
||||
}, [onlineServerId, pathname, router])
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import { shortenPath } from '@/utils/shorten-path'
|
||||
import { type AggregatedAgent } from '@/hooks/use-aggregated-agents'
|
||||
import { useSessionStore } from '@/stores/session-store'
|
||||
import { AgentStatusDot } from '@/components/agent-status-dot'
|
||||
import { buildAgentNavigationKey, startNavigationTiming } from '@/utils/navigation-timing'
|
||||
import { buildHostWorkspaceAgentRoute } from '@/utils/host-routes'
|
||||
|
||||
interface AgentListProps {
|
||||
@@ -271,13 +270,6 @@ export function AgentList({
|
||||
|
||||
const serverId = agent.serverId
|
||||
const agentId = agent.id
|
||||
const navigationKey = buildAgentNavigationKey(serverId, agentId)
|
||||
startNavigationTiming(navigationKey, {
|
||||
from: 'home',
|
||||
to: 'agent',
|
||||
params: { serverId, agentId },
|
||||
})
|
||||
|
||||
const shouldReplace = pathname.startsWith('/h/')
|
||||
const navigate = shouldReplace ? router.replace : router.push
|
||||
|
||||
|
||||
@@ -67,17 +67,12 @@ import {
|
||||
} from "./use-bottom-anchor-controller";
|
||||
import { createMarkdownStyles } from "@/styles/markdown-styles";
|
||||
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||
import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
|
||||
import { getMarkdownListMarker } from "@/utils/markdown-list";
|
||||
import { buildHostWorkspaceFileRoute } from "@/utils/host-routes";
|
||||
|
||||
const isUserMessageItem = (item?: StreamItem) => item?.kind === "user_message";
|
||||
const isToolSequenceItem = (item?: StreamItem) =>
|
||||
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 {
|
||||
scrollToBottom(reason?: BottomAnchorLocalRequest["reason"]): void;
|
||||
prepareForViewportChange(): void;
|
||||
@@ -116,7 +111,6 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
|
||||
[isMobile]
|
||||
);
|
||||
const [isNearBottom, setIsNearBottom] = useState(true);
|
||||
const streamItemCountRef = useRef(0);
|
||||
const [expandedInlineToolCallIds, setExpandedInlineToolCallIds] = useState<Set<string>>(new Set());
|
||||
const openFileExplorer = usePanelStore((state) => state.openFileExplorer);
|
||||
const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout);
|
||||
@@ -462,74 +456,6 @@ export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamView
|
||||
[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 renderModel = useMemo<AgentStreamRenderModel>(() => {
|
||||
const pendingPermissionsNode =
|
||||
|
||||
@@ -38,7 +38,6 @@ import { useCheckoutPrStatusQuery } from "@/hooks/use-checkout-pr-status-query";
|
||||
import { useHorizontalScrollOptional } from "@/contexts/horizontal-scroll-context";
|
||||
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
||||
import { Fonts } from "@/constants/theme";
|
||||
import { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf";
|
||||
import { shouldAnchorHeaderBeforeCollapse } from "@/utils/git-diff-scroll";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -66,11 +65,6 @@ function openURLInNewTab(url: string): void {
|
||||
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"]>;
|
||||
|
||||
interface HighlightedTextProps {
|
||||
@@ -199,75 +193,14 @@ const DiffFileHeader = memo(function DiffFileHeader({
|
||||
onHeaderHeightChange,
|
||||
testID,
|
||||
}: DiffFileSectionProps) {
|
||||
const expandStartRef = useRef<number | null>(null);
|
||||
const layoutYRef = useRef<number | null>(null);
|
||||
const pressHandledRef = useRef(false);
|
||||
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(() => {
|
||||
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);
|
||||
}, [file.path, onToggle, isExpanded, hunkCount, lineCount, tokenCount, shouldLogFileMetrics]);
|
||||
|
||||
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]);
|
||||
}, [file.path, onToggle]);
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -506,30 +439,6 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi
|
||||
const headerHeightByPathRef = useRef<Record<string, number>>({});
|
||||
const bodyHeightByPathRef = useRef<Record<string, number>>({});
|
||||
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(() => {
|
||||
setIsManualRefresh(true);
|
||||
void refreshDiff();
|
||||
@@ -712,28 +621,6 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi
|
||||
setDiffModeOverride(null);
|
||||
}, [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) =>
|
||||
state.getStatus({ serverId, cwd, actionId: "commit" })
|
||||
);
|
||||
|
||||
@@ -68,7 +68,6 @@ import {
|
||||
buildToolCallDisplayModel,
|
||||
} from "@/utils/tool-call-display";
|
||||
import { resolveToolCallIcon } from "@/utils/tool-call-icon";
|
||||
import { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf";
|
||||
import { parseInlinePathToken, type InlinePathTarget } from "@/utils/inline-path";
|
||||
import { getMarkdownListMarker } from "@/utils/markdown-list";
|
||||
import { openExternalUrl } from "@/utils/open-external-url";
|
||||
@@ -1786,10 +1785,6 @@ interface ToolCallProps {
|
||||
onInlineDetailsExpandedChange?: (expanded: boolean) => void;
|
||||
}
|
||||
|
||||
const TOOL_CALL_LOG_TAG = "[ToolCall]";
|
||||
const TOOL_CALL_COMMIT_THRESHOLD_MS = 16;
|
||||
|
||||
|
||||
export const ToolCall = memo(function ToolCall({
|
||||
toolName,
|
||||
args,
|
||||
@@ -1806,7 +1801,6 @@ export const ToolCall = memo(function ToolCall({
|
||||
}: ToolCallProps) {
|
||||
const { openToolCall } = useToolCallSheet();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const toggleStartRef = useRef<number | null>(null);
|
||||
|
||||
// Check if we're on mobile (use bottom sheet) or desktop (inline expand)
|
||||
const isMobile =
|
||||
@@ -1849,7 +1843,6 @@ export const ToolCall = memo(function ToolCall({
|
||||
const displayName = displayModel.displayName;
|
||||
const summary = displayModel.summary;
|
||||
const errorText = displayModel.errorText;
|
||||
const iconCategory = effectiveDetail?.type ?? toolName.trim().toLowerCase();
|
||||
const IconComponent = resolveToolCallIcon(toolName, effectiveDetail);
|
||||
|
||||
// Check if there's any content to display
|
||||
@@ -1862,9 +1855,6 @@ export const ToolCall = memo(function ToolCall({
|
||||
: false);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
if (!isMobile && isPerfLoggingEnabled()) {
|
||||
toggleStartRef.current = getNowMs();
|
||||
}
|
||||
if (isMobile) {
|
||||
openToolCall({
|
||||
toolName,
|
||||
@@ -1878,33 +1868,6 @@ export const ToolCall = memo(function ToolCall({
|
||||
}
|
||||
}, [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(() => {
|
||||
if (!onInlineDetailsHoverChange || isMobile || isExpanded) {
|
||||
return;
|
||||
|
||||
@@ -76,6 +76,8 @@ class FakeDaemonClient {
|
||||
return { rttMs: 0 };
|
||||
}
|
||||
|
||||
setReconnectEnabled(_enabled: boolean): void {}
|
||||
|
||||
setConnectionState(next: ConnectionState): void {
|
||||
this.state = next;
|
||||
if (next.status === "disconnected") {
|
||||
|
||||
@@ -3,7 +3,6 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import {
|
||||
DaemonClient,
|
||||
type ConnectionState,
|
||||
type DaemonClientDiagnosticsEvent,
|
||||
type FetchAgentsOptions,
|
||||
} from "@server/client/daemon-client";
|
||||
import {
|
||||
@@ -38,10 +37,6 @@ import {
|
||||
import { createTauriWebSocketTransportFactory } from "@/utils/tauri-daemon-transport";
|
||||
import { applyFetchedAgentDirectory } from "@/utils/agent-directory-sync";
|
||||
import { useSessionStore, type Agent } from "@/stores/session-store";
|
||||
import {
|
||||
recordDaemonClientDiagnostics,
|
||||
recordHostRuntimeCreateClient,
|
||||
} from "@/runtime/perf-diagnostics/host-runtime-diagnostics";
|
||||
|
||||
export type HostRuntimeConnectionStatus =
|
||||
| "idle"
|
||||
@@ -461,16 +456,6 @@ function probeIntervalForConnection(
|
||||
function createDefaultDeps(): HostRuntimeControllerDeps {
|
||||
return {
|
||||
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 localTransportFactory = createTauriLocalDaemonTransportFactory();
|
||||
const base = {
|
||||
@@ -478,8 +463,6 @@ function createDefaultDeps(): HostRuntimeControllerDeps {
|
||||
clientId,
|
||||
clientType: "mobile" as const,
|
||||
runtimeGeneration,
|
||||
onDiagnosticsEvent: (event: DaemonClientDiagnosticsEvent) =>
|
||||
recordDaemonClientDiagnostics(host.serverId, event),
|
||||
};
|
||||
if (connection.type === "directSocket" || connection.type === "directPipe") {
|
||||
return new DaemonClient({
|
||||
@@ -1074,6 +1057,9 @@ export class HostRuntimeController {
|
||||
}
|
||||
|
||||
const nextGeneration = this.snapshot.clientGeneration + 1;
|
||||
if (existingClient) {
|
||||
existingClient.setReconnectEnabled(true);
|
||||
}
|
||||
const client = existingClient ?? this.deps.createClient({
|
||||
host: this.host,
|
||||
connection,
|
||||
@@ -1264,15 +1250,10 @@ export class HostRuntimeStore {
|
||||
}
|
||||
|
||||
private async bootstrapDesktop(): Promise<void> {
|
||||
const t0 = performance.now();
|
||||
const elapsed = () => Math.round(performance.now() - t0);
|
||||
try {
|
||||
console.info("[HostRuntime] desktop bootstrap: starting", { elapsedMs: elapsed() });
|
||||
await Promise.allSettled([
|
||||
(async () => {
|
||||
console.info("[HostRuntime] desktop bootstrap: startManagedDaemon", { elapsedMs: elapsed() });
|
||||
const daemon = await startManagedDaemon();
|
||||
console.info("[HostRuntime] desktop bootstrap: managed daemon ready", { elapsedMs: elapsed(), serverId: daemon.serverId });
|
||||
if (!daemon.serverId) {
|
||||
return;
|
||||
}
|
||||
@@ -1285,12 +1266,10 @@ export class HostRuntimeStore {
|
||||
label: daemon.hostname ?? undefined,
|
||||
connection,
|
||||
});
|
||||
console.info("[HostRuntime] desktop bootstrap: host connection upserted", { elapsedMs: elapsed() });
|
||||
})().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 () => {
|
||||
console.info("[HostRuntime] desktop bootstrap: probing localhost", { elapsedMs: elapsed() });
|
||||
const { client, serverId, hostname } = await connectToDaemon(
|
||||
{
|
||||
id: `bootstrap:${DEFAULT_LOCALHOST_ENDPOINT}`,
|
||||
@@ -1299,17 +1278,13 @@ export class HostRuntimeStore {
|
||||
},
|
||||
{ timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS }
|
||||
);
|
||||
console.info("[HostRuntime] desktop bootstrap: localhost probe succeeded", { elapsedMs: elapsed(), serverId });
|
||||
await this.upsertDirectConnection({
|
||||
serverId,
|
||||
endpoint: DEFAULT_LOCALHOST_ENDPOINT,
|
||||
label: hostname ?? undefined,
|
||||
existingClient: client,
|
||||
});
|
||||
console.info("[HostRuntime] desktop bootstrap: direct connection upserted", { elapsedMs: elapsed() });
|
||||
})().catch(() => {
|
||||
console.info("[HostRuntime] desktop bootstrap: localhost probe failed", { elapsedMs: elapsed() });
|
||||
}),
|
||||
})().catch(() => undefined),
|
||||
]);
|
||||
} catch (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 type { Agent } from "@/contexts/session-context";
|
||||
import type { StreamItem } from "@/types/stream";
|
||||
import {
|
||||
buildAgentNavigationKey,
|
||||
endNavigationTiming,
|
||||
} from "@/utils/navigation-timing";
|
||||
import { startPerfMonitor } from "@/utils/perf-monitor";
|
||||
import {
|
||||
checkoutStatusQueryKey,
|
||||
type CheckoutStatusPayload,
|
||||
@@ -115,27 +110,6 @@ export function AgentReadyScreen({
|
||||
const lastConnectionError = runtimeSnapshot?.lastError ?? null;
|
||||
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) {
|
||||
return (
|
||||
<AgentSessionUnavailableState
|
||||
@@ -294,15 +268,6 @@ function AgentScreenContent({
|
||||
}
|
||||
openFileExplorer();
|
||||
}, [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
|
||||
const explorerOpenGesture = useExplorerOpenGesture({
|
||||
enabled: isMobile && mobileView === "agent",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { SidebarMenuToggle } from "@/components/headers/menu-header";
|
||||
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
||||
import { usePanelStore } from "@/stores/panel-store";
|
||||
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 }) {
|
||||
const { theme } = useUnistyles();
|
||||
@@ -21,6 +21,7 @@ export function OpenProjectScreen({ serverId: _serverId }: { serverId: string })
|
||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||
const needsTrafficLightInset = !isMobile && !desktopAgentListOpen && getIsTauriMac();
|
||||
const trafficLightInset = needsTrafficLightInset ? trafficLightPadding.left : 0;
|
||||
const dragHandlers = useTauriDragHandlers();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMobile) {
|
||||
@@ -29,7 +30,7 @@ export function OpenProjectScreen({ serverId: _serverId }: { serverId: string })
|
||||
}, [isMobile, openAgentList]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.container} {...dragHandlers}>
|
||||
<View style={[styles.menuToggle, { paddingTop: insets.top, paddingLeft: trafficLightInset }]}>
|
||||
<SidebarMenuToggle />
|
||||
</View>
|
||||
@@ -56,6 +57,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
userSelect: "none",
|
||||
},
|
||||
menuToggle: {
|
||||
position: "absolute",
|
||||
|
||||
@@ -5,7 +5,6 @@ import { AgentInputArea } from '@/components/agent-input-area'
|
||||
import { FileDropZone } from '@/components/file-drop-zone'
|
||||
import { AgentStreamView } from '@/components/agent-stream-view'
|
||||
import type { ImageAttachment } from '@/components/message-input'
|
||||
import { MAX_CONTENT_WIDTH } from '@/constants/layout'
|
||||
import { useAgentFormState } from '@/hooks/use-agent-form-state'
|
||||
import { useDraftAgentCreateFlow } from '@/hooks/use-draft-agent-create-flow'
|
||||
import { useHostRuntimeSession } from '@/runtime/host-runtime'
|
||||
@@ -260,8 +259,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
alignSelf: 'center',
|
||||
maxWidth: MAX_CONTENT_WIDTH,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
},
|
||||
contentContainer: {
|
||||
|
||||
@@ -26,7 +26,6 @@ import type {
|
||||
WorkspaceDescriptorPayload,
|
||||
} from "@server/shared/messages";
|
||||
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
|
||||
import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
|
||||
import {
|
||||
createAgentLastActivityCoalescer,
|
||||
type AgentLastActivityCommitter,
|
||||
@@ -357,30 +356,8 @@ interface SessionStoreActions {
|
||||
|
||||
type SessionStore = SessionStoreState & SessionStoreActions;
|
||||
|
||||
const SESSION_STORE_LOG_TAG = "[SessionStore]";
|
||||
let sessionStoreUpdateCount = 0;
|
||||
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
|
||||
function createInitialSessionState(serverId: string, client: DaemonClient, audioPlayer: ReturnType<typeof useAudioPlayer>): SessionState {
|
||||
return {
|
||||
@@ -452,7 +429,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (prev.sessions[serverId]) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("initializeSession", serverId);
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -470,7 +446,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (!session) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("clearSession", serverId);
|
||||
const nextSessions = { ...prev.sessions };
|
||||
delete nextSessions[serverId];
|
||||
let nextActivity = prev.agentLastActivity;
|
||||
@@ -507,12 +482,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
return prev;
|
||||
}
|
||||
|
||||
logSessionStoreUpdate("updateSessionClient", serverId, {
|
||||
wasNull: session.client === null,
|
||||
isNowConnected: client.isConnected,
|
||||
isNowConnecting: client.isConnecting,
|
||||
});
|
||||
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -549,12 +518,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
return prev;
|
||||
}
|
||||
|
||||
logSessionStoreUpdate("updateSessionServerInfo", serverId, {
|
||||
serverId: info.serverId,
|
||||
hostname: nextHostname,
|
||||
version: nextVersion,
|
||||
});
|
||||
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -584,7 +547,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (!session || session.isPlayingAudio === playing) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("setIsPlayingAudio", serverId, { playing });
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -602,8 +564,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (!session || session.focusedAgentId === agentId) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("setFocusedAgentId", serverId, { agentId });
|
||||
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -629,7 +589,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (session.messages === nextMessages) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("setMessages", serverId, { count: nextMessages.length });
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -650,7 +609,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (session.currentAssistantMessage === nextMessage) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("setCurrentAssistantMessage", serverId, { length: nextMessage.length });
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -672,7 +630,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (session.agentStreamTail === nextState) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("setAgentStreamTail", serverId, { agentCount: nextState.size });
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -693,7 +650,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (session.agentStreamHead === nextState) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("setAgentStreamHead", serverId, { agentCount: nextState.size });
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -745,12 +701,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
return prev;
|
||||
}
|
||||
|
||||
logSessionStoreUpdate("setAgentStreamState", serverId, {
|
||||
agentId,
|
||||
changedTail,
|
||||
changedHead,
|
||||
});
|
||||
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -776,7 +726,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
}
|
||||
const nextHead = new Map(session.agentStreamHead);
|
||||
nextHead.delete(agentId);
|
||||
logSessionStoreUpdate("clearAgentStreamHead", serverId, { agentId });
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -798,9 +747,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (session.agentTimelineCursor === nextState) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("setAgentTimelineCursor", serverId, {
|
||||
agentCount: nextState.size,
|
||||
});
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -818,9 +764,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
return prev;
|
||||
}
|
||||
const nextGeneration = session.historySyncGeneration + 1;
|
||||
logSessionStoreUpdate("bumpHistorySyncGeneration", serverId, {
|
||||
generation: nextGeneration,
|
||||
});
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -847,10 +790,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
}
|
||||
const nextMap = new Map(session.agentHistorySyncGeneration);
|
||||
nextMap.set(agentId, currentGeneration);
|
||||
logSessionStoreUpdate("markAgentHistorySynchronized", serverId, {
|
||||
agentId,
|
||||
generation: currentGeneration,
|
||||
});
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -884,11 +823,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
nextApplied.delete(agentId);
|
||||
}
|
||||
|
||||
logSessionStoreUpdate("setAgentAuthoritativeHistoryApplied", serverId, {
|
||||
agentId,
|
||||
applied,
|
||||
});
|
||||
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -913,7 +847,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (session.initializingAgents === nextState) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("setInitializingAgents", serverId, { count: nextState.size });
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -935,7 +868,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (session.agents === nextAgents) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("setAgents", serverId, { count: nextAgents.size });
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -957,7 +889,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (session.workspaces === nextWorkspaces) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("setWorkspaces", serverId, { count: nextWorkspaces.size });
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -988,7 +919,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (!changed) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("mergeWorkspaces", serverId, { count: nextEntries.length });
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -1007,7 +937,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
}
|
||||
const next = new Map(session.workspaces);
|
||||
next.delete(workspaceId);
|
||||
logSessionStoreUpdate("removeWorkspace", serverId, { workspaceId });
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -1074,7 +1003,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (session.pendingPermissions === nextPerms) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("setPendingPermissions", serverId, { count: nextPerms.size });
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -1096,7 +1024,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (session.fileExplorer === nextState) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("setFileExplorer", serverId, { agentCount: nextState.size });
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -1118,7 +1045,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (session.queuedMessages === nextValue) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("setQueuedMessages", serverId, { agentCount: nextValue.size });
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -1136,7 +1062,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (!session || session.hasHydratedAgents === hydrated) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("setHasHydratedAgents", serverId, { hydrated });
|
||||
return {
|
||||
...prev,
|
||||
sessions: {
|
||||
@@ -1153,7 +1078,6 @@ export const useSessionStore = create<SessionStore>()(
|
||||
if (!session || session.hasHydratedWorkspaces === hydrated) {
|
||||
return prev;
|
||||
}
|
||||
logSessionStoreUpdate("setHasHydratedWorkspaces", serverId, { hydrated });
|
||||
return {
|
||||
...prev,
|
||||
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,
|
||||
clientType: "mobile" as const,
|
||||
suppressSendErrors: true,
|
||||
reconnect: { enabled: false },
|
||||
...(connection.type === "directSocket" || connection.type === "directPipe"
|
||||
? localTransportFactory
|
||||
? { transportFactory: localTransportFactory }
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
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 = {
|
||||
text: string;
|
||||
@@ -56,14 +49,11 @@ function splitIntoWords(text: string): string[] {
|
||||
}
|
||||
|
||||
function computeWordLevelDiff(oldLine: string, newLine: string): { oldSegments: DiffSegment[]; newSegments: DiffSegment[] } {
|
||||
const shouldLog = isPerfLoggingEnabled();
|
||||
const startMs = shouldLog ? getNowMs() : 0;
|
||||
const oldWords = splitIntoWords(oldLine);
|
||||
const newWords = splitIntoWords(newLine);
|
||||
|
||||
const m = oldWords.length;
|
||||
const n = newWords.length;
|
||||
const matrixSize = m * n;
|
||||
|
||||
// LCS to find common words
|
||||
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 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 {
|
||||
oldSegments,
|
||||
newSegments,
|
||||
@@ -154,8 +128,6 @@ function computeWordLevelDiff(oldLine: string, newLine: string): { oldSegments:
|
||||
}
|
||||
|
||||
export function buildLineDiff(originalText: string, updatedText: string): DiffLine[] {
|
||||
const shouldLog = isPerfLoggingEnabled();
|
||||
const startMs = shouldLog ? getNowMs() : 0;
|
||||
const originalLines = splitIntoLines(originalText);
|
||||
const updatedLines = splitIntoLines(updatedText);
|
||||
|
||||
@@ -166,7 +138,6 @@ export function buildLineDiff(originalText: string, updatedText: string): DiffLi
|
||||
|
||||
const m = originalLines.length;
|
||||
const n = updatedLines.length;
|
||||
const matrixSize = m * n;
|
||||
const dp: number[][] = Array.from({ length: m + 1 }, () =>
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,13 +102,6 @@ const consoleLogger: Logger = {
|
||||
error: (obj, msg) => console.error(msg, obj),
|
||||
}
|
||||
|
||||
function getNowMs(): number {
|
||||
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
|
||||
return performance.now()
|
||||
}
|
||||
return Date.now()
|
||||
}
|
||||
|
||||
export type {
|
||||
DaemonTransport,
|
||||
DaemonTransportFactory,
|
||||
@@ -180,26 +173,8 @@ export type DaemonClientConfig = {
|
||||
baseDelayMs?: number
|
||||
maxDelayMs?: number
|
||||
}
|
||||
onDiagnosticsEvent?: (event: DaemonClientDiagnosticsEvent) => void
|
||||
}
|
||||
|
||||
export type DaemonClientDiagnosticsEvent =
|
||||
| {
|
||||
type: 'transport_message_timing'
|
||||
messageType: string
|
||||
payloadBytes: number
|
||||
parseMs: number
|
||||
validateMs: number
|
||||
totalMs: number
|
||||
outcome: 'ok' | 'parse_error' | 'validation_error'
|
||||
}
|
||||
| {
|
||||
type: 'transport_binary_frame'
|
||||
channel: BinaryMuxChannel
|
||||
messageType: number
|
||||
payloadBytes: number
|
||||
}
|
||||
|
||||
export type SendMessageOptions = {
|
||||
messageId?: string
|
||||
images?: Array<{ data: string; mimeType: string }>
|
||||
@@ -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
|
||||
// ============================================================================
|
||||
@@ -2873,7 +2840,6 @@ export class DaemonClient {
|
||||
}
|
||||
|
||||
private handleTransportMessage(data: unknown): void {
|
||||
const startedAtMs = getNowMs()
|
||||
const rawData =
|
||||
data && typeof data === 'object' && 'data' in data ? (data as { data: unknown }).data : data
|
||||
|
||||
@@ -2897,12 +2863,6 @@ export class DaemonClient {
|
||||
if (rawBytes) {
|
||||
const frame = decodeBinaryMuxFrame(rawBytes)
|
||||
if (frame) {
|
||||
this.emitDiagnosticsEvent({
|
||||
type: 'transport_binary_frame',
|
||||
channel: frame.channel,
|
||||
messageType: frame.messageType,
|
||||
payloadBytes: frame.payload?.byteLength ?? 0,
|
||||
})
|
||||
this.handleBinaryFrame(frame)
|
||||
return
|
||||
}
|
||||
@@ -2913,52 +2873,19 @@ export class DaemonClient {
|
||||
}
|
||||
|
||||
let parsedJson: unknown
|
||||
let parseMs = 0
|
||||
try {
|
||||
const parseStartedAtMs = getNowMs()
|
||||
parsedJson = JSON.parse(payload)
|
||||
parseMs = getNowMs() - parseStartedAtMs
|
||||
} catch {
|
||||
this.emitDiagnosticsEvent({
|
||||
type: 'transport_message_timing',
|
||||
messageType: 'unknown',
|
||||
payloadBytes: payload.length,
|
||||
parseMs: 0,
|
||||
validateMs: 0,
|
||||
totalMs: getNowMs() - startedAtMs,
|
||||
outcome: 'parse_error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const validateStartedAtMs = getNowMs()
|
||||
const parsed = WSOutboundMessageSchema.safeParse(parsedJson)
|
||||
const validateMs = getNowMs() - validateStartedAtMs
|
||||
if (!parsed.success) {
|
||||
const msgType = (parsedJson as { type?: string })?.type ?? 'unknown'
|
||||
this.emitDiagnosticsEvent({
|
||||
type: 'transport_message_timing',
|
||||
messageType: msgType,
|
||||
payloadBytes: payload.length,
|
||||
parseMs,
|
||||
validateMs,
|
||||
totalMs: getNowMs() - startedAtMs,
|
||||
outcome: 'validation_error',
|
||||
})
|
||||
this.logger.warn({ msgType, error: parsed.error.message }, 'Message validation failed')
|
||||
return
|
||||
}
|
||||
|
||||
this.emitDiagnosticsEvent({
|
||||
type: 'transport_message_timing',
|
||||
messageType: parsed.data.type,
|
||||
payloadBytes: payload.length,
|
||||
parseMs,
|
||||
validateMs,
|
||||
totalMs: getNowMs() - startedAtMs,
|
||||
outcome: 'ok',
|
||||
})
|
||||
|
||||
if (parsed.data.type === 'pong') {
|
||||
return
|
||||
}
|
||||
@@ -3018,6 +2945,10 @@ export class DaemonClient {
|
||||
}
|
||||
}
|
||||
|
||||
setReconnectEnabled(enabled: boolean): void {
|
||||
this.config = { ...this.config, reconnect: { ...this.config.reconnect, enabled } }
|
||||
}
|
||||
|
||||
private scheduleReconnect(input?: {
|
||||
reason?: string
|
||||
event?: string
|
||||
|
||||
Reference in New Issue
Block a user