Skip to content

Commit f86f299

Browse files
Copilotjeffkl
andcommitted
Eliminate unnecessary graph-walk restarts for stale transitive children in DependencyGraphResolver
Co-authored-by: jeffkl <[email protected]>
1 parent bda1011 commit f86f299

1 file changed

Lines changed: 58 additions & 30 deletions

File tree

src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.cs

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -895,25 +895,33 @@ private async Task<Dictionary<LibraryDependencyIndex, ResolvedDependencyGraphIte
895895
// Cache the result of IsNewerThanNET10 since it is invariant for the entire method
896896
bool enablePruningWarnings = IsNewerThanNET10(projectTargetFramework.FrameworkName);
897897

898-
// Used to start over when a dependency has multiple descendants of an item to be evicted.
898+
// A restart is needed when evicting one item invalidates a previous eviction (a cascade eviction).
899+
//
900+
// Consider this graph:
899901
//
900902
// Project
901903
// ├── A 1.0.0
902-
// │ └── B 1.0.0
903-
// │ └── C 1.0.0
904-
// │ └── D 1.0.0
905-
// └── X 2.0.0
906-
// └── Y 2.0.0
907-
// └── G 2.0.0
908-
// └── B 2.0.0
904+
// │ └── Q 1.0.0
905+
// └── B 1.0.0
906+
// └── Q 2.0.0
907+
// └── C 2.0.0
908+
// └── B 2.0.0
909+
//
909910
// The items are processed in the following order:
910-
// Chose A 1.0.0 and X 1.0.0
911-
// Chose B 1.0.0 and Y 2.0.0
912-
// Chose C 1.0.0 and G 2.0.0
913-
// Chose D 1.0.0 and B 2.0.0, but B 2.0.0 should evict C 1.0.0 and D 1.0.0
911+
// A 1.0.0 and B 1.0.0 and C 2.0.0 are chosen.
912+
// Q 1.0.0 is chosen initially, but Q 2.0.0 (from B 1.0.0's subtree) evicts it.
913+
// evictions[Q1] records a winning path through B 1.0.0.
914+
// B 2.0.0 (from C 2.0.0's subtree) evicts B 1.0.0.
914915
//
915-
// In this case, the entire walk is started over and B 1.0.0 is left out of the graph, leading to C 1.0.0 and D 1.0.0 also being left out.
916+
// Since Q 2.0.0's winning path went through B 1.0.0, which is now itself evicted,
917+
// Q 1.0.0's eviction is no longer valid and must be reconsidered.
918+
// Because Q 1.0.0 was already dequeued and rejected, the walk must restart from the beginning.
919+
// On the next pass, evictions[B1] prevents B 1.0.0 from being processed, so Q 2.0.0 is never
920+
// reached and Q 1.0.0 is chosen without competition.
916921
//
922+
// Transitive children of an evicted item that are already in resolvedDependencyGraphItems (for
923+
// example C and D in B 1.0.0's subtree) are handled without a restart by removing them directly
924+
// from resolvedDependencyGraphItems; items still in the queue are skipped by the lazy path check.
917925
StartOver:
918926
restartCount++;
919927

@@ -931,7 +939,6 @@ private async Task<Dictionary<LibraryDependencyIndex, ResolvedDependencyGraphIte
931939

932940
GraphItem<RemoteResolveResult> currentGraphItem = await currentDependencyGraphItem.GetGraphItemAsync(_request.Project.RestoreMetadata, projectTargetFramework.PackagesToPrune, enablePruningWarnings, isRootProject, targetGraphName, _logger);
933941

