Platform / 平台
macOS
Lithe version / Lithe 版本
e19928bd (main)
OS and architecture / 操作系统与架构
macOS 15.7.3, Apple Silicon (arm64)
Environment / 环境信息
Rust: 1.94.0(release 构建实测,非 debug)
目标项目规模: 18 个 Maven 模块 / 9765 个 .java 文件 / 12611 个可见文件 / 218 MB
Steps to reproduce / 复现步骤
- 准备一个约 1 万个
.java 文件的多模块 Maven 项目
- 用 release 构建调用 lithe-core 的
spring.index 命令,payload 传入 workspace.snapshot 返回的完整 paths 列表
- 记录耗时
(或:在应用中打开该项目,观察 Spring 面板的索引状态持续时间)
Expected behavior / 预期行为
Spring 索引应在与项目规模成合理比例的时间内完成(该规模预期为秒级到数十秒),且不应在每次文件变更时全量重扫。
Actual behavior / 实际行为
release 构建下 spring.index 耗时 648 秒(约 11 分钟),平均每个 Java 文件 66ms。
根因是正则表达式在热路径上被反复编译。rust/lithe-core/src/languages/spring.rs 中的 bean_index 对每个文件的每一行都会重新编译若干正则:
// rust/lithe-core/src/languages/spring.rs:1141
fn has_annotation(context: &str, name: &str) -> bool {
Regex::new(&format!(r"@{}(?:\s|\(|$)", regex::escape(name)))
.unwrap()
.is_match(context)
}
bean_index 逐行调用链上的编译次数:
| 调用 |
每行编译次数 |
has_component_annotation(6 个注解各一次 has_annotation) |
6 |
has_annotation(context, "Bean") |
1 |
is_injection_context(Autowired / Inject / Resource) |
3 |
constructor_regex(type_name)(在逐行循环体内调用) |
1 |
即每行 11 次以上,乘以数百万行源码。
spring.rs 中共有 20 处 Regex::new,endpoint_index、declared_supertypes、parse_constructor_injections、quoted_values、qualifier_names、component_name、injection_qualifier 等函数都存在同类问题(部分在循环体内,部分用 format! 动态拼接后编译)。
放大因素
reloadProjectServices 挂在工作区文件监听上,每批文件变更都会重新调用 AppModel.loadProjectServices,其中 springFeature.load 默认 refreshDependencyMetadata: true。也就是说保存一次文件就会触发一次全量索引。SpringFeatureModel 用 generation token 丢弃过期结果,但已经派发出去的 Rust 任务没有取消机制,会跑完整个 648 秒并持续占用 CPU。
附带问题(同类,建议一并评估)
runConfig.inspect 为计算 staleFingerprint 指纹会重新遍历并 SHA-256 哈希全部 9765 个 .java 文件,单次 1.33 秒。它同样在每次项目服务重载时被调用,且没有复用工作区快照已有的文件列表。
修复方向
- 将
spring.rs 中的静态模式改为 LazyLock<Regex> 常量
constructor_regex 等按类型名拼接的正则提到逐行循环外,每个文件只构建一次
has_annotation 改为对固定注解集合使用预编译表
Logs or diagnostic output / 日志或诊断输出
# lithe-core release 构建,同一次运行内的相对耗时
workspace.snapshot: elapsed=256.10ms bytes=2632023 ok=true files=12611
spring.index: elapsed=648090.35ms bytes=4862785 ok=true
runConfig.inspect: elapsed=1325.28ms bytes=1431623 ok=true
runConfig.resolve: elapsed=598.18ms bytes=18929 ok=true
# 注:本次实测 metadataRepositories 传空、refreshDependencyMetadata=false。
# 应用实际调用会传入 ~/.m2 等仓库且 refreshDependencyMetadata=true,
# 首次打开还需叠加最多 20000 个 JAR 的元数据扫描(MAX_METADATA_ARCHIVES)。
Checklist / 提交前检查
Platform / 平台
macOS
Lithe version / Lithe 版本
e19928bd(main)OS and architecture / 操作系统与架构
macOS 15.7.3, Apple Silicon (arm64)
Environment / 环境信息
Steps to reproduce / 复现步骤
.java文件的多模块 Maven 项目spring.index命令,payload 传入workspace.snapshot返回的完整paths列表(或:在应用中打开该项目,观察 Spring 面板的索引状态持续时间)
Expected behavior / 预期行为
Spring 索引应在与项目规模成合理比例的时间内完成(该规模预期为秒级到数十秒),且不应在每次文件变更时全量重扫。
Actual behavior / 实际行为
release 构建下
spring.index耗时 648 秒(约 11 分钟),平均每个 Java 文件 66ms。根因是正则表达式在热路径上被反复编译。
rust/lithe-core/src/languages/spring.rs中的bean_index对每个文件的每一行都会重新编译若干正则:bean_index逐行调用链上的编译次数:has_component_annotation(6 个注解各一次has_annotation)has_annotation(context, "Bean")is_injection_context(Autowired / Inject / Resource)constructor_regex(type_name)(在逐行循环体内调用)即每行 11 次以上,乘以数百万行源码。
spring.rs中共有 20 处Regex::new,endpoint_index、declared_supertypes、parse_constructor_injections、quoted_values、qualifier_names、component_name、injection_qualifier等函数都存在同类问题(部分在循环体内,部分用format!动态拼接后编译)。放大因素
reloadProjectServices挂在工作区文件监听上,每批文件变更都会重新调用AppModel.loadProjectServices,其中springFeature.load默认refreshDependencyMetadata: true。也就是说保存一次文件就会触发一次全量索引。SpringFeatureModel用generationtoken 丢弃过期结果,但已经派发出去的 Rust 任务没有取消机制,会跑完整个 648 秒并持续占用 CPU。附带问题(同类,建议一并评估)
runConfig.inspect为计算staleFingerprint指纹会重新遍历并 SHA-256 哈希全部 9765 个.java文件,单次 1.33 秒。它同样在每次项目服务重载时被调用,且没有复用工作区快照已有的文件列表。修复方向
spring.rs中的静态模式改为LazyLock<Regex>常量constructor_regex等按类型名拼接的正则提到逐行循环外,每个文件只构建一次has_annotation改为对固定注解集合使用预编译表Logs or diagnostic output / 日志或诊断输出
Checklist / 提交前检查