eclipse-jdt / eclipse-jdt/eclipse.jdt.core

`JavaProject.computePackageFragmentRoots()` scales quadratically with classpath size

Open
#4,929 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.