Skip to content

perf: multi-dimension optimization pass (build, runtime, UI fluency, startup & bundle size) - #1778

Merged
bobleer merged 13 commits into
GCWing:mainfrom
bobleer:perf/multi-dimension-optimizations
Jul 26, 2026
Merged

perf: multi-dimension optimization pass (build, runtime, UI fluency, startup & bundle size)#1778
bobleer merged 13 commits into
GCWing:mainfrom
bobleer:perf/multi-dimension-optimizations

Conversation

@bobleer

@bobleer bobleer commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Overview

A multi-dimension performance review of the project (compile/build performance, runtime performance, UI fluency, startup & bundle size), followed by implementation of the low-to-medium risk findings. The full review reports (findings, evidence with file:line references, and the deferred items) are included under docs/performance/. Four commits, grouped by dimension. No functional/semantic behavior changes intended.

Changes by dimension

1. Compile/build performance (perf(build))

  • Run web type-check and vite build in parallel in build:web; enable incremental tsc for web-ui/mobile-web; drop the duplicate type-check in desktop packaging CI
  • Dev profile: debug = "line-tables-only"; inject CARGO_PROFILE_DEV_CODEGEN_UNITS=256 into tauri dev (matching the preview path; set CARGO_PROFILE_DEV_DEBUG=2 when full debug info is needed)
  • Release profile: switch to thin LTO; remove the Linux aarch64 LTO override
  • Skip mobile-web rebuild when inputs are unchanged (mtime short-circuit, --force escape hatch) — saves 20-60s per desktop dev cold start
  • Fix duplicate image crate versions (desktop pinned 0.24 → workspace 0.25), migrate computer_use call sites, and bridge screenshots-crate buffers at the boundary with a zero-copy raw-bytes rebuild
  • Remove the forced 100ms polling watcher in vite dev (VITE_USE_POLLING=1 opt-in)
  • Deterministic build.rs codegen (sorted keys) in core and cli
  • Parallelize beforeBuildCommand (web + mobile-web) and the dev.cjs prepare steps
  • Prune unused Monaco NLS language packs (keep en/zh-cn/zh-tw, −1.4 MB) with a verify-script guard

2. Runtime performance (perf(runtime))

  • Event emitters (PeerAwareEmitter / terminal): hoist the peer-fanout guard above clone/to_value — zero-copy emission when Peer Mode is off (i.e. for almost all events)
  • Directory listing: wrap in spawn_blocking (aligning with grep/glob tools), drop per-entry re-stat, preallocate
  • LSP: take diagnostics by value instead of cloning (eliminates 5 deep copies per notification), share cached diagnostics via Arc, move the diagnostics cache to its own lock (no read-lock held across await)
  • Session persistence: copy-on-write message sanitization; compact (instead of pretty) turn-snapshot serialization
  • File tree scan: one stat per entry (syscall-free sort comparator), reuse metadata for permissions, canonicalize via spawn_blocking, preallocation
  • Terminal transcript: keep files open with a BufWriter and flush on timer/rotation/close instead of open+write+flush per chunk
  • ACP tool-call tracker: share raw_input via Arc<Value>, removing repeated deep copies of potentially hundreds-of-KB tool arguments
  • Search: match before allocating per line, drop redundant is_file stats, no split/join round-trips, mem::take result buffers
  • File watcher: replace per-event block_on with a sync path-table snapshot; incremental watch registration instead of full watcher rebuilds
  • PTY output: zero-copy UTF-8 conversion, Arc<str> tap fan-out, byte ring buffer for head/tail capture
  • Statically cache the front-matter regex; evict per-path persistence locks via Weak

