From c250dccc05072e63f3bc4a168c20459bc1089ea9 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 4 Sep 2026 11:05:28 +0200 Subject: [PATCH 1/3] feat: Optimize extensionQuery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: - Root cause found: LocalVSCodeService.extensionQuery (backing /vscode/gallery/extensionquery) unconditionally fetched every active version — all pre-releases included, unbounded by default — for every matched extension, solely to derive the single "latest version" for display, even when the client's query flags requested no version details at all. The code even carried a comment acknowledging "extensionquery is really slow." - Fix: Added a target-platform-aware bulk "latest version per extension" query (ExtensionVersionJooqRepository.findLatest(ids, targetPlatform) / RepositoryService.findLatestVersions(ids, targetPlatform)), mirroring the efficient CROSS APPLY pattern already used by the REST search path (LocalRegistryService). LocalVSCodeService.extensionQuery now: (1) only fetches the full active-version list when a flag actually needs it (FLAG_INCLUDE_VERSIONS/FLAG_INCLUDE_VERSION_PROPERTIES/FLAG_INCLUDE_LATEST_VERSION_ONLY), and (2) always computes the "latest" metadata via the new direct DB query instead of pulling the full history into Java and sorting it. - Verified: full ./gradlew test suite passes (had to update mocks in VSCodeAPITest, LocalVSCodeServiceTest, and add coverage in RepositoryServiceSmokeTest for the new repository method). --- .../openvsx/adapter/LocalVSCodeService.java | 20 ++++++++++++------- .../ExtensionVersionJooqRepository.java | 6 +++++- .../repositories/RepositoryService.java | 4 ++++ .../adapter/LocalVSCodeServiceTest.java | 1 + .../openvsx/adapter/VSCodeAPITest.java | 5 +++++ .../RepositoryServiceSmokeTest.java | 1 + 6 files changed, 29 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java b/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java index 487010a52..ed9818bdd 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java @@ -213,8 +213,18 @@ public ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaul // see https://github.com/eclipse/openvsx/issues/1394 var extensionsMap = extensionsList.stream() .collect(Collectors.toMap(Extension::getId, Function.identity(), (a, b) -> a)); - List allActiveExtensionVersions = repositories - .findActiveExtensionVersions(extensionsMap.keySet(), targetPlatform, maxPreReleaseVersions); + + // The full active version list (all pre-releases included, unless capped) is only needed to populate + // the response's per-extension version list, which itself is only included for these flags. Skipping + // the fetch otherwise avoids pulling an extension's entire (potentially unbounded) version history + // just to compute "latest", which repositories.findLatestVersions below does directly in the database. + var needsVersionList = test(flags, FLAG_INCLUDE_LATEST_VERSION_ONLY) + || test(flags, FLAG_INCLUDE_VERSIONS) + || test(flags, FLAG_INCLUDE_VERSION_PROPERTIES); + List allActiveExtensionVersions = needsVersionList + ? repositories + .findActiveExtensionVersions(extensionsMap.keySet(), targetPlatform, maxPreReleaseVersions) + : Collections.emptyList(); List extensionVersions; if (test(flags, FLAG_INCLUDE_LATEST_VERSION_ONLY)) { @@ -262,11 +272,7 @@ public ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaul fileResources = Collections.emptyMap(); } - var latestVersions = allActiveExtensionVersions.stream() - .collect(Collectors.groupingBy(ev -> ev.getExtension().getId())) - .values() - .stream() - .map(list -> versions.getLatest(list, false)) + var latestVersions = repositories.findLatestVersions(extensionsMap.keySet(), targetPlatform).stream() .collect(Collectors.toMap(ev -> ev.getExtension().getId(), ev -> ev)); var extensionQueryResults = new ArrayList(); diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionJooqRepository.java b/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionJooqRepository.java index a6a1adc75..33b4717d8 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionJooqRepository.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/ExtensionVersionJooqRepository.java @@ -1014,7 +1014,11 @@ public Map findLatestIsPreview(Collection extensionIds) { } public List findLatest(Collection extensionIds) { - var latestQuery = findLatestQuery(null, false, true); + return findLatest(extensionIds, null); + } + + public List findLatest(Collection extensionIds, String targetPlatform) { + var latestQuery = findLatestQuery(targetPlatform, false, true); latestQuery.addSelect( EXTENSION_VERSION.ID, EXTENSION_VERSION.VERSION, diff --git a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java index 933d1a776..a0a109e02 100644 --- a/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java +++ b/server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java @@ -970,6 +970,10 @@ public List findLatestVersions(Collection extensionIds) return extensionVersionJooqRepo.findLatest(extensionIds); } + public List findLatestVersions(Collection extensionIds, String targetPlatform) { + return extensionVersionJooqRepo.findLatest(extensionIds, targetPlatform); + } + public Map findLatestVersionsIsPreview(Collection extensionIds) { return extensionVersionJooqRepo.findLatestIsPreview(extensionIds); } diff --git a/server/src/test/java/org/eclipse/openvsx/adapter/LocalVSCodeServiceTest.java b/server/src/test/java/org/eclipse/openvsx/adapter/LocalVSCodeServiceTest.java index 8ffe03717..529819770 100644 --- a/server/src/test/java/org/eclipse/openvsx/adapter/LocalVSCodeServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/adapter/LocalVSCodeServiceTest.java @@ -74,6 +74,7 @@ void testDuplicateExtensionsInSearch() { .thenReturn(List.of(extension, extension)); Mockito.when(repositories.findActiveExtensionVersions(any(), any(), anyInt())) .thenReturn(List.of(extensionVersion)); + Mockito.when(repositories.findLatestVersions(any(), any())).thenReturn(List.of(extensionVersion)); Mockito.when(versions.getLatest(anyList(), anyBoolean())).thenReturn(extensionVersion); var result = vsCodeService.extensionQuery(param, 10); diff --git a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java index 448644a7a..0fa121926 100644 --- a/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java +++ b/server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java @@ -1120,6 +1120,11 @@ private void mockExtensionVersions( anyInt())) .thenReturn(extVersions); + var latest = extVersions.stream().min(ExtensionVersion.SORT_COMPARATOR).orElse(null); + Mockito + .when(repositories.findLatestVersions(eq(Set.of(extension.getId())), eq(queryTargetPlatform))) + .thenReturn(latest != null ? List.of(latest) : Collections.emptyList()); + mockFileResources(extVersions); } diff --git a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java index 3ac51042a..e9fadd89a 100644 --- a/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java +++ b/server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java @@ -339,6 +339,7 @@ void testExecuteQueries() { () -> repositories.findActivePersonalAccessTokensAndType(userData, PersonalAccessTokenType.LLT), () -> repositories.findAllPersonalAccessTokensByVersion(0), () -> repositories.findLatestVersions(List.of(1L)), + () -> repositories.findLatestVersions(List.of(1L), "targetPlatform"), () -> repositories.hasSameVersion(extVersion), () -> repositories.hasActiveReview(extension, userData), () -> repositories.findLatestVersionsIsPreview(List.of(1L)), From e69cdaf8ed21c1f96ce0cb6472fbb7ad7255bfad Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 7 Sep 2026 14:28:37 +0200 Subject: [PATCH 2/3] fix: address extensionQuery review feedback - Trim the version-list comment to state the constraint, not narrate the code. - Reuse needsVersionList instead of re-testing the same flags in the branch below. - Skip the extra findLatestVersions DB round-trip when the active-version list was already fetched uncapped, since it's then a complete source for "latest"; keep the direct query when maxPreReleaseVersions caps pre-releases, since a capped list can miss the true latest for the requested target platform. Co-Authored-By: Claude Sonnet 5 --- .../openvsx/adapter/LocalVSCodeService.java | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java b/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java index ed9818bdd..67b53a5ac 100644 --- a/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java +++ b/server/src/main/java/org/eclipse/openvsx/adapter/LocalVSCodeService.java @@ -214,10 +214,8 @@ public ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaul var extensionsMap = extensionsList.stream() .collect(Collectors.toMap(Extension::getId, Function.identity(), (a, b) -> a)); - // The full active version list (all pre-releases included, unless capped) is only needed to populate - // the response's per-extension version list, which itself is only included for these flags. Skipping - // the fetch otherwise avoids pulling an extension's entire (potentially unbounded) version history - // just to compute "latest", which repositories.findLatestVersions below does directly in the database. + // Version details (and thus the potentially unbounded active-version history) are only needed + // for these flags; "latest" is computed separately below without fetching this list. var needsVersionList = test(flags, FLAG_INCLUDE_LATEST_VERSION_ONLY) || test(flags, FLAG_INCLUDE_VERSIONS) || test(flags, FLAG_INCLUDE_VERSION_PROPERTIES); @@ -234,7 +232,7 @@ public ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaul .stream() .map(list -> versions.getLatest(list, true)) .collect(Collectors.toList()); - } else if (test(flags, FLAG_INCLUDE_VERSIONS) || test(flags, FLAG_INCLUDE_VERSION_PROPERTIES)) { + } else if (needsVersionList) { extensionVersions = allActiveExtensionVersions; } else { extensionVersions = Collections.emptyList(); @@ -272,8 +270,21 @@ public ExtensionQueryResult extensionQuery(ExtensionQueryParam param, int defaul fileResources = Collections.emptyMap(); } - var latestVersions = repositories.findLatestVersions(extensionsMap.keySet(), targetPlatform).stream() - .collect(Collectors.toMap(ev -> ev.getExtension().getId(), ev -> ev)); + // ponytail: reuse the already-fetched list for "latest" only when it's uncapped (complete) - + // a pre-release cap ranks across all target platforms combined, so a capped list can miss the + // true latest for this platform; that case still queries the database directly. + Map latestVersions; + if (needsVersionList && maxPreReleaseVersions < 0) { + latestVersions = allActiveExtensionVersions.stream() + .collect(Collectors.groupingBy(ev -> ev.getExtension().getId())) + .values() + .stream() + .map(list -> versions.getLatest(list, false)) + .collect(Collectors.toMap(ev -> ev.getExtension().getId(), ev -> ev)); + } else { + latestVersions = repositories.findLatestVersions(extensionsMap.keySet(), targetPlatform).stream() + .collect(Collectors.toMap(ev -> ev.getExtension().getId(), ev -> ev)); + } var extensionQueryResults = new ArrayList(); for (var extension : extensionsList) { From ba69ba1fc9570ea9acb9a58f7a8121cd7b4975ec Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 7 Sep 2026 14:37:44 +0200 Subject: [PATCH 3/3] test: cover extensionQuery latest-version fetch strategy Pin down the optimization from the two preceding commits: no fetch of findActiveExtensionVersions when no version-list flag is requested, reuse of the fetched list for "latest" when uncapped, and a direct findLatestVersions query when the list is capped (since a capped list can miss the true latest for the target platform). Co-Authored-By: Claude Sonnet 5 --- .../adapter/LocalVSCodeServiceTest.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/server/src/test/java/org/eclipse/openvsx/adapter/LocalVSCodeServiceTest.java b/server/src/test/java/org/eclipse/openvsx/adapter/LocalVSCodeServiceTest.java index 529819770..5a388ff73 100644 --- a/server/src/test/java/org/eclipse/openvsx/adapter/LocalVSCodeServiceTest.java +++ b/server/src/test/java/org/eclipse/openvsx/adapter/LocalVSCodeServiceTest.java @@ -14,6 +14,7 @@ import java.util.List; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; @@ -81,6 +82,76 @@ void testDuplicateExtensionsInSearch() { assertThat(result.results()).hasSize(1); } + @AfterEach + void resetMaxPreReleaseVersions() { + vsCodeService.maxPreReleaseVersions = -1; + } + + @Test + void testExtensionQuery_noVersionFlags_skipsActiveVersionsFetch() { + var extension = mockExtension(); + var extensionVersion = mockExtensionVersion(extension, 1, "0.1.0", "linux"); + + var criterion = new ExtensionQueryParam.Criterion(Criterion.FILTER_EXTENSION_ID, "test-1"); + var filter = new ExtensionQueryParam.Filter(List.of(criterion), 0, 0, 0, 0); + var param = new ExtensionQueryParam(List.of(filter), 0); + + Mockito.when(repositories.findActiveExtensionsByPublicId(any(), any())) + .thenReturn(List.of(extension)); + Mockito.when(repositories.findLatestVersions(any(), any())).thenReturn(List.of(extensionVersion)); + + vsCodeService.extensionQuery(param, 10); + + Mockito.verify(repositories, Mockito.never()).findActiveExtensionVersions(any(), any(), anyInt()); + Mockito.verify(repositories, Mockito.times(1)).findLatestVersions(any(), any()); + } + + @Test + void testExtensionQuery_versionsFlagUncapped_reusesFetchedListForLatest() { + assertThat(vsCodeService.maxPreReleaseVersions).isNegative(); + + var extension = mockExtension(); + var extensionVersion = mockExtensionVersion(extension, 1, "0.1.0", "linux"); + + var criterion = new ExtensionQueryParam.Criterion(Criterion.FILTER_EXTENSION_ID, "test-1"); + var filter = new ExtensionQueryParam.Filter(List.of(criterion), 0, 0, 0, 0); + var param = new ExtensionQueryParam(List.of(filter), FLAG_INCLUDE_VERSIONS); + + Mockito.when(repositories.findActiveExtensionsByPublicId(any(), any())) + .thenReturn(List.of(extension)); + Mockito.when(repositories.findActiveExtensionVersions(any(), any(), anyInt())) + .thenReturn(List.of(extensionVersion)); + Mockito.when(versions.getLatest(anyList(), eq(false))).thenReturn(extensionVersion); + + vsCodeService.extensionQuery(param, 10); + + Mockito.verify(repositories, Mockito.times(1)).findActiveExtensionVersions(any(), any(), anyInt()); + Mockito.verify(repositories, Mockito.never()).findLatestVersions(any(), any()); + } + + @Test + void testExtensionQuery_versionsFlagCapped_queriesLatestDirectly() { + vsCodeService.maxPreReleaseVersions = 5; + + var extension = mockExtension(); + var extensionVersion = mockExtensionVersion(extension, 1, "0.1.0", "linux"); + + var criterion = new ExtensionQueryParam.Criterion(Criterion.FILTER_EXTENSION_ID, "test-1"); + var filter = new ExtensionQueryParam.Filter(List.of(criterion), 0, 0, 0, 0); + var param = new ExtensionQueryParam(List.of(filter), FLAG_INCLUDE_VERSIONS); + + Mockito.when(repositories.findActiveExtensionsByPublicId(any(), any())) + .thenReturn(List.of(extension)); + Mockito.when(repositories.findActiveExtensionVersions(any(), any(), anyInt())) + .thenReturn(List.of(extensionVersion)); + Mockito.when(repositories.findLatestVersions(any(), any())).thenReturn(List.of(extensionVersion)); + + vsCodeService.extensionQuery(param, 10); + + Mockito.verify(repositories, Mockito.times(1)).findActiveExtensionVersions(any(), any(), anyInt()); + Mockito.verify(repositories, Mockito.times(1)).findLatestVersions(any(), any()); + } + // ---------- UTILITY ----------// private Extension mockExtension() {