perf(core): Spring 索引正则改为只编译一次,491.74s 降至 2.40s - #306
Conversation
…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
1lck
left a comment
There was a problem hiding this comment.
改动方向和当前行为等价性没有问题,但这里仍有两处会增加后续维护和性能回归风险的设计,请处理后再合并。
|
@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
|
@1lck 感谢评审,两条 [Important] 都已处理, 注解识别 — 引入封闭的 重复模式 — 两处一字不差的 Java 参数声明正则合并为模块级 行为等价性重新验证。 在 三轮 SHA-256 完全一致,2338 个 bean、4479 个注入点、4103 个端点逐字节相同。耗时 2.67 秒(基线 491.74 秒)。 验证: 关于 issue 下"没做增量索引"那个推测,我在 PR 正文里放了一节说明:本 PR 未改动扫描范围,仍是全量,所以单次耗时的成因是逐行的正则重复编译而非全量本身。增量因为 |
1lck
left a comment
There was a problem hiding this comment.
前两条建议已经确认修复并关闭。这里还有一个需要补的点,见行内评论。
上一轮仍然是两份名单:枚举和 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
修复 #299。
问题
在约 1 万个 Java 文件的 Maven 项目上,release 构建下
spring.index耗时 648 秒。因为reloadProjectServices挂在工作区文件监听上,每批文件变更都会重新触发一次。原因
不是扫描范围的问题,是正则在热路径上被反复编译。
bean_index对每个文件的每一行无条件发生的编译:has_annotation(&context, "Bean")is_injection_context(&context)(Autowired / Inject / Resource)constructor_regex(type_name)(在逐行循环体内)命中类型声明的行再加
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 构建:行为等价性。 同一份输入,改动前后各保存一次完整
spring.index响应:七类产出逐项一致:properties 16、values 327、propertyReferences 111、diagnostics 1870、beans 2338、injections 4479、endpoints 4103。这覆盖了单元测试触及不到的真实代码形态,包括排序结果。
脚本。
新增
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 秒,进一步优化收益有限,且会在缺乏证据的情况下改动一个工作正常的函数。