首页
/ CodeGraph Rust 抽取内核迁移:从 napi-rs 架构、字节级等价门禁到 20 语言 DEFAULT-ON 的完整工程记录

CodeGraph Rust 抽取内核迁移:从 napi-rs 架构、字节级等价门禁到 20 语言 DEFAULT-ON 的完整工程记录

2026-09-06 15:56:49作者:袁立春Spencer

本文基于 CodeGraph 仓库中的设计文档 rust-kernel-migration-plan.md 展开,系统讲解 CodeGraph 如何把"解析 + 抽取"这一索引热路径从 JS/WebAssembly 树形遍历迁移到 napi-rs 原生 Rust 内核:包括 napi-rs 缓冲区契约、按语言 walker 的"bug-for-bug"移植方法论、字节级等价的门禁体系、per-file wasm 降级策略,以及 Linux 内核级(200 万节点)性能弧中 WAL 治理、解析池 sizing、cFnPtr 原生化等一系列被真实测量驱动的设计修正。读完本文,你可以理解一个"可选原生加速层"如何在保证输出字节不变的前提下安全落地,并能复现其构建、路由开关与等价验证的完整工作流。

1. 项目动机:数据驱动的性能差距

迁移计划开篇给出的核心判断是:CodeGraph 与对标方案 codebase-memory-mcp(cbm)在全量索引上的剩余差距集中在 parse+extract 阶段,其成本下限是"每个节点一次 JS↔WASM 的 marshaling"。计划文档(2026-07-16,M3 Pro 实测)给出的证据是:

测量项(dubbo,4,402 个 Java 文件) 结果
7 个 wasm worker 流水线的 parse-loop 4,700ms
同一批文件用 Rust tree-sitter 解析+遍历(rayon spike) 202ms
同一批文件、单 Rust 线程 1,067ms
dubbo 全量 init:CodeGraph / cbm 11.1s / 7.1s(1.55×)
Linux 内核,同为 2 CPU/6GB 容器 CodeGraph 27min 完成;cbm 两次都死在 0.16%

原始计划预期的终点状态是:dubbo 类仓库 parse-loop 从 4.7s 降到 1.0–1.5s、总时长约 7.5s,与 cbm 在其最强面上持平,同时保住 CodeGraph 已有的全部优势(增量 sync 2.4–2.8×、Agent A/B 决定性、调用图密度 1.3–2.3×、字节级确定性、受限硬件包络)。

值得注意的是,计划文档自己明确标注了被测量推翻的预期:多核机器上 dubbo 的 parse-loop 瓶颈后来被证明是"单写者 SQLite 落库"(占 dubbo parse-loop 的 94%),而不是抽取本身——8 个 wasm worker 早已把抽取 CPU 藏在主线程之后。因此内核的真实收益集中在"worker CPU 受限"的场景:2 CPU/6GB 的 CI 包络(excalidraw ~1.5×、dubbo ~1.25×、django 1.32×、prometheus 1.46×)与 vscode 级 Mac 场景(1.28×)。这一点在本文后文的"直接落库"小节中会看到完整的测量链条。

2. 内核的边界:什么被移植,什么永远不碰

迁移计划 §2 定义了让整个项目"安全"的那条边界。内核就是一个 napi-rs crate(codegraph-kernel/),链接 tree-sitter C 库与原生 grammar,只替换 parse worker 内部的 parse+extract 遍历,输入是每文件一个 (filePath, content, language),输出是扁平类型化缓冲区(nodes / edges / unresolved refs),每个文件只跨一次 JS 边界。它运行在既有的 ExtractionResult 契约之后。

永远不移植的部分(从第一天起原样工作):name-matcher 与 import-resolver、全部框架 resolver(src/resolution/frameworks/)、全部 36 个 synthesis 通道、MCP/explore、sync/watcher、installer。它们消费的是图和原始源码,而不是 AST 本身。

共存是永久性的:一种语言只有在等价门禁通过后才会路由到内核;其余语言可以永久留在 wasm 路径。没有"切换大日",按语言回滚 = 从 DEFAULT_ROUTED 里摘掉该语言。

分发策略:预编译的 .node 随现有发布管线按平台分发;同一 crate 编译成 wasm 是通用兜底。"安装零原生构建"这一性质保持不变。

从源码看,这个边界在 codegraph-kernel/src/lib.rs 的模块注释中被完整复述:

"Replaces ONLY the parse+extract walk inside the parse workers, behind the existing ExtractionResult contract…… Calls are synchronous by design: the existing ParseWorkerPool workers already parallelize per-file, so each worker thread drives its own kernel call (do NOT rebuild the pool on the Rust side)."

即:不要在 Rust 侧重建线程池——TS 侧的 ParseWorkerPool 已按文件并行,Rust 侧只做同步调用。这是计划 Phase 0 第 1 条的原文要求。

