perf: Avoid repeated test discovery for resolved test items - #1916
Conversation
There was a problem hiding this comment.
Pull request overview
Optimizes test discovery by caching resolved test-item children while supporting explicit invalidation.
Changes:
- Coalesces concurrent resolutions and tracks invalidation versions.
- Reuses resolved project/class children.
- Forces refresh after manual and classpath updates, with new tests.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/controller/testController.ts |
Adds resolution caching, coalescing, and invalidation. |
src/controller/testItemDataCache.ts |
Tracks resolution versions. |
src/controller/utils.ts |
Invalidates and synchronizes document test items. |
src/commands/testExplorerCommands.ts |
Forces discovery during refreshes. |
test/suite/testController.loadChildren.test.ts |
Tests resolution reuse and cancellation. |
test/suite/controllerUtils.updateItemForDocument.test.ts |
Tests document-update invalidation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
src/controller/testController.ts:109
- If this project resolution is invalidated and there is no concurrent waiter,
loadChildrencompletes successfully while the project remains unresolved.expandTestsimmediately traverses the current children after awaiting it, so a document update racing initial discovery can leave the run with only a partial project tree and omit tests. Keep the outerloadChildrencall retrying the latest version unless its token was cancelled.
if (token?.isCancellationRequested || resolutionVersion !== getResolutionVersion(item)) {
return;
src/controller/testController.ts:121
- A class resolution invalidated by a concurrent forced subtree refresh also returns as if resolution succeeded. Its caller can then continue with missing or stale methods even though
canResolveChildrenis still true. Retry the current class version in the outerloadChildrenloop unless cancellation caused the return.
if (token?.isCancellationRequested || resolutionVersion !== getResolutionVersion(item)) {
return;
Local performance validationValidated the latest PR commit MethodThe benchmark used the same VS Code session for both measurements:
The collapse assertion ensures the cached result is not measuring a project that was already expanded. All 10 automation steps passed. Results
Relevant machine-readable step output: Environment
This validates that the Project-resolution cache remains effective after the latest concurrency and metadata-consistency changes. As expected, the first discovery remains expensive; batch Class/method discovery is separate follow-up work.
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/controller/utils.ts:246
findTestTypesAndMethodscan return a still-testable class withchildren: nullafter its last test method is removed: the Java model leavesJavaTestItem.childrenunset when there are no children.synchronizeItemsRecursivelyskips that falsy value, retaining the old method items, and this then marks the class resolved so those stale methods are reused indefinitely. Normalize missing children to an empty list before marking the class resolved.
markTestClassesResolvedRecursively(testTypeItem);
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/controller/testController.ts:82
- A caller with its own cancellation token can get stuck here behind a shared discovery started by another caller without that token. For example, cancelling a test run while it is coalesced with the Test Explorer's long-running project scan will not let the run return until that scan finishes. Race the shared promise with this caller's cancellation event, while leaving the shared promise in the map for its owner and other waiters.
await pendingResolution;
src/controller/testController.ts:72
- The file-deletion watcher removes items directly but does not invalidate the owning project's resolution version. If a project discovery computed its result before the deletion and completes afterward, it can reinsert the deleted class and mark the project resolved; this new early return then makes that stale class persist on subsequent runs. Invalidate the project (and relevant in-flight class resolutions) in
onDidDeletebefore mutating the tree, as the document-update path now does.
} else if (!item.canResolveChildren) {
return;
Changyong Gong (chagong)
left a comment
There was a problem hiding this comment.
One cache-invalidation race remains in the file-deletion path; details are inline.
Summary
This reduces repeated discovery work on subsequent runs. It does not change the initial JUnit 5 discovery cost in the language server, which is tracked separately in eclipse-jdtls/eclipse.jdt.ls#3870.
Related to #1915
Testing
npm run compilenpm run lintnpm test: the new tests pass; the suite still reports the same 12 pre-existing failures from unavailable Java delegate handlers and the existingenqueueTestCasesexport mismatch