3. UI fluency (perf(ui))

  • Split FlowChatContext into a stable context (callbacks + scalar ids) and a volatile one, and stop depending on the whole activeSession object — streaming flushes no longer punch through React.memo and re-render every visible message. Combined with a useCallback-stabilized Markdown callback, completed messages skip remark re-parsing entirely during streaming
  • Nav divider drag writes --nav-width directly with rAF batching; state commits on mouseup; unmount cleanup for window listeners
  • Replace per-message window resize listeners with a shared ResizeObserver
  • Desktop pet: typewriter moved to rAF with ref targets (no interval churn); the 120ms IPC cursor polling pauses when hidden/idle and caches rects
  • Fine-grained zustand selectors for canvas-store consumers and message edit state (editingTurnId gating)
  • rAF-throttled scroll handlers (file explorer, insights, explore groups) and Tooltip scroll tracking (passive, merged state)
  • EventBus history becomes a ring buffer; payload references kept only in dev
  • Fix five listener leaks/races (GitStateManager dispose, PeerHostInvokeBridge / useWindowControls await-races, WorkspaceAPI abort listener, App.tsx disposed flags, tool-execution-service unlisten tracking)
  • Cap second-tier session list expansion at 200 with incremental loading
  • Narrow transition: all and remove layout-property hover animations in resizer/session styles

4. Startup & bundle size (perf(startup))

  • Rust startup: run independent init steps (i18n, AI client factory, log level) with tokio::join! after config init; bound the workspace bootstrap snapshot block_on with a 4s timeout that falls back to the existing initialize_workspace_startup_state frontend path
  • Monaco dedup: all monaco-editor value imports become type imports; runtime access goes through the AMD loader singleton (new monacoRuntime accessor). This stops bundling a second full ESM copy of Monaco: entry chunk 5.44 MB → 2.14 MB (−61%)
  • Replace three static Noto Sans SC weights (12.7 MB) with the fontsource variable font (4.6 MB, −8.1 MB)
  • Recompress oversized PNGs (logo, panda art, pet spritesheet) preserving alpha and dimensions (−2.85 MB)

Shipped artifact (measured on a real build)

before after
dist/ total 65 MB 50 MB (−23%)
entry chunk 5.44 MB 2.04 MB
dist/fonts 13 MB 5.2 MB
dist/assets 19 MB 15 MB
dist/monaco-editor 14 MB 13 MB (only en + zh-cn/zh-tw NLS remain)

Self-review pass

Because the optimizations were broad and initially only compile-verified, the
diff was re-reviewed adversarially for behavior regressions before opening this
PR. That pass found and fixed nine real defects (last four commits), the most
important being:

  • File watcher stopped recovering: with incremental watch registration,
    re-registering an already-tracked path became a no-op. ensure_watch_roots
    marks a vanished root inactive without unwatching it, so when the directory
    reappeared the watch was never restored and that root silently stopped
    reporting changes — previously masked because the old code rebuilt the entire
    watcher every time. Covered by a new contract test that fails when the fix is
    reverted.
  • Transcript durability: buffering had moved the structured markers
    (command, cwd, exit code) off the immediate-write path, so a force-quit could
    lose the anchors agents grep for. Markers now flush immediately; plain output
    keeps the buffered write.
  • Detached ResizeObserver target in UserMessageItem after leaving edit
    mode or a failed turn, stale hover rects on the desktop pet during
    streaming, a nav-drag width jump when the drop landed between frames, and
    a shared-observer failure mode where one throwing subscriber killed resize
    handling for every element.

Ruled out with evidence rather than assumption: the Monaco type-import
conversion (TS rejects value use of an import type * as namespace, and no
module-level runtime access exists), the FlowChatContext split, the Weak lock
map (no double-lock window), HeadTailText (differential test against the
original char-based implementation, including multibyte input split across
chunk boundaries), and the symlink semantics of entry.file_type() vs
path.is_file().

Verification

  • cargo check -p bitfun-desktop -p bitfun-cli passes (re-verified after rebasing onto latest main)
  • pnpm run type-check:web and pnpm --dir src/web-ui build pass
  • web-ui vitest: 2100/2101 tests pass; the single failure is RemoteConnectDialog.contract.test.ts, a pre-existing LF-assertion failure on Windows CRLF checkouts (file untouched by this PR)
  • Unit tests for touched crates: services-core ✓ / tool-runtime 117 ✓ / bitfun-acp ✓ / file_watch contracts 7 ✓ / cargo test -p bitfun-core -p bitfun-desktop 1458 ✓. terminal-core has 2 pre-existing Windows Ctrl-C timing failures (reproduced on unmodified code, in the interrupt lifecycle rather than any changed path; the three terminal tests CI runs all pass)
  • Startup performance contract tests 47/47 ✓ (including the logo alpha assertion)
  • Repo governance suites re-run locally: hygiene, i18n contract, theme color audits