3. Phase 0 脚手架与缓冲区契约 v1

计划文档 Phase 0 列了 5 步:crate 脚手架、缓冲区契约、通用 emitter、构建集成(预编译 + kill switch + 缺失即回退 wasm)、CI 断言原生 grammar 与 wasm grammar 来自同一 grammar 源码修订。实际交付记录(§3a)有几处对原计划的重要修正:

Crate 与构建。 codegraph-kernel/ 是 napi 3 + tree-sitter 0.25 的 crate,无 CLI 依赖。构建脚本 scripts/build-kernel.sh 执行 cargo build 并把产物暂存到 codegraph-kernel/prebuilds/<platform>-<arch>/codegraph-kernel.node,npm 侧对应 npm run build:kernel(见 package.json)。Crate 导出三个入口:extractFilecontractInfogrammarInfo——在 lib.rs 中可见:contract_info() 返回 ABI 版本、内核版本、node-kind/edge-kind 表与支持语言列表;grammar_info(language) 返回该语言 grammar 的 ABI、node-kind 数与 field 表,专供"grammar 源码等价"门禁比对。

缓冲区契约 v1(五个 Buffer)。 布局定义在 codegraph-kernel/src/buffers.rs,TS 侧镜像是 src/extraction/kernel/layout.ts,两者必须字节级一致,任何布局变更都要 bump KERNEL_ABI_VERSION(当前为 2):

  • meta(36 字节)u8 ABI 版本 + 3 字节 padding + node/edge/ref 计数、arena 长度、errors-JSON 在 arena 中的偏移与长度、内核侧耗时(f64,仅供内省)。
  • node 行(96 字节):NodeKind 索引、可见性、三态 bool 标志位(isExported/isAsync/isStatic/isAbstract,每对 bit 是 present+value)、1-based 起止行/0-based 起止列、name、qualifiedName、id(Rust 侧用 sha256 计算,与 TS 的 generateNodeId 字节一致——有测试钉死)、docstring、signature、decorators、typeParameters、returnType、extraJson 逃生槽,以及为 Arc 3.2 预留的 per-node 指标槽。
  • edge 行(44 字节):source/target 行索引(NONE=0xFFFFFFFF 时改用 IdStr)、EdgeKind 索引、provenance、行/列、metadataJson。
  • ref 行(40 字节):fromNode 行索引、ReferenceKind(含 200 = function_ref)、flags(bit 0 是 ruby/php 移植时引入的 REF_FLAG_FILE_PATH——mixin/trait implements 引用携带抽取文件路径的唯一反规范化字段)、行/列、referenceName、candidates、fromNodeIdStr。
  • arena:所有字符串以 (offset,len) 指向 UTF-8 arena。

关键的 wire contract 事实:src/types.tsNODE_KINDS/EDGE_KINDS 数组的顺序就是线上契约(EDGE_KINDS 为此改成了运行时数组)。

路由与 loader。 路由逻辑在 extractFromSource(tree-sitter.ts)内:先 tryKernelExtract,wasm 的 TreeSitterExtractor 作兜底,且任何内核错误都按文件回退。loader(src/extraction/kernel/loader.ts)的查找顺序是 CODEGRAPH_KERNEL_PATH<pkgroot>/kernel/(发布包布局)→ <pkgroot>/codegraph-kernel/prebuilds/<plat>-<arch>/(源码运行)。loader 在路由前会校验 ABI 与 kind 表:陈旧的 .node 会静默降级到 wasm,CODEGRAPH_KERNEL_DEBUG=1 可看到原因。逃生槽落地为对已解码结果post(result, source)(而不是原始 buffer)——TS 逻辑要的是解码后的对象;见 src/extraction/kernel/index.ts 中的 POST_PASSES(目前为空表)。

grammar 等价门禁改变了一条产品路径。 行为等价测试(__tests__/kernel-grammar-parity.test.ts:逐 id 比对 ABI + node-kind + field 表)第一天就发现 tree-sitter-wasms 发布的是 2023 年的 TS/JS grammar(^0.20.x),而 crates.io 是新版。解决方案是从 crate 的精确修订 vendor 全新 wasm 进 src/extraction/wasm/(tree-sitter-typescript v0.23.2、tree-sitter-javascript v0.25.0,各仓库 CHECKED-IN 的 parser.c + tree-sitter-cli 0.25.10 + emcc)。结果是生产 wasm 的 TS/JS grammar 顺带被升级了,后续 R2/R3 的 parity diff 就是 grammar 中性的。此后铁律:crate 与 vendor wasm 必须一起 bump,否则 grammar-parity 测试失败——Cargo.toml 里每种 grammar 的注释都在反复强调这一点。

