From ef2a349515e9f0dc483abc363ddb6e935327edd1 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Mon, 10 Aug 2026 13:57:15 +0200 Subject: [PATCH] Detect plugins lazily when loading a site from disk PlatformConfiguration.reconcile() calls SiteEntry.loadFromDisk(), which eagerly scanned the plugins directory and opened every jar on the site to read its manifest. On the IDE startup path this runs synchronously on the UI thread: WorkbenchActionBuilder asks for bundle groups to decide whether to contribute the Welcome and Tips and Tricks actions, which builds the PlatformConfiguration and triggers the scan. Only feature entries are ever consulted on that path, so the jar scan is wasted work and cost seconds on installations with many plugins. Drop the eager detectPlugins() call. Every consumer of the plugin data already null-checks pluginEntries and detects on demand, and the persistence path (SiteEntry.toXML) writes only site attributes and feature entries, so nothing is lost from platform.xml. The change stamp bookkeeping is untouched, and a deferred scan sees pluginEntries == null exactly as the eager one did, so it still performs a full scan rather than a timestamp comparison. --- .../META-INF/MANIFEST.MF | 1 + .../tests/AutomatedConfiguratorSuite.java | 2 + .../configurator/tests/SiteEntryTests.java | 95 +++++++++++++++++++ .../internal/configurator/SiteEntry.java | 2 +- 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 update/org.eclipse.update.configurator.tests/src/org/eclipse/update/internal/configurator/tests/SiteEntryTests.java diff --git a/update/org.eclipse.update.configurator.tests/META-INF/MANIFEST.MF b/update/org.eclipse.update.configurator.tests/META-INF/MANIFEST.MF index 8055e8a6da0..dc96ac8a45c 100644 --- a/update/org.eclipse.update.configurator.tests/META-INF/MANIFEST.MF +++ b/update/org.eclipse.update.configurator.tests/META-INF/MANIFEST.MF @@ -14,4 +14,5 @@ Export-Package: org.eclipse.update.configurator.tests, org.eclipse.update.internal.configurator.tests Eclipse-BundleShape: dir Import-Package: org.junit.jupiter.api;version="[5.14.0,6.0.0)", + org.junit.jupiter.api.io;version="[5.14.0,6.0.0)", org.junit.platform.suite.api;version="[1.14.0,2.0.0)" diff --git a/update/org.eclipse.update.configurator.tests/src/org/eclipse/update/configurator/tests/AutomatedConfiguratorSuite.java b/update/org.eclipse.update.configurator.tests/src/org/eclipse/update/configurator/tests/AutomatedConfiguratorSuite.java index f0f8abd84fd..6c6e2cbaf54 100644 --- a/update/org.eclipse.update.configurator.tests/src/org/eclipse/update/configurator/tests/AutomatedConfiguratorSuite.java +++ b/update/org.eclipse.update.configurator.tests/src/org/eclipse/update/configurator/tests/AutomatedConfiguratorSuite.java @@ -14,6 +14,7 @@ package org.eclipse.update.configurator.tests; import org.eclipse.update.internal.configurator.tests.FeatureEntryTests; +import org.eclipse.update.internal.configurator.tests.SiteEntryTests; import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.Suite; @@ -25,6 +26,7 @@ @Suite @SelectClasses({ // FeatureEntryTests.class, // + SiteEntryTests.class, // }) public class AutomatedConfiguratorSuite { diff --git a/update/org.eclipse.update.configurator.tests/src/org/eclipse/update/internal/configurator/tests/SiteEntryTests.java b/update/org.eclipse.update.configurator.tests/src/org/eclipse/update/internal/configurator/tests/SiteEntryTests.java new file mode 100644 index 00000000000..f50599aa16a --- /dev/null +++ b/update/org.eclipse.update.configurator.tests/src/org/eclipse/update/internal/configurator/tests/SiteEntryTests.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.update.internal.configurator.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.jar.Attributes; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +import org.eclipse.update.internal.configurator.Configuration; +import org.eclipse.update.internal.configurator.FeatureEntry; +import org.eclipse.update.internal.configurator.PluginEntry; +import org.eclipse.update.internal.configurator.SiteEntry; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +@SuppressWarnings("restriction") +public class SiteEntryTests { + + @TempDir + Path siteRoot; + + @Test + public void testLoadFromDiskDetectsFeaturesWithoutScanningPlugins() throws Exception { + createFeature("test.feature", "1.0.0"); + Path pluginJar = createPluginJar("test.plugin", "1.0.0"); + + SiteEntry site = createSite(); + site.loadFromDisk(0); + + FeatureEntry[] features = site.getFeatureEntries(); + assertEquals(1, features.length); + assertEquals("test.feature", features[0].getFeatureIdentifier()); + + // the jar is untouched so far, deleting it now leaves an eager scan nothing to have found + Files.delete(pluginJar); + assertEquals(0, site.getAllPluginEntries().length); + } + + @Test + public void testPluginsAreDetectedOnFirstAccess() throws Exception { + createFeature("test.feature", "1.0.0"); + createPluginJar("test.plugin", "1.0.0"); + + SiteEntry site = createSite(); + site.loadFromDisk(0); + + PluginEntry[] plugins = site.getAllPluginEntries(); + assertEquals(1, plugins.length); + assertEquals("test.plugin", plugins[0].getPluginIdentifier()); + assertEquals("1.0.0", plugins[0].getPluginVersion()); + } + + private SiteEntry createSite() throws Exception { + SiteEntry site = new SiteEntry(siteRoot.toUri().toURL()); + site.setConfig(new Configuration()); + return site; + } + + private void createFeature(String id, String version) throws Exception { + Path featureDir = Files.createDirectories(siteRoot.resolve("features").resolve(id + "_" + version)); + Files.writeString(featureDir.resolve("feature.xml"), "\n" // + + "\n"); + } + + private Path createPluginJar(String id, String version) throws Exception { + Path pluginsDir = Files.createDirectories(siteRoot.resolve("plugins")); + Path jar = pluginsDir.resolve(id + "_" + version + ".jar"); + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); + attributes.putValue("Bundle-ManifestVersion", "2"); + attributes.putValue("Bundle-SymbolicName", id); + attributes.putValue("Bundle-Version", version); + try (OutputStream out = Files.newOutputStream(jar); JarOutputStream jarOut = new JarOutputStream(out, manifest)) { + // the manifest written by the stream is all the detection needs + } + return jar; + } +} diff --git a/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/SiteEntry.java b/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/SiteEntry.java index 26661f79aa8..9761210ed74 100644 --- a/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/SiteEntry.java +++ b/update/org.eclipse.update.configurator/src/org/eclipse/update/internal/configurator/SiteEntry.java @@ -674,7 +674,7 @@ public void loadFromDisk(long lastChange) throws CoreException{ featuresChangeStamp = lastChange; pluginsChangeStamp = lastChange; detectFeatures(); - detectPlugins(); + // plugins are detected on first access, the scan opens every jar on the site } /**