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 Electron app-data namespace, 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​

Desktop startup is gated by the local runtime and the current route's first-screen readiness conditions. On a cold start, Electron creates a new sidecar and the executor reports ready through stdout. The Renderer sends renderer.startupReady after the current surface and its required data are usable, starting the window handoff. Debug and release builds use the same handoff sequence.

The preferred initial window size is 1440×960. If either dimension reaches or exceeds the selected display's work area, both the splash and main window start with standard native maximization; otherwise they remain normal windows. A maximized handoff inherits that state directly without converting full-display dimensions back into normal-window frame bounds.

The splash animation stage supports native window dragging, while the bottom status area and recovery buttons remain interactive. The stage and workbench use the appearance resolved for the same launch. Comet trails outline the mark from all sides at density 2.5 and speed 1.0, with long tails that taper and fade. Bright heads disappear before first contact with any glyph, leaving only contour strokes on the mark. Light mode uses an off-white surface with pigmented blue strokes; dark mode uses luminous heads. The completed mark immediately starts a left-to-right gradient sweep; waiting schedules no Canvas redraws. The footer shares the stage background, and the theme is set before styles and animation first paint to avoid a color flash.

The complete mark and strands scale uniformly within the window and the footer's top edge, with clearance from the status content. If an extremely short window's footer covers the full-window center, decorative drawing is omitted until space returns, continuing the same animation timeline.

During handoff, the splash completes formation, freezes the current gradient position, and advances progress to 100% over approximately 200ms. After two frame confirmations and a 50ms hold, the host aligns the main window's position and size and uses capturePage() to confirm a nonempty composited main-window frame. It then fades out the splash through native window opacity over 600ms on macOS and Windows. Electron native window opacity has no effect on Linux. The host records startup success after closing the splash and focusing the main window. Reduced motion presents the completed frame directly and skips the settling, hold, and fade animations.

When the startup screen remains visible, correlate Frontend logging initialized, app IPC stdio ready, the current route's data-loading entries such as runtime work list finished, and the host's renderer-startup-ready, main-window-show, and startup-splash-close records. These distinguish executor cold startup, first-screen data loading, and window handoff. Measure renderer-startup-ready to startup-splash-close separately because that interval includes the completed frame, main-window compositing, and fade. Investigate background cloud synchronization through its corresponding sync logs.

Normal desktop E2E reloads share reloadMainWindow from workspace-flows.mjs. A connected control channel does not imply a completed native startup handoff; the helper waits for splash closure, reloads, and waits for the replacement client. Combining reloadApp and awaitReadyAfter inside a scenario bypasses that gate, which can invalidate an in-flight capturePage() and hide the main window.

First-Paint Gates and Idle Work​

Workbench first-paint gating requires the local executor and the current route's usable surface and required data. Features that need Codex start it on demand. Maintenance such as plugin auto-updates, bundled marketplace preparation, and temporary-image cleanup runs through one renderer idle-task scheduler.

An idle task starts only when all of these conditions hold:

  • The workbench first paint is available.
  • There has been no recent keyboard, pointer, touch, or wheel input, and the renderer receives a sufficiently large idle-callback slice.
  • CPU utilization, available memory, and system idle time sampled by the Electron main process remain below the configured pressure limits.

Pending requests with the same task ID are coalesced, and idle tasks start serially. New user input postpones work that has not started, and a failed pressure probe prevents execution. The scheduler only decides when work may begin; a task must not perform synchronous heavy work on the renderer main thread. File scanning and copying belongs in the Executor blocking pool, directory cleanup uses asynchronous I/O in the Electron main process, and network synchronization remains asynchronous.

After first paint, the bundled plugin marketplace computes a deterministic content SHA-256. When .wework-content-sha256 in the destination matches the current content, Wework skips the copy. Changed content is replaced through a staging directory, and the hash participates in the Codex local-marketplace registration key. Bundled plugins therefore refresh after a Wework upgrade without recopied content on every launch of the same version.

Diagnosing Runtime Task Creation​

The frontend writes key task-creation stages to the persisted log with the [Wework] Runtime task create diagnostic prefix. This does not require Performance Diagnostics to be enabled. Records contain only the stage, task/device identifiers, model identifiers, elapsed time, and result; they do not contain message content, credentials, or model connection configuration.

