Skip to content

Commit e5bf77d

Browse files
committed
MRESOLVER-247: Introduce skipper to avoid unnecessary dependency resolution
1 parent 69aeda6 commit e5bf77d

File tree

4 files changed

+559
-9
lines changed

4 files changed

+559
-9
lines changed

maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollector.java

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,8 @@ public CollectResult collectDependencies( RepositorySystemSession session, Colle
253253

254254
DefaultVersionFilterContext versionContext = new DefaultVersionFilterContext( session );
255255

256-
Args args = new Args( session, trace, pool, context, versionContext, request );
256+
Args args =
257+
new Args( session, trace, pool, context, versionContext, request, new DependencyResolveSkipper() );
257258
Results results = new Results( result, session );
258259

259260
DependencySelector rootDepSelector =
@@ -277,6 +278,7 @@ public CollectResult collectDependencies( RepositorySystemSession session, Colle
277278
false );
278279
}
279280

281+
args.skipper.report();
280282
errorPath = results.errorPath;
281283
}
282284

@@ -396,6 +398,8 @@ private void processDependency( Args args, Results results, DependencyProcessing
396398
return;
397399
}
398400

401+
//Resolve newer version first to maximize benefits of skipper
402+
Collections.reverse( versions );
399403
for ( Version version : versions )
400404
{
401405
Artifact originalArtifact = dependency.getArtifact().setVersion( version.toString() );
@@ -500,15 +504,19 @@ private void doRecurse( Args args, DependencyProcessingContext parentContext,
500504
List<DependencyNode> children = args.pool.getChildren( key );
501505
if ( children == null )
502506
{
503-
args.pool.putChildren( key, child.getChildren() );
504-
505507
List<DependencyNode> parents = new ArrayList<>( parentContext.parents );
506-
parents.add( child );
507-
for ( Dependency dependency : descriptorResult.getDependencies() )
508+
boolean skipResolve = args.skipper.skipResolve( child, parents );
509+
if ( !skipResolve )
508510
{
509-
args.dependencyProcessingQueue.add(
510-
new DependencyProcessingContext( childSelector, childManager, childTraverser, childFilter,
511-
childRepos, descriptorResult.getManagedDependencies(), parents, dependency ) );
511+
parents.add( child );
512+
for ( Dependency dependency : descriptorResult.getDependencies() )
513+
{
514+
args.dependencyProcessingQueue.add(
515+
new DependencyProcessingContext( childSelector, childManager, childTraverser, childFilter,
516+
childRepos, descriptorResult.getManagedDependencies(), parents, dependency ) );
517+
}
518+
args.pool.putChildren( key, child.getChildren() );
519+
args.skipper.cacheWithDepth( child, parents );
512520
}
513521
}
514522
else
@@ -697,9 +705,11 @@ static class Args
697705

698706
final CollectRequest request;
699707

708+
final DependencyResolveSkipper skipper;
709+
700710
Args( RepositorySystemSession session, RequestTrace trace, DataPool pool,
701711
DefaultDependencyCollectionContext collectionContext, DefaultVersionFilterContext versionContext,
702-
CollectRequest request )
712+
CollectRequest request, DependencyResolveSkipper skipper )
703713
{
704714
this.session = session;
705715
this.request = request;
@@ -709,6 +719,7 @@ static class Args
709719
this.pool = pool;
710720
this.collectionContext = collectionContext;
711721
this.versionContext = versionContext;
722+
this.skipper = skipper;
712723
}
713724

714725
}
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
package org.eclipse.aether.internal.impl.collect;
2+
3+
/*
4+
* Licensed to the Apache Software Foundation (ASF) under one
5+
* or more contributor license agreements. See the NOTICE file
6+
* distributed with this work for additional information
7+
* regarding copyright ownership. The ASF licenses this file
8+
* to you under the Apache License, Version 2.0 (the
9+
* "License"); you may not use this file except in compliance
10+
* with the License. You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing,
15+
* software distributed under the License is distributed on an
16+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17+
* KIND, either express or implied. See the License for the
18+
* specific language governing permissions and limitations
19+
* under the License.
20+
*/
21+
22+
import org.eclipse.aether.artifact.Artifact;
23+
import org.eclipse.aether.graph.DependencyNode;
24+
import org.eclipse.aether.util.artifact.ArtifactIdUtils;
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
28+
import java.util.HashMap;
29+
import java.util.LinkedHashMap;
30+
import java.util.List;
31+
import java.util.Map;
32+
import java.util.concurrent.atomic.AtomicInteger;
33+
34+
final class DependencyResolveSkipper
35+
{
36+
private static final Logger LOGGER = LoggerFactory.getLogger( DependencyResolveSkipper.class );
37+
38+
private Map<DependencyNode, DependencyResolveResult> resolveResults = new LinkedHashMap<>( 256 );
39+
private CacheManager cacheManager = new CacheManager();
40+
private CoordinateManager coordinateManager = new CoordinateManager();
41+
42+
DependencyResolveSkipper()
43+
{
44+
// enables default constructor
45+
}
46+
47+
Map<DependencyNode, DependencyResolveResult> report()
48+
{
49+
LOGGER.trace( "Skipped {} nodes as having deeper depth.",
50+
resolveResults.entrySet().stream().filter( n -> n.getValue().skippedAsDeeper ).count() );
51+
LOGGER.trace( "Skipped {} nodes as having version conflict.",
52+
resolveResults.entrySet().stream().filter( n -> n.getValue().skippedAsVersionConflict ).count() );
53+
LOGGER.trace( "Resolved {} nodes.",
54+
resolveResults.entrySet().stream().filter( n -> n.getValue().resolve ).count() );
55+
LOGGER.trace( "Force resolved {} nodes for scope selection.",
56+
resolveResults.entrySet().stream().filter( n -> n.getValue().forceResolve ).count() );
57+
58+
return resolveResults;
59+
}
60+
61+
62+
boolean skipResolve( DependencyNode node, List<DependencyNode> parents )
63+
{
64+
DependencyResolveResult resolveResult = new DependencyResolveResult( node );
65+
resolveResults.put( node, resolveResult );
66+
67+
int depth = parents.size() + 1;
68+
coordinateManager.createCoordinate( node, depth );
69+
70+
if ( cacheManager.isVersionConflict( node ) )
71+
{
72+
//skip resolving version conflict losers (omitted conflict)
73+
resolveResult.skippedAsVersionConflict = true;
74+
LOGGER.trace( "Skipped resolving node: {} as version conflict",
75+
ArtifactIdUtils.toId( node.getArtifact() ) );
76+
}
77+
else if ( coordinateManager.isLeftmost( node, parents ) )
78+
{
79+
/*
80+
* Force resolving the node to retain conflict paths when its coordinate is more left than last resolved.
81+
* This is because maven picks the widest scope present among conflicting dependencies.
82+
*/
83+
resolveResult.forceResolve = true;
84+
LOGGER.trace( "Force resolving node: {} for scope selection",
85+
ArtifactIdUtils.toId( node.getArtifact() ) );
86+
}
87+
else if ( cacheManager.getWinnerDepth( node ) <= depth )
88+
{
89+
//skip resolve if depth deeper (omitted duplicate)
90+
resolveResult.skippedAsDeeper = true;
91+
LOGGER.trace( "Skipped resolving node: {} as the node's depth is deeper than winner",
92+
ArtifactIdUtils.toId( node.getArtifact() ) );
93+
}
94+
else
95+
{
96+
resolveResult.resolve = true;
97+
LOGGER.trace( "Resolving node: {}",
98+
ArtifactIdUtils.toId( node.getArtifact() ) );
99+
}
100+
101+
if ( resolveResult.toResolve() )
102+
{
103+
coordinateManager.updateLeftmost( node );
104+
return false;
105+
}
106+
107+
return true;
108+
}
109+
110+
void cacheWithDepth( DependencyNode node, List<DependencyNode> parents )
111+
{
112+
boolean parentForceResolve = parents.stream()
113+
.anyMatch( n -> resolveResults.containsKey( n ) && resolveResults.get( n ).forceResolve );
114+
if ( parentForceResolve )
115+
{
116+
LOGGER.trace(
117+
"Won't cache as current node: {} inherits from a force-resolved node and will be omitted duplicate",
118+
ArtifactIdUtils.toId( node.getArtifact() ) );
119+
}
120+
else
121+
{
122+
cacheManager.cacheWinnerDepth( node, parents.size() + 1 );
123+
}
124+
}
125+
126+
127+
static final class DependencyResolveResult
128+
{
129+
DependencyNode current;
130+
boolean skippedAsVersionConflict; //omitted conflict
131+
boolean skippedAsDeeper; //omitted duplicate
132+
boolean resolve; //node to resolve (winner node)
133+
boolean forceResolve; //force resolve (duplicate code) for scope selection
134+
135+
DependencyResolveResult( DependencyNode current )
136+
{
137+
this.current = current;
138+
}
139+
140+
boolean toResolve()
141+
{
142+
return resolve || forceResolve;
143+
}
144+
}
145+
146+
static final class CacheManager
147+
{
148+
149+
/**
150+
* artifact -> depth, only cache winners.
151+
*/
152+
private final HashMap<Artifact, Integer> winnerDepths = new HashMap<>( 256 );
153+
154+
155+
/**
156+
* versionLessId -> Artifact, only cache winners
157+
*/
158+
private final Map<String, Artifact> winnerGAs = new HashMap<>( 256 );
159+
160+
boolean isVersionConflict( DependencyNode node )
161+
{
162+
String ga = ArtifactIdUtils.toVersionlessId( node.getArtifact() );
163+
if ( winnerGAs.containsKey( ga ) )
164+
{
165+
Artifact result = winnerGAs.get( ga );
166+
return !node.getArtifact().getVersion().equals( result.getVersion() );
167+
}
168+
169+
return false;
170+
}
171+
172+
void cacheWinnerDepth( DependencyNode node, int depth )
173+
{
174+
LOGGER.trace( "Artifact {} with depth {} cached", node.getArtifact(), depth );
175+
winnerDepths.put( node.getArtifact(), depth );
176+
winnerGAs.put( ArtifactIdUtils.toVersionlessId( node.getArtifact() ), node.getArtifact() );
177+
}
178+
179+
int getWinnerDepth( DependencyNode node )
180+
{
181+
return winnerDepths.getOrDefault( node.getArtifact(), Integer.MAX_VALUE );
182+
}
183+
184+
}
185+
186+
187+
static final class CoordinateManager
188+
{
189+
private final Map<Integer, AtomicInteger> sequenceGen = new HashMap<>( 256 );
190+
191+
/**
192+
* Dependency node -> Coordinate
193+
*/
194+
private final Map<DependencyNode, Coordinate> coordinateMap = new HashMap<>( 256 );
195+
196+
/**
197+
* Leftmost coordinate of given artifact
198+
*/
199+
private final Map<Artifact, Coordinate> leftmostCoordinates = new HashMap<>( 256 );
200+
201+
202+
Coordinate getCoordinate( DependencyNode node )
203+
{
204+
return coordinateMap.get( node );
205+
}
206+
207+
Coordinate createCoordinate( DependencyNode node, int depth )
208+
{
209+
int seq = sequenceGen.computeIfAbsent( depth, k -> new AtomicInteger() ).incrementAndGet();
210+
Coordinate coordinate = new Coordinate( depth, seq );
211+
coordinateMap.put( node, coordinate );
212+
return coordinate;
213+
}
214+
215+
void updateLeftmost( DependencyNode current )
216+
{
217+
leftmostCoordinates.put( current.getArtifact(), getCoordinate( current ) );
218+
}
219+
220+
boolean isLeftmost( DependencyNode node, List<DependencyNode> parents )
221+
{
222+
Coordinate leftmost = leftmostCoordinates.get( node.getArtifact() );
223+
if ( leftmost != null && leftmost.depth <= parents.size() )
224+
{
225+
DependencyNode sameLevelNode = parents.get( leftmost.depth - 1 );
226+
if ( getCoordinate( sameLevelNode ).sequence < leftmost.sequence )
227+
{
228+
return true;
229+
}
230+
}
231+
232+
return false;
233+
}
234+
}
235+
236+
static final class Coordinate
237+
{
238+
int depth;
239+
int sequence;
240+
241+
Coordinate( int depth, int sequence )
242+
{
243+
this.depth = depth;
244+
this.sequence = sequence;
245+
}
246+
247+
@Override
248+
public String toString()
249+
{
250+
return "{"
251+
+ "depth="
252+
+ depth
253+
+ ", sequence="
254+
+ sequence
255+
+ '}';
256+
}
257+
}
258+
259+
260+
}

maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/collect/DefaultDependencyCollectorTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
import org.eclipse.aether.util.graph.manager.DefaultDependencyManager;
7070
import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
7171
import org.eclipse.aether.util.graph.manager.TransitiveDependencyManager;
72+
import org.eclipse.aether.util.graph.selector.ExclusionDependencySelector;
7273
import org.eclipse.aether.util.graph.version.HighestVersionFilter;
7374
import org.junit.Before;
7475
import org.junit.Test;
@@ -101,6 +102,12 @@ private Dependency newDep( String coords, String scope )
101102
return new Dependency( new DefaultArtifact( coords ), scope );
102103
}
103104

105+
private Dependency newDep( String coords, String scope, Collection<Exclusion> exclusions )
106+
{
107+
Dependency d = new Dependency( new DefaultArtifact( coords ), scope );
108+
return d.setExclusions(exclusions);
109+
}
110+
104111
@Before
105112
public void setup()
106113
{
@@ -222,6 +229,26 @@ public void testMissingDependencyDescription()
222229
}
223230
}
224231

232+
@Test
233+
public void testExclusion1()
234+
throws DependencyCollectionException {
235+
collector.setArtifactDescriptorReader(newReader("managed/"));
236+
parser = new DependencyGraphParser("artifact-descriptions/managed/");
237+
session.setDependencyManager(new TransitiveDependencyManager());
238+
239+
ExclusionDependencySelector exclSel1 = new ExclusionDependencySelector();
240+
session.setDependencySelector(exclSel1);
241+
242+
Dependency root1 = newDep("gid:root:ext:ver", "compile", Collections.singleton(new Exclusion("gid", "transitive-1", "", "ext")));
243+
Dependency root2 = newDep("gid:root:ext:ver", "compile", Collections.singleton(new Exclusion("gid", "transitive-2", "", "ext")));
244+
List<Dependency> dependencies = Arrays.asList(root1, root2);
245+
CollectRequest request = new CollectRequest(dependencies, null, Arrays.asList(repository));
246+
request.addManagedDependency(newDep("gid:direct:ext:managed-by-dominant-request"));
247+
request.addManagedDependency( newDep( "gid:transitive-1:ext:managed-by-root" ) );
248+
CollectResult result = collector.collectDependencies(session, request);
249+
System.err.println(result);
250+
}
251+
225252
@Test
226253
public void testDuplicates()
227254
throws DependencyCollectionException

0 commit comments

Comments
 (0)