fsearch 索引越用越大内存与磁盘占用排查瘦身实录【免费下载链接】fsearchWhole-disk file search for macOS: fuzzy names, typo tolerance, indexed content grep. ~1 ms over 8M files.项目地址: https://gitcode.com/gh_mirrors/fsea/fsearch毫秒级全盘搜索的代价是必须先付一笔索引账全盘的目录与文件名被预先读进来、压缩成二进制索引再常驻内存等待每一次查询。社区里被反复安利的 FSearch 是 Linux 上的 GTK 工具而本文分析的这份仓库则是 macOS 上同名不同项目的 Rust 实现README 明确写着Whole-disk file search for macOS——它用getattrlistbulk一次扫描 770 万个文件按名字搜索中位数只要 1.3 毫秒。但快的背后磁盘上的index.bin是数百 MB 级的大文件daemon 内存常年在 30~135 MB 之间波动某些操作后内存足迹还会瞬间暴涨到 1 GB。索引到底存了什么、内存被谁吃了、为什么会越用越大、能怎么瘦身——这篇实录完全基于仓库源码逐条拆解不靠猜测。一、先看现象770 万个文件的资源账本README.md 给出了官方实测基线M4 Max磁盘上 770 万个文件与目录指标数值全盘按名字找文件p50 1.3 ms文件内容搜索p50 9 ms新建/改名/删除的文件可见~0.1 s首次全盘爬取~20 s一次性daemon 内存30–135 MB这份数据已经说明问题搜索本身只花毫秒真正的成本沉淀在两个地方——磁盘上的索引文件以及daemon 进程里常驻的内存。它们都随磁盘上的条目数增长。README 里还对比了 Chromium 源码树50.9 万文件下的表现名字搜索 1.1 ms、内容搜索 5.6 ms、启动即用 50 ms、内存 50 MB。文件越多索引越大这是逃不掉的线性关系。二、账本拆解索引文件里到底存了什么一个 mmap 的扁平大文件名称索引不是数据库不是一堆散文件而是 src/index.rs 里描述的一个单一扁平 blob所有磁盘条目按目录块DFS 顺序排进一个文件16 个 section 依次排列靠 4 KiB 文件头里的 magicFSIDX007和一组偏移量定位。加载就是Mmap::map整个文件零解析。排布上有个精妙的 trick目录的 children 在文件里连续且有序方便路径查找整棵子树是单一区间dir_start..dir_end——所以in:这类目录范围过滤是区间裁剪range bound而不是逐条过滤。名字驻留interning是压缩的第一招同样在 src/index.rs 的文档注释里写得明白7.5M 个条目共享约 2M 个不同的名字所以每个名字只存一次附带它的字符掩码char mask和一个首字母/单词首哈希查询给唯一名字打分而不是给条目打分。这两招让内存占用大幅收敛每个名字一份字符掩码 8 字节 名字偏移 4 字节 名字字节本身平均十几字节每个条目一份名字 id、类型、父目录、大小、mtime外加按名字分组的倒排入口合计约 25 字节/条。按section_lens的公式粗算7.7M 条目的固定开销约 160~170 MB2M 个唯一名字约 55~60 MB目录表约 21 字节/目录再占约 10 MB加上 64 字节对齐和头部 paddingindex.bin的规模就是250~300 MB 量级——这与 src/engine.rs 注释里一次 compact 写入约 280 MB的数字互相印证。内容索引是第二份账除了名字索引还有一份 trigram 内容索引位于~/Library/Application Support/FSearch/content/由不可变、可 mmap 的 segment 文件seg-XXXXXX.fsc组成。每个 trigram 的倒排列表用 delta varint列表稀疏时或 bitset超过 1/8 文档都含该 trigram 时编码单个 segment 构建时的文件数据上限被限制在 64 MiBSEG_BYTES合并时的临时内存也封顶在 96 MiBMERGE_CAP见 src/content.rs。所以磁盘占用其实有两本账名字索引数百 MB 内容索引segments 按需增长。排查时只看index.bin是看不全的。三、定位内存到底被谁吃了daemon 常驻 30~135 MB这笔账在源码里能拆成四份1. 基座索引mmap 主动预取index.bin整体 mmap 进地址空间但页面是按需换入的。src/live.rs 的Live::new会调用base.prefault()把查询高频扫描的六个 section名字掩码、名字偏移、名字字节、条目名字 id、类型、父目录一次性触页常驻其余大小、mtime、目录表按需换页。这是内存的大头且不可省——搜索的毫秒级响应就靠它。2. overlay事件先落内存攒够再落盘增量更新不走改文件而是维护一个内存 overlayBTreeMap路径, 条目 一个 dead bitsetFSEvents 推送的变化先进内存搜索时把 base 和 overlay 合并。而落盘compact是延后的overlaydead 超过 50,000 条或距上次保存超过 12 小时才重写一次index.binsrc/engine.rs 的COMPACT_PENDING/COMPACT_EVERY常量。也就是说文件系统越活跃内存里的 overlay 越大直到触发 compact 才回落到低水位。3. 查询缓存宽泛查询会瞬时吃几十 MBsrc/query.rs 明确标注了三个池子最近一次查询的名称打分表NameCache在继续输入时复用dense 名称表池每张约 24 MB源码注释原话目录 memo 池约 13 MB。一次宽泛查询比如输入单个字母会让这些大表全部被触页搜索停止后要等60 秒空闲trim_if_idle(60s)才会被回收。所以查询完内存没降下来多半是这里——它是正常的延迟回收设计不是泄漏。4. 大块分配的经典陷阱malloc 的 dirty 内存这是源码注释里最值得读的一段src/main.rsmacOS 的 malloc 会把释放的大块内存保持映射且脏导致 daemon 在一次索引构建后足迹高达~1 GB而实际存活数据只有 ~2 MB。项目的解法是一个自定义全局分配器Alloc超过 1 MiB 的分配直接走mmap/munmap用完即还构建和内容批处理结束后再调用malloc_zone_pressure_relief把分配器手头的内存交还给 OSrelease_memory。此外full_build完成后特意重新从文件 re-map 索引让它是可被内核逐出的 page cache而不是匿名内存。顺带一提正文里其实没有独立的正则缓存——grep 用的正则每次构建真正的临时大户是上面这套名称表与目录 memo。这提醒我们排查时要对着源码找证据而不是凭印象猜。四、为什么会越用越大四类膨胀源把机制梳理完越用越大可以归结为四个真实来源条目增长直接放大索引磁盘文件变多 → overlay 越攒越多 → compact 时index.bin重写为更大的文件。活跃磁盘上 12 小时的窗口会被 50k 阈值提前打断实测注释说繁忙磁盘大约每小时一次、每次约 280 MB 写入。内容索引的纳入范围trigram 索引只收文本文件TEXT_EXTS白名单且单文件上限 1 MiBMAX_FILE但如果你在$HOME下囤了海量.log、.jsonl、.lock它们都会被索引增量 segment 靠8 段合一的 tiered merge 控制段数段数本身不会失控但总字节数随文件增长。FSEvents 丢事件后的兜底重扫内核丢事件或历史窗口过期时relist_changed会对自上次同步以来 mtime 变过的所有目录重新列目录src/engine.rs这是一次代价不小的突发资源峰索引本身不变大但瞬时内存和 IO 会顶起来。索引范围失控有 Full Disk Access 时默认索引全盘/firmlink 下的/Users等算一次macOS 的 Desktop/Documents/Downloads 等 consent-gated 文件夹也在内。范围越大账越厚。五、瘦身操作清单以下每一条都对应源码里真实存在的机制不涉及任何魔改① 先体检fsearch statusstatus是内置命令返回的 JSON 字段直接对应 src/engine.rs 的Status结构entries条目数、dirs目录数、overlay内存中待落盘条目、removed待清理的墓碑数、index_bytes索引文件字节、content_docs/content_segments/content_bytes内容索引三件套、content_pending后台积压、full_disk_access。体检先看这几个数overlay removed是否逼近 5 万阈值、content_pending是否长期不为 0。② 缩小索引范围让部分目录不进索引不给 Full Disk Accessdaemon 会跳过Desktop、Documents、Downloads、Library/Mobile Documents、Library/Containers、Library/Group Containers、Library/CloudStorage、Pictures/Photos Library.photoslibrary、/Volumes等 consent-gated 目录src/engine.rs 的gated()索引体积和首次构建时间都会明显下降。FSEARCH_RESTRICT环境变量即使有 Full Disk Access设置它也会强制走跳过逻辑——适合索引只要工作区、不要个人文件的场景。③ 管住内容索引的胃口内容索引的排除名单是编译期内置的src/content.rs 的SKIP_DIRS覆盖node_modules、.git、target、Library、DerivedData、build、vendor等五十多个目录名SKIP_UNDER_HOME覆盖go/pkg、.cursor/extensions、.local/share等SKIP_SUFFIXES覆盖.app、.framework、.photoslibrary等 bundle。用户侧能做的别在$HOME里堆会被当作文本索引进来的巨型.log/.jsonl文件超 1 MiB 不进索引但堆积在 1 MiB 以下同样摊厚 segments需要内容检索就尽量让它们落在排除名单内如build/、cache/否则就是持续为内容索引续费。④ 手动触发 compact / 重建通过 stdio/API 发{op:save}daemon 会立即调度一次 compactsrc/server.rs 的save分支 →engine.save()把 overlay 摊平回index.bin内存水位随即回落——适合刚倒腾完海量文件、短期内不再动盘的场景。索引明显臃肿比如曾经索引过整棵被删除的巨型目录树时删除index.bin与content/目录后重启 daemonIndex::load读不到旧档会触发一次全量重建约 20 秒生成一份干净、紧凑的新索引。fsearch uninstall只移除登录项、明确保留索引src/main.rs不会误伤数据。注意索引写入是原子替换tmp rename中断不会损坏旧索引重建失败也不会丢失原有数据。⑤ 防 iCloud 占位符膨胀启动时setiopolicy_np关闭了 dataless 文件的物化策略src/main.rs索引和读取绝不会触发 iCloud 占位符下载。这保证了索引动作本身不会反向撑大磁盘——排查时如果发现~/Library/CloudStorage下的文件被无缘无故下载先怀疑其他工具不是它。六、长期维护给索引一份健康检查清单把上面所有机制收拢成可执行的周期检查每周一次fsearch status记录entries、index_bytes、content_bytes三组数。正常形态是三者同步缓慢增长如果entries平稳而index_bytes陡增优先怀疑文件名碎片化导致 interning 命中率下降本该被共享的名字没有被共享。关注overlay removed持续高位说明磁盘改动频繁compact 会按 12h/5 万条阈值兜底若想主动控内存在批量操作结束后发一次{op:save}。content_pending长期非零说明内容索引在追落后进度。单文件改动 2 秒后CONTENT_QUIET、持续写入的文件最多等 5 分钟CONTENT_MAX_WAIT才会被重索引——这是防抖设计不是卡死真正的积压通常来自一次性搬入大量新文本文件。宽查询后内存不回落先等 60 秒名称表与目录 memo 的回收周期仍不回落再用 status 对比overlay而不是急着重启——把延迟回收和真泄漏区分开是维护索引最重要的一条认知。index.bin增速对标文件数增速macOS 侧全盘条目量级不变、但索引却明显变大时检查是否新挂载了卷/Volumes默认进索引或$HOME下出现了新的巨型文本目录。结语这份索引的资源账本并不神秘mmap 的扁平名字索引 延迟落盘的 overlay 按需构建的内容 segments 一批查询期缓冲池构成了毫秒级搜索的全部代价。源码里处处是刻意的资源控制——Alloc分配器对抗 macOS malloc 的 dirty 内存、prefault只预热热区、60 秒空闲回收缓存、50k 阈值触发 compact。理解这些机制之后瘦身就不是删文件重启的玄学而是一份可以定期对照执行、每一步都有源码依据的检查清单范围、时机、缓存、重建四件事各归其位。最后留一句自查口诀索引变大先看范围内存不降先等一分钟后台积压再看内容索引的账。【免费下载链接】fsearchWhole-disk file search for macOS: fuzzy names, typo tolerance, indexed content grep. ~1 ms over 8M files.项目地址: https://gitcode.com/gh_mirrors/fsea/fsearch创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?