Tolaria 基于 Git 的增量 Vault 缓存9000 笔记秒级启动的实现剖析【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria导读本文以 Tolaria 的架构决策记录 ADR-0014docs/adr/0014-git-based-vault-cache.md为骨架深入讲解其以 Git 作为变更检测机制的增量缓存方案缓存文件的组织方式、同 commit / 跨 commit 两套增量更新路径、原子写入与并发保护、缓存失效与迁移机制并结合 cache.rs 源码与测试用例逐层印证。读完你将掌握git HEAD git diff/status 增量扫描这一可复用的桌面端知识库加速范式。背景全量扫描为何不可接受Tolaria原代号 Laputa是一个桌面端 Markdown 知识库管理应用vault 中的笔记、Type 定义、视图.yml、附件以纯文本形式存放在 git 仓库内。ADR-0014 给出了明确的问题定量一个9000 个 Markdown 文件的 vault每次启动全量扫描需要数秒启动路径上的扫描不仅解析 frontmatter、正文摘要、wikilink 关系还要为每个文件补齐 Git 提交历史中的创建/修改时间mod.rs 中resolve_entry_dates的逻辑成本更高更麻烦的是vault 会被外部编辑器、git pull、Finder 等外部手段修改缓存必须能正确感知这些外部变化否则会出现界面还显示旧笔记的错误。因此设计目标不是简单的缓存 过期清空而是只重解析自上次扫描以来确实变化过的文件增量对已提交变更commit与未提交变更working tree都保持正确在缓存未命中、损坏、版本升级时可靠地回退到全量扫描缓存写入绝不能破坏 vault 本身外部存放 原子写。决策概览用 Git 当变更传感器ADR-0014 的决策原文可概括为使用 Git 作为变更检测机制。缓存把所有VaultEntry序列化为 JSON存放在~/.laputa/cache/vault-hash.json加载时比较缓存记录的 HEAD commit hash 与当前 HEADhash 相同只重解析未提交uncommitted的变化文件hash 不同用git diff找出两次 commit 之间变化的文件选择性重解析全量重扫仅在缓存未命中Missing或版本号升级version bump时发生。配套的 ADR-0024docs/adr/0024-cache-outside-vault.md进一步约束了缓存的位置早期版本的缓存.laputa-cache.json直接放在 vault 内部会污染 git status、可能被误提交、还会干扰 vault 扫描本身缓存文件自身也是 vault 中的一个文件。因此最终落地为外部缓存路径 旧缓存自动迁移删除。为什么在三个备选方案中选中 GitADR-0014 的方案对比表如下方案机制优点缺点A选定Git 增量缓存复用已有 git 基建变更检测精确同时覆盖已提交与未提交变更要求 vault 是 git 仓库缓存失效逻辑复杂B文件修改时间mtime无需 git 即可工作跨文件系统不可靠iCloud、Dropbox 同步会改 mtime存在时钟偏差C文件内容 hash永远正确必须读每个文件算 hash等于没缓存mtime 方案的致命伤在同步盘场景iCloud/Dropbox 同步会大面积改写 mtime导致缓存频繁整体失效内容 hash 方案则把读全部文件的成本从启动期挪到缓存校验期同样无法接受。Git 方案的关键洞察是git 已经在维护一份精确的、与文件系统解耦的变更账本应用只需付出几次 git 命令的代价即可获得哪些文件变了的答案。缓存文件组织外部存放 确定性命名目录与文件名源码 cache.rs 给出了完整的路径计算逻辑缓存目录默认~/.laputa/cache/可用环境变量LAPUTA_CACHE_DIR覆盖测试隔离依赖此变量文件名 vault_path_hash(vault)的 16 位十六进制 .json即vault-hash.jsonhash 由DefaultHasher对规范化后的 vault 绝对路径计算保证同一路径结果确定、不同路径结果不同对应测试test_vault_path_hash_is_deterministic与test_different_vaults_get_different_hashes并发写锁文件为同名的.lock后缀文件临时文件采用name.uuid.tmp格式保证多进程/多线程下临时文件名不冲突。缓存内容结构struct VaultCache { version: u32, // CACHE_VERSION当前为 14 vault_path: String, // 写入时的 vault 路径用于跨机器/移动目录失效检测 commit_hash: String, // 写入时的 git HEAD entries: VecVaultEntry, }其中VaultEntry是 vault 扫描的最小产物定义见 entry.rs序列化后的 JSON 字段携带笔记的完整索引信息path、title、isA、belongsTo、relatedTo、modifiedAt、createdAt、fileSize、snippet、relationships、Type 相关的icon/color/order/sidebarLabel、outgoingLinks、properties、wordCount、hasH1、fileKind等。也就是说缓存不是文件清单而是整个 vault 的可序列化索引快照前端打开 vault 时可以直接消费这些条目渲染列表、搜索与关系图。vault_path字段的校验是防呆设计从另一台机器 clone 或移动 vault 目录后缓存里的绝对路径已失效cache_requires_full_rescan会因此判定需要全量重扫。测试test_scan_vault_cached_invalidates_stale_vault_path模拟了篡改缓存路径模拟他机 clone的场景并验证了失效逻辑。为什么必须放在 vault 外不会出现在git status里不污染用户仓库ADR-0024 的核心动机缓存文件自身不会被 vault 扫描器当作内容文件重复解析避免扫描器扫描自己的循环问题旧版本遗留的.laputa-cache.json会被migrate_legacy_cache自动迁移到新位置并从 git 跟踪与磁盘中删除git rm --cached --ignore-unmatchfs::remove_file对应测试test_legacy_cache_migration断言迁移后新文件存在、旧文件消失。加载判定四种状态与两条增量路径缓存读取状态机load_cache把磁盘上的缓存文件归为四类状态cache.rsMissing文件不存在首次启动或已被删除LoadedJSON 解析成功附带字节级 fingerprintInvalid文件存在但 JSON 解析失败如磁盘写了一半、手动编辑坏文件Unreadable文件存在但无法读取权限错误等。scan_vault_cached的主流程cache.rs入口 scan_vault_cached(vault) ├─ 校验 vault 路径存在且为目录 ├─ migrate_legacy_cache(vault) # 首次运行迁移旧缓存 ├─ resolve_git_workspace(vault) │ └─ 失败非 git 仓库→ 直接全量 scan_vault不缓存 ├─ git_head_hash(workspace) # rev-parse HEAD │ └─ 失败 → 直接全量 scan_vault ├─ load_cache(vault) │ ├─ Missing → 走全量扫描分支 │ ├─ Invalid → 删掉坏缓存文件走全量扫描分支 │ ├─ Unreadable → 仅告警走全量扫描分支 │ └─ Loaded │ ├─ 版本或 vault_path 不匹配 → 全量重扫用旧指纹做 CAS 写入 │ ├─ commit_hash 当前 HEAD → update_same_commit未提交增量 │ └─ commit_hash ! 当前 HEAD → update_different_commitgit diff 增量 └─ 无缓存 → scan_and_cache_full全量扫描并写缓存路径一同 commit 增量update_same_commit当缓存的commit_hash与当前 HEAD 一致时说明提交历史没有前进变化只可能发生在 working tree外部编辑器保存、Finder 新增/删除、未 commit 的修改。此时用git status --porcelain收集修改/暂存文件再用git ls-files --others --exclude-standard收集未跟踪文件注释明确解释了为何补这一步status --porcelain对新增目录只显示?? dir/会隐藏目录内的具体文件ls-files才能枚举出每个新文件从缓存 entries 中剔除这些文件的旧条目只对变化文件调用parse_md_file/parse_non_md_file重新解析无论如何都执行prune_stale_entries——即使 git 报告无变化也会把磁盘上已不存在的文件从缓存中清掉覆盖 Finder 删除等 git 感知不到的变更这正是注释里强调的always prunes stale entries even when git reports no changes若无变化且无剪枝直接返回缓存不重写缓存文件配合 ADR-0166 的冷热启动优化。关键细节git status --porcelain的行格式是XY pathparse_porcelain_line取前两个字符做状态码、其余为路径路径还要经过workspace.vault_relative_path与normalize_relative_path处理屏蔽大小写、别名如 macOS/private/tmp与/tmp差异并且过滤隐藏路径段。路径二跨 commit 增量update_different_commit当缓存记录的 hash 落后于当前 HEAD用户git pull、git commit或切换到别的分支时执行git diff from_hash..to_hash --name-only -- vault_pathspec得到两次 commit 间变化的所有文件再叠加git_uncommitted_files未提交的修改/暂存/未跟踪文件保证增量集合同时覆盖已提交与未提交变更剔除缓存中这些文件的旧条目重新解析用当前 HEAD hash写回缓存。注意这里 diff 收集的是 vault 内所有非隐藏文件而不只是 .md注释Includes all non-hidden files (not just .md) so the cache picks up view files (.yml), binary assets, etc.——因为 vault 扫描器本身会识别视图文件views/*.yml与各类资源缓存必须与扫描器的文件面保持一致。测试test_incremental_different_commit_picks_up_yml_file验证了提交一个.yml视图文件后增量更新能把它纳入 entries。何时走全量重扫缓存 Missing首次启动、缓存被删缓存 InvalidJSON 损坏删除后重扫cache_requires_full_rescanversion不等于当前CACHE_VERSION或vault_path与当前路径不一致他机 clone / 目录移动reload_vault/refresh_vault_cache显式触发的强制刷新。CACHE_VERSION当前为 14注释记录了 bump 历史v12: fix gray_matter YAML sanitization… v14: preserve scalar-array custom frontmatter properties in VaultEntry。版本号的每次提升意味着旧的索引结构与新解析器不再兼容必须强制全量重扫以免陈旧字段污染界面。测试test_stale_cache_version_forces_rescan_of_archived_yes构造了一个旧版本解析器把Archived: Yes误判为 false的陈旧缓存验证版本失效后重扫能纠正结果。原子写入与并发安全临时文件 原子 renamewrite_cachecache.rs遵循经典的原子替换范式1. 尝试获取写锁.lock 文件create_new 语义 2. 读当前缓存指纹与加载时的期望指纹比对CAS 前提 3. 序列化 VaultCache 为 JSON 4. 写入 uuid.tmp 临时文件并 sync_all 刷盘 5. fs::rename 覆盖正式缓存文件 6. unix 下额外 sync 父目录保证目录项落盘崩溃安全任何一步失败都不会留下半个缓存文件临时文件会被清理测试test_cache_write_no_tmp_file_left断言写完后缓存目录中不存在.tmp文件顺序一致rename 在同文件系统内是原子的读者永远看到旧完整版或新完整版指纹 CAS写入前把磁盘上的指纹与加载时读到的指纹比对若已被其他扫描更新则跳过避免旧数据覆盖新数据SkippedConcurrentUpdate。写锁与陈旧锁回收写锁文件以create_new(true)方式创建并写入 PID已存在则视为有其他活跃写入者SkippedActiveWriter锁文件超过CACHE_WRITE_LOCK_STALE_SECS 30秒未更新即判定为陈旧锁予以删除后重试——防止进程崩溃留下死锁测试test_write_cache_skips_when_writer_lock_is_held验证锁存在时写入被跳过且不产生缓存文件test_write_cache_skips_overwriting_newer_cache验证指纹不匹配时返回SkippedConcurrentUpdate且保留较新内容。这套锁 指纹 CAS 原子 rename的组合让增量更新天然支持多窗口/多进程并发的场景Tolaria 支持独立笔记窗口与多个 mounted workspace。失效、迁移与崩溃安全强制失效reload_vaultADR-0014 明确约定reload_vault命令在重扫前删除缓存文件保证显式刷新一定读到磁盘真相。当前实现更精细——命令层的 scan_cmds.rs 调用的是vault::refresh_vault_cache它采用保留旧快照、后台重建、原子替换的路径先把现有缓存文件的字节指纹作为期望指纹即使旧缓存是坏的也能事务性替换注释解释了为何不能用解析再比较——那会把损坏文件误判成并发写入全量重扫后一次性替换期间read_vault_snapshot仍能读到旧快照应用崩溃也不会让下次启动失去可用缓存。同时invalidate_cache仍被保留为独立的公开 API测试test_invalidate_cache_deletes_cache_file与test_invalidate_then_scan_forces_full_rescan验证其删除文件与强制重扫的行为供需要在启动路径上强制清缓存的场景使用。旧缓存自动迁移migrate_legacy_cache在每次scan_vault_cached/refresh_vault_cache入口执行若 vault 内存在.laputa-cache.json而外部缓存不存在将其复制到新位置临时文件 renamegit rm --cached --ignore-unmatch .laputa-cache.json将其移出 git 跟踪从磁盘删除旧文件。这保证升级到外部缓存方案的用户无需手动清理旧缓存数据也不会丢失。剪枝与去重prune_stale_entries每次写缓存前执行剔除path指向的文件在磁盘上已不存在的条目覆盖外部删除按大小写折叠case-folded的相对路径去重——这是为 macOS APFS 等大小写不敏感文件系统上的仅大小写重命名准备的测试test_case_rename_no_duplicates模拟删除Note.md、新建note.md后断言缓存无重复条目。随后 entries 按modified_at降序排序再序列化保证缓存内部顺序稳定。冷启动热启动与快照优先演进ADR-0014 之后的 ADR-0166docs/adr/0166-snapshot-first-progressive-vault-startup.md把这一缓存机制升级为stale-while-revalidate 的快照优先启动管线read_vault_snapshot只做版本/路径校验后直接返回缓存 entries不跑任何 git 命令、不做逐文件磁盘检查、不排序、不写缓存——它读取的是一个结构合法但可能过期的快照React 立即用快照渲染、清除阻塞式 loadinglist_vault随后在后台做 git 对账与删除清理只替换该 workspace 的 entries同 commit 且无变化的对账返回缓存而不重写相同内容对应 cache.rs 中update_same_commit的早退分支嵌套 mounted workspace 复用最近祖先已供应的 entries重复路径优先最具体的包含 workspace启动里程碑被埋点测量热启动活跃 vault 目标 800ms、React shell 目标 300ms仅上报时间/计数不上报 vault 路径与笔记内容。也就是说ADR-0014 奠定了以 git 做增量对账的正确性基础ADR-0166 则把它与 UI 渲染解耦把对账从阻塞路径挪到后台让大规模 vault 的启动体验从数秒等待进一步收敛到快照秒开 后台静默刷新。关键实现细节与边界处理从源码与测试中可以整理出若干容易踩坑的实现要点这些也是评审缓存方案时的通用检查清单非 git vault 优雅降级resolve_git_workspace或git rev-parse HEAD失败时直接退回全量scan_vault测试test_scan_vault_cached_no_git验证无 git 目录也能扫出 1 条 entry。注意此时不写缓存——没有 git 就没有可靠的增量基准写缓存反而可能误导后续加载。这与 ADR-0085 的non-git vault support决策衔接嵌套仓库路径范围vault 可以是父 git 仓库的子目录vault_pathspec保证 diff/status 只关心 vault 子树内的文件父仓库其他目录的变化不会触发本 vault 重解析测试test_nested_vault_incremental_changes_exclude_parent_files非 ASCII 路径git 的core.quotePath会转义中文等非 ASCII 路径porcelain 解析与相对路径还原必须正确处理测试test_git_uncommitted_files_preserves_chinese_markdown_path未跟踪子目录ls-files --others与去重逻辑确保新建子目录中的文件也能进入增量集合测试test_update_same_commit_new_files_in_new_subdirectory并发写入的最终一致性指纹 CAS 写锁让谁先完成谁生效日志区分SkippedConcurrentUpdate与SkippedActiveWriter便于排障日期来源的合并Git 日期get_all_file_dates_for_workspace与文件系统 mtime 通过resolve_entry_dates合并fs.max(git)取较大者并配合测试中的PANIC_ON_GIT_DATE_LOOKUP守卫——热缓存命中路径严禁再次加载全量 git 日期历史保证启动路径的 I/O 预算可控。总结可复用的增量索引范式ADR-0014 本质上回答了一个通用问题如何为一个纯文本文件集合维护一份始终新鲜的二级索引而不用每次全量重建Tolaria 的答案是三层组合变化感知以 git 为单一事实来源HEAD hash 判断提交是否前进git diff/git status --porcelain/git ls-files给出精确的变更文件集合索引存储外部目录 确定性 hash 文件名 原子 rename 写锁/指纹 CAS保证缓存既不污染数据仓库也不会在并发或崩溃场景下损坏失效纪律版本号 bump 驱动全量重扫、vault_path 校验防跨机污染、剪枝清删除、显式 reload 强制刷新把缓存正确性从概率问题变成确定性保证。这套设计在 Tolaria 仓库中可完整追溯决策依据见 docs/adr/0014-git-based-vault-cache.md 与 docs/adr/0024-cache-outside-vault.md实现主体在 src-tauri/src/vault/cache.rs入口接线在 src-tauri/src/vault/mod.rs 与 src-tauri/src/commands/vault/scan_cmds.rs后续演进见 docs/adr/0166-snapshot-first-progressive-vault-startup.md。若你的项目同样以 git 管理文本型内容笔记、文档库、配置集这套git 账本 外部原子缓存的增量索引方案值得直接借鉴。【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考