Skip to content

Commit e0fa3bc

Browse files
committed
Introduce skipper to avoid unnecessary dependency resolution
1 parent 69aeda6 commit e0fa3bc

File tree

4 files changed

+542
-9
lines changed

4 files changed

+542
-9
lines changed

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

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,9 @@ 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,
258+
new DependencyResolutionSkipper() );
257259
Results results = new Results( result, session );
258260

259261
DependencySelector rootDepSelector =
@@ -277,6 +279,7 @@ public CollectResult collectDependencies( RepositorySystemSession session, Colle
277279
false );
278280
}
279281

282+
args.skipper.report();
280283
errorPath = results.errorPath;
281284
}
282285

@@ -396,6 +399,8 @@ private void processDependency( Args args, Results results, DependencyProcessing
396399
return;
397400
}
398401

402+
//Resolve newer version first to maximize benefits of skipper
403+
Collections.reverse( versions );
399404
for ( Version version : versions )
400405
{
401406
Artifact originalArtifact = dependency.getArtifact().setVersion( version.toString() );
@@ -500,15 +505,19 @@ private void doRecurse( Args args, DependencyProcessingContext parentContext,
500505
List<DependencyNode> children = args.pool.getChildren( key );
501506
if ( children == null )
502507
{
503-
args.pool.putChildren( key, child.getChildren() );
504-
505508
List<DependencyNode> parents = new ArrayList<>( parentContext.parents );
506-
parents.add( child );
507-
for ( Dependency dependency : descriptorResult.getDependencies() )
509+
boolean skipResolve = args.skipper.skipResolution( child, parents );
510+
if ( !skipResolve )
508511
{
509-
args.dependencyProcessingQueue.add(
510-
new DependencyProcessingContext( childSelector, childManager, childTraverser, childFilter,
511-
childRepos, descriptorResult.getManagedDependencies(), parents, dependency ) );
512+
parents.add( child );
513+
for ( Dependency dependency : descriptorResult.getDependencies() )
514+
{
515+
args.dependencyProcessingQueue.add(
516+
new DependencyProcessingContext( childSelector, childManager, childTraverser, childFilter,
517+
childRepos, descriptorResult.getManagedDependencies(), parents, dependency ) );
518+
}
519+
args.pool.putChildren( key, child.getChildren() );
520+
args.skipper.cacheWithDepth( child, parents );
512521
}
513522
}
514523
else
@@ -697,9 +706,11 @@ static class Args
697706

698707
final CollectRequest request;
699708

709+
final DependencyResolutionSkipper skipper;
710+
700711
Args( RepositorySystemSession session, RequestTrace trace, DataPool pool,
701712
DefaultDependencyCollectionContext collectionContext, DefaultVersionFilterContext versionContext,
702-
CollectRequest request )
713+
CollectRequest request, DependencyResolutionSkipper skipper )
703714
{
704715
this.session = session;
705716
this.request = request;
@@ -709,6 +720,7 @@ static class Args
709720
this.pool = pool;
710721
this.collectionContext = collectionContext;
711722
this.versionContext = versionContext;
723+
this.skipper = skipper;
712724
}
713725

714726
}
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
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 DependencyResolutionSkipper
35+
{
36+
private static final Logger LOGGER = LoggerFactory.getLogger( DependencyResolutionSkipper.class );
37+
38+
private Map<DependencyNode, DependencyResolutionResult> results = new LinkedHashMap<>( 256 );
39+
private CacheManager cacheManager = new CacheManager();
40+
private CoordinateManager coordinateManager = new CoordinateManager();
41+
42+
DependencyResolutionSkipper()
43+
{
44+
// enables default constructor
45+
}
46+
47+
void report()
48+
{
49+
if ( LOGGER.isTraceEnabled() )
50+
{
51+
LOGGER.trace( "Skipped {} nodes as having deeper depth",
52+
results.entrySet().stream().filter( n -> n.getValue().skippedAsDeeperDepth ).count() );
53+
LOGGER.trace( "Skipped {} nodes as having version conflict",
54+
results.entrySet().stream().filter( n -> n.getValue().skippedAsVersionConflict ).count() );
55+
LOGGER.trace( "Resolved {} nodes",
56+
results.entrySet().stream().filter( n -> n.getValue().resolve ).count() );
57+
LOGGER.trace( "Forced resolving {} nodes for scope selection",
58+
results.entrySet().stream().filter( n -> n.getValue().forceResolution ).count() );
59+
}
60+
}
61+
62+
public Map<DependencyNode, DependencyResolutionResult> getResults()
63+
{
64+
return results;
65+
}
66+
67+
boolean skipResolution( DependencyNode node, List<DependencyNode> parents )
68+
{
69+
DependencyResolutionResult result = new DependencyResolutionResult( node );
70+
results.put( node, result );
71+
72+
int depth = parents.size() + 1;
73+
coordinateManager.createCoordinate( node, depth );
74+
75+
if ( cacheManager.isVersionConflict( node ) )
76+
{
77+
//skip resolving version conflict losers (omitted for conflict)
78+
result.skippedAsVersionConflict = true;
79+
if ( LOGGER.isTraceEnabled() )
80+
{
81+
LOGGER.trace( "Skipped resolving node: {} as version conflict",
82+
ArtifactIdUtils.toId( node.getArtifact() ) );
83+
}
84+
}
85+
else if ( coordinateManager.isLeftmost( node, parents ) )
86+
{
87+
/*
88+
* Force resolving the node to retain conflict paths when its coordinate is more left than last resolved.
89+
* This is because Maven picks the widest scope present among conflicting dependencies.
90+
*/
91+
result.forceResolution = true;
92+
if ( LOGGER.isTraceEnabled() )
93+
{
94+
LOGGER.trace( "Force resolving node: {} for scope selection",
95+
ArtifactIdUtils.toId( node.getArtifact() ) );
96+
}
97+
}
98+
else if ( cacheManager.getWinnerDepth( node ) <= depth )
99+
{
100+
//skip resolving if depth deeper (omitted for duplicate)
101+
result.skippedAsDeeperDepth = true;
102+
if ( LOGGER.isTraceEnabled() )
103+
{
104+
LOGGER.trace( "Skipped resolving node: {} as the node's depth is deeper than winner",
105+
ArtifactIdUtils.toId( node.getArtifact() ) );
106+
}
107+
}
108+
else
109+
{
110+
result.resolve = true;
111+
if ( LOGGER.isTraceEnabled() )
112+
{
113+
LOGGER.trace( "Resolving node: {}",
114+
ArtifactIdUtils.toId( node.getArtifact() ) );
115+
}
116+
}
117+
118+
if ( result.toResolve() )
119+
{
120+
coordinateManager.updateLeftmost( node );
121+
return false;
122+
}
123+
124+
return true;
125+
}
126+
127+
void cacheWithDepth( DependencyNode node, List<DependencyNode> parents )
128+
{
129+
boolean parentForceResolution = parents.stream()
130+
.anyMatch( n -> results.containsKey( n ) && results.get( n ).forceResolution );
131+
if ( parentForceResolution )
132+
{
133+
if ( LOGGER.isTraceEnabled() )
134+
{
135+
LOGGER.trace(
136+
"Won't cache as node: {} inherits from a force-resolved node and will be omitted for duplicate",
137+
ArtifactIdUtils.toId( node.getArtifact() ) );
138+
}
139+
}
140+
else
141+
{
142+
cacheManager.cacheWinnerDepth( node, parents.size() + 1 );
143+
}
144+
}
145+
146+
147+
static final class DependencyResolutionResult
148+
{
149+
DependencyNode current;
150+
boolean skippedAsVersionConflict; //omitted for conflict
151+
boolean skippedAsDeeperDepth; //omitted for duplicate
152+
boolean resolve; //node to resolve (winner node)
153+
boolean forceResolution; //force resolving (duplicate node) for scope selection
154+
155+
DependencyResolutionResult( DependencyNode current )
156+
{
157+
this.current = current;
158+
}
159+
160+
boolean toResolve()
161+
{
162+
return resolve || forceResolution;
163+
}
164+
}
165+
166+
static final class CacheManager
167+
{
168+
169+
/**
170+
* artifact -> depth, only cache winners.
171+
*/
172+
private final HashMap<Artifact, Integer> winnerDepths = new HashMap<>( 256 );
173+
174+
175+
/**
176+
* versionLessId -> Artifact, only cache winners
177+
*/
178+
private final Map<String, Artifact> winnerGAs = new HashMap<>( 256 );
179+
180+
boolean isVersionConflict( DependencyNode node )
181+
{
182+
String ga = ArtifactIdUtils.toVersionlessId( node.getArtifact() );
183+
if ( winnerGAs.containsKey( ga ) )
184+
{
185+
Artifact result = winnerGAs.get( ga );
186+
return !node.getArtifact().getVersion().equals( result.getVersion() );
187+
}
188+
189+
return false;
190+
}
191+
192+
void cacheWinnerDepth( DependencyNode node, int depth )
193+
{
194+
LOGGER.trace( "Artifact {} with depth {} cached", node.getArtifact(), depth );
195+
winnerDepths.put( node.getArtifact(), depth );
196+
winnerGAs.put( ArtifactIdUtils.toVersionlessId( node.getArtifact() ), node.getArtifact() );
197+
}
198+
199+
int getWinnerDepth( DependencyNode node )
200+
{
201+
return winnerDepths.getOrDefault( node.getArtifact(), Integer.MAX_VALUE );
202+
}
203+
204+
}
205+
206+
207+
static final class CoordinateManager
208+
{
209+
private final Map<Integer, AtomicInteger> sequenceGen = new HashMap<>( 256 );
210+
211+
/**
212+
* Dependency node -> Coordinate
213+
*/
214+
private final Map<DependencyNode, Coordinate> coordinateMap = new HashMap<>( 256 );
215+
216+
/**
217+
* Leftmost coordinate of given artifact
218+
*/
219+
private final Map<Artifact, Coordinate> leftmostCoordinates = new HashMap<>( 256 );
220+
221+
222+
Coordinate getCoordinate( DependencyNode node )
223+
{
224+
return coordinateMap.get( node );
225+
}
226+
227+
Coordinate createCoordinate( DependencyNode node, int depth )
228+
{
229+
int seq = sequenceGen.computeIfAbsent( depth, k -> new AtomicInteger() ).incrementAndGet();
230+
Coordinate coordinate = new Coordinate( depth, seq );
231+
coordinateMap.put( node, coordinate );
232+
return coordinate;
233+
}
234+
235+
void updateLeftmost( DependencyNode current )
236+
{
237+
leftmostCoordinates.put( current.getArtifact(), getCoordinate( current ) );
238+
}
239+
240+
boolean isLeftmost( DependencyNode node, List<DependencyNode> parents )
241+
{
242+
Coordinate leftmost = leftmostCoordinates.get( node.getArtifact() );
243+
if ( leftmost != null && leftmost.depth <= parents.size() )
244+
{
245+
DependencyNode sameLevelNode = parents.get( leftmost.depth - 1 );
246+
if ( getCoordinate( sameLevelNode ).sequence < leftmost.sequence )
247+
{
248+
return true;
249+
}
250+
}
251+
252+
return false;
253+
}
254+
}
255+
256+
static final class Coordinate
257+
{
258+
int depth;
259+
int sequence;
260+
261+
Coordinate( int depth, int sequence )
262+
{
263+
this.depth = depth;
264+
this.sequence = sequence;
265+
}
266+
267+
@Override
268+
public String toString()
269+
{
270+
return "{"
271+
+ "depth="
272+
+ depth
273+
+ ", sequence="
274+
+ sequence
275+
+ '}';
276+
}
277+
}
278+
279+
280+
}

0 commit comments

Comments
 (0)