发布接线。 发布 workflow 的 kernel matrix job 构建 6 个目标平台(macos-14 ×2、ubuntu-22.04 ×2、windows-latest ×2),全部 continue-on-error——内核处处可选,工具链抖动永不阻塞发布;release-bundle.node 存在时装进 lib/kernel/;发布作业用 CODEGRAPH_KERNEL_EXPECT=1 跑内核测试(缺二进制在那儿是失败,其他地方是跳过)。

4. R2:通用 .scm emitter 被推翻,改用 per-language walker

计划 Phase 0 原设想是"通用 emitter:.scm 查询文件 + 每语言小型 Rust 配置 + TS post 逃生钩子"。R2 的实战记录(§4a)明确宣布这一设计被取代:真实的 TS/JS parity 需要查询表达不了的逻辑(extractCall 的 receiver-限定 callee、store/RTK/组件识别、fn-ref 捕获与门控、value-ref 影子剪枝、docstring wrapper 上爬),因此 R2 用了一个约 1,900 行的专做 per-language walkercodegraph-kernel/src/tsjs/),逐函数、逐 bug 地镜像 TreeSitterExtractor 的 TS/JS 路径;emitter.rsqueries/ 被删除(git 里还有)。T1 语言(java/python/go)预期也都是 walker。

Parity 证据链(macOS):

  • scripts/kernel-parity.mjs 逐文件对 kernel 与 wasm 两条路径做全对象多集合 diff——本仓库 353/353 文件、excalidraw 643/643 文件(10,650 nodes / 10,726 edges / 68,307 refs)零 diff。
  • 入库的 torture fixtures(tests/fixtures/kernel-parity/)覆盖组件/HOC/styled、zustand-with-middleware、RTK endpoints+hooks、vuex/pinia、fn-refs(含 this.x 与影子门控)、value-refs(含影子剪枝)、装饰器、enum、类型别名成员+元组契约、re-exports、JSX。
  • __tests__/kernel-tsjs-parity.test.tsnpm test 中严格全对象比对保活。

严格比对抓到了一个真实解码器 bug:decode 侧预填了 refs 的 filePath/language,而 wasm 抽取器根本不设这两个字段(store 端用 ?? filePath 反规范化)。修复后确立了接缝契约:"恰好是 extractFromSource 的返回值",而不是"store 加工后的产物"。

性能(M3 Pro,excalidraw 643 文件/7MB):抽取单线程 487ms(kernel)vs 1,255ms(wasm),2.6×,输出一致。端到端 init 在 11 核主机上只从 ~3.4s 移到 ~3.2s——大核机器上 parse 本来就是小且已被池并行的切片,收益集中在受限硬件(2 核 CI 类)与内核级 parse(R6)。

5. R3–R6:等价门禁、逐语言 DEFAULT-ON 与内核级复验

5.1 R3 门禁:TS/JS DEFAULT-ON 与"编码相关错误恢复"这一真实发现

