Skip to content

Commit 6a26333

Browse files
committed
Add ChainedReexportPerformanceTest
This test verifies performance in a chained re-export scenario (A->B->C->D, all re-exporting and exporting many packages) to ensure O(N) scaling and avoid redundant processing.
1 parent 0ef3dd3 commit 6a26333

File tree

4 files changed

+267
-51
lines changed

4 files changed

+267
-51
lines changed

ui/org.eclipse.pde.core/src/org/eclipse/pde/internal/core/RequiredPluginsClasspathContainer.java

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ private List<IClasspathEntry> computePluginEntriesByModel() throws CoreException
219219
return List.of();
220220
}
221221

222-
Map<BundleDescription, Set<Rule>> map = retrieveVisiblePackagesFromState(desc);
222+
Map<BundleDescription, List<Rule>> map = retrieveVisiblePackagesFromState(desc);
223223

224224
// Add any library entries contributed via classpath contributor
225225
// extension (Bug 363733)
@@ -331,8 +331,8 @@ private static synchronized Stream<IClasspathContributor> getClasspathContributo
331331
return Stream.concat(fClasspathContributors.stream(), PDECore.getDefault().getClasspathContributors());
332332
}
333333

334-
private Map<BundleDescription, Set<Rule>> retrieveVisiblePackagesFromState(BundleDescription desc) {
335-
Map<BundleDescription, Set<Rule>> visiblePackages = new HashMap<>();
334+
private Map<BundleDescription, List<Rule>> retrieveVisiblePackagesFromState(BundleDescription desc) {
335+
Map<BundleDescription, List<Rule>> visiblePackages = new HashMap<>();
336336
StateHelper helper = BundleHelper.getPlatformAdmin().getStateHelper();
337337
addVisiblePackagesFromState(helper, desc, visiblePackages);
338338
if (desc.getHost() != null) {
@@ -342,7 +342,7 @@ private Map<BundleDescription, Set<Rule>> retrieveVisiblePackagesFromState(Bundl
342342
}
343343

344344
private void addVisiblePackagesFromState(StateHelper helper, BundleDescription desc,
345-
Map<BundleDescription, Set<Rule>> visiblePackages) {
345+
Map<BundleDescription, List<Rule>> visiblePackages) {
346346
if (desc == null) {
347347
return;
348348
}
@@ -352,9 +352,11 @@ private void addVisiblePackagesFromState(StateHelper helper, BundleDescription d
352352
if (exporter == null) {
353353
continue;
354354
}
355-
Set<Rule> list = visiblePackages.computeIfAbsent(exporter, e -> new LinkedHashSet<>());
355+
List<Rule> list = visiblePackages.computeIfAbsent(exporter, e -> new ArrayList<>());
356356
Rule rule = getRule(helper, desc, export);
357-
list.add(rule);
357+
if (!list.contains(rule)) {
358+
list.add(rule);
359+
}
358360
}
359361
}
360362

@@ -366,7 +368,7 @@ private Rule getRule(StateHelper helper, BundleDescription desc, ExportPackageDe
366368
}
367369

368370
protected void addDependencyViaImportPackage(BundleDescription desc, Set<BundleDescription> added,
369-
Map<BundleDescription, Set<Rule>> map, List<IClasspathEntry> entries) throws CoreException {
371+
Map<BundleDescription, List<Rule>> map, List<IClasspathEntry> entries) throws CoreException {
370372
if (desc == null || !added.add(desc)) {
371373
return;
372374
}
@@ -384,12 +386,12 @@ protected void addDependencyViaImportPackage(BundleDescription desc, Set<BundleD
384386
}
385387

386388
private void addDependency(BundleDescription desc, Set<BundleDescription> added,
387-
Map<BundleDescription, Set<Rule>> map, List<IClasspathEntry> entries) throws CoreException {
389+
Map<BundleDescription, List<Rule>> map, List<IClasspathEntry> entries) throws CoreException {
388390
addDependency(desc, added, map, entries, true);
389391
}
390392

391393
private void addDependency(BundleDescription desc, Set<BundleDescription> added,
392-
Map<BundleDescription, Set<Rule>> map, List<IClasspathEntry> entries, boolean useInclusion)
394+
Map<BundleDescription, List<Rule>> map, List<IClasspathEntry> entries, boolean useInclusion)
393395
throws CoreException {
394396
if (desc == null || !added.add(desc)) {
395397
return;
@@ -432,7 +434,7 @@ private void addDependency(BundleDescription desc, Set<BundleDescription> added,
432434
}
433435
}
434436

435-
private boolean addPlugin(BundleDescription desc, boolean useInclusions, Map<BundleDescription, Set<Rule>> map,
437+
private boolean addPlugin(BundleDescription desc, boolean useInclusions, Map<BundleDescription, List<Rule>> map,
436438
List<IClasspathEntry> entries) throws CoreException {
437439
IPluginModelBase model = PluginRegistry.findModel((Resource) desc);
438440
if (model == null || !model.isEnabled()) {
@@ -460,7 +462,7 @@ private boolean addPlugin(BundleDescription desc, boolean useInclusions, Map<Bun
460462
return true;
461463
}
462464

463-
private List<Rule> getInclusions(Map<BundleDescription, Set<Rule>> map, IPluginModelBase model) {
465+
private List<Rule> getInclusions(Map<BundleDescription, List<Rule>> map, IPluginModelBase model) {
464466
BundleDescription desc = model.getBundleDescription();
465467
if (desc == null || "false".equals(System.getProperty("pde.restriction")) //$NON-NLS-1$ //$NON-NLS-2$
466468
|| !(fModel instanceof IBundlePluginModelBase) || TargetPlatformHelper.getTargetVersion() < 3.1) {
@@ -470,12 +472,12 @@ private List<Rule> getInclusions(Map<BundleDescription, Set<Rule>> map, IPluginM
470472
if (desc.getHost() != null) {
471473
desc = (BundleDescription) desc.getHost().getSupplier();
472474
}
473-
Set<Rule> rules = map.getOrDefault(desc, Set.of());
474-
return (rules.isEmpty() && !ClasspathUtilCore.hasBundleStructure(model)) ? null : new ArrayList<>(rules);
475+
List<Rule> rules = map.getOrDefault(desc, List.of());
476+
return (rules.isEmpty() && !ClasspathUtilCore.hasBundleStructure(model)) ? null : rules;
475477
}
476478

477479
private void addHostPlugin(HostSpecification hostSpec, Set<BundleDescription> added,
478-
Map<BundleDescription, Set<Rule>> map, List<IClasspathEntry> entries) throws CoreException {
480+
Map<BundleDescription, List<Rule>> map, List<IClasspathEntry> entries) throws CoreException {
479481
BaseDescription desc = hostSpec.getSupplier();
480482

481483
if (desc instanceof BundleDescription host) {
@@ -619,7 +621,7 @@ private void addJunit5RuntimeDependencies(Set<BundleDescription> added, List<ICl
619621
}
620622

621623
// add dependency with exclude all rule
622-
Map<BundleDescription, Set<Rule>> rules = Map.of(desc, Set.of());
624+
Map<BundleDescription, List<Rule>> rules = Map.of(desc, List.of());
623625
addPlugin(desc, true, rules, entries);
624626
}
625627
}
@@ -672,24 +674,21 @@ private void addExtraModel(BundleDescription desc, Set<BundleDescription> added,
672674
if (added.contains(bundleDesc)) {
673675
return;
674676
}
675-
Map<BundleDescription, Set<Rule>> rules = new HashMap<>();
677+
Map<BundleDescription, List<Rule>> rules = new HashMap<>();
676678
findExportedPackages(bundleDesc, desc, rules);
677679
addDependency(bundleDesc, added, rules, entries, true);
678680
}
679681
}
680682

681683
protected final void findExportedPackages(BundleDescription desc, BundleDescription projectDesc,
682-
Map<BundleDescription, Set<Rule>> map) {
684+
Map<BundleDescription, List<Rule>> map) {
683685
if (desc != null) {
684686
Queue<BundleDescription> queue = new ArrayDeque<>();
685687
queue.add(desc);
686688
while (!queue.isEmpty()) {
687689
BundleDescription bdesc = queue.remove();
688-
if (map.containsKey(bdesc)) {
689-
continue;
690-
}
691690
ExportPackageDescription[] expkgs = bdesc.getExportPackages();
692-
Set<Rule> rules = new LinkedHashSet<>();
691+
List<Rule> rules = new ArrayList<>();
693692
for (ExportPackageDescription expkg : expkgs) {
694693
boolean discouraged = restrictPackage(projectDesc, expkg);
695694
IPath path = IPath.fromOSString(expkg.getName().replace('.', '/') + "/*"); //$NON-NLS-1$
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package org.eclipse.pde.core.tests.internal.classpath;
2+
3+
import static org.junit.jupiter.api.Assertions.assertTrue;
4+
5+
import java.io.File;
6+
import java.io.FileOutputStream;
7+
import java.io.IOException;
8+
import java.nio.file.Files;
9+
import java.util.List;
10+
import java.util.jar.Attributes;
11+
import java.util.jar.JarOutputStream;
12+
import java.util.jar.Manifest;
13+
import java.util.zip.ZipEntry;
14+
15+
import org.eclipse.core.resources.IProject;
16+
import org.eclipse.core.resources.IWorkspaceDescription;
17+
import org.eclipse.core.resources.IncrementalProjectBuilder;
18+
import org.eclipse.core.resources.ResourcesPlugin;
19+
import org.eclipse.core.runtime.NullProgressMonitor;
20+
import org.eclipse.core.runtime.jobs.Job;
21+
import org.eclipse.jdt.core.IClasspathEntry;
22+
import org.eclipse.jdt.core.JavaCore;
23+
import org.eclipse.pde.core.plugin.IPluginModelBase;
24+
import org.eclipse.pde.core.plugin.PluginRegistry;
25+
import org.eclipse.pde.core.project.IBundleProjectDescription;
26+
import org.eclipse.pde.core.project.IBundleProjectService;
27+
import org.eclipse.pde.core.project.IRequiredBundleDescription;
28+
import org.eclipse.pde.core.target.ITargetDefinition;
29+
import org.eclipse.pde.core.target.ITargetLocation;
30+
import org.eclipse.pde.core.target.ITargetPlatformService;
31+
import org.eclipse.pde.internal.core.PDECore;
32+
import org.eclipse.pde.internal.ui.wizards.tools.UpdateClasspathJob;
33+
import org.eclipse.pde.ui.tests.runtime.TestUtils;
34+
import org.eclipse.pde.ui.tests.util.ProjectUtils;
35+
import org.eclipse.pde.ui.tests.util.TargetPlatformUtil;
36+
import org.eclipse.swt.widgets.Display;
37+
import org.junit.jupiter.api.AfterAll;
38+
import org.junit.jupiter.api.AfterEach;
39+
import org.junit.jupiter.api.BeforeAll;
40+
import org.junit.jupiter.api.BeforeEach;
41+
import org.junit.jupiter.api.Test;
42+
import org.osgi.framework.Version;
43+
44+
public class ChainedReexportPerformanceTest {
45+
46+
@BeforeAll
47+
public static void beforeAll() throws Exception {
48+
ProjectUtils.deleteAllWorkspaceProjects();
49+
}
50+
51+
@AfterAll
52+
public static void afterAll() throws Exception {
53+
// ProjectUtils.deleteAllWorkspaceProjects();
54+
}
55+
56+
private static final String CHAIN_PREFIX = "Chain_";
57+
private static final int PACKAGE_COUNT = 1000;
58+
private static final int BUNDLE_CHAIN_DEPTH = 5;
59+
private static final boolean DEBUG = false;
60+
private File targetDir;
61+
62+
@BeforeEach
63+
public void setUp() throws Exception {
64+
// Disable auto-building
65+
IWorkspaceDescription desc = ResourcesPlugin.getWorkspace().getDescription();
66+
desc.setAutoBuilding(false);
67+
ResourcesPlugin.getWorkspace().setDescription(desc);
68+
69+
targetDir = Files.createTempDirectory("pde_chain_perf_target").toFile();
70+
System.out.println("Target Platform Location: " + targetDir.getAbsolutePath());
71+
createChainedTargetPlatform();
72+
}
73+
74+
@AfterEach
75+
public void tearDown() throws Exception {
76+
// Restore auto-building
77+
IWorkspaceDescription desc = ResourcesPlugin.getWorkspace().getDescription();
78+
desc.setAutoBuilding(true);
79+
ResourcesPlugin.getWorkspace().setDescription(desc);
80+
}
81+
82+
private void createChainedTargetPlatform() throws Exception {
83+
// Create a chain of bundles: B_0 -> B_1 -> ... -> B_N (all re-exporting)
84+
for (int i = 0; i < BUNDLE_CHAIN_DEPTH; i++) {
85+
String name = CHAIN_PREFIX + i;
86+
String exports = createPackageExports(name);
87+
String requires = (i > 0) ? (CHAIN_PREFIX + (i - 1) + ";visibility:=reexport") : null;
88+
createBundle(targetDir, name, exports, requires);
89+
}
90+
91+
// Set Target Platform
92+
ITargetPlatformService tps = PDECore.getDefault().acquireService(ITargetPlatformService.class);
93+
ITargetDefinition target = tps.newTarget();
94+
target.setTargetLocations(new ITargetLocation[] { tps.newDirectoryLocation(targetDir.getAbsolutePath()) });
95+
TargetPlatformUtil.loadAndSetTarget(target);
96+
}
97+
98+
private String createPackageExports(String bundleName) {
99+
StringBuilder sb = new StringBuilder();
100+
for (int i = 0; i < PACKAGE_COUNT; i++) {
101+
if (sb.length() > 0) {
102+
sb.append(",");
103+
}
104+
sb.append(bundleName).append(".pkg.").append(i);
105+
}
106+
return sb.toString();
107+
}
108+
109+
private void createBundle(File dir, String name, String exports, String requires) throws IOException {
110+
File jarFile = new File(dir, name + ".jar");
111+
try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile))) {
112+
Manifest manifest = new Manifest();
113+
Attributes main = manifest.getMainAttributes();
114+
main.put(Attributes.Name.MANIFEST_VERSION, "1.0");
115+
main.put(new Attributes.Name("Bundle-ManifestVersion"), "2");
116+
main.put(new Attributes.Name("Bundle-SymbolicName"), name);
117+
main.put(new Attributes.Name("Bundle-Version"), "1.0.0");
118+
if (exports != null) {
119+
main.put(new Attributes.Name("Export-Package"), exports);
120+
}
121+
if (requires != null) {
122+
main.put(new Attributes.Name("Require-Bundle"), requires);
123+
}
124+
125+
ZipEntry entry = new ZipEntry("META-INF/MANIFEST.MF");
126+
jos.putNextEntry(entry);
127+
manifest.write(jos);
128+
jos.closeEntry();
129+
}
130+
}
131+
132+
@Test
133+
public void testChainedReexportPerformance() throws Exception {
134+
IBundleProjectService service = PDECore.getDefault().acquireService(IBundleProjectService.class);
135+
136+
String consumerName = "ConsumerBundle";
137+
IProject consumerProj = ResourcesPlugin.getWorkspace().getRoot().getProject(consumerName);
138+
consumerProj.create(null);
139+
consumerProj.open(null);
140+
141+
IBundleProjectDescription consumerDesc = service.getDescription(consumerProj);
142+
consumerDesc.setSymbolicName(consumerName);
143+
consumerDesc.setBundleVersion(new Version("1.0.0"));
144+
145+
// Require the last bundle in the chain with re-export
146+
IRequiredBundleDescription mainReq = service.newRequiredBundle(CHAIN_PREFIX + (BUNDLE_CHAIN_DEPTH - 1), null, false, true);
147+
consumerDesc.setRequiredBundles(new IRequiredBundleDescription[] { mainReq });
148+
149+
consumerDesc.setNatureIds(new String[] { JavaCore.NATURE_ID, IBundleProjectDescription.PLUGIN_NATURE });
150+
consumerDesc.apply(null);
151+
152+
// Build to ensure models are ready
153+
ResourcesPlugin.getWorkspace().build(IncrementalProjectBuilder.FULL_BUILD, new NullProgressMonitor());
154+
TestUtils.waitForJobs("Init", 500, 5000);
155+
156+
IPluginModelBase consumerModel = PluginRegistry.findModel(consumerProj);
157+
if (consumerModel == null) {
158+
throw new IllegalStateException("Consumer model not found");
159+
}
160+
161+
long start = System.currentTimeMillis();
162+
163+
// This triggers the computation
164+
UpdateClasspathJob.scheduleFor(List.of(consumerModel), false);
165+
waitForJobsAndUI(60000);
166+
167+
long elapsed = System.currentTimeMillis() - start;
168+
System.out.println("Classpath computation took: " + elapsed + "ms for chained re-exports.");
169+
170+
if (elapsed > 10000) {
171+
throw new AssertionError("Performance regression: Classpath computation took too long (" + elapsed + "ms)");
172+
}
173+
174+
IClasspathEntry[] resolvedClasspath = JavaCore.create(consumerProj).getRawClasspath();
175+
assertTrue(resolvedClasspath.length > 0, "Classpath should not be empty");
176+
177+
if (DEBUG) {
178+
long endTime = System.currentTimeMillis() + 6000000;
179+
while (System.currentTimeMillis() < endTime) {
180+
if (!Display.getDefault().readAndDispatch()) {
181+
Display.getDefault().sleep();
182+
}
183+
}
184+
}
185+
}
186+
187+
private void waitForJobsAndUI(long timeoutMillis) {
188+
long start = System.currentTimeMillis();
189+
190+
while (System.currentTimeMillis() - start < timeoutMillis) {
191+
// Process UI events
192+
while (Display.getDefault().readAndDispatch()) {
193+
// Keep processing
194+
}
195+
196+
// Check if all jobs are done
197+
if (Job.getJobManager().isIdle()) {
198+
// Process any final UI events
199+
while (Display.getDefault().readAndDispatch()) {
200+
// Keep processing
201+
}
202+
return;
203+
}
204+
205+
try {
206+
Thread.sleep(50);
207+
} catch (InterruptedException e) {
208+
Thread.currentThread().interrupt();
209+
return;
210+
}
211+
}
212+
213+
throw new AssertionError("Timeout waiting for jobs to complete");
214+
}
215+
}

0 commit comments

Comments
 (0)