
这几天都在VibeCoding,也没按时更新公号文章。就把昨晚遇到的一个bug,拿出给大家分享一下,看看AI的分析思路。
起因:在做一个自娱自乐的桌面小工具,用electron做的。本地commit了很多次后,突然发现启动后,内存持续暴涨,就让AI帮我看看问题。
它首次的分析如下:
**Findings**1. 不是主进程,也不是 GPU 进程在涨,问题明确出在某个 renderer 进程。采样里:- `PID 1968` `--type=renderer` 从 `4.02 GB` 涨到 `4.68 GB`- 时间只有 `4 秒`- 其余进程几乎不动: - 主进程 `PID 4000` 稳定在 `112 MB` - 另一个 renderer `PID 9096` 稳定在 `119 MB` - GPU `PID 14164` 稳定在 `234 MB` - network utility `PID 16264` 稳定在 `53 MB`2. 这不是“整体架构偏重”那么简单了,是单个渲染窗口里有持续分配且没有释放的行为。因为如果只是“新窗口本来就重”,你会看到启动后稳定在一个高值;现在是同一个 renderer 在几秒内持续爬升,这更像:- 某个组件在不断追加 DOM- 某个 effect / observer / timer 在反复创建对象- 某个预览组件在循环重渲染大对象- 某个 iframe / plot / media / pdf 渲染面板在反复初始化3. 从代码结构看,最可疑的仍然是“重型内容 tab 的 renderer 侧组件”,不是 Electron 主进程窗口管理。窗口创建代码在 [main.ts](d:/work/md-projects/apps/DeskPilot/src/electron/main.ts#L536) 和 [main.ts](d:/work/md-projects/apps/DeskPilot/src/electron/main.ts#L1086)。这些代码会增加基础内存,但不会解释“4 秒涨 600MB+”。真正异常的是 renderer 内部某处在持续制造内容。**当前判断**高概率是某个 tab pane 在 document window 中进入了循环式增长,优先怀疑这些方向:- `NotebookTabPane` - 大 JSON 解析 - Plotly 输出初始化/重复渲染 - markdown/html 转换后 DOM 膨胀- `WebPageTabPane` - iframe 页面自身脚本/资源持续增长- `PdfTabPane` - Chromium PDF viewer 渲染缓存- `MediaTabPane` - 视频纹理/缓冲- 某个共享 UI 逻辑在 document window 中重复触发 state 更新**为什么我这么判断**因为现在有两个 renderer:- 一个稳定在 `~119MB`- 一个暴涨到 `4.6GB+`这说明不是“所有窗口都天然很重”,而是某一个窗口里的内容/组件有问题。**建议下一步**最有效的不是继续猜,而是立刻做“按 tab 类型二分”:1. 只开一个空白/普通文本 tab 到新窗口,采样一次2. 再测 markdown3. 再测 pdf4. 再测网页5. 再测 notebook6. 再测视频/图片这样 5 分钟内就能锁定具体是哪类 pane。目前结论已经足够明确:`DeskPilot` 的持续暴涨是某个 **renderer 窗口内容级问题**,不是主进程问题。我看了一下,觉得它思路确实很清晰,但未找到真正的原因。于是就是手动一个个的回退,终于定位到问题,代码如下:
ipcMain.handle("workspace:index-files", async (_event, rootPath: string) => { if (typeof rootPath !== "string" || !rootPath) { throw new Error("Workspace root path is required."); } const stat = await fs.stat(rootPath); if (!stat.isDirectory()) { throw new Error("Workspace root path is invalid."); } return indexWorkspaceFiles(rootPath); });让AI帮着分析,下面是它的分析结果
问题基本明确了,就是这条链路:1. [main.ts](d:/work/md-projects/apps/DeskPilot/src/electron/main.ts) 里的 `ipcMain.handle("workspace:index-files", ...)`2. [main.tsx](d:/work/md-projects/apps/DeskPilot/src/renderer/main.tsx) 里的 `refreshSearchIndex(...)`3. 多处调用 `refreshSearchIndex(result.rootPath)` / `refreshSearchIndex(rootPath || directoryPath)`**本质问题**`workspace:index-files` 不是查当前目录,也不是增量更新,而是每次都:- 从工作区根目录开始- 递归遍历整个工作区- 收集所有文件到 `results` 数组- 通过 IPC 整包传回 renderer- renderer 再 `setIndexedFiles(...)` 保存一整份也就是一次调用会同时制造两份大对象:- 主进程里一份 `results`- 渲染进程里一份 `indexedFiles`如果工作区文件很多,这个数组会非常大。**为什么会“持续暴增”**不是只有打开搜索才会调它。你现在的代码里,这个全量索引会在很多地方被触发:- 工作区打开时 - [main.tsx](d:/work/md-projects/apps/DeskPilot/src/renderer/main.tsx)- 目录展开加载子节点时 - [main.tsx](d:/work/md-projects/apps/DeskPilot/src/renderer/main.tsx)- 工作区树变更时 - [main.tsx](d:/work/md-projects/apps/DeskPilot/src/renderer/main.tsx)尤其是目录展开这段最危险:每次 `readDirectory` 成功后,又立刻对整个工作区做一次全量递归索引。不是索引这个目录,是索引整个 root。所以现象就会变成:- 打开工作区,扫一遍- 展开目录,再扫一遍- 恢复路径、显示目录链,再扫几遍- 多次调用可能并发重叠- 每次都创建巨大的文件列表数组- 主进程和 renderer 都不断分配新内存- GC 来不及回收时,看起来就是内存一路猛涨你把这个 handler 注释掉后恢复正常,正好说明问题不在 Tiptap,而在“全量文件索引 IPC”这条链路。**和 `temp4` 的关系**真正把问题带进来的,不是后面的 Jupyter 或代码块复制,而是 `temp4` 之后的第一次提交 `4a068c8`。那次把原来基于树的本地 `flattenFiles(...)`/局部更新逻辑,改成了 `refreshSearchIndex -> workspace:index-files -> 全工作区递归扫描`。**为什么两个 markdown tab 也会涨**因为这件事和 tab 类型无关。只要主窗口还在工作区模式下,左侧树、恢复目录、打开工作区这些动作都可能触发全量索引。你恰好在开 markdown tab 时看到涨,不代表是 markdown 引起的。**结论**`ipcMain.handle("workspace:index-files")` 的问题不是“代码写错”,而是“策略错了”:- 代价太大- 触发太频繁- 还是全量扫描- 还跨 IPC 传整包结果- 没有 debounce、没有单飞、没有取消、没有增量**建议修法**优先级从高到低:1. 不要在 `loadDirectoryChildren` 后调用全量 `refreshSearchIndex`2. 打开工作区时最多索引一次,目录展开不要重新全量扫3. 搜索改成按需触发,而不是平时常驻维护全量索引4. 如果必须维护索引,做单飞和防抖5. 更稳的是回到 `temp4` 那种基于现有树/局部更新的策略如果你要,我下一步可以直接帮你改:- 先移除目录展开后的全量 `refreshSearchIndex`- 再把搜索索引改成“只在打开搜索框时构建一次” 这样风险最低,见效最快。怎么样?这个分析是不是很厉害,我以前也做过两年前端web开发,自愧不如。所以,还没用AI编程的朋友,请立刻马上用起来,它不光可以帮我们解决实际问题,更能促进我们这些普通程序员快速成长。

这里是 AI研习手记,记录一个后端程序员学习 AI、拆解 AI 工程落地的过程。如果你也在从传统开发转向 AI 工程,欢迎一起交流。
夜雨聆风