Skip to content

Commit

Permalink
Use topologicalSort with Identifiable on ResolvedTarget
Browse files Browse the repository at this point in the history
Fix fixes a serious performance regression introduced for deep target dependency graphs in #7160. After converting `ResolvedTarget` to a value type, its synthesized `Hashable` conformance traversed the whole dependency tree multiple times when using `topologicalSort`. This made package resolution seemingly hang for packages that had a lot of nested target graphs.

We already have an implementation of `topologicalSort` on `Identifiable`, thus we should use that instead. I've also added a performance test for this in `PackageGraphPerfTests` to prevent future regressions in this area.
  • Loading branch information
MaxDesiatov committed Dec 20, 2023
1 parent 5f85bff commit e542e7a
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 74 deletions.
4 changes: 4 additions & 0 deletions Sources/PackageGraph/GraphLoadingNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,7 @@ extension GraphLoadingNode: CustomStringConvertible {
}
}
}

extension GraphLoadingNode: Identifiable {
public var id: PackageIdentity { self.identity }
}
1 change: 0 additions & 1 deletion Sources/PackageGraph/PackageGraph+Loading.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import OrderedCollections
import PackageLoading
import PackageModel

import func TSCBasic.topologicalSort
import func TSCBasic.bestMatch

extension PackageGraph {
Expand Down
22 changes: 10 additions & 12 deletions Sources/PackageGraph/Resolution/ResolvedTarget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@

import PackageModel

import func TSCBasic.topologicalSort

/// Represents a fully resolved target. All the dependencies for this target are also stored as resolved.
public struct ResolvedTarget: Hashable {
/// Represents dependency of a resolved target.
Expand Down Expand Up @@ -72,7 +70,7 @@ public struct ResolvedTarget: Hashable {

/// The name of this target.
public var name: String {
return underlying.name
self.underlying.name
}

/// Returns dependencies which satisfy the input build environment, based on their conditions.
Expand All @@ -84,49 +82,49 @@ public struct ResolvedTarget: Hashable {

/// Returns the recursive dependencies, across the whole package-graph.
public func recursiveDependencies() throws -> [Dependency] {
return try TSCBasic.topologicalSort(self.dependencies) { $0.dependencies }
try topologicalSort(self.dependencies) { $0.dependencies }
}

/// Returns the recursive target dependencies, across the whole package-graph.
public func recursiveTargetDependencies() throws -> [ResolvedTarget] {
return try TSCBasic.topologicalSort(self.dependencies) { $0.dependencies }.compactMap { $0.target }
try topologicalSort(self.dependencies) { $0.dependencies }.compactMap { $0.target }
}

/// Returns the recursive dependencies, across the whole package-graph, which satisfy the input build environment,
/// based on their conditions.
/// - Parameters:
/// - environment: The build environment to use to filter dependencies on.
public func recursiveDependencies(satisfying environment: BuildEnvironment) throws -> [Dependency] {
return try TSCBasic.topologicalSort(dependencies(satisfying: environment)) { dependency in
return dependency.dependencies.filter { $0.satisfies(environment) }
try topologicalSort(dependencies(satisfying: environment)) { dependency in
dependency.dependencies.filter { $0.satisfies(environment) }
}
}

/// The language-level target name.
public var c99name: String {
return underlying.c99name
self.underlying.c99name
}

/// Module aliases for dependencies of this target. The key is an
/// original target name and the value is a new unique name mapped
/// to the name of its .swiftmodule binary.
public var moduleAliases: [String: String]? {
return underlying.moduleAliases
self.underlying.moduleAliases
}

/// Allows access to package symbols from other targets in the package
public var packageAccess: Bool {
return underlying.packageAccess
self.underlying.packageAccess
}

/// The "type" of target.
public var type: Target.Kind {
return underlying.type
self.underlying.type
}

/// The sources for the target.
public var sources: Sources {
return underlying.sources
self.underlying.sources
}

let packageIdentity: PackageIdentity
Expand Down
42 changes: 42 additions & 0 deletions Sources/SPMTestSupport/ResolvedTarget+Mock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2017 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import PackageGraph
import PackageModel

extension ResolvedTarget {
public static func mock(
packageIdentity: PackageIdentity,
name: String,
deps: ResolvedTarget...,
conditions: [PackageCondition] = []
) -> ResolvedTarget {
ResolvedTarget(
packageIdentity: packageIdentity,
underlying: SwiftTarget(
name: name,
type: .library,
path: .root,
sources: Sources(paths: [], root: "/"),
dependencies: [],
packageAccess: false,
swiftVersion: .v4,
usesUnsafeFlags: false
),
dependencies: deps.map { .target($0, conditions: conditions) },
defaultLocalization: nil,
supportedPlatforms: [],
platformVersionProvider: .init(implementation: .minimumDeploymentTargetDefault)
)
}
}

20 changes: 19 additions & 1 deletion Tests/PackageGraphPerformanceTests/PackageGraphPerfTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2017 Apple Inc. and the Swift project authors
// Copyright (c) 2014-2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
Expand Down Expand Up @@ -161,4 +161,22 @@ final class PackageGraphPerfTests: XCTestCasePerf {
}
}
}

func testRecursiveDependencies() throws {
var resolvedTarget = ResolvedTarget.mock(packageIdentity: "pkg", name: "t0")
for i in 1..<1000 {
resolvedTarget = ResolvedTarget.mock(packageIdentity: "pkg", name: "t\(i)", deps: resolvedTarget)
}

let N = 100
measure {
do {
for _ in 0..<N {
_ = try resolvedTarget.recursiveTargetDependencies()
}
} catch {
XCTFail("Loading package graph is not expected to fail in this test.")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2018 Apple Inc. and the Swift project authors
// Copyright (c) 2014-2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
Expand All @@ -14,84 +14,58 @@ import XCTest

import PackageGraph
@testable import PackageModel
import SPMTestSupport

extension ResolvedTarget {
fileprivate init(
packageIdentity: String,
name: String,
deps: ResolvedTarget...,
conditions: [PackageCondition] = []
) {
self.init(
packageIdentity: .init(packageIdentity),
underlying: SwiftTarget(
name: name,
type: .library,
path: .root,
sources: Sources(paths: [], root: "/"),
dependencies: [],
packageAccess: false,
swiftVersion: .v4,
usesUnsafeFlags: false
),
dependencies: deps.map { .target($0, conditions: conditions) },
defaultLocalization: nil,
supportedPlatforms: [],
platformVersionProvider: .init(implementation: .minimumDeploymentTargetDefault)
)
}
}

class TargetDependencyTests: XCTestCase {
final class ResolvedTargetDependencyTests: XCTestCase {
func test1() throws {
let t1 = ResolvedTarget(packageIdentity: "pkg", name: "t1")
let t2 = ResolvedTarget(packageIdentity: "pkg", name: "t2", deps: t1)
let t3 = ResolvedTarget(packageIdentity: "pkg", name: "t3", deps: t2)
let t1 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t1")
let t2 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t2", deps: t1)
let t3 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t3", deps: t2)

XCTAssertEqual(try t3.recursiveTargetDependencies(), [t2, t1])
XCTAssertEqual(try t2.recursiveTargetDependencies(), [t1])
}

func test2() throws {
let t1 = ResolvedTarget(packageIdentity: "pkg", name: "t1")
let t2 = ResolvedTarget(packageIdentity: "pkg", name: "t2", deps: t1)
let t3 = ResolvedTarget(packageIdentity: "pkg", name: "t3", deps: t2, t1)
let t4 = ResolvedTarget(packageIdentity: "pkg", name: "t4", deps: t2, t3, t1)
let t1 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t1")
let t2 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t2", deps: t1)
let t3 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t3", deps: t2, t1)
let t4 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t4", deps: t2, t3, t1)

XCTAssertEqual(try t4.recursiveTargetDependencies(), [t3, t2, t1])
XCTAssertEqual(try t3.recursiveTargetDependencies(), [t2, t1])
XCTAssertEqual(try t2.recursiveTargetDependencies(), [t1])
}

func test3() throws {
let t1 = ResolvedTarget(packageIdentity: "pkg", name: "t1")
let t2 = ResolvedTarget(packageIdentity: "pkg", name: "t2", deps: t1)
let t3 = ResolvedTarget(packageIdentity: "pkg", name: "t3", deps: t2, t1)
let t4 = ResolvedTarget(packageIdentity: "pkg", name: "t4", deps: t1, t2, t3)
let t1 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t1")
let t2 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t2", deps: t1)
let t3 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t3", deps: t2, t1)
let t4 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t4", deps: t1, t2, t3)

XCTAssertEqual(try t4.recursiveTargetDependencies(), [t3, t2, t1])
XCTAssertEqual(try t3.recursiveTargetDependencies(), [t2, t1])
XCTAssertEqual(try t2.recursiveTargetDependencies(), [t1])
}

func test4() throws {
let t1 = ResolvedTarget(packageIdentity: "pkg", name: "t1")
let t2 = ResolvedTarget(packageIdentity: "pkg", name: "t2", deps: t1)
let t3 = ResolvedTarget(packageIdentity: "pkg", name: "t3", deps: t2)
let t4 = ResolvedTarget(packageIdentity: "pkg", name: "t4", deps: t3)
let t1 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t1")
let t2 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t2", deps: t1)
let t3 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t3", deps: t2)
let t4 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t4", deps: t3)

XCTAssertEqual(try t4.recursiveTargetDependencies(), [t3, t2, t1])
XCTAssertEqual(try t3.recursiveTargetDependencies(), [t2, t1])
XCTAssertEqual(try t2.recursiveTargetDependencies(), [t1])
}

func test5() throws {
let t1 = ResolvedTarget(packageIdentity: "pkg", name: "t1")
let t2 = ResolvedTarget(packageIdentity: "pkg", name: "t2", deps: t1)
let t3 = ResolvedTarget(packageIdentity: "pkg", name: "t3", deps: t2)
let t4 = ResolvedTarget(packageIdentity: "pkg", name: "t4", deps: t3)
let t5 = ResolvedTarget(packageIdentity: "pkg", name: "t5", deps: t2)
let t6 = ResolvedTarget(packageIdentity: "pkg", name: "t6", deps: t5, t4)
let t1 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t1")
let t2 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t2", deps: t1)
let t3 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t3", deps: t2)
let t4 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t4", deps: t3)
let t5 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t5", deps: t2)
let t6 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t6", deps: t5, t4)

// precise order is not important, but it is important that the following are true
let t6rd = try t6.recursiveTargetDependencies()
Expand All @@ -108,12 +82,12 @@ class TargetDependencyTests: XCTestCase {
}

func test6() throws {
let t1 = ResolvedTarget(packageIdentity: "pkg", name: "t1")
let t2 = ResolvedTarget(packageIdentity: "pkg", name: "t2", deps: t1)
let t3 = ResolvedTarget(packageIdentity: "pkg", name: "t3", deps: t2)
let t4 = ResolvedTarget(packageIdentity: "pkg", name: "t4", deps: t3)
let t5 = ResolvedTarget(packageIdentity: "pkg", name: "t5", deps: t2)
let t6 = ResolvedTarget(
let t1 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t1")
let t2 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t2", deps: t1)
let t3 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t3", deps: t2)
let t4 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t4", deps: t3)
let t5 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t5", deps: t2)
let t6 = ResolvedTarget.mock(
packageIdentity: "pkg",
name: "t6",
deps: t4,
Expand All @@ -135,10 +109,10 @@ class TargetDependencyTests: XCTestCase {
}

func testConditions() throws {
let t1 = ResolvedTarget(packageIdentity: "pkg", name: "t1")
let t2 = ResolvedTarget(packageIdentity: "pkg", name: "t2", deps: t1)
let t2NoConditions = ResolvedTarget(packageIdentity: "pkg", name: "t2", deps: t1)
let t2WithConditions = ResolvedTarget(
let t1 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t1")
let t2 = ResolvedTarget.mock(packageIdentity: "pkg", name: "t2", deps: t1)
let t2NoConditions = ResolvedTarget.mock(packageIdentity: "pkg", name: "t2", deps: t1)
let t2WithConditions = ResolvedTarget.mock(
packageIdentity: "pkg",
name: "t2",
deps: t1,
Expand Down

0 comments on commit e542e7a

Please sign in to comment.