Skip to main content

Performance Diagnostics

Wework includes an opt-in frontend performance diagnostics switch for investigating release builds that become slow after running for a while. The diagnostics code only runs after it is explicitly enabled; when disabled, the app does not install React Profiler and does not collect interval samples.

Debugging Multiple Instances

For everyday development, pnpm --filter wework dev:mac uses the release app's Executor Home by default, so projects and tasks are shared with the locally installed release Wework. Each Wework process still communicates with its own executor child through stdio, preventing endpoint collisions or attachment to another executor. Use pnpm --filter wework dev:mac -- --executor-isolation when projects and tasks must be isolated temporarily.

ai:verify and desktop E2E do not use that shared default. They explicitly create a temporary Executor Home, projects directory, device ID, and unique Tauri identifier, isolating tasks, projects, application data, and the single-instance lock from release and other verification sessions.

Development instances share one Cargo target directory by default so executor source changes can reuse incremental build artifacts. Set WEGENT_DISABLE_SHARED_CARGO_TARGET=1 to use the project's default target directory when investigating shared build-cache issues.

Diagnosing Startup Time

The desktop startup screen waits only for the local executor to report ready through stdout; debug builds do not delay the workbench to finish an animation cycle. On a cold start, Tauri starts a new sidecar directly and does not discover or attach to an existing executor.

When the startup screen remains visible, align Frontend logging initialized in the frontend log with app IPC stdio ready in the executor log. The interval primarily measures local executor cold startup. Later entries such as runtime work list finished identify workbench data-loading time. Do not mistake a background cloud synchronization timeout for the local startup gate.

Enabling Diagnostics

Press the hidden shortcut in the Wework window:

macOS: Cmd + Option + Shift + P
Windows/Linux: Ctrl + Alt + Shift + P

The shortcut opens the Developer Commands menu. Select Enable Performance Diagnostics to write the wework:perf-debug flag in localStorage and reload the app; open the menu again and select Disable Performance Diagnostics to disable diagnostics and reload.

Development builds can also toggle diagnostics through a URL parameter:

?weworkPerf=1  # enable
?weworkPerf=0 # disable

For local reproduction, set VITE_WEWORK_PERF_DEBUG=1 to enable diagnostics by default.

Debug Panel

The Debug Panel command in the Developer Commands menu helps diagnose the currently active Wework runtime task. It shows:

  • The active runtime task address, whether the task is known, the raw running value, task status, and pane-derived running state.
  • The current pane send phase, message counts, queued messages, transcript loading state, subagent state, and goal state.
  • A field and expected UI style comparison between transcript-loaded messages and the current streaming output.
  • Recent console.debug logs.

The Debug Panel can be expanded, collapsed, refreshed, copied as a snapshot, and cleared. When collapsed, it leaves only a small status bar in the lower-right corner so it does not block the main UI.

Runtime Memory Snapshots

Debug Panel snapshots include a lightweight memory summary for the active runtime pane to help investigate WebView or executor memory spikes:

  • Message count, role distribution, status distribution, and content-length totals.
  • Processing block count, block type distribution, and tool-output length totals.
  • Queued messages, guidance messages, code-comment context count, and transcript range state.
  • The raw running value from the runtime work list and the running state derived by the pane.

Snapshots only include summaries. They do not copy full command output, raw Codex events, or full transcript content into the Debug Panel. When raw payloads are needed, inspect executor logs or Web Inspector samples instead of moving large text through the frontend snapshot path.

Runtime Transcript and List Payloads

To reduce frontend and executor memory pressure, runtime task lists, runtime handle summaries, and transcript responses keep only fields required by the UI. Large raw payloads such as command output, streaming deltas, cached messages, and raw request/response bodies are not sent to the frontend through runtime work list payloads.

Conversation rendering still uses WorkbenchMessage values produced from transcript loads and message actions. Task lists and status polling are for status, titles, running state, and workspace metadata. When investigating slow list refreshes or memory growth while switching tasks, first check whether raw messages or command output have been reintroduced into the runtime list, handle, or transcript metadata path.

Pane Cache and Resource Lifetimes

The desktop workbench caches at most 10 ordinary panes and evicts them in least-recently-used order. An inactive pane that is no longer running releases transcript messages, historical DOM, pagination ranges, navigation indexes, and processing expansion state; returning to it reloads from the original runtime transcript.

Tauri conversations use one @tanstack/react-virtual message-row virtualizer for every conversation size instead of switching implementations at a message-count threshold. The virtualizer uses anchorTo: 'end' to anchor the list at its end, while scroll snapshots are consistently represented as the distance from the viewport bottom to the list bottom. Its shared ResizeObserver measures mounted message rows. An active streaming message must remain in the virtual range even when it is outside the visible range and overscan, so its growth continues to reach TanStack Virtual's measurement pipeline; otherwise, replacing its estimated height with its real height when it remounts can corrupt the historical reading position. While the user remains at the bottom, height changes preserve the end distance. After the user scrolls upward, the list instead records the first visible text scroll anchor and its viewport offset, then restores that text anchor when streaming content is remeasured. This keeps bottom-follow behavior without allowing the text being read to drift upward during streaming. The rendered range keeps 2 rows of overscan on each side. Message rows no longer use IntersectionObserver as a second windowing layer; an individual oversized Markdown response retains independent chunk windowing to bound the DOM inside one visible message. Remaining IntersectionObserver usage covers independent behavior such as bottom-follow state and attachment previews.

Each conversation stores only a bounded TanStack measurement snapshot alongside its distance-from-bottom scroll snapshot. Changes to this path must cover short and long conversations, streaming bottom-follow behavior, continuous measurement of offscreen streaming messages, text-anchor stability after scrolling upward, historical-position restoration, reopen after switching away, forced mounting for turn navigation, and cache eviction when a task is archived.

Terminal and built-in browser sessions are stateful active resources and do not follow ordinary pane eviction. A pane remains mounted while it owns a Terminal or browser tab so its terminal process and page session survive task switches. After the corresponding resources close, the pane is subject to the ordinary cache limit again. Changes to this boundary must continue to cover ordinary-pane LRU eviction, resource-pane retention, message-row virtualization, and the desktop memory E2E.

Local Codex Streaming Logs

The local executor keeps Codex delta details enabled by default so developers can diagnose streaming order, phase classification, and final-content overwrite issues. By default, it records raw Codex delta events and run-state classification summaries.

The Enable Stream Logs / Disable Stream Logs command in the Developer Commands menu toggles both frontend local chat stream logs and Codex executor stream logs. Prefer this command during live investigation; it keeps frontend console.debug stream subscription/event logs and executor Codex stream details under the same switch.

To avoid excessive logs in debug builds during long responses or high-frequency token output, runtime work cache/emit mapping logs are disabled by default. Those logs add extra records for the cache path and UI event dispatch path of the same delta, and are only needed when diagnosing local runtime work routing.

Available environment variables:

WEGENT_CODEX_STREAM_DEBUG=0          # disable raw Codex delta / classification details
WEGENT_CODEX_STREAM_DEBUG=1 # enable raw Codex delta / classification details (default)
WEGENT_CODEX_STREAM_MAPPING_DEBUG=1 # enable runtime work cache/emit mapping details

Streaming Message Rendering

Wework separates high-frequency executor text deltas from the visible Markdown cadence. Message state still receives and retains the complete content in real time, while AssistantMarkdown uses a lightweight buffer to advance visible text on browser frames. It catches up adaptively when the backlog grows, then retains a small character reserve and drains it slowly near the tail to smooth executor bursts and short delivery gaps. The renderer immediately aligns with complete content when streaming ends, content is replaced, or an update is not append-only, preserving final-message correctness.

Streaming messages skip full Pretext height measurement and use a stable offscreen intrinsic height. Completed messages are measured precisely and cached. Height lookup first uses the message object and width, avoiding repeated full-text hashes for unchanged historical messages during every stream update. Stable props and memo boundaries also keep the composer, workspace actions, right workspace, and bottom terminal from rerendering for every text delta.

While the bottom Terminal panel is being resized, height updates are coalesced to browser animation frames and height transitions are disabled. This prevents high-frequency pointer events from causing excessive React updates and Terminal layout work. Releasing the pointer must commit the final height and restore the transitions used when opening or closing the panel.

Distinguish these cases when investigating streaming stalls:

  • The frame rate is stable but output alternates between fast and slow: inspect stream message event intervals. Executor batching or network/IPC delivery gaps are usually responsible.
  • Long frames, dense style recalculation, or Markdown parsing appear: check whether code bypasses the text buffer, destabilizes Streamdown component references, or reintroduces per-character DOM animation.
  • GC time is unexpectedly high: verify whether Web Inspector has Heap Allocations enabled. That instrument can significantly amplify GC during longer recordings and should be disabled when diagnosing interaction smoothness alone.

Streaming-buffer unit tests live in wework/src/components/chat/useBufferedStreamingText.test.ts. Changes to the reserve or advance rate must continue to cover Unicode boundaries, non-append updates, and immediate alignment when streaming ends.

Collected Data

When enabled, the diagnostics module records:

  • Browser long tasks.
  • Event loop lag above 120ms.
  • A 5-second sample of memory, DOM node count, resource count, and visibility state.
  • React root commit durations above 24ms.
  • Manual mark events.

The latest 300 events are kept in memory and exposed through window.__WEWORK_PERF__. Diagnostics data is not uploaded to the server.

Capturing Evidence

Release builds compile Tauri Web Inspector support by default, while the main WebView remains non-inspectable so its native WebKit context menu does not contain Inspect Element. When the user selects Open Web Inspector from the hidden Developer Commands menu, the native command dynamically enables WKWebView.isInspectable and opens the Inspector. This command is independent of the Performance Diagnostics switch and requires macOS 13.3 or newer. The built-in browser is a separate WebView and retains right-click Inspect Element. Set WEWORK_RELEASE_DEVTOOLS=0 when a distribution must omit Inspector support. To open it automatically for a local diagnostic launch, use:

WEWORK_WEBVIEW_DEVTOOLS=1 /path/to/WeWork.app/Contents/MacOS/WeWork

After Web Inspector opens, run this when the app becomes slow:

window.__WEWORK_PERF__.snapshot();

The snapshot includes the current URL, page visibility, DOM node count, memory snapshot, navigation timing, resource count, recent events, and Wework process-group data. macOS reparents WebKit XPC processes to PID 1; diagnostics use LaunchServices to associate the current Wework instance with its Web Content, GPU, and Networking processes.

Each process group reports both rss_kib and physical_footprint_kib. RSS includes shared mappings and reclaimable resident pages and is commonly much larger than actual memory pressure. Prefer physical_footprint_kib when investigating leaks or system resource usage, and treat RSS as a secondary residency metric. When comparing multiple snapshots, focus on:

  • Whether memory.usedJSHeapSize keeps growing.
  • Whether processMemory.groups[].physical_footprint_kib keeps growing after a task completes and cools down.
  • Whether growth belongs to webkit-webcontent, codex-app-server, executor, or main.
  • Whether domNodeCount keeps growing.
  • Dense longtask or event-loop-lag events.
  • Repeated slow-react-commit events.

The workbench's full-height sidebar and content-wide top bar should use ordinary semantic backgrounds instead of applying backdrop-filter to large persistent surfaces. These filters can cause WebKit to retain additional graphics backing stores for the entire region. When investigating Web Content memory, compare physical_footprint_kib before and after the change at the same window size and page state, and exclude the temporary reclaimable high-water mark created by Web Inspector heap snapshots from the steady-state baseline.

Manual marks can also be added:

window.__WEWORK_PERF__.mark("before-open-task", { taskId: "..." });

Disabling Diagnostics

Press the hidden shortcut to open the Developer Commands menu, then select Disable Performance Diagnostics to disable diagnostics and reload. The console can also disable it:

localStorage.removeItem("wework:perf-debug");
location.reload();

After diagnostics are disabled, window.__WEWORK_PERF__ is not installed and React Profiler no longer wraps the app root.