门禁工具升级到 ORDER-sensitive(相同多重集合但发射顺序不同会改变 rowid 进而改变 resolution),并用 scripts/dump-graph.mjs 做自然键全库 dump:

  1. 图等价——字节级,不是 ≤0.5%。 全量 init dump diff(kernel vs wasm):express(13,712 行)、excalidraw(89,898)、**vscode(2,378,238 行)**全部字节一致;对照组 flask(Python)字节一致且耗时不变。抽取级顺序敏感 sweep:本仓库 352/354(+2 defer)、vscode 12,055/12,106(+51 defer),0 diff。
  2. 唯一重大发现——错误恢复与编码相关。 相同 grammar 字节(parser.c/scanner.h sha 校验)、相同 tree-sitter core(0.25.10),但带 parse error 的文件在 UTF-8(native)与 UTF-16(web-tree-sitter)下的错误恢复不同(把发散文件以 UTF-16 原生解析可精确复现 wasm 树)。发生率 0%(express)/ 0.31%(excalidraw)/ 0.42%(vscode)。策略:has_error() 的文件一律通过 defer: 信号让给 wasm 抽取器——错误文件按构造获得 parity,99.6%+ 文件走快路径,且 harness 在 defer 超过 10% 时失败(坏内核无法藏住)。同一信号后来还承载了第二个罕见场景(#1581):walker 按 AST 层递归,病态嵌套文件(clang 的 16,384 花括号 parser_overflow.c)会撑爆原生栈,SIGSEGV 直接杀死整个 indexer 且 JS 无法捕获。codegraph-kernel/src/stack.rs 现在在每次递归入口检查线程真实栈边界(stack_guard! 宏,见 lib.rs),run_guarded 把越界的遍历转成 defer:,文件落到 wasm 路径,进程存活——由 __tests__/kernel-deep-nesting.test.ts 钉死。
  3. 检索不变量:kernel 索引的 excalidraw 上,mutateElement → renderStaticScene 经 explore 端到端连通;合成边家族齐备(408 jsx-render / 46 react-render / 14 interface-impl / 1 callback)。
  4. Agent A/B 被证明是空转:字节级一致的 DB 意味着 A/B 无意义——与 #1320–#1322 性能 PR 相同的论据,靠 dump-diff 门禁放行。
  5. 性能:vscode init 105.4s → 82.1s(11 核 Mac,1.28×);excalidraw 在 2 CPU/6GB Linux 容器 6.2–7.1s → 4.3–4.8s(~1.5×)。
  6. 平台:Linux arm64 容器内 22 个内核测试在 CODEGRAPH_KERNEL_EXPECT=1 下全绿;Windows VM 当时暂缓(缺 .node 会回退 wasm,风险良性)。
  7. 全套 2,465 个测试在 default-on 路由下通过——整个抽取测试语料在装了 .node 的机器上对 TS/JS 实际走内核。

DEFAULT-ON 的代码落点在 src/extraction/kernel/index.tsDEFAULT_ROUTED;当前它已扩展到 20 个语言(含 R7a 的 c/cpp 与 R7b 的 rust/csharp/ruby/php/swift/kotlin/r/lua/luau/scala/dart),每个语言条目都附带门禁证据注释(sweep 仓库、deferral 区间、"deferral 突增才是 walker bug 信号"之类的判定标准)。CODEGRAPH_KERNEL_LANGS=<langs|all> 替换默认集合,CODEGRAPH_KERNEL=0 全局杀。

5.2 R4:Java(含 Lombok 合成)与跨语言 ID 碰撞 bug

Java walker 在 codegraph-kernel/src/java.rs,覆盖 package 命名空间、javadoc、annotation→decorates、type_list 继承、static-final→constant、enum 成员、匿名类(含 TS 侧 0-based 行号的怪癖,bug-for-bug 镜像)、this.field 解包、Foo.getInstance().bar() 链编码、method_reference fn-refs(this::x / Type::m),以及完整的 Lombok 成员合成器(@Getter/@Setter/@Data/@Value/@Builder/@ToString/@EqualsAndHashCode/@Slf4j 家族,按精确 classQN::name 去重)。

门禁抓到的真实跨语言 bug:retrofit 压缩过的网站 JS 暴露了 fn-ref 去重与 value-ref 自检必须比较 node ID 字符串而不是表行——(kind,name,line) 相同的节点会 ID 碰撞(压缩单行代码里第 3 行可能有几十个 function e),TS 侧以 ${fromNodeId}|${name} 为键。修复落在两个 walker(每行 node_ids),且该 bug 在 tsjs 中同样潜在。

诚实的基准结论:dubbo 在 11 核 M3 Pro 上端到端基本持平(11.0–11.6s),因为该阶段瓶颈在主线程(读文件 + store + SQLite)而非 worker CPU;worker CPU 绑定时才见收益——dubbo 2 CPU/6GB Linux 27.8–28.6s → 22.3–22.8s(~1.25×)。

5.3 R5:Python + Go;R6:Linux 内核级复验

Python/Go walker(codegraph-kernel/src/python.rscodegraph-kernel/src/go.rs)镜像了大量 TS 侧语义细节:Python 的 decorator 只认裸标识符(call-kind 怪癖镜像)、fn-in-class→method、self.x fn-ref 候选按裸名、属性 callee 的 namedChild(1) 回退;Go 的 receiver 方法 QN + "先出现的 struct" contains 边、embedding→extends、interface method_elems→method 节点、复合字面量保留包限定符、顶层 var/const 初始化子句归属(#693)、2 跳字段链(#1276)、New().Method() 重编码(#645/#608)、GO_SPEC fn-ref 层。

Sweep 100%(flask 83/83、django 3,035/3,038 三个错误文件 defer、gin 99/99、prometheus 978/979);全量 init dump 字节一致:flask(10,833 行)、gin(17,540)、django(360,794)prometheus(213,758)。2 CPU/6GB 包络:django 22.0→16.7s(1.32×)、prometheus 15.0→10.3s(1.46×)。

R6 在 cg1212 容器(2 CPU/6GB)复验 Linux 内核:完成,exit 0,26.4min,与 ~27min 基线无回归,图规模相同(2,048,664 nodes / 6,405,964 edges)。分阶段耗时:scan 1.3s(70,239 文件)、parse-loop 371.9s(6.2min,未变)、edge-index-recreate 78.9s、callback-synthesis 350.3s、resolution 1,149.7s(19.2min——真正的墙,P1 领域)、maintenance 47.2s。计划中"parse 6m→2m"的预期被证伪:Linux 树是 63,810 个 C/H 文件对 422 个 Python,~99% C 是尚未移植的 T2 语言——预期顺延到 C/C++ 移植(后来的 R7a)。

6. 直接落库解码(direct-to-store)与"墙到底在哪"

R4 之后引入的直接落库:kernel 路由文件从 parse worker 把扁平 buffer 一路送到 store worker,在那边解码+收尾(tryKernelExtractRawExtractionResult.kernelBuffersKernelStoreBundledecodeKernelBundle)。主线程每文件工作降到 O(1)+内容哈希,永不物化 per-node 对象,两次 postMessage 都传扁平字节;带 extract() hook 的框架文件保留解码路径。对应实现在 src/extraction/kernel/index.tstryKernelExtractRaw,校验 meta 的 ABI 版本、读计数与 errors-JSON)与 materializeKernelResult(主线程 store/测试等非写者路径的物化兜底)。

仪器化测量给出关键结论:dubbo 的 parse-loop 墙 94% 是 store-writer 忙时(kernel arm 4,493ms 中 4,202ms)。多核全量索引的墙是单写者 SQLite 摄入——不是抽取,不是主线程。d2s 仍改善 writer lane ~11%(4,726→4,202ms,buffer 省掉 writer 上的 structured-clone 反序列化)并解放主线程,但剩余 cbm 差距是 store 架构问题。

文档还完整记录了 store 弧的后续两轮(2026-07-19):第一轮 parse 期索引延迟(beginBulkParseLoad/endBulkParseLoad,仅全量索引生效)——dubbo parse-loop 4,306→1,787ms(−58%),Linux 内核 8c 包络 14.2min(机制:批量重建的 B 树比增量生长的更致密,后续所有索引中介读取省页);第二轮 resolution ref 索引窗口(beginBulkRefLoad/endBulkRefLoad)——Linux 8c resolution 423.4→275.9s,envelope ~11.0min。门禁始终是同一套:dubbo/gson 等 dump 字节一致、linux 计数精确 + dump sha 复现、测试套件全绿。

7. 按语言迁移追踪表:T1/T2/T3 分层与"移植配方"

计划 §4 用一张表管理每种语言,分层定义:T1 = 基本是 walker+映射配置;T2 = 需要留在 TS 的专属 pre/post pass;T3 = 不是普通 tree-sitter 遍历(独立/多 grammar 抽取器),最后迁或永不迁——wasm/TS 是它的好归宿。追踪表覆盖 README "Language Support" 的全部语言(34 个 logo,含 Metal、CUDA、Terraform/OpenTofu、Pascal/Delphi),并要求与 README 保持同步。

截至 R7b 完成(2026-07-20),20 个语言已 DEFAULT_ROUTED,全部走同一套门禁:小/中/大型真实仓库的 parity sweep 零 diff → 全量 init dump-diff 字节一致 ×3 → 进 DEFAULT_ROUTED + 测试 + changelog。几个有代表性的移植记录(完整版在追踪表行内,每行都列了仓库、文件数、deferral 与 quirk 清单文档路径):

  • rust(#1371,首个 R7b):grammar 升到 tree-sitter-rust v0.24.2(crate + vendor wasm 同升);sweep 0-diff 于 ripgrep/tokio/rust-analyzer(rust-analyzer 271 个 defer 是 token-macro-table 源码——T![~][$]——双臂同错,grammar 固有);dump 字节一致 ×3。quirk 清单见 rust-lang-kernel-port-checklist.md
  • csharp:唯一无需 grammar 预处理的移植(#717 vendor wasm 验证与 crate 0.23.5 表级相同);#237 的 #if preParse 通过路由点 hoist 留在 TS 侧。
  • lua/luau:一个 walker 两个方言(ccpp 式 dialect flag);lua 是第二个 vendor grammar-C(v0.4.1 不在 crates.io),luau 是纯 crate pin。
  • scala:第三个 vendor grammar-C(master@0aca5d0a6f,35MB parser.c,树中最大 grammar);生产 wasm 自 #91 起就用该修订,所以"grammar parity 行"就是全部对齐证明。
  • dart:第四个 vendor grammar-C,byte-copy 的 wasm 消灭了 tree-sitter-wasms 的未固定 github 依赖隐患;逐 bug 移植了 sibling-body 双重遍历缺陷(有专门 fixture + bloc kind-census 抽检)。
  • c/cpp(R7a,T2):单一双语 walker(codegraph-kernel/src/ccpp/),所有 preParse 空白处理留在 TS 侧——这是后来定义的"preParse hoist":语言的 offset-preserving preParse hook 在两个内核入口之前运行(见 index.tspreParsedSource),使 c/cpp/metal/cuda 的空白化保持 TS 侧,双臂解析相同字节。门禁中实测 C/C++ parse-error 发生率每仓库 9–42%(此前语言是 0–0.42%),所以 sweep 加了 --max-deferral 0.5

未迁行的已知陷阱(vbnet 的 patched grammar + C external scanner 是"全树最高的 grammar 构建风险"、erlang 的 npm 包被劫持、cobol 的 copybook 解析、nix 的 ABI-15 重建等)都逐条列在追踪表里,供后续移植者直接消费。

任何移植期间不得回归的不变量(抽取侧,会在门禁中显现):node 元数据从源码重读、绝不持久化;parse 提交保持文件顺序(#1015);MAX_FILE_SIZE 跳过;生成文件检测;CODEGRAPH_PARSE_WORKERS 语义;框架 extract() hook 在 kernel pass 之后仍按文件跑在 TS 侧。

移植新语言的"被验证的配方"(约一天/T1)

  1. 读它的 languages/<lang>.ts 配置以及 tree-sitter.ts 中它走过的每一个分支(visitNode 分派、extractCall 语言分支、继承子句、function-ref.ts 的 fn-ref spec、value-ref 剪枝)。逐 bug 移植——怪癖也要(每个 walker 头部注释列了自己的怪癖)。
  2. 加 crates.io grammar;从同一 tag vendor wasm(克隆 tag,parser.c 与 cargo registry 副本 sha 比对,用 CHECKED-IN 的 parser.c 跑 tree-sitter-cli 0.25.10 build --wasm,放进 src/extraction/wasm/,加入 VENDORED_WASM_LANGS)——tree-sitter-wasms 对多数语言是 2023 年产物。
  3. Torture fixture + parity sweeps(小/中/大型真实仓库)→ 全量 init dump-diff 字节一致 → 加进 DEFAULT_ROUTED + 测试 + changelog。

8. 等价门禁(每语言必跑,无例外)

计划 §5 明确:不期望与手写抽取器字节级相同——门禁是行为等价:

  1. 图等价:对该语言 3 个真实仓库(小/中/大)分别用 wasm 路径与内核路径构建做全量索引,用 dump-graph 模式(自然键)dump。node/edge/ref delta ≤0.5%,且每个 diff 类别人工分类("上周 13-edge 超类型可见性 bug 正是这样抓到的——小 diff 是真的")。
  2. 检索不变量:该语言的规范流在 codegraph_explore 中仍端到端连通;node 计数稳定;合成边抽检。
  3. Agent A/B 无回归,按标准方法论:--model sonnet --effort high 永远、每臂 ≥2 次、预热 daemon、CODEGRAPH_NO_PROMPT_HOOK=1、提示中禁止 subagent 委派。
  4. 性能:该语言仓库全量索引改善;未迁移的对照仓库不变;套件绿;平台敏感项过 Linux docker + Windows VM。

9. P1 性能弧:测量如何一次次重塑设计

这是文档中信息密度最高的部分(§7a.1–§7a.11),也最能体现该仓库的工程方法论。目标是"8 核普通主机上 Linux 内核级索引 <10min",起点是 R6 的 26.4min。

第一轮测量(§7a.1)推翻了前提:resolution 从来不是串行的——池 sizing(resolver-pool.tstryCreatemin(os.cpus().length − 2, 6),≥2 才启用)用的是不看 cpuset 的 os.cpus(),2 CPU 容器里看到 Docker VM 的 8 个 CPU,跑 6 个 worker 分时片 2 核。同时暴露两个结构性缺陷:8 核/7GB 容器 cgroup OOM(池只按核数 sizing,没有内存项)、WAL 膨胀(4.6GB DB 长出 22.2GB WAL——池化 superphase 连续写而 6 个 worker 持有重叠读快照,checkpoint 永远无法截断到最旧读者之前)。

**P1 实施弧(#1332–#1336)**用了三次失败/诊断性内核级运行才走对,每次失败都教会设计一个事实,且每个事实最终都在代码+测试中强制执行:

  1. WAL backlog 与 WAL 文件是两种资源:被动回填约束 backlog,文件只有当某次提交发现零读者标记才停止增长——实践中从未发生。收敛 = 回填 + 停泊屏障处 TRUNCATE + 4× 软上限的文件大小硬触发。dubbo:251MB→69MB 峰值,dump 字节一致。
  2. cgroup v2 memory.current 计入可回收 page cache:parse 后读到 57MB 可用并静默禁用池;把 inactive_file 抵扣回去后同一台机器读到 4.4GB。
  3. 2 个真核上池输给串行(853s vs 1,150s:冷 worker 缓存+序列化+分时片超过并行度);池化 synthesis 被 cFnPtrEdges(306s/358s)Amdahl 锁死。最终 sizing:min(availableParallelism − 1, 6) + 内存项 + CODEGRAPH_RESOLVE_WORKERS 旋钮。
  4. 2 核上 parse 也需要 ≥2 worker(1 worker = +34%)。
  5. 静默失败模式烧掉了三个 25 分钟周期——所有 valve/sizing 决策现在都在 CODEGRAPH_SYNTH_TIMINGS / CODEGRAPH_WAL_VALVE_DEBUG 下打印。

记录运行的数字:2c/6GB 从 26.4min(R6)→ 21.6 → 20.4min(#1336,WAL 峰值 1.57GB,−14×,计数 2,048,664/6,405,964 字节精确);8c/7GB 18.3min 无 OOM(池按内存项 sizing 到 4)。8 核结论:差距被精确刻画——当时判断 resolution 在核规模上是"核不变"的(835.9s pooled-4-on-8 ≈ 812.5s sequential-on-2),18.3min 里 ~14min 是 resolution+synthesis。

去二次化(#1339)CODEGRAPH_RESOLVE_PROFILE 的分阶段归属推翻了 founding 假设——resolveOne 只占 ~433s 批循环的 ~93s。最大一笔是 countGuard:每批 COUNT(*) 是 O(remaining),换成累加 SQLite changes,93.9s → 0.0s;envelope 19.3min。

cFnPtr 弧(#1341 → #1364 → #1365):该 pass 占内核级 synthesis 的 86%(306s)。先在容器内对活库做 standalone probe(~4min/轮替代 25min init)迭代到 2.07×,然后分两步原生化:step 1 是 TS 侧 fuse-then-link 重构(每文件只 strip 一次,survival filter 门控重放,−22%);step 2 是内核里的 codegraph-kernel/src/cfnptr.rs cfnptr_scan_files——16 文件一批摊薄 NAPI 边界,用手写字节状态机而非 regex crate(JS 引擎语义是 spec:\w/\b 是 ASCII 而 \s 是 Unicode 类,含 NBSP/U+2000-200A/FEFF;lastIndex 续扫;可观测的回溯维度结构性复现),且原生 stripper 按 UTF-16 code unit 置空(astral 字符两个空格)使输出与 TS stripper 字符串一致。整弧 cFnPtr 230s → 150.9s(−34%),callback-synthesis 250→171.1s。门禁:边缘流 diff(git/redis/vim/SameBoy 705/852/433/180 条边完全相同)、活库 probe-hash 复现、linux 计数精确 + dump sha 复现、全套测试绿 ×2。

per-ref 测量轮(§7a.6/§7a.7):新分阶段表推翻"核不变"判断——8c 时 settle 只有 3.6s(池双缓冲有效,worker 吞掉 3.17M 精确匹配);真正的 8c 异常是 writes-under-readers(deletes 37.7→118.8s:主线程 B 树写在读者在场时慢 ~3×)。五组判别运行证明机制是 WAL read-through 深度受读者 pin 约束,修复是池空闲边界处 worker 连接回收ResolverPool.recycleWorkers + QueryBuilder.rebind,每 8 批重开一次只读连接,节奏 25→8 由测量迭代):superphase 715→633.6s(−11.4%),8c 最佳 14.8min,全程字节中性。两个缓存实验(nameCache 扩容、lazy candidates 解析)当天被测量杀死并回滚——"11µs 不是重取开销,floor 是一条索引查找周围的 per-ref JS"。

被杀死的最后一根杠杆(§7a.11):continuous-shallow WAL 探针的三臂结果显示 fold I/O 是固定预算——基线已把大部分藏在重叠的离线程 timer pass 里,强制更多 fold 只是把成本搬家(三臂在 ~2.5% 内收敛)。代码回滚,valve+recycling 保持为该族最优。诚实台账:<10min-on-8c 目标的剩余质量在 resolution ~575s superphase 与 parse ~190s writer floor——两者都是 store 架构弧,也就是 cbm 的 dubbo 标杆所在。

10. 路线图:内核之后的事

Arc 3 图丰富度(§7b,产品优先级,按序):test→subject 边(索引期一等公民)、per-node 代码指标(复杂度/cognitive/is_test——内核使这近乎免费,缓冲区契约已留 metrics 槽)、引用读写区分(USAGE vs WRITES,最高价值最高风险,范围限定到导出/状态相关符号)、异常流边、Doc Section 节点、IaC 节点。同时明确不值得追(在对标 cache schema 中验证过):per-variable 节点膨胀(占其节点数 85%)、DB 体积对齐、核心引擎里的相似向量。

Parked 项(需显式批准):单文件 SEA 二进制、团队共享图工件、完全原生重写——带数据地拒绝:"护城河(2,444 个测试、字节级确定性、本周两个被门禁抓到的 bug)活在 TS 参考实现里。"

11. 实战速查:构建、开关与验证命令

  • 构建内核npm run build:kernel(需 rustup;等价于 scripts/build-kernel.sh,支持 --target <rust-triple> --platform <plat-arch> 交叉编译,发布 workflow 用它构建 6 目标 prebuild 矩阵)→ 暂存 codegraph-kernel/prebuilds/<plat>-<arch>/codegraph-kernel.nodenpm run buildnpm test
  • 路由开关DEFAULT_ROUTEDsrc/extraction/kernel/index.ts)为默认集;CODEGRAPH_KERNEL_LANGS=<langs|all> 替换集合;CODEGRAPH_KERNEL=0 全局 kill;CODEGRAPH_KERNEL_PATH 指定显式 .nodeCODEGRAPH_KERNEL_DEBUG=1 查看未加载原因。
  • Parity sweep(快速内环)node scripts/kernel-parity.mjs <file-or-dir>... [--lang ts,tsx] [--max-deferral 0.5]——顺序敏感、全对象 diff;C/C++ 用 0.5 的 deferral 上限(宏密集 C 树 10–40% 文件带 parse error 是常态,git 19% / protobuf 26% / fmt 42%)。
  • Dump gate(终极门禁):init 两次(kernel arm vs CODEGRAPH_KERNEL=0),各自 node scripts/dump-graph.mjs 自然键全库 dump,cmp 字节比对。
  • 测试:脚手架测试 tests/kernel-scaffold.test.ts 覆盖 wire 契约/解码器/路由/kill switch/按文件回退,二进制缺失即跳过,CODEGRAPH_KERNEL_EXPECT=1 时缺二进制算失败;逐语言 parity 套件(如 tests/kernel-tsjs-parity.test.tskernel-cpppp-paritykernel-rustlang-parity 等)连同 torture fixtures 常驻 npm testtests/fixtures/kernel-parity/ 下是 20+ 语言的 torture 文件,且 TS/JS 系 fixture 有内存派生的 CRLF 变体钉死跨平台行为。

12. "已付学费"的陷阱清单(不要重学)

计划文档 §0a 专门留了一节给接手者的陷阱清单,每一条都对应一次真实事故:

  • 错误恢复与编码相关(UTF-8 native vs UTF-16 web-tree-sitter)→ 所有 walker 对 has_error() 文件发 defer: 信号;发生率 0–0.42%,harness 在 >10% defer 时失败。
  • Node ID 碰撞于同 (kind,name,line)——压缩代码里是常态。任何以 node ID 为键的去重/自检必须比较 ID 字符串而不是表行(每个 walker 的 node_ids vec)。
  • 位置与 JS 字符串切片是 UTF-16 的textutil::col16/slice_utf16)——那是 web-tree-sitter 报告的东西,也是 .slice(0,100) 的含义。
  • 接缝契约恰好是 extractFromSource 的返回值——refs 不带反规范化的 filePath/language。严格全对象 parity 比对的存在就是为了掩盖不了这种松散。
  • Grammar bump 时 crate 与 vendor wasm 必须同动,否则 kernel-grammar-parity 失败。
  • JS 多行 ^ 锚点在 \r(和 U+2028/U+2029)之后;regex crate 的 (?m)^ 只认 \n——Windows autocrlf 检出时 JS 参考实现的贪婪 \s* 会吃掉 CRLF 对的 \n,清洗后的 docstring 留一个裸 \r。由 O2 Windows 验证腿抓到(6 个 parity 失败),修复在 docstring.rsjs_multiline_strip;任何未来 walker 的 (?m) 正则都需同等审视。
  • 性能主张:先测量再相信——本计划自己的 §1/§6 预期被纠正了两次(多核 parse-loop 墙 = store-writer,§4d;cg1212 parse = C-bound,§4f)。

13. 结语:一个"测量驱动 + 字节级门禁"的迁移样本

CodeGraph 的 Rust 内核迁移值得研究的原因不只是 20 个语言的 DEFAULT-ON 结果,而是它展示了一套可复用的方法论:用一个可选的原生层(处处可回退、缺失无害)替换热路径;以"双臂字节级一致"代替"百分比容差"作为正确性定义;每轮性能工作先跑判别性测量(cpuset 盲的池 sizing、WAL read-through 深度、fold I/O 固定预算),失败运行产出设计事实而非废品;并把所有预期修正写回计划文档本身(SUPERSEDED 标注、被杀死的杠杆与数字一并归档)。对要在既有 JS 索引/工具链中嵌入原生加速的团队,这份文档与仓库中的门禁工具(kernel-parity.mjsdump-graph.mjs、grammar-parity 测试、CODEGRAPH_KERNEL_EXPECT 约定)就是可直接对照的工程蓝图;架构细节的姊妹文档见 native-extraction-kernel.md

登录后查看全文
热门项目推荐
相关项目推荐