Skip to content

fix(memory): bound renderer caches and avoid WebKit scrollbar allocations - #1097

Open
kogekiplay wants to merge 6 commits into
spacering-net:mainfrom
kogekiplay:fix/bounded-highlight-memory
Open

kogekiplay wants to merge 6 commits into
spacering-net:mainfrom
kogekiplay:fix/bounded-highlight-memory

Conversation

@kogekiplay

@kogekiplay kogekiplay commented Nov 6, 2026 •

Copy link
Copy Markdown

Web Content can grow into multiple GiB while the live JavaScript heap remains small. Repeated scrollbar updates trigger retained native WebKit allocations in OverlayScrollbars' container-relative transforms; renderer-wide workspace listings and independent highlighting caches add further retained data.

This change addresses those paths together:

  • WebKit scrollbars: patch OverlayScrollbars 2.15.1 to use its existing scroll-event implementation and percentage-based handle positioning. Disabling ScrollTimeline alone is insufficient because the CSS fallback also uses cqw/cqh. Scope the workaround to WebKit, including iOS browser shells, across all four package entrypoints; Chromium retains the native path. Preserve dragging, track clicks, RTL, resizing and normal animations.
  • Workspace search: discover and rank files in one shared native index with estimated retained/build budgets of 128/192 MiB, a five-minute TTL and a 200-result response cap. Over-budget workspaces retain only the best matches during traversal, including late/deep exact matches. Failed-build markers keep their original expiry so repeated fallback searches do not prevent a later rebuild. Empty queries share worker admission, and the canvas open-files listing makes no empty-query request. Per-consumer generations coalesce obsolete requests and propagate cancellation through desktop and HTTP transports into queued and running searches. Consumer tracking is bounded; searches have a cooperative five-minute deadline and a 330-second transport timeout. Closing a dialog clears local results without invalidating a warm shared index. Mention groups publish independently as each source becomes ready, preserving selection when slower file results arrive.
  • Syntax highlighting: share one lazy Shiki engine and a 16 MiB / 128-entry LRU across Markdown and code views. Do not cache streaming prefixes, coalesce identical requests, bound cold work to 16 requests, and unsubscribe unmounted views. Mounted overflow subscribers remain readable and retry in bounded batches when capacity becomes available; canceled subscribers leave the retry list. Full-content keys avoid equal-length snippet collisions. Oversized code remains readable as plaintext.

Native memory measurements on macOS 27.2 (26B123), using isolated system WKWebViews and synthetic content:

Case Observation
24 native scroll timelines with container-relative calc() transforms 293.4 → 2,323.8 MiB in a 40-second sampling span
Same timeline with a fixed-pixel endpoint Approximately 110–111 MiB over 90 seconds
Original OverlayScrollbars with 24 continuously scrolling regions 334.2 → 1,559.1 MiB in approximately 25 seconds; stopped at the footprint cap
Final patched library with 24 continuously scrolling regions Retained summaries through 29.2 minutes: 252.2 → 260.2 MiB; observed peak 276.3 MiB; the 30-minute probe completed normally

Measurement retention limitation: the completed one-shot probe was accidentally submitted as a launchd keepalive job; its automatic restart replaced the raw series after the 30-minute run exited with status 0. The table uses summaries retained before completion, with physical summaries through 29.2 minutes and a successful runtime health check at 29 min 50 s. It does not claim a complete preserved 30-minute trace. Both probe jobs have been stopped.

The patched run continuously checks that handles move, DOM count remains constant and no scrollbar ScrollTimeline animations exist. System WebKit geometry checks pass 16 combinations of axis, LTR/RTL, resize and scroll progress (maximum position error 0.234375 px); real WebKit input checks pass all eight drag/track-click cases. These results isolate the scrollbar allocation path. A follow-up build based on the official v0.42.1 tag, with all PR commits applied, was installed as 0.42.1+memory.3. The app bundle and live service version were verified. A 30-minute application observation retained all 120 physical-footprint samples and kept the same main/Web Content process identities throughout: Web Content started at 263.3 MiB, ended at 589.2 MiB, and peaked at 604.5 MiB in the periodic samples; macOS reported a lifetime peak of 661.0 MiB. The main process ended at 194.6 MiB. A live workspace search returned the exact deep match among two results in 0.249 s, transferring 241 bytes.

The application run did not reproduce the previous multi-GiB runaway, and memory repeatedly fell after temporary peaks. It does not establish zero growth: Web Content medians for minutes 10–15 / 15–20 / 20–25 / 25–30 were 557.0 / 548.8 / 579.0 / 587.9 MiB, and the fit after minute 10 was approximately +3.55 MiB/min under continued interactive use. Native WebKit allocated bytes were approximately 231.7 MiB at minute 15, 232.4 MiB at minute 27, and 239.5 MiB near minute 30. These results support a substantial improvement while leaving a smaller upward drift to distinguish from normal workload/caching. The full application trace is preserved locally; private app data and heap captures are not published.

