Skip to content

Commit 66a9a18

Browse files
authored
improvement(perf): eight verified cuts to workspace cold-load JavaScript (#6996)
* improvement(perf): eight verified cuts to workspace cold-load JavaScript Second round of load-time work, adversarially verified for strict behaviour preservation before implementation. Each item is an import-graph fix — none changes what renders, when it renders, or any data path: - knowledge/[id] imported one modal through the [documentId] components barrel, which also exports the chunk editor and therefore js-tiktoken (~2.5 MB gzip of BPE tables) on a route that never edits chunks. Deep import. - prepareBlockState moved out of stores/workflows/utils.ts into its own module. It is the only function there needing the block registry and the generated tool-outputs artifact (~476 KB gzip), and utils.ts is reached by the persistent shell — so every workspace route paid for a canvas-only helper, including a module-scope JSON.parse of a 5.4 MB string. - ExecutionSnapshot (the frozen-canvas modal) is now React.lazy behind its interaction gates, per the code-splitting procedure in sim-imports.md: deep import, dead barrel re-export deleted, sibling imports in log-details deepened to break the parent->child barrel cycle, local Suspense at both render sites. Takes ~7.6 MB of source off logs hydration. - The api contracts barrel no longer re-exports ./tools, ./selectors, ./v1, or ./demo-requests (~58 KB gzip of Zod schema construction on every route). Zero importers used the barrel path for any of them. - createCsvParser (streaming csv-parse, a Node Transform) moved to a server-only module so its stream polyfill leaves client bundles. Deliberately not re-exported from the lib/table barrel. - jszip is dynamically imported at both remaining static call sites (skill zip extraction, pptx parsing) — both already-async, user-triggered paths, mirroring the existing pattern in workflow import-export. - The desktop local-filesystem tool executor is dynamically imported in use-chat; a chunk-load failure now reports an error completion so the server-side tool call settles instead of hanging. Production build, JS downloaded before the load event, vs the previous release: /home 4.44 -> 3.87 MB /logs 4.44 -> 3.64 MB /knowledge 4.22 -> 3.68 MB /tables 4.17 -> 3.61 MB /files 4.68 -> 4.10 MB /w/[id] 4.80 -> 4.67 MB /home total after idle prefetch: 8.15 -> 5.52 MB The lazy snapshot was exercised end-to-end: its chunk loads when a log detail opens (off the route's cold path, warm before the View Snapshot click) and the modal renders without errors. Boundary baseline retightened. * improvement(logs): contain snapshot chunk-load failures and settle the local-fs tool on recovery failure Review round: wrap both lazy ExecutionSnapshot render sites in a small error boundary (Suspense handles the lazy import's pending state, not its rejection — a failed chunk load would have unwound to the route boundary and replaced the logs page over an optional modal; mirrors PreviewErrorBoundary), and contain rejections inside the local-filesystem executor's load-failure recovery so a failed completion report degrades to a log instead of an unhandled rejection. * fix(logs): recover cleanly from snapshot chunk failures * fix(logs): preserve snapshot modal while loading
1 parent 5a1602e commit 66a9a18

21 files changed

Lines changed: 736 additions & 370 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ import {
7474
TERMINAL_SESSION_RESOURCE_ID,
7575
} from '@/lib/copilot/resources/types'
7676
import { executeBrowserToolOnClient } from '@/lib/copilot/tools/client/browser-tool-execution'
77-
import { executeLocalFilesystemTool } from '@/lib/copilot/tools/client/local-filesystem'
7877
import {
7978
bindRunToolToExecution,
8079
cancelRunToolExecution,
@@ -2009,11 +2008,49 @@ export function useChat(
20092008
return
20102009
}
20112010
handledClientLocalFilesystemToolIdsRef.current.add(toolCallId)
2012-
executeLocalFilesystemTool(toolCallId, toolName, toolArgs, {
2011+
const options = {
20132012
workspaceId,
20142013
chatId: chatIdRef.current ?? selectedChatIdRef.current,
20152014
signal: abortControllerRef.current?.signal,
2016-
})
2015+
}
2016+
/**
2017+
* Dynamic on purpose: the local-filesystem executor only runs for desktop-local
2018+
* VFS tool calls, and a static import kept it in the shared chat chunk on every
2019+
* surface that mounts the composer. The guard, the dedupe add, and the option
2020+
* capture above stay synchronous, so re-entrancy behaviour is unchanged. If the
2021+
* chunk fails to load (deploy skew), the server-side tool call must still settle:
2022+
* report an error completion rather than leaving it hanging with the dedupe ref
2023+
* already marked handled.
2024+
*/
2025+
import('@/lib/copilot/tools/client/local-filesystem').then(
2026+
(m) => m.executeLocalFilesystemTool(toolCallId, toolName, toolArgs, options),
2027+
async (error) => {
2028+
logger.error('Failed to load local filesystem tool executor', { error })
2029+
/**
2030+
* The recovery itself can reject (the helper chunks or the completion POST can
2031+
* fail for the same reason the executor chunk did). Contain it: an unhandled
2032+
* rejection here would settle nothing and surface as a console error, exactly
2033+
* like the executor's own report-failure path, which also degrades to a log.
2034+
*/
2035+
try {
2036+
const [{ reportClientToolCompletion }, { ASYNC_TOOL_CONFIRMATION_STATUS }] =
2037+
await Promise.all([
2038+
import('@/lib/copilot/tools/client/completion'),
2039+
import('@/lib/copilot/async-runs/lifecycle'),
2040+
])
2041+
await reportClientToolCompletion(
2042+
toolCallId,
2043+
ASYNC_TOOL_CONFIRMATION_STATUS.error,
2044+
'Local filesystem tool failed to load'
2045+
)
2046+
} catch (reportError) {
2047+
logger.error('Failed to report local filesystem tool load failure', {
2048+
toolCallId,
2049+
error: reportError,
2050+
})
2051+
}
2052+
}
2053+
)
20172054
},
20182055
[workspaceId]
20192056
)

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,13 @@ import {
7777
useFolderAncestors,
7878
} from '@/app/workspace/[workspaceId]/components/folders'
7979
import { DocumentsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state'
80-
import { DocumentTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components'
80+
/**
81+
* Deep import on purpose: the `[documentId]/components` barrel also exports `ChunkEditor`,
82+
* which needs exact token counts and therefore `js-tiktoken` (~2.5 MB gzip of BPE rank
83+
* tables). Importing the modal through the barrel shipped the tokenizer to the document
84+
* LIST route, which never edits chunks.
85+
*/
86+
import { DocumentTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/document-tags-modal'
8187
import {
8288
ActionBar,
8389
AddConnectorModal,
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export { Dashboard } from './dashboard'
22
export { LogDetails, LogDetailsContent } from './log-details'
3-
export { ExecutionSnapshot } from './log-details/components/execution-snapshot'
43
export { FileCards } from './log-details/components/file-download'
54
export { TraceView } from './log-details/components/trace-view'
65
export { LogRowContextMenu } from './log-row-context-menu'

apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/index.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act, type ReactNode } from 'react'
5+
import { createRoot, type Root } from 'react-dom/client'
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7+
8+
const { mockToastError } = vi.hoisted(() => ({
9+
mockToastError: vi.fn(),
10+
}))
11+
12+
vi.mock('@sim/emcn', () => ({
13+
Loader: () => <span aria-hidden='true' />,
14+
Modal: ({
15+
children,
16+
open,
17+
onOpenChange,
18+
}: {
19+
children: ReactNode
20+
open: boolean
21+
onOpenChange: (open: boolean) => void
22+
}) =>
23+
open ? (
24+
<div>
25+
{children}
26+
<button type='button' onClick={() => onOpenChange(false)}>
27+
Close
28+
</button>
29+
</div>
30+
) : null,
31+
ModalBody: ({ children }: { children: ReactNode }) => <div>{children}</div>,
32+
ModalContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
33+
ModalDescription: ({ children }: { children: ReactNode }) => <p>{children}</p>,
34+
ModalHeader: ({ children }: { children: ReactNode }) => <h2>{children}</h2>,
35+
toast: { error: mockToastError },
36+
}))
37+
38+
import {
39+
SnapshotBoundary,
40+
SnapshotModalFallback,
41+
} from '@/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/snapshot-boundary'
42+
43+
const LOAD_ERROR = new Error('snapshot chunk failed')
44+
45+
function ThrowingSnapshot() {
46+
throw LOAD_ERROR
47+
}
48+
49+
describe('SnapshotBoundary', () => {
50+
let container: HTMLDivElement
51+
let root: Root
52+
53+
beforeEach(() => {
54+
vi.clearAllMocks()
55+
container = document.createElement('div')
56+
document.body.appendChild(container)
57+
act(() => {
58+
root = createRoot(container)
59+
})
60+
})
61+
62+
afterEach(() => {
63+
act(() => root.unmount())
64+
container.remove()
65+
})
66+
67+
it('contains a background pre-warm failure without notifying or closing', () => {
68+
const onLoadError = vi.fn()
69+
70+
act(() => {
71+
root.render(
72+
<SnapshotBoundary isOpen={false} onLoadError={onLoadError}>
73+
<ThrowingSnapshot />
74+
</SnapshotBoundary>
75+
)
76+
})
77+
78+
expect(container.childNodes).toHaveLength(0)
79+
expect(mockToastError).not.toHaveBeenCalled()
80+
expect(onLoadError).not.toHaveBeenCalled()
81+
})
82+
83+
it('notifies and closes an explicitly opened snapshot after a load failure', () => {
84+
const onLoadError = vi.fn()
85+
86+
act(() => {
87+
root.render(
88+
<SnapshotBoundary isOpen onLoadError={onLoadError}>
89+
<ThrowingSnapshot />
90+
</SnapshotBoundary>
91+
)
92+
})
93+
94+
expect(container.childNodes).toHaveLength(0)
95+
expect(mockToastError).toHaveBeenCalledWith(
96+
'Could not load the workflow snapshot. Refresh and try again.'
97+
)
98+
expect(onLoadError).toHaveBeenCalledOnce()
99+
})
100+
101+
it('keeps the modal shell visible while the snapshot bundle loads', () => {
102+
const onClose = vi.fn()
103+
104+
act(() => {
105+
root.render(<SnapshotModalFallback isOpen onClose={onClose} />)
106+
})
107+
108+
expect(container.textContent).toContain('Workflow State')
109+
expect(container.textContent).toContain('Loading run snapshot…')
110+
111+
const closeButton = container.querySelector('button')
112+
expect(closeButton).not.toBeNull()
113+
act(() => closeButton?.click())
114+
expect(onClose).toHaveBeenCalledOnce()
115+
})
116+
})
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
'use client'
2+
3+
import { Component, type ErrorInfo, type ReactNode } from 'react'
4+
import {
5+
Loader,
6+
Modal,
7+
ModalBody,
8+
ModalContent,
9+
ModalDescription,
10+
ModalHeader,
11+
toast,
12+
} from '@sim/emcn'
13+
import { createLogger } from '@sim/logger'
14+
15+
const logger = createLogger('ExecutionSnapshotBoundary')
16+
17+
interface SnapshotBoundaryProps {
18+
children: ReactNode
19+
isOpen: boolean
20+
onLoadError: () => void
21+
}
22+
23+
interface SnapshotBoundaryState {
24+
hasError: boolean
25+
}
26+
27+
const reportedErrors = new WeakSet<Error>()
28+
29+
interface SnapshotModalFallbackProps {
30+
isOpen: boolean
31+
onClose: () => void
32+
}
33+
34+
export function SnapshotModalFallback({ isOpen, onClose }: SnapshotModalFallbackProps) {
35+
return (
36+
<Modal
37+
open={isOpen}
38+
onOpenChange={(open) => {
39+
if (!open) onClose()
40+
}}
41+
>
42+
<ModalContent size='full' className='flex h-[90vh] flex-col'>
43+
<ModalHeader>Workflow State</ModalHeader>
44+
<ModalBody className='!p-0 flex min-h-0 flex-1 items-center justify-center overflow-hidden'>
45+
<ModalDescription className='sr-only'>
46+
Loading the workflow state snapshot for this execution
47+
</ModalDescription>
48+
<div className='flex items-center gap-2 text-[var(--text-secondary)]'>
49+
<Loader className='size-[16px]' animate />
50+
<span className='text-small'>Loading run snapshot…</span>
51+
</div>
52+
</ModalBody>
53+
</ModalContent>
54+
</Modal>
55+
)
56+
}
57+
58+
/**
59+
* Error boundary for the lazily loaded execution snapshot.
60+
*
61+
* `Suspense` handles the pending state of the lazy import but not its
62+
* rejection — a failed chunk load (deploy skew, offline) would otherwise
63+
* unwind to the route-level boundary and replace the whole logs page with an
64+
* error view over an optional modal. Mirrors `PreviewErrorBoundary` in the
65+
* file viewer: contain, log, degrade. The snapshot is an overlay, so the
66+
* degraded state renders nothing. Closed snapshots are mounted to pre-warm
67+
* their chunk and data, so a background failure is logged without interrupting
68+
* the user. If the user actually opens a failed snapshot, the caller closes
69+
* the modal state and a toast explains why it did not open.
70+
*
71+
* Callers must remount this boundary when the snapshot identity changes and
72+
* when a pre-warmed snapshot is explicitly opened. Error boundaries reset only
73+
* via remount; without both transitions, a failed pre-warm would leave the
74+
* later open action stuck in the already-tripped state.
75+
*/
76+
export class SnapshotBoundary extends Component<SnapshotBoundaryProps, SnapshotBoundaryState> {
77+
public state: SnapshotBoundaryState = { hasError: false }
78+
79+
public static getDerivedStateFromError(): SnapshotBoundaryState {
80+
return { hasError: true }
81+
}
82+
83+
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
84+
if (!reportedErrors.has(error)) {
85+
reportedErrors.add(error)
86+
logger.error('Execution snapshot failed to load', {
87+
error: error.message,
88+
componentStack: errorInfo.componentStack,
89+
})
90+
}
91+
92+
if (this.props.isOpen) {
93+
toast.error('Could not load the workflow snapshot. Refresh and try again.')
94+
this.props.onLoadError()
95+
}
96+
}
97+
98+
public render() {
99+
return this.state.hasError ? null : this.props.children
100+
}
101+
}

apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
'use client'
22

3-
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
3+
import {
4+
lazy,
5+
memo,
6+
Suspense,
7+
useCallback,
8+
useEffect,
9+
useLayoutEffect,
10+
useMemo,
11+
useRef,
12+
useState,
13+
} from 'react'
414
import {
515
Badge,
616
Button,
@@ -48,11 +58,17 @@ import { filterHiddenOutputKeys } from '@/lib/logs/execution/trace-spans/trace-s
4858
import type { TraceSpan } from '@/lib/logs/types'
4959
import { sendMothershipMessage } from '@/lib/mothership/events'
5060
import { DELETED_WORKFLOW_LABEL } from '@/lib/workflows/workflow-labels'
61+
/**
62+
* Deep imports on purpose: importing these back through the parent `logs/components`
63+
* barrel forms a parent->child cycle that would keep the barrel edge to the snapshot
64+
* alive and silently defeat the ExecutionSnapshot lazy split below.
65+
*/
5166
import {
52-
ExecutionSnapshot,
53-
FileCards,
54-
TraceView,
55-
} from '@/app/workspace/[workspaceId]/logs/components'
67+
SnapshotBoundary,
68+
SnapshotModalFallback,
69+
} from '@/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/snapshot-boundary'
70+
import { FileCards } from '@/app/workspace/[workspaceId]/logs/components/log-details/components/file-download'
71+
import { TraceView } from '@/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view'
5672
import { useLogDetailsResize } from '@/app/workspace/[workspaceId]/logs/hooks'
5773
import {
5874
logDetailsTabParam,
@@ -73,6 +89,17 @@ import { useLogDetailsUIStore } from '@/stores/logs/store'
7389
import { MAX_LOG_DETAILS_WIDTH_RATIO, MIN_LOG_DETAILS_WIDTH } from '@/stores/logs/utils'
7490
import type { ChatContext } from '@/stores/panel'
7591

92+
/**
93+
* Lazy per the code-splitting rule in `sim-imports.md`: the snapshot renders the workflow
94+
* preview canvas, whose graph is ~7.6 MB of source. Rendering is gated on the detail's
95+
* open state, so the chunk is fetched on first use, never during SSR or hydration.
96+
*/
97+
const ExecutionSnapshot = lazy(() =>
98+
import(
99+
'@/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot'
100+
).then((m) => ({ default: m.ExecutionSnapshot }))
101+
)
102+
76103
/**
77104
* Renders an already-apportioned integer credit value. `dollars` is only used
78105
* to distinguish a genuine zero ("0 credits") from a sub-credit charge that
@@ -679,13 +706,28 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP
679706

680707
{/* Frozen Canvas Modal */}
681708
{log.executionId && (
682-
<ExecutionSnapshot
683-
executionId={log.executionId}
684-
traceSpans={traceSpans}
685-
isModal
709+
<SnapshotBoundary
710+
key={`${log.executionId}:${isExecutionSnapshotOpen ? 'open' : 'closed'}`}
686711
isOpen={isExecutionSnapshotOpen}
687-
onClose={() => setIsExecutionSnapshotOpen(false)}
688-
/>
712+
onLoadError={() => setIsExecutionSnapshotOpen(false)}
713+
>
714+
<Suspense
715+
fallback={
716+
<SnapshotModalFallback
717+
isOpen={isExecutionSnapshotOpen}
718+
onClose={() => setIsExecutionSnapshotOpen(false)}
719+
/>
720+
}
721+
>
722+
<ExecutionSnapshot
723+
executionId={log.executionId}
724+
traceSpans={traceSpans}
725+
isModal
726+
isOpen={isExecutionSnapshotOpen}
727+
onClose={() => setIsExecutionSnapshotOpen(false)}
728+
/>
729+
</Suspense>
730+
</SnapshotBoundary>
689731
)}
690732
</>
691733
)

0 commit comments

Comments
 (0)