Skip to content

Commit bfde016

Browse files
authored
dependency resolution performance improvements (#3106)
motivation: better performance, happier users changes: * load root manifests in parallel * add caching to manifest loader * further tweaks to pubgrub data strucutures to make them thread-safe, and simplify flow * comments cleanup
1 parent d30183c commit bfde016

File tree

7 files changed

+265
-201
lines changed

7 files changed

+265
-201
lines changed

Sources/Basics/ConcurrencyHelpers.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ public final class ThreadSafeKeyValueStore<Key, Value> where Key: Hashable {
4242
}
4343
}
4444

45+
public var count: Int {
46+
self.lock.withLock {
47+
self.underlying.count
48+
}
49+
}
50+
4551
public var isEmpty: Bool {
4652
self.lock.withLock {
4753
self.underlying.isEmpty
@@ -53,6 +59,12 @@ public final class ThreadSafeKeyValueStore<Key, Value> where Key: Hashable {
5359
self.underlying.keys.contains(key)
5460
}
5561
}
62+
63+
public func mapValues<T>(_ transform: (Value) throws -> T) rethrows -> [Key : T] {
64+
try self.lock.withLock {
65+
try self.underlying.mapValues(transform)
66+
}
67+
}
5668
}
5769

5870
/// Thread-safe value boxing structure
@@ -62,6 +74,10 @@ public final class ThreadSafeBox<Value> {
6274

6375
public init() {}
6476

77+
public init(_ seed: Value) {
78+
self.underlying = seed
79+
}
80+
6581
@discardableResult
6682
public func memoize(body: () throws -> Value) rethrows -> Value {
6783
if let value = self.get() {
@@ -85,6 +101,12 @@ public final class ThreadSafeBox<Value> {
85101
self.underlying
86102
}
87103
}
104+
105+
public func put(_ newValue: Value) {
106+
self.lock.withLock {
107+
self.underlying = newValue
108+
}
109+
}
88110
}
89111

90112
// FIXME: mark as deprecated once async/await is available

Sources/PackageGraph/Pubgrub/PubgrubDependencyResolver.swift

Lines changed: 71 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,9 @@ public struct PubgrubDependencyResolver {
7676

7777
func decide(_ node: DependencyResolutionNode, at version: Version) {
7878
let term = Term(node, .exact(version))
79-
// FIXME: Shouldn't we check this _before_ making a decision?
80-
assert(term.isValidDecision(for: self.solution))
81-
8279
self.lock.withLock {
80+
// FIXME: Shouldn't we check this _before_ making a decision?
81+
assert(term.isValidDecision(for: self.solution))
8382
self.solution.decide(node, at: version)
8483
}
8584
}
@@ -168,7 +167,7 @@ public struct PubgrubDependencyResolver {
168167

169168
// If version solving failing, build the user-facing diagnostic.
170169
if let pubGrubError = error as? PubgrubError, let rootCause = pubGrubError.rootCause, let state = pubGrubError.state {
171-
let builder = DiagnosticReportBuilder(
170+
var builder = DiagnosticReportBuilder(
172171
root: root,
173172
incompatibilities: state.incompatibilities,
174173
provider: self.provider
@@ -442,7 +441,6 @@ public struct PubgrubDependencyResolver {
442441
return (overriddenPackages, rootIncompatibilities)
443442
}
444443

445-
// 👀 IMPORANT CHANGE
446444
/// Perform unit propagation, resolving conflicts if necessary and making
447445
/// decisions if nothing else is left to be done.
448446
/// After this method returns `solution` is either populated with a list of
@@ -616,27 +614,30 @@ public struct PubgrubDependencyResolver {
616614
}
617615

618616
private func computeCounts(for terms: [Term], completion: @escaping (Result<[Term: Int], Error>) -> Void) {
619-
let lock = Lock()
620-
var results = [Term: Result<Int, Error>]()
617+
if terms.isEmpty {
618+
return completion(.success([:]))
619+
}
620+
621+
let sync = DispatchGroup()
622+
let results = ThreadSafeKeyValueStore<Term, Result<Int, Error>>()
621623

622624
terms.forEach { term in
625+
sync.enter()
623626
provider.getContainer(for: term.node.package) { result in
624-
let result = result.flatMap { container in Result(catching: { try container.versionCount(term.requirement) }) }
625-
lock.withLock {
626-
results[term] = result
627-
if results.count == terms.count {
628-
do {
629-
completion(.success(try results.mapValues { try $0.get() }))
630-
} catch {
631-
completion(.failure(error))
632-
}
633-
}
634-
}
627+
defer { sync.leave() }
628+
results[term] = result.flatMap { container in Result(catching: { try container.versionCount(term.requirement) }) }
629+
}
630+
}
631+
632+
sync.notify(queue: self.queue) {
633+
do {
634+
completion(.success(try results.mapValues { try $0.get() }))
635+
} catch {
636+
completion(.failure(error))
635637
}
636638
}
637639
}
638640

639-
// 👀 IMPORANT CHANGE
640641
internal func makeDecision(state: State, completion: @escaping (Result<DependencyResolutionNode?, Error>) -> Void) {
641642
// If there are no more undecided terms, version solving is complete.
642643
let undecided = state.solution.undecided
@@ -651,49 +652,44 @@ public struct PubgrubDependencyResolver {
651652
let counts = try result.get()
652653
// forced unwraps safe since we are testing for count and errors above
653654
let pkgTerm = undecided.min { counts[$0]! < counts[$1]! }!
654-
provider.getContainer(for: pkgTerm.node.package) { result in
655-
do {
656-
let container = try result.get()
657-
// Get the best available version for this package.
658-
guard let version = try container.getBestAvailableVersion(for: pkgTerm) else {
659-
state.addIncompatibility(Incompatibility(pkgTerm, root: state.root, cause: .noAvailableVersion), at: .decisionMaking)
660-
return completion(.success(pkgTerm.node))
661-
}
662-
663-
// Add all of this version's dependencies as incompatibilities.
664-
let depIncompatibilities = try container.incompatibilites(
665-
at: version,
666-
node: pkgTerm.node,
667-
overriddenPackages: state.overriddenPackages,
668-
root: state.root
669-
)
670-
671-
var haveConflict = false
672-
for incompatibility in depIncompatibilities {
673-
// Add the incompatibility to our partial solution.
674-
state.addIncompatibility(incompatibility, at: .decisionMaking)
675-
676-
// Check if this incompatibility will statisfy the solution.
677-
haveConflict = haveConflict || incompatibility.terms.allSatisfy {
678-
// We only need to check if the terms other than this package
679-
// are satisfied because we _know_ that the terms matching
680-
// this package will be satisfied if we make this version
681-
// as a decision.
682-
$0.node == pkgTerm.node || state.solution.satisfies($0)
683-
}
684-
}
655+
// at this point the container is cached
656+
let container = try provider.getCachedContainer(for: pkgTerm.node.package)
657+
// Get the best available version for this package.
658+
guard let version = try container.getBestAvailableVersion(for: pkgTerm) else {
659+
state.addIncompatibility(Incompatibility(pkgTerm, root: state.root, cause: .noAvailableVersion), at: .decisionMaking)
660+
return completion(.success(pkgTerm.node))
661+
}
685662

686-
// Decide this version if there was no conflict with its dependencies.
687-
if !haveConflict {
688-
log("decision: \(pkgTerm.node.package)@\(version)")
689-
state.decide(pkgTerm.node, at: version)
690-
}
663+
// Add all of this version's dependencies as incompatibilities.
664+
let depIncompatibilities = try container.incompatibilites(
665+
at: version,
666+
node: pkgTerm.node,
667+
overriddenPackages: state.overriddenPackages,
668+
root: state.root
669+
)
691670

692-
completion(.success(pkgTerm.node))
693-
} catch {
694-
completion(.failure(error))
671+
var haveConflict = false
672+
for incompatibility in depIncompatibilities {
673+
// Add the incompatibility to our partial solution.
674+
state.addIncompatibility(incompatibility, at: .decisionMaking)
675+
676+
// Check if this incompatibility will statisfy the solution.
677+
haveConflict = haveConflict || incompatibility.terms.allSatisfy {
678+
// We only need to check if the terms other than this package
679+
// are satisfied because we _know_ that the terms matching
680+
// this package will be satisfied if we make this version
681+
// as a decision.
682+
$0.node == pkgTerm.node || state.solution.satisfies($0)
695683
}
696684
}
685+
686+
// Decide this version if there was no conflict with its dependencies.
687+
if !haveConflict {
688+
log("decision: \(pkgTerm.node.package)@\(version)")
689+
state.decide(pkgTerm.node, at: version)
690+
}
691+
692+
completion(.success(pkgTerm.node))
697693
} catch {
698694
completion(.failure(error))
699695
}
@@ -715,7 +711,7 @@ public struct PubgrubDependencyResolver {
715711
}
716712
}
717713

718-
private final class DiagnosticReportBuilder {
714+
private struct DiagnosticReportBuilder {
719715
let rootNode: DependencyResolutionNode
720716
let incompatibilities: [DependencyResolutionNode: [Incompatibility]]
721717

@@ -730,10 +726,10 @@ private final class DiagnosticReportBuilder {
730726
self.provider = provider
731727
}
732728

733-
func makeErrorReport(for rootCause: Incompatibility) throws -> String {
729+
mutating func makeErrorReport(for rootCause: Incompatibility) throws -> String {
734730
/// Populate `derivations`.
735731
func countDerivations(_ i: Incompatibility) {
736-
derivations[i, default: 0] += 1
732+
self.derivations[i, default: 0] += 1
737733
if case .conflict(let cause) = i.cause {
738734
countDerivations(cause.conflict)
739735
countDerivations(cause.other)
@@ -773,7 +769,7 @@ private final class DiagnosticReportBuilder {
773769
return stream.bytes.description
774770
}
775771

776-
private func visit(
772+
private mutating func visit(
777773
_ incompatibility: Incompatibility,
778774
isConclusion: Bool = false
779775
) throws {
@@ -1038,21 +1034,21 @@ private final class DiagnosticReportBuilder {
10381034
/// the incompatibility and how it as derived. If `isNumbered` is true, a
10391035
/// line number will be assigned to this incompatibility so that it can be
10401036
/// referred to again.
1041-
private func record(
1037+
private mutating func record(
10421038
_ incompatibility: Incompatibility,
10431039
message: String,
10441040
isNumbered: Bool
10451041
) {
10461042
var number = -1
10471043
if isNumbered {
10481044
number = lineNumbers.count + 1
1049-
lineNumbers[incompatibility] = number
1045+
self.lineNumbers[incompatibility] = number
10501046
}
10511047
let line = (number: number, message: message)
10521048
if isNumbered {
1053-
lines.append(line)
1049+
self.lines.append(line)
10541050
} else {
1055-
lines.insert(line, at: 0)
1051+
self.lines.insert(line, at: 0)
10561052
}
10571053
}
10581054
}
@@ -1073,10 +1069,10 @@ private final class PubGrubPackageContainer {
10731069

10741070
/// The map of dependencies to version set that indicates the versions that have had their
10751071
/// incompatibilities emitted.
1076-
private var emittedIncompatibilities: [PackageReference: VersionSetSpecifier] = [:]
1072+
private var emittedIncompatibilities = ThreadSafeKeyValueStore<PackageReference, VersionSetSpecifier>()
10771073

10781074
/// Whether we've emitted the incompatibilities for the pinned versions.
1079-
private var emittedPinnedVersionIncompatibilities: Bool = false
1075+
private var emittedPinnedVersionIncompatibilities = ThreadSafeBox(false)
10801076

10811077
init(underlying: PackageContainer, pinsMap: PinsStore.PinsMap, queue: DispatchQueue) {
10821078
self.underlying = underlying
@@ -1232,9 +1228,11 @@ private final class PubGrubPackageContainer {
12321228
if version == pinnedVersion, emittedIncompatibilities.isEmpty {
12331229
// We don't need to emit anything if we already emitted the incompatibilities at the
12341230
// pinned version.
1235-
if self.emittedPinnedVersionIncompatibilities { return [] }
1231+
if self.emittedPinnedVersionIncompatibilities.get() ?? false {
1232+
return []
1233+
}
12361234

1237-
self.emittedPinnedVersionIncompatibilities = true
1235+
self.emittedPinnedVersionIncompatibilities.put(true)
12381236

12391237
// Since the pinned version is most likely to succeed, we don't compute bounds for its
12401238
// incompatibilities.
@@ -1282,7 +1280,6 @@ private final class PubGrubPackageContainer {
12821280
}
12831281
}
12841282

1285-
// 👀 IMPORANT CHANGE
12861283
/// Method for computing bounds of the given dependencies.
12871284
///
12881285
/// This will return a dictionary which contains mapping of a package dependency to its bound.
@@ -1297,7 +1294,7 @@ private final class PubGrubPackageContainer {
12971294
timeout: DispatchTimeInterval
12981295
) throws -> (lowerBounds: [PackageReference: Version], upperBounds: [PackageReference: Version]) {
12991296
let preloadCount = 3
1300-
1297+
13011298
// nothing to do
13021299
if constraints.isEmpty {
13031300
return ([:], [:])
@@ -1388,7 +1385,6 @@ private final class PubGrubPackageContainer {
13881385
}
13891386
}
13901387

1391-
// 👀 IMPORANT CHANGE
13921388
/// An utility class around PackageContainerProvider that allows "prefetching" the containers
13931389
/// in parallel. The basic idea is to kick off container fetching before starting the resolution
13941390
/// by using the list of URLs from the Package.resolved file.
@@ -1431,9 +1427,7 @@ private final class ContainerProvider {
14311427
func getContainer(for identifier: PackageReference, completion: @escaping (Result<PubGrubPackageContainer, Error>) -> Void) {
14321428
// Return the cached container, if available.
14331429
if let container = self.containersCache[identifier] {
1434-
return self.queue.async {
1435-
completion(.success(container))
1436-
}
1430+
return completion(.success(container))
14371431
}
14381432

14391433
if let prefetchSync = self.prefetches[identifier] {
@@ -1447,7 +1441,7 @@ private final class ContainerProvider {
14471441
} else {
14481442
// if prefetch failed, remove from list of prefetches and try again
14491443
self.prefetches[identifier] = nil
1450-
self.getContainer(for: identifier, completion: completion)
1444+
return self.getContainer(for: identifier, completion: completion)
14511445
}
14521446
}
14531447
} else {

Sources/PackageGraph/Pubgrub/Term.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,8 @@ public struct Term: Equatable, Hashable {
104104
return true
105105
}
106106

107+
// From: https://github.com/dart-lang/pub/blob/master/lib/src/solver/term.dart
107108
public func relation(with other: Term) -> SetRelation {
108-
// From: https://github.com/dart-lang/pub/blob/master/lib/src/solver/term.dart
109-
110109
if self.node != other.node {
111110
fatalError("attempting to compute relation between different packages \(self) \(other)")
112111
}

0 commit comments

Comments
 (0)