主题
本页托底简历 bullet 1 的埋点:「封装 ripgrep 实现代码检索,10 万行级仓库单次检索 P95 < 200ms」。面试官对简历上的每个数字都有追问权,这页就是把 200ms 讲成一套方法论。
测量口径(怎么测的)
- 测什么:
executeTool("grep_code", ...)端到端耗时——含 rg 进程 spawn、扫描、结果格式化与截断,不只是 rg 本身的扫描时间。 - 怎么测:
System.nanoTime()包住调用,先预热 15 次(触发 JIT、文件系统缓存),再连续采样 100 次(5 组代表性 pattern 轮换),取分位数。 - 实测结果(2026-09-14,arm64 / JDK 17 / ripgrep 15.1.0):
| 路径 | P50 | P90 | P95 | P99 | Max |
|---|---|---|---|---|---|
| ripgrep 主路径 | 12.4ms | 18.9ms | 22.8ms | 26.5ms | 26.5ms |
| Java 兜底路径 | 18.1ms | 53.3ms | 64.9ms | 201.4ms | 201.4ms |
- 解读:主路径 P95 22.8ms,对简历的 200ms 有近 10 倍安全余量;兜底路径 P95 慢约 3 倍、P99 长尾差近 8 倍(201ms vs 26.5ms)——这正是"rg 做主路径、Java 只做兜底"的数据支撑。长尾来自大文件和深目录。
- 10 万行级仓库哪来的:本项目自身 main 源码约 3.3 万行,复制 4 份组成约 13 万行 的基准仓库(约 1000 个 Java 文件),文件分布(单文件行数、目录深度、包结构)与真实项目一致,不是一万行一个文件的极端构造。
- 口径一致性:简历写的是"单次检索",指一次 grep_code 调用的端到端延迟,不是批量扫描全仓库的时间。
rg 为什么快(原理层)
- Rust regex 引擎:NFA 状态机模拟,不存在 Java
Pattern的回溯爆炸;正则先做字面量前缀预过滤(literal extraction),用 memchr/SIMD 在字节层先跳过不可能的区间,绝大多数输入根本进不了状态机。 - 并行目录遍历:多线程分块扫描文件,文件级并行,吃满多核。
- I/O 策略:mmap/大块读取,少拷贝;按字节处理不按字符,省掉解码。
- 智能跳过:感知 .gitignore/.ignore 语义(跳过 target/、node_modules/),检测二进制文件直接跳过——这两条纯自研很容易漏。
Java 兜底(降级路径)
RipgrepCodeSearchEngine用ProcessBuilder调 rg 二进制;兜底触发条件:rg 未安装、进程启动失败、执行超时。兜底实现是JavaCodeSearchEngine:纯 Java 遍历目录 + 正则逐文件匹配,语义一致但更慢——实测 P95 64.9ms(主路径 3 倍)、P99 201ms(主路径近 8 倍),长尾差距最明显。- 兜底价值:保证工具在裸环境(无 rg 的 CI/容器)不失效——和 CommandGuard"辅助防线"同一种设计哲学:快路径失效要有正确性等价的慢路径。
- rg 找不到时的探测在启动阶段完成一次,不在热路径上反复探测。
为什么封装 rg 而不是自研
性能白拿(上述四条自研成本极高)、.gitignore 语义天然正确、维护成本低。自研只在一种情况下必要:目标环境没有 rg——所以保留 Java 兜底而不是强依赖。
分层检索分工(和 RAG 的关系)
grep_code(rg):精确字面/正则定位,即时性,结果就是行号,零索引零一致性维护。search_code(RAG):自然语言找"大概在哪",语义匹配,依赖离线索引。glob_files:按文件名找文件。- 三者是"精确→语义→文件名"的互补,不是替代:Agent 先 grep 后 read 为主路径,search_code 只在不知道关键词时用。
追问 Q&A
Q1:每次调用都 spawn 一个 rg 进程,进程启动不贵吗? A:Unix 下 fork+exec 是毫秒级,相比扫描本身(10 万行 10ms 上下)可接受;且每次调用参数不同(pattern/glob 都变),长驻进程反而要做协议解析和生命周期管理。真要优化,rg 自带 --json 模式可以长驻批处理,这是明确的演进方向而不是当前瓶颈。
Q2:为什么不用纯 Java 流式扫? A:三点:Java Pattern 回溯灾难(恶意/复杂正则会卡死)、单线程遍历吃不满核、没有 .gitignore 感知会把 target/ 全扫一遍。自研等于把 rg 十年打磨重做一遍,还做不对。
Q3:为什么不给 grep 建索引(像 Sourcegraph 那样)? A:索引换来的是首次查询更快,代价是索引构建/增量更新/一致性问题——代码刚改完索引还没更新,grep 出旧结果比慢更致命。grep 的价值就是零状态、即时、永远新鲜。需要语义搜索的场景才值得上索引(search_code 那条线)。
Q4:200ms 都花在哪了? A:大致三段:进程 spawn(毫秒级)、rg 扫描(与匹配模式和相关文件量相关)、结果格式化与截断(防刷屏的 head_limit/max_chars 处理,微秒级)。瓶颈在前两段,且都被 200ms 覆盖。
Q5:为什么报 P95 不报平均? A:平均会被大量快查询拉低,掩盖长尾;工具调用体验取决于"最差那一次"——一次 2 秒的等待比一百次 50ms 更影响体感。P95 是对"上限体验"的承诺。