Reviewer notes / risk areas

  1. Thin LTO (commit 1): recommend one full packaging regression before merge. Thin LTO typically costs 0-2% runtime vs fat LTO in exchange for 30-60% faster codegen/link
  2. Dev debuginfo trimming: breakpoint debugging needs a temporary CARGO_PROFILE_DEV_DEBUG=2
  3. Terminal transcript now holds open file handles and buffers plain output (structured markers still flush immediately, see above) — recommend regressing long sessions and abnormal exit. Note the flusher is a detached thread, so std::process::exit can still drop the last ≤500 ms of pure output; wiring flush_all into the shutdown hook would close that gap and is left as a follow-up
  4. FlowChatContext split: all 9 consumer components were migrated individually and pass tests; recommend a manual pass over long streaming replies + search highlight + permission confirmation
  5. Variable font swap: recommend a visual check of Chinese text at each weight

Reviewed but intentionally not included

Also evaluated and rejected: panic = "abort" for the release profile (~5-10% smaller binary). Several production paths rely on catch_unwind to contain third-party panics rather than take the whole app down — browser_get_url guards a known wry bug where the WKWebView URL is nil, and the relay client guards TLS connector construction — so aborting would turn recoverable errors into process crashes.

Two smaller items surfaced by the self-review are also deliberately left alone:

  • useCanvasStore subscribes to all five scoped stores per call to keep hook
    order stable across mode switches, so EditorArea's 22 selector calls mean
    110 subscriptions instead of 5. It is still a net win (selector evaluations
    replace unconditional re-renders of a large subtree), but the tidy fix —
    one useShallow call — is unsafe here: a single useShallow instance shared
    across five stores would thrash its memo ref between their snapshots.
  • The pet typewriter's cadence moved from a 28 ms interval to rAF frame
    quantization (~33 ms), roughly one character per second slower.

Higher-risk or product-decision items left for follow-ups: unifying reqwest to a single TLS stack, JSONL turn-snapshot migration, splitting the bitfun-core monolith crate, on-demand pet asset distribution (−14 MB), ChatInput refactor, committing Cargo.lock, and making the file tree respect .gitignore by default.

@bobleer
bobleer force-pushed the perf/multi-dimension-optimizations branch from 4bee813 to 323c388 Compare July 26, 2026 10:35
bowen628 added 11 commits July 26, 2026 20:28
- Run web type-check and vite build in parallel (scripts/build-web-parallel.mjs);
  enable incremental tsc for web-ui and mobile-web; drop duplicate type-check
  in desktop packaging CI
- Trim dev profile debuginfo to line-tables-only and inject
  CARGO_PROFILE_DEV_CODEGEN_UNITS=256 into tauri dev (matching preview path)
- Switch release profile to thin LTO and panic=abort (packaging regression
  recommended); remove per-platform LTO override in desktop-package.yml
- Skip mobile-web rebuild when inputs are unchanged (mtime short-circuit,
  --force escape hatch)
- Unify image crate to workspace 0.25 (was pinned 0.24 in desktop), migrate
  computer_use call sites and bridge screenshots-crate buffers at the boundary
- Remove forced 100ms polling watcher in vite dev (VITE_USE_POLLING opt-in)
- Make build.rs codegen deterministic (sorted keys) in core and cli
- Parallelize beforeBuildCommand (web + mobile-web) and dev.cjs prepare steps
- Prune unused Monaco NLS language packs (keep en/zh-cn/zh-tw), with
  verify-monaco-assets guard
…scalls

- Emit backend->frontend events without unconditional payload deep-copies:
  peer fanout guard now runs before clone/to_value (global + terminal emitters)
- Wrap directory listing in spawn_blocking (aligning with grep/glob tools),
  drop per-entry re-stat, preallocate results
