Skip to content

perf(core): Spring 索引正则改为只编译一次,491.74s 降至 2.40s - #306

Merged
1lck merged 8 commits into
1lck:previewfrom
Farewell0375:fix/299-spring-index-regex-compilation
Aug 29, 2026
Merged

perf(core): Spring 索引正则改为只编译一次,491.74s 降至 2.40s#306
1lck merged 8 commits into
1lck:previewfrom
Farewell0375:fix/299-spring-index-regex-compilation

Conversation

@Farewell0375

Copy link
Copy Markdown
Contributor

修复 #299

问题

在约 1 万个 Java 文件的 Maven 项目上,release 构建下 spring.index 耗时 648 秒。因为 reloadProjectServices 挂在工作区文件监听上,每批文件变更都会重新触发一次。

原因

不是扫描范围的问题,是正则在热路径上被反复编译。

bean_index每个文件的每一行无条件发生的编译:

调用 每行编译次数
has_annotation(&context, "Bean") 1
is_injection_context(&context)(Autowired / Inject / Resource) 3
constructor_regex(type_name)(在逐行循环体内) 1

命中类型声明的行再加 has_component_annotation 的 6 次和 has_annotation(&context, "Primary") 的 1 次。

按每行 5 次保底、数千个文件计,量级在一千万次左右。单次 Regex::new 约 60 微秒,乘出来正好是观测到的耗时量级。

改动

19 处固定模式改为函数内 static LazyLock<Regex> 定义在使用它的函数体内,作用域不外泄,函数体其余部分不动。仓库中现有的 OnceLock 都用于 Mutex<HashMap> 这类可变缓存,属于另一种场景。

constructor_regex 提出逐行循环。 它依赖声明类型名,无法做成全局常量,但对同一个文件是不变的。原先在算 constructor_count 时编译一次,又在逐行循环里每行重新编译一次;现在每文件构建一次复用。顺带去掉了一层已经多余的 if let 嵌套。

has_annotation 改为查表。 这是最热的一处。本模块识别的 11 个注解名预编译进 LazyLock<HashMap>,未命中的名字回退到即时编译,这样将来有调用方开始识别新注解时不会静默失效。

扫描范围、遍历顺序、结果排序均未改变。 本 PR 只降低常数因子,不引入增量索引。

验证

性能。dev860/Backend/Service(7774 个 Java 文件、10063 个快照文件)上,release 构建:

改动前  491.74 秒
改动后    2.40 秒

行为等价性。 同一份输入,改动前后各保存一次完整 spring.index 响应:

before.json  3519348 bytes  ca1d210f94d3207e869381b8a3a42b6ce27a2872c3a673f3ca736c66cc5b42d4
after.json   3519348 bytes  ca1d210f94d3207e869381b8a3a42b6ce27a2872c3a673f3ca736c66cc5b42d4
cmp → 逐字节相同

七类产出逐项一致:properties 16、values 327、propertyReferences 111、diagnostics 1870、beans 2338、injections 4479、endpoints 4103。这覆盖了单元测试触及不到的真实代码形态,包括排序结果。

脚本。

./scripts/verify-rust-core-comments.sh  通过
./scripts/verify-rust-core.sh           通过
./scripts/verify-core.sh                通过
cargo test -p lithe-core                264 + 5 全部通过

