diff --git a/maven-resolver-util/pom.xml b/maven-resolver-util/pom.xml index 623474974..8b5756a58 100644 --- a/maven-resolver-util/pom.xml +++ b/maven-resolver-util/pom.xml @@ -58,6 +58,16 @@ junit-jupiter-params test + + org.openjdk.jmh + jmh-core + test + + + org.openjdk.jmh + jmh-generator-annprocess + test + @@ -66,6 +76,21 @@ com.github.siom79.japicmp japicmp-maven-plugin + + org.apache.maven.plugins + maven-compiler-plugin + + + default-testCompile + + full + + org.openjdk.jmh.generators.BenchmarkProcessor + + + + + diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverJMHBenchmark.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverJMHBenchmark.java new file mode 100644 index 000000000..4a8dd31e3 --- /dev/null +++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConflictResolverJMHBenchmark.java @@ -0,0 +1,339 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.eclipse.aether.util.graph.transformer; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.DependencyGraphTransformationContext; +import org.eclipse.aether.graph.DefaultDependencyNode; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.graph.DependencyNode; +import org.eclipse.aether.internal.test.util.TestUtils; +import org.eclipse.aether.internal.test.util.TestVersion; +import org.eclipse.aether.internal.test.util.TestVersionConstraint; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +@Warmup(iterations = 1, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 2, time = 2, timeUnit = TimeUnit.SECONDS) +public class ConflictResolverJMHBenchmark { + private static final RepositorySystemSession session = TestUtils.newSession(); + + private ConflictResolver classic; + private ConflictResolver path; + + @Setup + public void setup() { + classic = new ClassicConflictResolver( + new ConfigurableVersionSelector(), + new JavaScopeSelector(), + new SimpleOptionalitySelector(), + new JavaScopeDeriver()); + path = new PathConflictResolver( + new ConfigurableVersionSelector(), + new JavaScopeSelector(), + new SimpleOptionalitySelector(), + new JavaScopeDeriver()); + } + + public static void main(String... args) throws RunnerException { + new Runner(new OptionsBuilder() + .include(ConflictResolverJMHBenchmark.class.getSimpleName()) + .build()) + .run(); + } + + @Benchmark + public void uniqueSnake_20_path() throws RepositoryException { + uniqueSnake(path, 20); + } + + @Benchmark + public void uniqueSnake_20_classic() throws RepositoryException { + uniqueSnake(classic, 20); + } + + @Benchmark + public void uniqueSnake_40_path() throws RepositoryException { + uniqueSnake(path, 40); + } + + @Benchmark + public void uniqueSnake_40_classic() throws RepositoryException { + uniqueSnake(classic, 40); + } + + @Benchmark + public void uniqueSnakeWithRootCycle_20_path() throws RepositoryException { + uniqueSnakeWithRootCycle(path, 20); + } + + @Benchmark + public void uniqueSnakeWithRootCycle_20_classic() throws RepositoryException { + uniqueSnakeWithRootCycle(classic, 20); + } + + @Benchmark + public void uniqueSnakeWithRootCycle_40_path() throws RepositoryException { + uniqueSnakeWithRootCycle(path, 40); + } + + @Benchmark + public void uniqueSnakeWithRootCycle_40_classic() throws RepositoryException { + uniqueSnakeWithRootCycle(classic, 40); + } + + @Benchmark + public void symmetricBinaryTreeUnique_5_path() throws RepositoryException { + symmetricBinaryTree(path, 5, Integer.MAX_VALUE); + } + + @Benchmark + public void symmetricBinaryTreeUnique_5_classic() throws RepositoryException { + symmetricBinaryTree(classic, 5, Integer.MAX_VALUE); + } + + @Benchmark + public void symmetricBinaryTreeMod50_5_path() throws RepositoryException { + symmetricBinaryTree(path, 5, 50); + } + + @Benchmark + public void symmetricBinaryTreeMod50_5_classic() throws RepositoryException { + symmetricBinaryTree(classic, 5, 50); + } + + @Benchmark + public void symmetricBinaryTreeUnique_10_path() throws RepositoryException { + symmetricBinaryTree(path, 10, Integer.MAX_VALUE); + } + + @Benchmark + public void symmetricBinaryTreeUnique_10_classic() throws RepositoryException { + symmetricBinaryTree(classic, 10, Integer.MAX_VALUE); + } + + @Benchmark + public void symmetricBinaryTreeMod50_10_path() throws RepositoryException { + symmetricBinaryTree(path, 10, 50); + } + + @Benchmark + public void symmetricBinaryTreeMod50_10_classic() throws RepositoryException { + symmetricBinaryTree(classic, 10, 50); + } + + @Benchmark + public void diamondFan_10x5_path() throws RepositoryException { + diamondFan(path, 5, 10); + } + + @Benchmark + public void diamondFan_10x5_classic() throws RepositoryException { + diamondFan(classic, 5, 10); + } + + @Benchmark + public void diamondFan_20x10_path() throws RepositoryException { + diamondFan(path, 10, 20); + } + + @Benchmark + public void diamondFan_20x10_classic() throws RepositoryException { + diamondFan(classic, 10, 20); + } + + /** + * A "snake", plain chain of unique dependencies of given length. + */ + private static void uniqueSnake(ConflictResolver conflictResolver, int length) throws RepositoryException { + DependencyNode root = makeDependencyNode("group-id", "root", "1.0"); + DependencyNode last = root; + for (int i = 0; i < length; i++) { + DependencyNode dep = makeDependencyNode("group-id", "dep-" + i, "1.0"); + last.setChildren(mutableList(dep)); + last = dep; + } + + DependencyNode transformedNode = transform(conflictResolver, root); + + assertSame(root, transformedNode); + } + + /** + * A "snake", plain chain of unique dependencies of given length, where last dep points back to root forming a + * cycle. + */ + private static void uniqueSnakeWithRootCycle(ConflictResolver conflictResolver, int length) + throws RepositoryException { + DependencyNode root = makeDependencyNode("group-id", "root", "1.0"); + DependencyNode last = root; + for (int i = 0; i < length; i++) { + DependencyNode dep = makeDependencyNode("group-id", "dep-" + i, "1.0"); + last.setChildren(mutableList(dep)); + last = dep; + } + last.setChildren(mutableList(root)); + + DependencyNode transformedNode = transform(conflictResolver, root); + + assertSame(root, transformedNode); + } + + /** + * A symmetric binary tree with given depth. Provided modulo is to create conflicts, if larger that total tree nodes, + * tree will be "unique" (no conflicts). + */ + private static void symmetricBinaryTree(ConflictResolver conflictResolver, int depth, int modulo) + throws RepositoryException { + DependencyNode root = makeDependencyNode("group-id", "root", "1.0"); + int level = 2; + int idCounter = 1; + ArrayDeque stack = new ArrayDeque<>(); + stack.push(root); + for (int i = 0; i < depth; i++) { + ArrayList children = new ArrayList<>(); + while (!stack.isEmpty()) { + DependencyNode node = stack.pop(); + DependencyNode left = makeDependencyNode("group-id", "d" + idCounter++ % modulo, "1.0"); + DependencyNode right = makeDependencyNode("group-id", "d" + idCounter++ % modulo, "1.0"); + node.setChildren(mutableList(left, right)); + children.add(left); + children.add(right); + } + stack.addAll(children); + } + + DependencyNode transformedNode = transform(conflictResolver, root); + + assertSame(root, transformedNode); + } + + /** + * A "diamond fan": {@code width} independent chains of length {@code depth}, all converging + * on the same shared artifact at two different versions (simulating a real version conflict). + * This is the topology where CCR's O(N²) re-walk per conflict group shows up most clearly. + * + * Graph shape (width=3, depth=2): + * root + * ├── chain-0-0 → chain-0-1 → shared:1.0 + * ├── chain-1-0 → chain-1-1 → shared:2.0 (conflict: 1.0 vs 2.0) + * └── chain-2-0 → chain-2-1 → shared:1.0 + */ + private static void diamondFan(ConflictResolver conflictResolver, int depth, int width) throws RepositoryException { + DependencyNode root = makeDependencyNode("group-id", "root", "1.0"); + List chains = new ArrayList<>(width); + + for (int w = 0; w < width; w++) { + DependencyNode chainHead = makeDependencyNode("group-id", "chain-" + w + "-0", "1.0"); + DependencyNode last = chainHead; + for (int d = 1; d < depth; d++) { + DependencyNode dep = makeDependencyNode("group-id", "chain-" + w + "-" + d, "1.0"); + last.setChildren(mutableList(dep)); + last = dep; + } + // All chains converge on the same artifact at alternating versions — real conflict + String version = (w % 2 == 0) ? "1.0" : "2.0"; + DependencyNode shared = makeDependencyNode("group-id", "shared", version); + last.setChildren(mutableList(shared)); + chains.add(chainHead); + } + + root.setChildren(chains); + + DependencyNode result = transform(conflictResolver, root); + assertNotNull(result); + // After conflict resolution, all paths to "shared" must agree on one version + assertEquals(1, countDistinctVersions(result, "shared")); + } + + private static int countDistinctVersions(DependencyNode node, String artifactId) { + // BFS/DFS to count distinct versions of the given artifactId in the resolved graph + java.util.Set versions = new java.util.HashSet<>(); + collectVersions(node, artifactId, versions); + return versions.size(); + } + + private static void collectVersions(DependencyNode node, String artifactId, java.util.Set versions) { + if (node.getArtifact() != null + && artifactId.equals(node.getArtifact().getArtifactId()) + && node.getData().get(ConflictResolver.NODE_DATA_WINNER) == null) { + versions.add(node.getArtifact().getVersion()); + } + for (DependencyNode child : node.getChildren()) { + collectVersions(child, artifactId, versions); + } + } + + private static DependencyNode transform(ConflictResolver conflictResolver, DependencyNode root) + throws RepositoryException { + DependencyGraphTransformationContext context = TestUtils.newTransformationContext(session); + root = conflictResolver.transformGraph(root, context); + assertNotNull(root); + return root; + } + + private static DependencyNode makeDependencyNode(String groupId, String artifactId, String version) { + return makeDependencyNode(groupId, artifactId, version, "compile"); + } + + private static DependencyNode makeDependencyNode(String groupId, String artifactId, String version, String scope) { + return makeDependencyNode(groupId, artifactId, version, null, scope); + } + + private static DependencyNode makeDependencyNode( + String groupId, String artifactId, String version, String classifier, String scope) { + DefaultDependencyNode node = (classifier != null && !classifier.isEmpty()) + ? new DefaultDependencyNode(new Dependency( + new DefaultArtifact(groupId + ':' + artifactId + ":jar:" + classifier + ":" + version), scope)) + : new DefaultDependencyNode( + new Dependency(new DefaultArtifact(groupId + ':' + artifactId + ':' + version), scope)); + node.setVersion(new TestVersion(version)); + node.setVersionConstraint(new TestVersionConstraint(node.getVersion())); + return node; + } + + private static List mutableList(DependencyNode... nodes) { + return new ArrayList<>(Arrays.asList(nodes)); + } +} diff --git a/pom.xml b/pom.xml index 173fe0e0f..b92f74eee 100644 --- a/pom.xml +++ b/pom.xml @@ -117,6 +117,7 @@ 3.6.1 4.0.3 4.1.1 + 1.37 [3.8.8,) [21,) @@ -325,6 +326,17 @@ commons-compress 1.28.0 + + + org.openjdk.jmh + jmh-core + ${jmhVersion} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmhVersion} +