远程 / 大型 Vault:文件树全量递归导致「解析不出来」¶
状态:已修复(eager 产品目录 + 非产品浅层 / 按需 list + 扩展忽略名)
影响面:打开 Vault 时的左侧文件树(本地与远程同一套建树逻辑;远程 SFTP 下体感最重)
相关代码:
src/lib/vault—loadVaultTree/listVaultDirChildren/shouldIgnoreTreeName/isEagerTreeRel/replaceTreeNodeChildrensrc/components/sidebar/file-tree.tsx— 展开时触发懒加载、pending spinnersrc/App.tsx—onLoadDirChildren合并子树test/vault-tree.test.ts— 忽略规则、eager 判定、pending 合并- 产品约定:
../frontend/shell.md§2.1;远程设计:../backend/remote.md§6
1. 问题现象¶
- 打开远程 Vault(SSH/SFTP)时,连接本身可能成功(catalog / 远程徽章正常),但左侧文件树长时间空白或一直 busy,像「解析不出来」。
- 同一目录若在服务器本机
ls/ 本地挂载打开,往往仍可用,用户易误判为「远程路径错误」或「SFTP 坏了」。 - 失败时
refreshTree可能 catch 后setTree([]),或卡在数万次串行remote_list上直至超时。
2. 根因¶
2.1 建树是「打开时全量递归」,不是文档里的懒加载(主因)¶
设计写过文件树应「懒加载 list」,实现却是:
loadVaultTree(root)
→ 对每个目录 remoteList / readDir
→ 子目录递归,直到 depth > 12
每个目录 = 一次 Host remote_list(SFTP readdir),且 SFTP 侧多有锁、串行。
远程延迟下:list 次数 × 单次 RTT 直接决定打开耗时。
2.2 真实 Vault 常「产品目录 + 巨量非产品树」混放¶
产品需要深扫的是:
| 根目录 | 用途 |
|---|---|
papers/ |
paper marker、组织夹、下载/精读图标 |
notes/ / plans/ |
笔记与计划 |
.agents/ |
bundled / 用户 skills |
但用户工作区根下还可能有完整代码仓、实验 run、依赖树等。旧实现会把这些一并递归进文件树。
在「论文 + 大型代码/实验树」混合的远程库上,按旧忽略规则模拟建树:
| 指标 | 量级(该复现库) |
|---|---|
| readdir / SFTP list | ~1.4×10⁴ 次 |
| 目录节点 | ~1.4×10⁴ |
| 文件节点 | ~4.5×10⁴ |
| 其中非产品树占比 | list 次数的绝大部分(约 97%+) |
产品侧 papers/ 一类通常只有数百次 list;真正拖垮打开的是深层非产品子树。
2.3 旧忽略集合过窄¶
旧规则大致只跳过:.git、.agentero、node_modules、target、dist 以及多数点开头名。
仍会扫入(示例类):
- 名称不以点开头的依赖/构建产物目录
- 嵌套在非产品树里的包与 vendored 代码
- 其它本应用从不需要展示的缓存/工具目录
点开头的 .venv 等虽会被「点目录」规则跳过,但非隐藏的巨型项目树仍全量进入递归。
2.4 远程 vs 本地体感差几个数量级¶
| 环境 | 同一套全量递归 |
|---|---|
服务器本机 scandir |
整树可在 ~0.1 s 级完成(盘很快) |
| 客户端经 SFTP 串行 list | 按 ~20–100 ms/次 估,整树约 数分钟~十余分钟 |
因此:不是路径无效,而是打开策略不可扩展。
3. 修复策略¶
3.1 两类目录¶
| 类型 | 根名(vault-relative 顶段) | 打开时 | 展开时 |
|---|---|---|---|
| Eager | papers、notes、plans、.agents |
全量递归(保留 paper marker / skill 语义) | — |
| Lazy | 其它根下目录(代码、论文草稿夹等) | 只 list 一层(文件 + 子目录壳) | 子目录 childrenPending,展开再 list 一层 |
3.2 忽略名永不 list¶
TREE_IGNORE_NAMES + 点目录策略(保留 .agents、.env.example)+ *.egg-info:
- VCS / Host 内部:
.git、.agentero、… - 依赖与缓存:
node_modules、.venv、venv、__pycache__、site-packages、各类.*_cache、… - 编辑器 / 工具:
.codex、.idea、.vscode、…
命中则不进入树、不发 list。
3.3 UI¶
FileNode.childrenPending:未 list 的目录仍显示为可展开文件夹。- 展开时
listVaultDirChildren→replaceTreeNodeChildren合并。 - 加载中可显示行内 spinner;失败 toast,可再次展开重试。
4. 修复前后数据(同一混合型远程 Vault)¶
方法:在服务器上用与前后端一致的忽略 / eager / 浅层规则模拟 walk(本地 scandir 计次数);远程耗时按 list 次数 × 实测单次 SFTP list 延迟 估算(同会话串行 list,约数十 ms/次量级)。
4.1 工作量¶
| 指标 | 修复前 | 修复后 | 变化 |
|---|---|---|---|
| readdir / SFTP list | ~13 600 | ~390 | 约 35× 更少 |
| 目录节点(进树) | ~13 600 | ~400 | 约 34× 更少 |
| 文件节点(进树) | ~44 700 | ~2 200 | 约 20× 更少 |
| 待展开 pending 目录 | 0 | 少量(非产品一层下的子目录壳) | 按需再 list |
4.2 服务器本机 walk(无网络)¶
| 修复前 | 修复后 | |
|---|---|---|
| 墙钟 | ~0.11 s | ~0.004 s |
| 加速 | — | 约 25–30× |
说明:盘上 list 不是瓶颈,对比的是调用次数与算法分支。
4.3 远程打开估算(串行 SFTP)¶
| 假设单次 list | 修复前 | 修复后 | 加速 |
|---|---|---|---|
| ~20 ms | ~4–5 min | ~8 s | ~35× |
| ~50–60 ms(本机 CLI 批测量级) | ~10–15 min | ~20–25 s | ~35× |
| ~100 ms | ~20+ min | ~40 s | ~35× |
定性结论:
- 修复前:远程大库打开文件树常落在数分钟以上,UI 等同失败。
- 修复后:同库打开落在数十秒内(主要成本仍是 eager 的
papers/等产品树);非产品根目录一层几乎立刻可见。 - 收益与 list 次数比一致:约 35×。
4.4 修复后 list 构成(典型)¶
| 部分 | 策略 | 占比(该复现) |
|---|---|---|
产品 papers/ 等 eager 树 |
全量 | 绝大多数(~90%+ list) |
| 其它根目录 | 每根 1 次浅层 | 极少 |
| 忽略名 | 0 | 0 |
因此:若仍慢,下一步优化应针对 papers/ 分层 / 批量 list,而不是再砍非产品树。
5. 验收建议¶
- 打开「产品目录 + 大型非产品子树」的远程 Vault:顶层与
papers/应在可接受时间内出现;非产品根可展开一层,不一次性扫尽深层。 - 忽略名(依赖、venv、
.git等)不出现在树中。 - 仅
papers/的精简 Vault:行为与修复前一致(仍 eager 全量)。 - 展开 lazy 目录失败时 toast,树不整页清空;再次展开可重试。
- 单元测试:
test/vault-tree.test.ts(忽略、eager、pending 合并)。
6. 后续(非本次)¶
papers/组织夹过深时仍可能数百次 list:可考虑 Host 批量list_tree、或 org 层懒加载。- 刷新树时若保留已展开 lazy 子树,可减少重复 list(当前刷新仍按
loadVaultTree重算 eager + 浅层)。 - 打开进度 toast(「正在加载远程文件树…」)改善体感。
7. 决议记录¶
| 议题 | 决议 |
|---|---|
| 是否全树懒加载 | 否:产品目录必须 eager(marker / skills) |
| 非产品目录 | 打开 list 一层;子目录 pending,展开再 list |
| 忽略策略 | 应用不需要的目录/缓存永不 list |
| 文档是否写具体主机/路径 | 否(可复现条件用结构描述,不写环境专有路径) |