The earlier 5.3-million-entry workspace probe retained approximately 116 MiB in the compact index. Warm filename searches took 0.17–0.54 seconds and transferred approximately 0.1–13 KB, versus approximately 925 MiB for the previous full renderer listing. Cold discovery still takes a full walk (64–104 seconds in those release probes). The existing ignore rules and authorized linked-root behavior are preserved in desktop and server modes.

Validation:

  • 764 frontend test files / 11,341 tests passed; full frontend lint, production TypeScript checking and static export passed.
  • Rust server library: 6,244 passed / 6 ignored; server binary: 2 passed. Desktop cargo check passed. Desktop Clippy (--all-targets --features test-utils) and server Clippy passed with -D warnings.
  • Frozen dependency installation applies the patch; native scrolling/input/geometry checks above pass.

Copilot review follow-up: 6a0f496a covers failed-build TTL, empty-query admission, obsolete traversal cancellation, progressive mention groups, local reset versus index refresh, and HTTP deadlines. d68c4849 covers retrying mounted highlighting subscribers after saturation. Tests exercise each reported condition, including out-of-order cancellation and keeping consumers independent.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved workspace-search timeouts, redundant traversals, and shared blocking can return empty results and stall searches and mention suggestions for over a minute.

7 open issues
What changed in this PR

Reduces Codeg’s renderer memory use by moving workspace search into shared native storage and consolidating syntax-highlighting caches.

Changes:

  • Adds budgeted native indexing, capped search results and full-traversal fallback.
  • Migrates mention, dialog and canvas searches to the bounded API in desktop and server modes.
  • Shares a lazy Shiki engine and bounded token cache, with regression coverage.
File Description
src/​lib/​code-highlighter.ts Adds shared highlighting, cache budgets and plaintext fallback.
src/​lib/​code-highlighter.test.ts Covers cache bounds, streaming, coalescing and subscriptions.
src/​lib/​api.ts Exposes workspace-search preparation and query APIs.
src/​hooks/​use-file-tree.ts Fetches bounded, debounced matches and rejects stale responses.
src/​hooks/​use-file-tree.test.ts Covers debounce, workspace changes and refresh behavior.
src/​components/​message/​completed-turn-streaming-mode.test.tsx Updates highlighting mocks to the shared service.
src/​components/​conversations/​search-command-dialog.tsx Uses query-driven search only while open.
src/​components/​chat/​composer/​use-reference-search.ts Fetches bounded native file matches for mentions.
src/​components/​chat/​composer/​use-reference-search.test.ts Covers native preparation and selective file fetching.
src/​components/​canvas/​add-node-menu.tsx Requests capped, files-only search results.
src/​components/​ai-elements/​streamdown-plugins.ts Loads the shared highlighting plugin lazily.
src/​components/​ai-elements/​streamdown-plugins.test.ts Updates shared-plugin loading mocks.
src/​components/​ai-elements/​reasoning.test.tsx Updates reasoning-view highlighting mocks.
src/​components/​ai-elements/​message.test.tsx Updates message-view highlighting mocks.
src/​components/​ai-elements/​code-block.tsx Replaces independent caches with shared highlighting subscriptions.
src-tauri/​src/​web/​router.rs Registers workspace-search HTTP routes.
src-tauri/​src/​web/​handlers/​folders.rs Adds search and preparation HTTP handlers.
src-tauri/​src/​lib.rs Registers desktop workspace-search commands.
src-tauri/​src/​commands/​workspace_file_search.rs Implements compact indexing, ranking, budgets and regression tests.
src-tauri/​src/​commands/​mod.rs Exposes the workspace-search module.
src-tauri/​src/​commands/​folders.rs Refactors traversal into reusable visitors with early termination.

Review effort: Max

Comment on lines +388 to +395
if fresh && !refresh {
if limit == 0 {
return Vec::new();
}
if let Some(index) = cache.as_ref().and_then(|c| c.index.as_ref()) {
return index.search(query, limit, files_only);
}
}
Comment on lines +448 to +449
if query.trim().is_empty() {
return tokio::task::spawn_blocking(move || {
.await
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()));
}
let mut cache = CACHE.lock().await;
Comment on lines +404 to +405
const [files, sessions, commits] = await Promise.all([
filesPromise,
Comment thread src/hooks/use-file-tree.ts Outdated
const reset = useCallback(() => {
loadedForPathRef.current = null
generation.current += 1
refresh.current = true
Comment thread src/lib/api.ts Outdated
Comment on lines +5045 to +5049
return getTransport().call("search_workspace_files", {
path,
query,
...options,
})
Comment thread src/lib/code-highlighter.ts Outdated
Comment on lines +159 to +160
if (pending.size >= HIGHLIGHT_LIMITS.inFlight) {
return rawCodeTokens(options.code)
@kogekiplay kogekiplay changed the title fix(memory): bound workspace search and syntax highlighting caches fix(memory): bound renderer caches and avoid WebKit scrollbar allocations Nov 7, 2026
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.

2 participants