新增 spring_index_does_not_treat_longer_annotations_as_recognized_ones,用 @BeanFactory@ServiceLocator@RestControllerAdvice 三个诱饵锁住 (?:\s|\(|$) 的边界语义——如果有人把匹配改成朴素的 contains,这三个都会被误判成 bean 或控制器。

已执行 cargo fmt

关于增量索引

issue 下有评论认为成因是"没有做增量索引而是每次都走全量"。全量重跑确实存在,但它决定的是重跑频率,不是单次耗时:本 PR 没有改动扫描范围,仍然是全量,单次仍从 491.74 秒降到 2.40 秒且输出逐字节一致。

增量在这里也不是小改动。bean_index 先扫描全部源码建立全局 supertype 映射,再拿所有 bean 解析每个注入点;改一个文件会影响其他文件的解析结果(例如新增 @Primary 会改变别处所有歧义注入的结果)。朴素的按文件增量会产生错误结果,需要配套依赖追踪。建议作为独立议题评估——单次成本降到 2.4 秒之后,其收益已大幅下降。

不在本 PR 范围内

annotation_context 每行构造 Vec<String> 并 join 的分配开销没有处理。前述改动之后总耗时已是 2.4 秒,进一步优化收益有限,且会在缺乏证据的情况下改动一个工作正常的函数。

…line

bean_index rebuilt regular expressions inside its per-line loop: five on every
line through has_annotation, is_injection_context, and constructor_regex, plus
seven more on each type declaration. Over a workspace with thousands of Java
sources that reached roughly ten million compilations.

Hold the fixed patterns in function-scoped LazyLock statics, cache the patterns
for the annotations this module recognizes, and hoist the type-dependent
constructor pattern out of the line loop. The scan is still full and the
traversal order is unchanged.

On a 7774-file Maven workspace this takes spring.index from 491.74s to 2.40s
with a byte-identical response: same 3519348 bytes and SHA-256 across 16
properties, 327 values, 111 property references, 1870 diagnostics, 2338 beans,
4479 injections, and 4103 endpoints.

Refs 1lck#299
@Farewell0375
Farewell0375 requested a review from 1lck as a code owner August 28, 2026 08:59

@1lck 1lck left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

改动方向和当前行为等价性没有问题,但这里仍有两处会增加后续维护和性能回归风险的设计,请处理后再合并。

Comment thread rust/lithe-core/src/languages/spring.rs Outdated
Comment thread rust/lithe-core/src/languages/spring.rs Outdated
@1lck

1lck commented Aug 28, 2026

Copy link
Copy Markdown
Owner

@Farewell0375 你好 以上是一些pr阻塞点 麻烦处理一下哦 如果您有一些其他好的方案也可以在此PR下与我讨论

评审指出两处会让性能修复无声回退或让解析行为分叉的设计。

注解缓存集合原先与各调用方独立维护,新增 has_annotation(context, "...") 而忘记
同步缓存时,功能和测试都照常通过,但那一处会退回逐次编译正则。改为封闭的
SpringAnnotation 类型:模式表由 ALL 构建,调用方只能引用具体的 case,即时编译的
fallback 已移除。component_name 的注解候选也改为从 COMPONENTS 生成,不再重复罗列。

parse_record_components 与 parse_constructor_injections 原先各自维护一份完全相同
的 Java 参数声明正则。合并为模块级的 JAVA_PARAMETER_DECLARATION。

补三个内联测试:每个受支持注解都有编译好的模式且保持前缀边界;组件识别与命名
认同同一组注解;两条参数解析路径对同一声明得到一致结果。

行为等价性重新验证:dev860/Backend/Service(7774 个 Java 文件)上 spring.index
的完整响应 SHA-256 与本 PR 之前和改动前三轮完全一致,仍为 3519348 字节。

Refs 1lck#299
@Farewell0375

Copy link
Copy Markdown
Contributor Author

@1lck 感谢评审,两条 [Important] 都已处理,e8d8838c。细节回在对应批注下了,这里总结:

注解识别 — 引入封闭的 SpringAnnotation 枚举,模式表由 ALL 构建,即时编译的 fallback 已移除。调用方只能引用 case,新增注解漏掉 name() 的穷尽 match 会编译失败。component_name 的候选集也改为从 COMPONENTS 生成,不再重复罗列。

重复模式 — 两处一字不差的 Java 参数声明正则合并为模块级 JAVA_PARAMETER_DECLARATION

行为等价性重新验证。dev860/Backend/Service(7774 个 Java 文件、10063 个快照文件)上重跑完整 spring.index,与最初基线逐字节比对:

改动前              3519348 bytes  ca1d210f...cc5b42d4
本 PR 上一版        3519348 bytes  ca1d210f...cc5b42d4
本次评审修改后      3519348 bytes  ca1d210f...cc5b42d4

三轮 SHA-256 完全一致,2338 个 bean、4479 个注入点、4103 个端点逐字节相同。耗时 2.67 秒(基线 491.74 秒)。

验证verify-rust-core.shverify-rust-core-comments.shverify-core.sh、测试稳定性门禁全部通过;cargo test -p lithe-core 267 个测试通过(新增 3 个)。已跑 cargo fmt

关于 issue 下"没做增量索引"那个推测,我在 PR 正文里放了一节说明:本 PR 未改动扫描范围,仍是全量,所以单次耗时的成因是逐行的正则重复编译而非全量本身。增量因为 bean_index 的跨文件 bean 解析语义需要依赖追踪(改一个文件会影响别处的注入解析),朴素按文件增量会出错,建议作为独立议题评估。

@1lck 1lck left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

前两条建议已经确认修复并关闭。这里还有一个需要补的点,见行内评论。

Comment thread rust/lithe-core/src/languages/spring.rs Outdated
上一轮仍然是两份名单:枚举和 ALL。加了新 case 但漏掉 ALL 时编译与测试都会过,
运行到 pattern() 才 panic。

改用 spring_annotations! 宏,从同一份 Variant => "Name" 声明生成枚举、name()、
ALL 与 pattern()。pattern() 对 self 做穷尽 match,各 case 返回自己的
LazyLock<Regex>,不再查表,因此漏掉模式的 case 会被编译器拒绝,运行期也不再有
可 panic 的查表路径。ALL 现在只服务测试,标记为 cfg(test) 以免留下未使用的
生产代码。

Refs 1lck#299
自查发现两处收尾不干净。

上一版为压小 diff 保留了 12 行 let x = &*STATIC 中转,但同一文件里
JAVA_PARAMETER_DECLARATION 与 component_name 已经直接使用静态名,形成了两种
并存的写法。改为一律直接使用,中转行全部删除。

SpringAnnotation 的 PartialEq、Eq、Hash 是上一版 HashMap 查表的残留,改成穷尽
match 后已无消费方,Debug 也没有格式化点在用。收敛为 Clone、Copy。

行为等价性重新验证:dev860/Backend/Service 上 spring.index 的完整响应 SHA-256
与最初基线仍然一致(3519348 字节)。重命名涉及 field、method、class、pattern
等常见标识符,字节比对是确认没有误伤的依据。

Refs 1lck#299
@Farewell0375

Farewell0375 commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

更正:先前留言说「已关联并关闭 #299」不准确。#299 已重新打开,是否关闭等维护者确认后再定。#306#299 的交叉引用仍在。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants