Repository navigation
fix(memory): bound renderer caches and avoid WebKit scrollbar allocations - #1097
kogekiplay wants to merge 9 commits into
Conversation
There was a problem hiding this comment.
🟡 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
Fallback traversal resets the failed-build marker indefinitely · New Empty-query file search bypasses traversal limits · New Superseded searches repeat full traversals under the global mutex · New File search delays all suggestion groups from rendering · New Closing the dialog unnecessarily invalidates the shared index · New Search timeout is too short for cold workspace traversals · New Overflow highlight requests never retry after engine readiness · New
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
| 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); | ||
| } | ||
| } |
| 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; |
| const [files, sessions, commits] = await Promise.all([ | ||
| filesPromise, |
| const reset = useCallback(() => { | ||
| loadedForPathRef.current = null | ||
| generation.current += 1 | ||
| refresh.current = true |
| return getTransport().call("search_workspace_files", { | ||
| path, | ||
| query, | ||
| ...options, | ||
| }) |
| if (pending.size >= HIGHLIGHT_LIMITS.inFlight) { | ||
| return rawCodeTokens(options.code) |
|
方案结论:可以拆分发布,而且建议这样推进。 共享高亮缓存、WebKit 内存修复、Tauri 回调清理都有独立价值。当前工作区搜索的调度与缓存问题,只需阻塞搜索这一批,无需让其他优化一起等待。 以下意见基于当前提交 感谢这轮补充,查询取消、高亮重试和回调清理都在推进问题解决。建议将当前 PR 拆成下面四个独立 PR,各自验证、合并和发布;顺序表示建议优先级,没有必须依次发布的限制。
前两批可以优先整理送审;第三批完成局部修复后可以独立推进。 第四批单独确认方案,不作为其他批次的前置条件。先发布前三批能够交付各自的内存改善;大工作区全量文件列表进入渲染进程的问题,则仍需第四批解决。 已确认的问题可以分别带入对应 PR,避免拆分后遗漏:
拆分时,请把每组实现、测试和依赖补丁配置一起带上;工作区搜索的前后端接口及调用方应保持配套。已有提交基本按模块划分,但共享的 此前的 139 项聚焦前端测试、相关 ESLint 和生产 TypeScript 检查通过,上述问题由额外复现确认;没有把 GitHub CI 状态作为判断依据。拆分后,各 PR 仍需针对自己的最终内容验证。当前完整 PR 不建议整体合并,独立批次可以在各自问题解决、验证通过后先发布。 |

Following the maintainer's review, this PR now tracks the independent splits below. The original combined implementation should not be merged as a whole.
All four new branches are based directly on official main
5c03d7af865f54cf42a994327daf2c45ab4a946f, with independent dependency patch configuration. The search draft does not block the other batches.The later HTML-preview cache change (
f5a0ccc3) remains preserved on this original branch for a separate follow-up; it is not mixed into these four splits. No installed application was replaced or restarted for this split/validation work.The search split has since been updated onto official main
c340c76e34075da83602712e07352b543e6aa380and completed inc4c095c8; its original draft implementation is superseded.Original combined implementation and historical validation
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
ScrollTimelinealone is insufficient because the CSS fallback also usescqw/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.
Tauri event callbacks: retain the callback ID independently of the native event registry. Unsubscribe and rejected subscription creation explicitly release that callback even when the registration eval arrives late. Native unlisten still runs if the registry cleanup is missing or throws. A regression reproduces 100 retained callbacks after 100 disposed subscriptions before the fix and zero after it; coverage also includes rejected creation, independent subscribers and idempotent cleanup.
Native memory measurements on macOS 27.2 (26B123), using isolated system WKWebViews and synthetic content:
calc()transformsMeasurement 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
ScrollTimelineanimations 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 officialv0.42.1tag, with all PR commits applied, was installed as0.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:
cargo checkpassed. Desktop Clippy (--all-targets --features test-utils) and server Clippy passed with-D warnings.Copilot review follow-up:
6a0f496acovers failed-build TTL, empty-query admission, obsolete traversal cancellation, progressive mention groups, local reset versus index refresh, and HTTP deadlines.d68c4849covers retrying mounted highlighting subscribers after saturation. Tests exercise each reported condition, including out-of-order cancellation and keeping consumers independent.Callback-lifetime follow-up validation on the official 0.42.1 base plus this PR: 768 frontend test files / 11,391 tests passed, including five callback-lifetime regressions; full frontend lint, production TypeScript checking and static export passed.
Two-hour passive application follow-up (November 9, 2026, UTC+08:00): a preserved 10:38:02–12:38:02 trace contains 481 samples, with a maximum gap of 16.5 seconds. The first 19.5 minutes used
memory.3; the authorized callback-fix restart at 10:57:44 switched to0.42.1+memory.4. The same new main/Web Content processes then remained alive for 100.25 minutes (402 samples): Web Content's sampled range was 265.3–516.5 MiB and its final value was 289.5 MiB; the native summary recorded a lifetime peak of 642.5 MiB. Its footprint rose during part of this segment, then fell from 452.0 to 298.6 MiB around 12:10 without restarting, and remained near 289.5 MiB for the last approximately 20 minutes. Native WebKit allocated bytes similarly fell from about 282.8 to 124.7 MiB before settling near 138.1 MiB. This did not reproduce the earlier multi-GiB runaway. The trace is an uncontrolled passive observation with no activity attribution, so it neither proves zero growth for every workload nor isolates the callback fix's causal effect. The complete trace and process-segment records are preserved locally; no private app data was uploaded.Cross-round long-horizon review: the original renderer was followed by PID + process start identity. Earlier runs measured approximately
memory.1248 → 9,370 MiB in 30 minutes,memory.2536 → 2,924 MiB over 98 minutes, andmemory.3around 263 → 589 MiB over 30 minutes. In the November 9 run, the samememory.4renderer was 289.5 MiB at 12:38 and later showed repeated rising floors: 30-minute medians/minima progressed through roughly 968/944, 1,019/1,018, 1,106/897, 889/823, 893/882, and 1,291/964 MiB before the later memory.5/memory.6 restarts. These workload periods are uncontrolled and are not an A/B proof, but the same-process floor movement is why the investigation remains open. Short-term retractions and additional WebKit processes are explicitly separated from the original renderer. The overnight controller now emitslong_horizon_process_floorand does not mark the issue stable just because the most recent 30-minute slope is negative.