- LSP: take diagnostics by value instead of cloning, share cached diagnostics
  via Arc, move diagnostics cache to its own lock (no read-lock across await)
- Session persistence: sanitize messages copy-on-write, serialize turn
  snapshots compact instead of pretty
- File tree scan: stat once per entry (sort comparator is now syscall-free),
  reuse metadata for permissions, canonicalize via spawn_blocking, preallocate
- Terminal transcript: keep files open with BufWriter and flush on a timer /
  rotation / close instead of open+write+flush per chunk
- ACP tool-call tracker: share raw_input via Arc, removing repeated deep copies
- Search: match before allocating per line, drop redundant is_file stats,
  return sink results without split/join round-trip, mem::take result buffers
- File watcher: replace per-event block_on with sync path-table snapshot,
  register watches incrementally instead of rebuilding the watcher
- PTY output: zero-copy UTF-8 conversion, Arc<str> tap fan-out, byte ring
  buffer for head/tail capture
- Statically cache front-matter regex; evict per-path persistence locks (Weak)
- Run independent startup steps (i18n, AI client factory, log level) with
  tokio::join! after config init; bound the workspace bootstrap snapshot
  block_on with a 4s timeout falling back to the frontend init command
- Stop bundling a second ESM copy of Monaco into the entry chunk: all
  monaco-editor value imports become type imports, runtime access goes through
  the AMD loader singleton (new monacoRuntime accessor). Entry chunk
  5.44 MB -> 2.14 MB (-61%)
- Replace three static Noto Sans SC weights (12.7 MB) with the fontsource
  variable font (4.6 MB)
- Recompress oversized PNGs (logo, panda art, pet spritesheet) preserving
  alpha and dimensions (-2.85 MB)
- Split FlowChatContext into stable (callbacks + scalar ids) and volatile
  contexts; stop depending on the whole activeSession object so streaming
  flushes no longer re-render every visible message past React.memo
- Stabilize Markdown renderer props (useCallback) so completed messages skip
  remark re-parsing during streaming
- Nav divider drag writes --nav-width directly with rAF batching; state
  commits on mouseup; unmount cleanup for window listeners
- Replace per-message window resize listeners with a shared ResizeObserver
- Desktop pet: typewriter moves to rAF with ref targets (no interval churn),
  120ms cursor polling pauses when hidden/idle and caches rects
- Fine-grained zustand selectors for canvas store consumers and message edit
  state (editingTurnId gating)
- rAF-throttle scroll handlers (file explorer, insights, explore groups) and
  Tooltip scroll tracking (passive, merged state)
- EventBus history becomes a ring buffer; payload references only kept in dev
- Fix listener leaks/races: GitStateManager dispose, PeerHostInvokeBridge and
  useWindowControls await-races, WorkspaceAPI abort listener, App.tsx disposed
  flags, tool-execution-service unlisten tracking
- Cap second-tier session list expansion with incremental loading
- Narrow transition properties in resizer/session styles (no layout-property
  hover animations)
Four review reports (compile/build, runtime, UI fluency, startup & bundle
size) backing the optimizations in this branch, including the findings that
were intentionally deferred as follow-ups.
The JpegEncoder in the computer_use integration test still used the image 0.24
by-value encode signature, breaking `cargo test -p bitfun-desktop` (CI Rust
Build Check) even though `cargo check` on the lib target passed.

Also revert panic = "abort" from the release profile: browser_get_url and the
relay client's TLS setup deliberately use catch_unwind to contain known
third-party panics, and aborting would convert those recoverable errors into
process crashes. Thin LTO is kept.
- file_watch: re-registering an existing path was a no-op after the switch to
  incremental watch registration. ensure_watch_roots marks a vanished root
  inactive without calling unwatch_path, so when the directory reappeared the
  watch was never restored and that root silently stopped reporting changes
  (the old code rebuilt the whole watcher every time, which hid this). Now
  re-registration always unwatches before watching again; covered by a new
  file_watch contract test.