When an optimistic task is visible but the executor never receives runtime.tasks.create, find the last stage logged in this sequence:

  1. workbench-model-prepare-*: the workbench starts and completes its initial model preparation.
  2. workbench-runtime-create-dispatched: the workbench begins calling the runtime creation API.
  3. hybrid-local-device-discovery-* and hybrid-route-resolved: the hybrid service completes device discovery and selects the local or cloud route.
  4. hybrid-create-forwarded: the creation request is forwarded to the selected runtime API.
  5. local-device-resolved, local-primary-model-prepared, local-supervisor-model-prepared, and local-payload-built: the local/remote Executor IPC client completes device resolution, model preparation, and payload construction.
  6. local-rpc-dispatched and local-rpc-resolved: runtime.tasks.create is sent and returns.
  7. hybrid-create-resolved or hybrid-create-failed: the hybrid service observes the final result.

A missing next stage usually means the call stalled between the two records. Correlate the same taskId across the frontend log, cloud WebSocket RPC log, and executor log to identify whether model synchronization, device discovery, payload construction, IPC dispatch, or the executor response is blocked.

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 DevTools 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.

Codex 0.147 history reads must follow the historyMode selected when the thread was persisted. The executor first reads metadata with thread/read(includeTurns: false): paginated threads use thread/turns/list(itemsView: notLoaded) followed by thread/items/list for the complete items of each turn, while legacy threads use thread/turns/list(itemsView: full) directly. Legacy rollout stores do not support thread/items/list, so the executor must not force item pagination based only on the Codex version.

Codex filters <codex_internal_context> from history APIs, so Wework must preserve visible goal and continuation input by clientUserMessageId when sending it, then merge that presentation with provider items while loading the transcript. For pre-upgrade legacy conversations without a local presentation, the executor may restore the first request from the thread preview only when the first turn on the oldest page has no user message at all. It must not add the preview when that turn already contains text, image, or attachment messages.

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.

Electron conversations use one @tanstack/react-virtual message-row virtualizer for every conversation size instead of switching implementations at a message-count threshold. Follow and read modes use anchorTo: 'end'; reading disables followOnAppend and the end tolerance. Navigation and scrollbar dragging use anchorTo: 'start'. Scroll snapshots store the distance from the bottom, with message and text-anchor identities while reading. Synchronous layout and one shared ResizeObserver write real row heights through the same resizeItem entry. 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. Chunks whose rich Markdown is not mounted retain lightweight plain text so rapid scrolling cannot expose a height-only blank region. Remaining IntersectionObserver usage covers independent behavior such as bottom-follow state and attachment previews.

Desktop conversations use normal DOM coordinates: zero at the top and scrollHeight - clientHeight at the bottom. TanStack, scrollbar and navigation share this range without negative-offset conversion. useConversationVirtualizer owns virtual geometry; the controller owns follow, read, navigation and drag policy. Row and Markdown caches validate width, typography and content. Ordinary scrolling does not automatically start diagnostic capture.

In non-split mode, the pane-stack parent owns the shared workbench content width and passes that stable value to a newly selected task pane. A pane's own measurement is only a fallback for startup, zero-width measurements, and split mode. The new pane must decide whether to dock the environment information panel from the shared width on its first frame. Rendering the chat at width 0 first and then subtracting the 320px side panel on the next frame causes a visible horizontal flash in the messages and composer.

A single assistant message may contain many tool blocks and be split into multiple ToolBlocksDisplay segments. Derived data that depends on the complete message, such as file-edit durations, must be computed once at message scope and then mapped into each display segment; each segment must not rescan the complete message. Use an empty-result fast path when the corresponding display blocks are absent, and avoid creating split arrays or sets while matching every block's tool name.

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.

Cloud Terminal Load and Acceptance​

See cloud terminal compatibility and rollout for protocol boundaries and deployment order. Run these commands sequentially from the repository root with project dependencies, uv, Cargo, and redis-server installed.