934-
LibraryDependencyTarget typeConstraint = currentDependencyGraphItem.LibraryDependency.LibraryRange.TypeConstraint;
935942
if (evictions.TryGetValue(currentDependencyGraphItem.LibraryRangeIndex, out (LibraryRangeIndex[] Path, LibraryDependencyIndex DependencyIndex, LibraryDependencyTarget TypeConstraint) eviction))
936943
{
937944
// If we evicted this same version previously, but the type constraint of currentRef is more stringent (package), then do not skip the current item - this is the one we want.
@@ -1059,14 +1066,28 @@ private async Task<Dictionary<LibraryDependencyIndex, ResolvedDependencyGraphIte
10591066
// Record an eviction for the item we are replacing. The eviction path is for the current item.
10601067
LibraryRangeIndex evictedLibraryRangeIndex = chosenResolvedItem.LibraryRangeIndex;
10611068

1062-
bool shouldStartOver = false;
1069+
// Record the eviction immediately so that:
1070+
// 1. The evicted range is skipped if re-encountered at dequeue time (LibraryRangeIndex key check).
1071+
// 2. Items still in the queue whose path passes through the evicted item are lazily skipped.
1072+
evictions[evictedLibraryRangeIndex] = (DependencyGraphItemIndexer.CreatePathToRef(currentDependencyGraphItem.Path, currentDependencyGraphItem.LibraryRangeIndex), currentDependencyGraphItem.LibraryDependencyIndex, chosenResolvedItem.LibraryDependency.LibraryRange.TypeConstraint);
10631073

1064-
// To "evict" a chosen item, we need to also remove all of its transitive children from the chosen list.
1074+
// Check whether any previously-recorded evictions have a winning path that passes through
1075+
// the item we just evicted. Those evictions are now invalid because the ancestor that caused
1076+
// them is itself evicted. The previously-rejected items must be re-evaluated, but they are
1077+
// no longer in the queue, so the entire walk must restart.
10651078
HashSet<LibraryRangeIndex>? evicteesToRemove = default;
10661079

10671080
foreach (KeyValuePair<LibraryRangeIndex, (LibraryRangeIndex[] Path, LibraryDependencyIndex DependencyIndex, LibraryDependencyTarget TypeConstraint)> evictee in evictions)
10681081
{
1069-
// See if the evictee is a descendant of the evicted item
1082+
// Skip the eviction entry we just recorded above; it was added before this loop
1083+
// started, so the dictionary enumerator will reach it, but it is not a pre-existing
1084+
// eviction and its path will never contain evictedLibraryRangeIndex by definition.
1085+
if (evictee.Key == evictedLibraryRangeIndex)
1086+
{
1087+
continue;
1088+
}
1089+
1090+
// See if the evictee's winning path passes through the item we just evicted
10701091
if (evictee.Value.Path.Contains(evictedLibraryRangeIndex))
10711092
{
10721093
// if evictee.Key (depIndex) == currentDepIndex && evictee.TypeConstraint == ExternalProject --> Don't remove it. It must remain evicted.
@@ -1086,28 +1107,35 @@ private async Task<Dictionary<LibraryDependencyIndex, ResolvedDependencyGraphIte
10861107
foreach (LibraryRangeIndex evicteeToRemove in evicteesToRemove)
10871108
{
10881109
evictions.Remove(evicteeToRemove);
1089-
1090-
// Indicate that we can't simply evict this item and instead we need to start over knowing that this item should be skipped
1091-
shouldStartOver = true;
10921110
}
1111+
1112+
// The previously-rejected items that are now un-evicted are no longer in the queue,
1113+
// so the walk must restart from the beginning.
1114+
goto StartOver;
10931115
}
10941116

1095-
foreach (KeyValuePair<LibraryDependencyIndex, ResolvedDependencyGraphItem> chosenItem in resolvedDependencyGraphItems)
1117+
// No cascade eviction requires a restart. Remove any already-resolved items whose ancestry
1118+
// passes through the evicted item; they are stale because the evicted item is no longer in
1119+
// the graph. Items still in the queue that are children of the evicted item will be skipped
1120+
// lazily by the path check at dequeue time.
1121+
List<LibraryDependencyIndex>? resolvedItemsToRemove = null;
1122+
1123+
foreach (KeyValuePair<LibraryDependencyIndex, ResolvedDependencyGraphItem> resolvedItem in resolvedDependencyGraphItems)
10961124
{
1097-
if (chosenItem.Value.Path.Contains(evictedLibraryRangeIndex))
1125+
if (resolvedItem.Value.Path.Contains(evictedLibraryRangeIndex))
10981126
{
1099-
// Indicate that we can't simply evict this item and instead we need to start over knowing that this item should be skipped
1100-
shouldStartOver = true;
1101-
break;
1127+
resolvedItemsToRemove ??= new List<LibraryDependencyIndex>(capacity: 4);
1128+
1129+
resolvedItemsToRemove.Add(resolvedItem.Key);
11021130
}
11031131
}
11041132

1105-
// Add the eviction to be used later; children of the evicted item that remain in the queue will be skipped lazily via the path check above
1106-
evictions[evictedLibraryRangeIndex] = (DependencyGraphItemIndexer.CreatePathToRef(currentDependencyGraphItem.Path, currentDependencyGraphItem.LibraryRangeIndex), currentDependencyGraphItem.LibraryDependencyIndex, chosenResolvedItem.LibraryDependency.LibraryRange.TypeConstraint);
1107-
1108-
if (shouldStartOver)
1133+
if (resolvedItemsToRemove != null)
11091134
{
1110-
goto StartOver;
1135+
foreach (LibraryDependencyIndex key in resolvedItemsToRemove)
1136+
{
1137+
resolvedDependencyGraphItems.Remove(key);
1138+
}
11111139
}
11121140

11131141
// Add the item to the list of chosen items

0 commit comments

Comments
 (0)