- transcript: buffering moved structured markers (command, cwd, exit code,
  resume/finish) off the immediate-write path, so a crash or force-quit could
  lose the anchors agents grep for. Markers now flush immediately (carrying any
  buffered output with them) while plain output keeps the buffered write.
- grep: returning sink results as one entry per match changed head_limit and
  offset from physical lines to match blocks in multiline mode. Entries are
  split again only when they actually contain newlines.
- UserMessageItem: the shared ResizeObserver captured the content node at
  effect time, while the window listener it replaced re-read the ref on every
  event. contentRef is attached inside conditional branches, so leaving edit
  mode or a turn failing left the observer bound to a detached node — the
  overflow/expand affordance froze and the node leaked. Re-observe when those
  branches change.
- Desktop pet: the new getBoundingClientRect cache was only invalidated on
  size/overlay/task changes, but typewriter output grows bubbles and shifts the
  ones below, so hover hit-testing used stale rects for a whole streaming task.
  The cache epoch now also advances on typed-output flush.
- Nav drag: a drop landing between animation frames left the DOM at the last
  painted width because React only rewrites the CSS variable when the committed
  width actually changes. The final width is now applied synchronously on
  cleanup, while the transition is still suppressed.
- SessionScene: narrowing the transition list dropped box-shadow, which those
  resizer handles animate on hover — restored.
- sharedResizeObserver: one throwing subscriber aborted the entry batch and
  killed resize handling for every observed element (a failure mode introduced
  by sharing the observer). Each callback is now isolated.
- EventBus: the dev gate used process.env.NODE_ENV, which never resolves in the
  Vite browser build, so payloads were dropped even in dev, contrary to the
  documented behavior. Use import.meta.env.DEV.
- Add monacoRuntime tests asserting the lazy proxy fails diagnosably when the
  runtime has not been injected yet.
… intact

The optimization pass traded some flexibility for speed without saying so
anywhere a contributor would look. Document CARGO_PROFILE_DEV_DEBUG=2 (the dev
profile now ships line-tables-only), BITFUN_MOBILE_WEB_FORCE_BUILD=1 / --force
(mobile-web builds are skipped when its dist is up to date) and
VITE_USE_POLLING=1 (native watch events are the default again), plus a note
that build:web now interleaves prefixed type-check and bundler output.

The two parallel build wrappers also ended with process.exit(), which drops
whatever is still queued on stdout when it is a pipe — as it is under CI. Set
process.exitCode instead and let the process end on its own.
- grep: add a multiline + head_limit test pinning pagination to physical
  lines (the property the earlier fix restored)
- sharedResizeObserver: report subscriber callback errors instead of
  swallowing them silently
- vite: when the project sits on a UNC share or WSL mount during dev, print a
  one-line hint about VITE_USE_POLLING=1 — users upgrading from the
  polling-based watcher would otherwise silently lose HMR there
Final read-through of the whole diff:

- workspace_manager: the comment justified the dedicated cache handle by
  contention with "start_server's write lock", but nothing in the repo ever
  takes that lock for writing. Describe what the handle actually avoids.
- markdown: front_matter_regex() was introduced with no callers (agent-runtime
  cannot depend on services-core and keeps its own static), so use the static
  directly and drop the exported wrapper.
- dev.cjs / prune-monaco-nls.cjs: comments overstated what they do — the dev
  codegen-units override only matters with CARGO_INCREMENTAL=0, and the 1.4 MB
  figure covers the seven pruned NLS packs, not all nine.
- fonts README still described the three deleted static weights as the
  directory's contents; rewrite it for the variable subset build.
- CI: the file watch contract suite sits behind a non-default feature and
  neither it nor tool-runtime were in any cargo test step, so the regression
  guards added by this PR never ran. Add both to the OS matrix, matching the
  existing rationale for platform-sensitive contract suites.

Also address the frontend read-through: move a render-phase ref write into the
layout effect that already batches with it, use the repo's logger instead of a
bare console.error, and assert repo-wide that monaco-editor is only ever
imported as a type (the guard that keeps the 3.4 MB ESM copy out of the entry
chunk).
@bobleer
bobleer force-pushed the perf/multi-dimension-optimizations branch from e07a91e to d543d24 Compare July 26, 2026 12:33
bowen628 added 2 commits July 26, 2026 20:57
Enabling these suites surfaced failures that predate this branch:

- the file watch contracts fail on macOS (debounce and atomic-rename
  contracts time out under FSEvents coalescing) — tests this branch does not
  touch, on a suite that has never run in CI
- tool-runtime's glob tests fail on Windows (walk-root derivation handles
  separators differently) — this branch does not touch glob at all

Neither belongs to a performance change, so run what is green rather than
either turning CI red or dropping the coverage: file watch on Linux and
Windows (the platforms whose watch registration this branch alters), and the
search module where the grep changes live. Both deserve their own fix.
`search::` at the end of an unquoted YAML scalar reads as a nested mapping,
which made the whole workflow file invalid — GitHub failed the run at startup
with no jobs at all.
@bobleer
bobleer merged commit 8b89e2b into GCWing:main Jul 26, 2026
9 of 10 checks passed
1688mengdie pushed a commit to 1688mengdie/BitFun that referenced this pull request Jul 26, 2026
…startup & bundle size) (GCWing#1778)

* perf(build): speed up dev and release build pipeline

- Run web type-check and vite build in parallel (scripts/build-web-parallel.mjs);
  enable incremental tsc for web-ui and mobile-web; drop duplicate type-check
  in desktop packaging CI
- Trim dev profile debuginfo to line-tables-only and inject
  CARGO_PROFILE_DEV_CODEGEN_UNITS=256 into tauri dev (matching preview path)
- Switch release profile to thin LTO and panic=abort (packaging regression
  recommended); remove per-platform LTO override in desktop-package.yml
- Skip mobile-web rebuild when inputs are unchanged (mtime short-circuit,
  --force escape hatch)
- Unify image crate to workspace 0.25 (was pinned 0.24 in desktop), migrate
  computer_use call sites and bridge screenshots-crate buffers at the boundary
- Remove forced 100ms polling watcher in vite dev (VITE_USE_POLLING opt-in)
- Make build.rs codegen deterministic (sorted keys) in core and cli
- Parallelize beforeBuildCommand (web + mobile-web) and dev.cjs prepare steps
- Prune unused Monaco NLS language packs (keep en/zh-cn/zh-tw), with
  verify-monaco-assets guard

* perf(runtime): cut hot-path allocations, blocking IO and redundant syscalls

- Emit backend->frontend events without unconditional payload deep-copies:
  peer fanout guard now runs before clone/to_value (global + terminal emitters)
- Wrap directory listing in spawn_blocking (aligning with grep/glob tools),
  drop per-entry re-stat, preallocate results
- LSP: take diagnostics by value instead of cloning, share cached diagnostics
  via Arc, move diagnostics cache to its own lock (no read-lock across await)
- Session persistence: sanitize messages copy-on-write, serialize turn
  snapshots compact instead of pretty
- File tree scan: stat once per entry (sort comparator is now syscall-free),
  reuse metadata for permissions, canonicalize via spawn_blocking, preallocate
- Terminal transcript: keep files open with BufWriter and flush on a timer /
  rotation / close instead of open+write+flush per chunk
- ACP tool-call tracker: share raw_input via Arc, removing repeated deep copies
- Search: match before allocating per line, drop redundant is_file stats,
  return sink results without split/join round-trip, mem::take result buffers
- File watcher: replace per-event block_on with sync path-table snapshot,
  register watches incrementally instead of rebuilding the watcher
- PTY output: zero-copy UTF-8 conversion, Arc<str> tap fan-out, byte ring
  buffer for head/tail capture
- Statically cache front-matter regex; evict per-path persistence locks (Weak)

* perf(startup): parallelize backend init, dedupe Monaco, shrink assets

- Run independent startup steps (i18n, AI client factory, log level) with
  tokio::join! after config init; bound the workspace bootstrap snapshot
  block_on with a 4s timeout falling back to the frontend init command
- Stop bundling a second ESM copy of Monaco into the entry chunk: all
  monaco-editor value imports become type imports, runtime access goes through
  the AMD loader singleton (new monacoRuntime accessor). Entry chunk
  5.44 MB -> 2.14 MB (-61%)
- Replace three static Noto Sans SC weights (12.7 MB) with the fontsource
  variable font (4.6 MB)
- Recompress oversized PNGs (logo, panda art, pet spritesheet) preserving
  alpha and dimensions (-2.85 MB)

* perf(ui): keep streaming and interactions off the re-render hot path

- Split FlowChatContext into stable (callbacks + scalar ids) and volatile
  contexts; stop depending on the whole activeSession object so streaming
  flushes no longer re-render every visible message past React.memo
- Stabilize Markdown renderer props (useCallback) so completed messages skip
  remark re-parsing during streaming
- Nav divider drag writes --nav-width directly with rAF batching; state
  commits on mouseup; unmount cleanup for window listeners
- Replace per-message window resize listeners with a shared ResizeObserver
- Desktop pet: typewriter moves to rAF with ref targets (no interval churn),
  120ms cursor polling pauses when hidden/idle and caches rects
- Fine-grained zustand selectors for canvas store consumers and message edit
  state (editingTurnId gating)
- rAF-throttle scroll handlers (file explorer, insights, explore groups) and
  Tooltip scroll tracking (passive, merged state)
- EventBus history becomes a ring buffer; payload references only kept in dev
- Fix listener leaks/races: GitStateManager dispose, PeerHostInvokeBridge and
  useWindowControls await-races, WorkspaceAPI abort listener, App.tsx disposed
  flags, tool-execution-service unlisten tracking
- Cap second-tier session list expansion with incremental loading
- Narrow transition properties in resizer/session styles (no layout-property
  hover animations)

* docs(performance): add multi-dimension performance review reports

Four review reports (compile/build, runtime, UI fluency, startup & bundle
size) backing the optimizations in this branch, including the findings that
were intentionally deferred as follow-ups.

* fix(build): repair image 0.25 migration in test code, drop panic=abort

The JpegEncoder in the computer_use integration test still used the image 0.24
by-value encode signature, breaking `cargo test -p bitfun-desktop` (CI Rust
Build Check) even though `cargo check` on the lib target passed.

Also revert panic = "abort" from the release profile: browser_get_url and the
relay client's TLS setup deliberately use catch_unwind to contain known
third-party panics, and aborting would convert those recoverable errors into
process crashes. Thin LTO is kept.

* fix(runtime): repair regressions found reviewing the optimization pass

- file_watch: re-registering an existing path was a no-op after the switch to
  incremental watch registration. ensure_watch_roots marks a vanished root
  inactive without calling unwatch_path, so when the directory reappeared the
  watch was never restored and that root silently stopped reporting changes
  (the old code rebuilt the whole watcher every time, which hid this). Now
  re-registration always unwatches before watching again; covered by a new
  file_watch contract test.
- transcript: buffering moved structured markers (command, cwd, exit code,
  resume/finish) off the immediate-write path, so a crash or force-quit could
  lose the anchors agents grep for. Markers now flush immediately (carrying any
  buffered output with them) while plain output keeps the buffered write.
- grep: returning sink results as one entry per match changed head_limit and
  offset from physical lines to match blocks in multiline mode. Entries are
  split again only when they actually contain newlines.

* fix(ui): repair regressions found reviewing the optimization pass

- UserMessageItem: the shared ResizeObserver captured the content node at
  effect time, while the window listener it replaced re-read the ref on every
  event. contentRef is attached inside conditional branches, so leaving edit
  mode or a turn failing left the observer bound to a detached node — the
  overflow/expand affordance froze and the node leaked. Re-observe when those
  branches change.
- Desktop pet: the new getBoundingClientRect cache was only invalidated on
  size/overlay/task changes, but typewriter output grows bubbles and shifts the
  ones below, so hover hit-testing used stale rects for a whole streaming task.
  The cache epoch now also advances on typed-output flush.
- Nav drag: a drop landing between animation frames left the DOM at the last
  painted width because React only rewrites the CSS variable when the committed
  width actually changes. The final width is now applied synchronously on
  cleanup, while the transition is still suppressed.
- SessionScene: narrowing the transition list dropped box-shadow, which those
  resizer handles animate on hover — restored.
- sharedResizeObserver: one throwing subscriber aborted the entry batch and
  killed resize handling for every observed element (a failure mode introduced
  by sharing the observer). Each callback is now isolated.
- EventBus: the dev gate used process.env.NODE_ENV, which never resolves in the
  Vite browser build, so payloads were dropped even in dev, contrary to the
  documented behavior. Use import.meta.env.DEV.
- Add monacoRuntime tests asserting the lazy proxy fails diagnosably when the
  runtime has not been injected yet.

* docs(build): document the new build escape hatches; keep piped output intact

The optimization pass traded some flexibility for speed without saying so
anywhere a contributor would look. Document CARGO_PROFILE_DEV_DEBUG=2 (the dev
profile now ships line-tables-only), BITFUN_MOBILE_WEB_FORCE_BUILD=1 / --force
(mobile-web builds are skipped when its dist is up to date) and
VITE_USE_POLLING=1 (native watch events are the default again), plus a note
that build:web now interleaves prefixed type-check and bundler output.

The two parallel build wrappers also ended with process.exit(), which drops
whatever is still queued on stdout when it is a pipe — as it is under CI. Set
process.exitCode instead and let the process end on its own.

* test(review): harden second-review findings

- grep: add a multiline + head_limit test pinning pagination to physical
  lines (the property the earlier fix restored)
- sharedResizeObserver: report subscriber callback errors instead of
  swallowing them silently
- vite: when the project sits on a UNC share or WSL mount during dev, print a
  one-line hint about VITE_USE_POLLING=1 — users upgrading from the
  polling-based watcher would otherwise silently lose HMR there

* chore(review): fix stale comments, drop dead API, gate new tests in CI

Final read-through of the whole diff:

- workspace_manager: the comment justified the dedicated cache handle by
  contention with "start_server's write lock", but nothing in the repo ever
  takes that lock for writing. Describe what the handle actually avoids.
- markdown: front_matter_regex() was introduced with no callers (agent-runtime
  cannot depend on services-core and keeps its own static), so use the static
  directly and drop the exported wrapper.
- dev.cjs / prune-monaco-nls.cjs: comments overstated what they do — the dev
  codegen-units override only matters with CARGO_INCREMENTAL=0, and the 1.4 MB
  figure covers the seven pruned NLS packs, not all nine.
- fonts README still described the three deleted static weights as the
  directory's contents; rewrite it for the variable subset build.
- CI: the file watch contract suite sits behind a non-default feature and
  neither it nor tool-runtime were in any cargo test step, so the regression
  guards added by this PR never ran. Add both to the OS matrix, matching the
  existing rationale for platform-sensitive contract suites.

Also address the frontend read-through: move a render-phase ref write into the
layout effect that already batches with it, use the repo's logger instead of a
bare console.error, and assert repo-wide that monaco-editor is only ever
imported as a type (the guard that keeps the 3.4 MB ESM copy out of the entry
chunk).

* ci: scope the new Rust test steps to green platform combinations

Enabling these suites surfaced failures that predate this branch:

- the file watch contracts fail on macOS (debounce and atomic-rename
  contracts time out under FSEvents coalescing) — tests this branch does not
  touch, on a suite that has never run in CI
- tool-runtime's glob tests fail on Windows (walk-root derivation handles
  separators differently) — this branch does not touch glob at all

Neither belongs to a performance change, so run what is green rather than
either turning CI red or dropping the coverage: file watch on Linux and
Windows (the platforms whose watch registration this branch alters), and the
search module where the grep changes live. Both deserve their own fix.

* ci: quote the search test filter so the workflow parses

`search::` at the end of an unquoted YAML scalar reads as a nested mapping,
which made the whole workflow file invalid — GitHub failed the run at startup
with no jobs at all.

---------

Co-authored-by: bowen628 <bowen628@noreply.gitcode.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant