eclipse-jdt / eclipse-jdt/eclipse.jdt.core
`JavaProject.computePackageFragmentRoots()` scales quadratically with classpath size
- Dominant language
- Java
- Stars
- 237
- Forks
- 195
- Avg merge
- 1d 10h
- Merged PRs (30d)
- 49
Description
During analysis of two heapdumps shared [here](https://github.com/eclipse-pde/eclipse.pde/pull/2253#issuecomment-4054961583) the following issue was discovered as a hotspot that can benefit from optimization:
### Performance Data
| Metric | WITH transitive | WITHOUT transitive | Ratio |
|--------|----------------:|-------------------:|------:|
| Self time (µs) | 13,996,125 | 958,035 | **14.6×** |
### Description
`computePackageFragmentRoots()` iterates over all resolved classpath entries and, for each `CPE_PROJECT` entry, recursively calls `getResolvedClasspath()` and re-enters `computePackageFragmentRoots()` on the required project:
```java
case IClasspathEntry.CPE_PROJECT:
JavaProject requiredProject = (JavaProject) JavaCore.create(requiredProjectRsc);
requiredProject.computePackageFragmentRoots(
requiredProject.getResolvedClasspath(), // re-resolves classpath
accumulatedRoots, rootIDs, ...);
```
This is the single most expensive regression, showing a **14.6× slowdown**. The super-linear scaling (much worse than the linear increase in classpath entries) suggests that the recursive expansion visits the same project multiple times from different transitive paths before the `rootIDs.contains()` guard triggers. Additionally, each recursive step calls `getResolvedClasspath()`, which itself does classpath resolution.
### Suggested Fix
1. **Cache package fragment roots per project**: Once a project's roots are computed, they shouldn't need recomputation. A per-build or per-operation cache mapping `(project, excludeTestCode)` → `IPackageFragmentRoot[]` would eliminate redundant traversals.
2. **Restructure the recursion**: Instead of recursing depth-first, gather all unique project dependencies first (breadth-first), then compute roots for each exactly once.
3. **Profile interaction with `getResolvedClasspath()`**: Each call to `getResolvedClasspath()` involves its own resolution overhead. Consider caching resolved classpaths more aggressively during build operations.
Contributor guide
Assessment
This issue has not been assessed yet.