Skip to content

feat: navigate into Maven dependency jars, with less memory - #3

Merged
1lck merged 2 commits into
mainfrom
feat/dependency-navigation
Nov 13, 2026
Merged

1lck merged 2 commits into
mainfrom
feat/dependency-navigation

Conversation

@1lck

@1lck 1lck commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

为什么

上一轮实测:插件在 Nacos 上能跳 95%,ruoyi-vue-pro 只有约 72%。跳不了的大多是依赖库里的声明,比如 Swagger 的 @Schema(description = ...)、Lombok、校验注解、Hutool 和 MyBatis-Plus 的方法。这个 PR 让插件能跳进 Maven 依赖库,同时把内存压下来。

改动

跳进 Maven 依赖库(新包 internal/deps)

  • maven.go:读工作区每个 pom.xml 和它的父 POM、导入的 BOM,再沿本地仓库(~/.m2/settings.xml 的 localRepository,默认 ~/.m2/repository)里的 POM 找传递依赖。规则和 Maven 一致:

    • 最近的声明优先;
    • 根模块的 dependencyManagement 决定版本;
    • test/provided/可选依赖不传递,exclusions 生效;
    • 工作区自己的模块不当成依赖;
    • 能读 ISO-8859-1 编码的 POM(很多 Apache 的 POM 是这种编码)。

    不运行 Maven、不联网,本地仓库没有的 jar 直接跳过。

  • jars.go:并行列出每个 jar 的目录。某个类第一次被查到时才读取:有 -sources.jar 就读源码,否则从 .class 生成签名存根。跳进去时写一份只读副本到 ${stateDir}/dependency-sources/<坐标>-<jar 身份哈希>/,不写本地仓库,也不写插件包。

  • classfile.go、signature.go、stub.go:读取 class 文件,生成 Java 存根,包括:

    • 泛型签名、成员类(含非静态内部类的构造器)、枚举、record、注解(带默认值);
    • 常量值;
    • 参数名取自调试信息(LocalVariableTable 或 MethodParameters),没有时用 arg0;
    • 和 java.lang 同名的同包类写全限定名。
  • version.go:按 Maven 的规则比较版本,版本是范围、或者引用了不认识的属性时,取本地已有的最高版本。

接入(workspace、codenav、lsp、main)

  • 扫描时和 JDK 并行读取依赖;pom.xml 变了、或距上次读取超过 5 分钟,就重新读取。JDK 和依赖合成一个库,同一个类出现多次时取第一个:JDK 在前,依赖按 classpath 顺序。
  • codenav.Library:库路径可以是 <jar 编号>!a/b/C.java。类名索引的键改成路径的子串,去掉了 srcs 表,15 万个类时少占约 10 MiB。
  • 新增 --no-dependencies 开关,可以关掉依赖库跳转。

其他命中率改进

  • 模块的 target/generated-sources、target/generated-test-sources 和 Gradle 的 build/generated 也进索引,即使被 .gitignore 忽略(JDT LS 也会索引这些)。
  • 字符串字面量和文本块、类字面量、Iface.super.m()、Lombok 日志注解生成的 log 字段(@Slf4j 对应 org.slf4j.Logger)都能推出类型,后面的调用能继续跳。

内存

  • 建索引时每个工作线程复用分词缓冲和读文件缓冲。Nacos 上建索引一共分配的内存从 198 MB 降到 86 MB。
  • 大扫描结束后把空闲内存还给系统(debug.FreeOSMemory)。
  • 服务器默认 GOGC=25(用户设置了 GOGC 时以用户为准)。不带依赖时,Nacos 跑完实测从 68 MiB 降到 47 MiB,延迟只多 0.01 ms。

工具与文档

  • cmd/nav-bench:README 里的数字都是用它测的。对真实项目随机取标识符,测命中率、延迟和内存,可以同时测 JDT LS。
  • 更新了 README(功能、实现方式、实测、和 JDT LS 对比、限制)、docs/releases/v0.0.1.md(0.0.1 还没发布)、codenav/UPSTREAM.md 和 doc.go。
  • 新增 .gitattributes,让测试数据在 Windows 上按原字节检出。

实测(Apple M4,各 1500 个随机位置)

本 PR 改动前 JDT LS 1.60
Nacos 能跳的比例(3 个种子) 98.7 / 98.2 / 99.3% 95.1%(种子 2) 98.8 / 98.5 / 99.5%
和 JDT LS 跳到同一处 1494 / 1494 / 1493 — —
ruoyi-vue-pro 能跳的比例 98.4 / 98.5% 70.9%(种子 2) —
跳转 p50 0.11–0.18 ms 0.11 ms 3.0–3.9 ms
第一次跳转 Nacos 0.24–0.42 s,ruoyi 0.46–0.50 s 0.11–0.18 s 导入 59–61 s
查找引用 p50 61–94 ms 57–90 ms 128–211 ms
内存(物理占用) Nacos 62–69 MiB,ruoyi 91 MiB 68 MiB / 约 80 MiB 约 3 GB

测试

  • go test ./... 和 go test -race ./... 全部通过。
  • 新增测试:
    • 依赖存根和 golden 文件逐字节比对,测试用的 jar 由 javac 编出后提交;
    • 存根解析出的声明正确;
    • 有 sources jar 时读源码;
    • 副本只读、能从副本映射回库路径、不写 jar;
    • 截断或改坏的 class 文件不会导致崩溃;
    • Maven 依赖解析(BOM、父 POM、最近优先、scope、optional、exclusions、版本取最高);
    • 能读 Latin-1 编码的 POM、能识别 settings.xml 里的仓库位置;
    • 版本排序;
    • workspace 端到端:从项目跳进依赖存根,在存根里继续跳,查找引用,确认本地仓库前后哈希一致;
    • 修改 pom 后新依赖生效;
    • 生成代码目录被列出;
    • codenav 的字面量、类字面量、super、log 推断,以及同名类取第一个。
  • codenav 的新测试在旧代码上会失败(已验证)。

不在本 PR

  • Gradle 项目的依赖库。
  • 泛型和 lambda 参数的类型推断:剩下的差异几乎都是这一类,比如 list.get(i).split()、比较器里的 o1.getX()、v -> v.getVersion()。

🤖 Generated with Claude Code

1lck and others added 2 commits November 12, 2026 19:43
Names the working tree does not declare now resolve into the project's
Maven dependencies, not only the JDK. Measured with the new nav-bench on
1,500 random identifiers per seed: Nacos 98.2-99.3% resolve (JDT LS
98.5-99.5%), 1493-1494 of 1500 to the same place as JDT LS; ruoyi-vue-pro
98.4-98.5% (was about 72%).

- internal/deps reads the working tree's pom.xml files with their parents
  and imported BOMs and follows dependencies through the local
  repository as Maven mediates them (nearest wins, root management sets
  versions, test/provided/optional are not transitive, exclusions apply),
  without running Maven or downloading anything.
- Only each jar's directory is listed up front. A class is read the first
  time a lookup needs it: from the -sources.jar when it was downloaded,
  else as a stub of signatures written from its class files (member
  classes, constants, annotation defaults, parameter names from the
  debug information). Copies go read-only under the state directory.
- Sources generated into target/generated-sources and Gradle's
  build/generated are indexed although .gitignore leaves them out.
- String and class literals, Iface.super and Lombok log fields type the
  calls on them, so "a".trim(), Foo.class.getName(), log.info(...) and
  Iface.super.m() resolve.
- Memory: indexing reuses token and read buffers (198 MB allocated on
  Nacos down to 86 MB), returns the scan's garbage to the system, and the
  server runs with GOGC=25 unless GOGC is set (68 MiB footprint down to
  47 MiB without dependencies). Library paths are short and keyed by a
  substring of themselves, so 154,000 dependency classes cost about
  20 MiB; with dependencies Nacos ends at 62-69 MiB, ruoyi at 91 MiB
  (JDT LS: 3 GB).
- cmd/nav-bench is the measuring tool behind the README's numbers.

--no-dependencies turns dependency jars off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@1lck
1lck merged commit be7f50b into main Nov 13, 2026
3 checks passed
@1lck
1lck deleted the feat/dependency-navigation branch November 13, 2026 03:46
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.

1 participant