env -u TERMINAL_LOAD_REDIS_URL ./scripts/run-terminal-load.sh quick
env -u TERMINAL_LOAD_REDIS_URL ./scripts/run-terminal-load.sh baseline
env -u TERMINAL_LOAD_REDIS_URL ./scripts/run-terminal-load.sh capacity
env -u TERMINAL_LOAD_REDIS_URL ./scripts/run-terminal-load.sh over-capacity
pnpm --filter wework e2e:desktop -- --cloud-only --segment core-task-flow
  • These load commands start temporary Redis bound only to 127.0.0.1 and destroy it afterward. They require no real passwords, user tokens, or API keys. load-token is only an Executor in-memory test placeholder.
  • Keeping TERMINAL_LOAD_REDIS_URL explicitly connects to the specified Redis; use a dedicated test instance. The script deletes only exact session keys created by the run, without scans or flushing. Shared-instance INFO includes unrelated traffic and cannot be attributed to this run.
  • baseline covers 3 Backends / 1000 sessions. capacity and over-capacity cover 8192 / 9000 sessions on one Backend to check uneven distribution, cache capacity, and eviction.
  • The Backend script tests the session store/cache and cross-instance invalidation. The Executor script uses production LocalSessionHandler with an in-memory PTY to test sequences, consumption ACK, consumer takeover, replay, and backpressure. Neither includes the full Socket.IO/network/xterm path; throughput is not end-to-end capacity.
  • Real Electron E2E covers v1/v2 rendering, missing/v1/v2 attach versions, v2-disabled negotiation, output above 512 KiB, resize, reconnect, protocol pinning, and close. v1 sends no consumption ACK. After Backend restart, wait for the target device's registration log from the new process before checking online status, so stale Redis presence cannot satisfy readiness. Evidence is stored in wework/test-results/desktop-e2e/<run>/terminal-compatibility-*.json. Desktop builds share resource directories and must not run concurrently.

Release Acceptance Gate​

Local tests do not replace sustained testing in a production-equivalent environment or mixed-version validation with actual release/rollback binaries. As of 2026-09-07, local unit/contract tests, both load profiles, and real Electron E2E passed; the complete historical-artifact matrix and sustained load below remain outstanding, not approved for release. The full CI classifier suite also still requires Bash 4+.

Run each row for at least 30 minutes with real Backend, Redis, Executor PTY, and Wework, covering multiple Backends and single-replica traffic skew:

SessionsOutput per sessionACK delayPurpose
10010 KiB/s0msBaseline
50010 KiB/s50msNormal concurrency
1000100 KiB/s200msHigh throughput
30001 KiB/s50msMany low-frequency terminals
819210 KiB/s200msSingle Backend cache capacity
90001 KiB/s50msOver-capacity eviction
1000100 KiB/s1000msForced backpressure

For each row, disconnect a random 10% of clients for 3 seconds, then restore connectivity and reattach within 1 second of connection recovery, verifying that v2 replay begins. Include delayed/lost ACKs, 10 MiB bursts, five parallel terminals, theme changes, and hide/reactivate. Also test Backend restart, Redis invalidation failure, stale-consumer controls, invalid handshakes, and rejection of protocol changes on existing sessions. Verify session identity and output markers; a recovered screen alone does not prove lossless recovery. Clean up test sessions and isolated resources afterward.

Acceptance targets, not measured local-script performance claims:

  • v2 sequence gaps, duplicate writes, and silent loss are zero. Check basic I/O and reconnection for v1, without imposing lossless replay guarantees that v1 does not provide.
  • Executor replay stays within 512 KiB, pauses reads at 384 KiB, and resumes at or below 128 KiB. Memory plateaus over 30 minutes instead of growing with total output.
  • Same-region input echo P95 ≤ 50ms; RTT-adjusted path P95 ≤ 20ms. For each sample, compute echo latency - paired RTT, sort the residuals, and select the ceil(0.95 × sample count)-th value (the Backend load script's nearest-rank estimator); do not subtract two P95 values. Ctrl-C stops continuous yes output at P95 ≤ 200ms.
  • After a 3-second outage, reattach within 1 second of connection recovery and begin v2 replay. After a 10 MiB burst, the Renderer remains interactive and displays the completion marker and subsequent commands.
  • Isolated Redis command increments show SCAN=0 and KEYS=0. For shared Redis, inspect code and exact-key operation counts instead of attributing global counters to the run.

Pressure and Observability​

  • Steady authenticated data forwarding does not access SQL. Session-cache hits avoid Redis metadata reads, not Socket.IO multi-replica Pub/Sub or ACK forwarding. Count connection, authentication, and session creation separately.
  • Each Backend caches 8192 entries by default and revalidates active authorization using exact keys every 4–5 seconds with jitter. Watch miss/eviction above capacity. Each process reuses a Redis pool; Pub/Sub owns a connection within it, with no terminal-specific connection cap. Listener disconnection rejects cached authorization; reconnection clears the cache.
  • Collect CPU, memory, network throughput, Socket output/ACK events, Redis connections, and rejections. Distinguish useful output from two-hop forwarding overhead.
  • Backend metrics: terminal_ws_events_total, terminal_session_cache_requests_total, terminal_session_cache_evictions_total, terminal_session_store_operations_total, and terminal_session_store_duration_seconds.
  • Executor /metrics: terminal_output_batches_total, terminal_output_bytes_total, terminal_replayed_batches_total, terminal_replay_bytes, terminal_ack_lag_bytes, and terminal_backpressured_sessions.
  • Wework performance events: remote-terminal-write and remote-terminal-replay-request. Metrics, traces, and diagnostic logs must not record raw terminal content or credentials. Use Redis INFO commandstats, never SCAN, KEYS, or MONITOR, for observation.

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.

Live process-text, thinking, and plan block updates carry only content_delta; they must not resend cumulative content after every delta or at the completion boundary. A completion update normally carries only status: done. If the provider completion snapshot contains only an unstreamed suffix, the executor sends that suffix as one final delta. Wework coalesces consecutive updates for the same block within a browser animation frame before updating React state, preventing quadratic IPC copying and rendering pressure during long-running tasks. Streaming block updates are droppable bulk events: terminal events retain priority under app IPC backpressure, and transcript reconciliation restores authoritative content. The Electron main process must not write every response.* delta to stdout; a synchronous console write can block the Browser main thread and all window input when a terminal or parent process does not consume output promptly.

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 DevTools 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.

Persistent Animation Rendering​

Status spinners and text shimmers are persistent animations: they can keep the Web Content process busy even when there is no input or streamed message. Implement these effects so that each frame changes only compositable transform or opacity. Do not animate paint-bound properties such as background-position or mask-position, and do not rotate a complex stroked SVG directly.

A spinner should keep its SVG static inside a fixed-size HTML wrapper and animate the wrapper's transform with will-change: transform. A text shimmer should keep the base text normally rendered and sweep a fixed-width clipping window over one highlighted text copy. The window and copy use synchronized, opposing transform animations so the highlighted glyphs remain stationary. Do not split a persistent animation by grapheme because its animation nodes and animationiteration events grow linearly with text length. A gradient mask may soften the sweep edge, but the mask position must remain static. Both implementations must preserve prefers-reduced-motion behavior.

Do not infer compositor behavior from CSS property names alone. will-change is only a hint, and apparently compositable properties such as clip-path can still cause per-frame main-thread work in the Electron version that ships with Wework. After changing a persistent animation, capture the old and new implementations for at least 10 seconds under the same Electron version, element count, and window state, and verify:

  • Pausing animations causes CPU usage to fall, establishing that the animation is causal.
  • The new implementation no longer emits per-frame Paint or PaintImage events.
  • UpdateLayoutTree and Layerize event counts and total durations no longer grow continuously with animation frames.
  • Direction, period, color, dimensions, and visibility states match the previous effect.

If the replacement still produces continuous painting or layerization, revise the implementation again. Visual smoothness or an occasional reduction in average CPU is not sufficient evidence.

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 Electron DevTools support by default, while the main WebView remains non-inspectable so its native Chromium context menu does not contain Inspect Element. When the user selects Open DevTools from the hidden Developer Commands menu, the native command dynamically enables webContents.openDevTools() and opens the Inspector. This command is independent of the Performance Diagnostics switch and requires macOS 13.3 or newer. Built-in-browser child WebViews enable Inspector only in debug builds. On macOS, the Inspector is forcibly detached before its frontend is first shown, so F12 opens a separate window without docking, resetting the child dimensions, or covering the workbench. Release builds disable the child-WebView Inspector through an explicit build cfg. Set WEWORK_RELEASE_DEVTOOLS=0 when a distribution must omit main-WebView Inspector support. To open the main-WebView Inspector automatically for a local diagnostic launch, use:

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

After DevTools 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 Chromium XPC processes to PID 1; diagnostics use LaunchServices to associate the current Wework instance with its Web Content, GPU, and Networking processes.

Performance Diagnostics deliberately keeps its own collector. It still reads macOS ps RSS through wework/electron/src/host/process-diagnostics.ts, while the Process Explorer reads Electron metrics plus systeminformation and uses Electron workingSetSize for Electron-owned processes. The numbers are close but not interchangeable: do not reuse one surface's memory thresholds for the other, and treat a consolidation as a separate change that re-baselines the memory E2E suites.

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 Chromium 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 DevTools heap snapshots from the steady-state baseline.

Streaming activity shimmer widths use dimensions from ResizeObserverEntry without synchronously calling getBoundingClientRect() on mount. The highlight starts at zero width until dimensions arrive, keeping the base text visible. Long turns repeatedly mount progress indicators, so synchronous layout reads followed by style writes accumulate forced-layout work. Use source maps from the same CI build to map profile hotspots back to source.

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.