Skip to content

Commit b30ec28

Browse files
MaxDesiatovfurby-tm
authored andcommitted
Add simple synthetic modules graph benchmarks (swiftlang#7465)
New `SyntheticModulesGraph` benchmark is introduced, which calls `loadModulesGraph` function that we already run against test fixtures in `ModulesGraphTests` and `BuildPlanTests`. The function under benchmark almost immediately delegates to `ModulesGraph.load`, which is used in real-world modules graph dependency resolution. Benchmark parameters are controlled with `SWIFTPM_BENCHMARK_MODULES_GRAPH_DEPTH` and `SWIFTPM_BENCHMARK_MODULES_GRAPH_WIDTH` environment variables, so in the future we should be able to plot graphs of benchmark metrics against depth and width of modules graph over a given range. Thresholds are now split into platform-specific directories so that thresholds recorded on x86_64 don't interfere with thresholds for arm64, same for the OS family.
1 parent bad6319 commit b30ec28

25 files changed

+257
-74
lines changed

Benchmarks/Benchmarks/PackageGraphBenchmarks/PackageGraphBenchmarks.swift

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly)
12
import Basics
23
import Benchmark
34
import Foundation
45
import PackageModel
6+
7+
@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly)
8+
import func PackageGraph.loadModulesGraph
9+
10+
import class TSCBasic.InMemoryFileSystem
511
import Workspace
612

713
let benchmarks = {
@@ -16,6 +22,32 @@ let benchmarks = {
1622
]
1723
}
1824

25+
let modulesGraphDepth: Int
26+
if let envVar = ProcessInfo.processInfo.environment["SWIFTPM_BENCHMARK_MODULES_GRAPH_DEPTH"],
27+
let parsedValue = Int(envVar) {
28+
modulesGraphDepth = parsedValue
29+
} else {
30+
modulesGraphDepth = 100
31+
}
32+
33+
let modulesGraphWidth: Int
34+
if let envVar = ProcessInfo.processInfo.environment["SWIFTPM_BENCHMARK_MODULES_GRAPH_WIDTH"],
35+
let parsedValue = Int(envVar) {
36+
modulesGraphWidth = parsedValue
37+
} else {
38+
modulesGraphWidth = 100
39+
}
40+
41+
let packagesGraphDepth: Int
42+
if let envVar = ProcessInfo.processInfo.environment["SWIFTPM_BENCHMARK_PACKAGES_GRAPH_DEPTH"],
43+
let parsedValue = Int(envVar) {
44+
packagesGraphDepth = parsedValue
45+
} else {
46+
packagesGraphDepth = 10
47+
}
48+
49+
let noopObservability = ObservabilitySystem.NOOP
50+
1951
// Benchmarks computation of a resolved graph of modules for a package using `Workspace` as an entry point. It runs PubGrub to get
2052
// resolved concrete versions of dependencies, assigning all modules and products to each other as corresponding dependencies
2153
// with their build triples, but with the build plan not yet constructed. In this benchmark specifically we're loading `Package.swift`
@@ -33,10 +65,57 @@ let benchmarks = {
3365
) { benchmark in
3466
let path = try AbsolutePath(validating: #file).parentDirectory.parentDirectory.parentDirectory
3567
let workspace = try Workspace(fileSystem: localFileSystem, location: .init(forRootPackage: path, fileSystem: localFileSystem))
36-
let system = ObservabilitySystem { _, _ in }
3768

3869
for _ in benchmark.scaledIterations {
39-
try workspace.loadPackageGraph(rootPath: path, observabilityScope: system.topScope)
70+
try workspace.loadPackageGraph(rootPath: path, observabilityScope: noopObservability)
71+
}
72+
}
73+
74+
75+
// Benchmarks computation of a resolved graph of modules for a synthesized package using `loadModulesGraph` as an
76+
// entry point, which almost immediately delegates to `ModulesGraph.load` under the hood.
77+
Benchmark(
78+
"SyntheticModulesGraph",
79+
configuration: .init(
80+
metrics: defaultMetrics,
81+
maxDuration: .seconds(10),
82+
thresholds: [
83+
.mallocCountTotal: .init(absolute: [.p90: 2500]),
84+
.syscalls: .init(absolute: [.p90: 0]),
85+
]
86+
)
87+
) { benchmark in
88+
let targets = try (0..<modulesGraphWidth).map { i in
89+
try TargetDescription(name: "Target\(i)", dependencies: (0..<min(i, modulesGraphDepth)).map {
90+
.target(name: "Target\($0)")
91+
})
92+
}
93+
let fileSystem = InMemoryFileSystem(
94+
emptyFiles: targets.map { "/benchmark/Sources/\($0.name)/empty.swift" }
95+
)
96+
let rootPackagePath = try AbsolutePath(validating: "/benchmark")
97+
98+
let manifest = Manifest(
99+
displayName: "benchmark",
100+
path: rootPackagePath,
101+
packageKind: .root(rootPackagePath),
102+
packageLocation: rootPackagePath.pathString,
103+
defaultLocalization: nil,
104+
platforms: [],
105+
version: nil,
106+
revision: nil,
107+
toolsVersion: .v5_10,
108+
pkgConfig: nil,
109+
providers: nil,
110+
cLanguageStandard: nil,
111+
cxxLanguageStandard: nil,
112+
swiftLanguageVersions: nil
113+
)
114+
115+
for _ in benchmark.scaledIterations {
116+
try blackHole(
117+
loadModulesGraph(fileSystem: fileSystem, manifests: [manifest], observabilityScope: noopObservability)
118+
)
40119
}
41120
}
42121
}

Benchmarks/README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# SwiftPM Benchmarks
22

3-
Benchmarks currently use [ordo-one/package-benchmark](https://github.com/ordo-one/package-benchmark) library for benchmarking.
3+
Benchmarks currently use [ordo-one/package-benchmark](https://github.com/ordo-one/package-benchmark) library for
4+
benchmarking.
45

56
## How to Run
67

7-
To run the benchmarks in their default configuration, run this commend in the `Benchmarks` subdirectory of the SwiftPM repository clone (the directory in which this `README.md` file is contained):
8+
To run the benchmarks in their default configuration, run this command in the `Benchmarks` subdirectory of the SwiftPM
9+
repository clone (the directory in which this `README.md` file is contained):
10+
811
```
912
swift package benchmark
1013
```
@@ -17,13 +20,17 @@ SWIFTPM_BENCHMARK_ALL_METRICS=true swift package benchmark
1720

1821
## Benchmark Thresholds
1922

20-
`Benchmarks/Thresholds` subdirectory contains recorded allocation and syscall counts for macOS on Apple Silicon when built with Swift 5.10. To record new thresholds, run the following command:
23+
`Benchmarks/Thresholds` subdirectory contains recorded allocation and syscall counts for macOS on Apple Silicon when
24+
built with Swift 5.10. To record new thresholds, run the following command:
2125

2226
```
23-
swift package --allow-writing-to-package-directory benchmark --format metricP90AbsoluteThresholds --path Thresholds/
27+
swift package --allow-writing-to-package-directory benchmark \
28+
--format metricP90AbsoluteThresholds \
29+
--path "Thresholds/$([[ $(uname) == Darwin ]] && echo macosx || echo linux)-$(uname -m)"
2430
```
2531

26-
To verify that recorded thresholds do not exceeded given relative or absolute values (passed as `thresholds` arguments to each benchmark's configuration), run this command:
32+
To verify that recorded thresholds do not exceeded given relative or absolute values (passed as `thresholds` arguments
33+
to each benchmark's configuration), run this command:
2734

2835
```
2936
swift package benchmark baseline check --check-absolute-path Thresholds/

Benchmarks/Thresholds/PackageGraphBenchmarks.PackageGraphLoading.p90.json

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"mallocCountTotal" : 10775,
3+
"syscalls" : 1508
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"mallocCountTotal" : 2427,
3+
"syscalls" : 0
4+
}

Sources/Basics/FileSystem/FileSystem+Extensions.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import class TSCBasic.FileLock
2121
import enum TSCBasic.FileMode
2222
import protocol TSCBasic.FileSystem
2323
import enum TSCBasic.FileSystemAttribute
24+
import class TSCBasic.InMemoryFileSystem
2425
import var TSCBasic.localFileSystem
2526
import protocol TSCBasic.WritableByteStream
2627

@@ -632,3 +633,48 @@ extension FileLock {
632633
return try Self.prepareLock(fileToLock: fileToLock.underlying, at: lockFilesDirectory?.underlying)
633634
}
634635
}
636+
637+
/// Convenience initializers for testing purposes.
638+
extension InMemoryFileSystem {
639+
/// Create a new file system with the given files, provided as a map from
640+
/// file path to contents.
641+
public convenience init(files: [String: ByteString]) {
642+
self.init()
643+
644+
for (path, contents) in files {
645+
let path = try! AbsolutePath(validating: path)
646+
try! createDirectory(path.parentDirectory, recursive: true)
647+
try! writeFileContents(path, bytes: contents)
648+
}
649+
}
650+
651+
/// Create a new file system with an empty file at each provided path.
652+
public convenience init(emptyFiles files: String...) {
653+
self.init(emptyFiles: files)
654+
}
655+
656+
/// Create a new file system with an empty file at each provided path.
657+
public convenience init(emptyFiles files: [String]) {
658+
self.init()
659+
self.createEmptyFiles(at: .root, files: files)
660+
}
661+
}
662+
663+
extension FileSystem {
664+
public func createEmptyFiles(at root: AbsolutePath, files: String...) {
665+
self.createEmptyFiles(at: root, files: files)
666+
}
667+
668+
public func createEmptyFiles(at root: AbsolutePath, files: [String]) {
669+
do {
670+
try createDirectory(root, recursive: true)
671+
for path in files {
672+
let path = try AbsolutePath(validating: String(path.dropFirst()), relativeTo: root)
673+
try createDirectory(path.parentDirectory, recursive: true)
674+
try writeFileContents(path, bytes: "")
675+
}
676+
} catch {
677+
fatalError("Failed to create empty files: \(error)")
678+
}
679+
}
680+
}

Sources/Basics/Graph/DirectedGraph.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,17 @@
1313
import struct DequeModule.Deque
1414

1515
/// Directed graph that stores edges in [adjacency lists](https://en.wikipedia.org/wiki/Adjacency_list).
16-
struct DirectedGraph<Node> {
17-
init(nodes: [Node]) {
16+
@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly)
17+
public struct DirectedGraph<Node> {
18+
public init(nodes: [Node]) {
1819
self.nodes = nodes
1920
self.edges = .init(repeating: [], count: nodes.count)
2021
}
2122

22-
private var nodes: [Node]
23+
public private(set) var nodes: [Node]
2324
private var edges: [[Int]]
2425

25-
mutating func addEdge(source: Int, destination: Int) {
26+
public mutating func addEdge(source: Int, destination: Int) {
2627
self.edges[source].append(destination)
2728
}
2829

@@ -31,7 +32,8 @@ struct DirectedGraph<Node> {
3132
/// - source: `Index` of a node to start traversing edges from.
3233
/// - destination: `Index` of a node to which a path could exist via edges from `source`.
3334
/// - Returns: `true` if a path from `source` to `destination` exists, `false` otherwise.
34-
func areNodesConnected(source: Int, destination: Int) -> Bool {
35+
@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly)
36+
public func areNodesConnected(source: Int, destination: Int) -> Bool {
3537
var todo = Deque<Int>([source])
3638
var done = Set<Int>()
3739

Sources/Basics/Graph/UndirectedGraph.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,17 @@
1313
import struct DequeModule.Deque
1414

1515
/// Undirected graph that stores edges in an [adjacency matrix](https://en.wikipedia.org/wiki/Adjacency_list).
16-
struct UndirectedGraph<Node> {
17-
init(nodes: [Node]) {
16+
@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly)
17+
public struct UndirectedGraph<Node> {
18+
public init(nodes: [Node]) {
1819
self.nodes = nodes
1920
self.edges = .init(rows: nodes.count, columns: nodes.count)
2021
}
2122

2223
private var nodes: [Node]
2324
private var edges: AdjacencyMatrix
2425

25-
mutating func addEdge(source: Int, destination: Int) {
26+
public mutating func addEdge(source: Int, destination: Int) {
2627
// Adjacency matrix is symmetrical for undirected graphs.
2728
self.edges[source, destination] = true
2829
self.edges[destination, source] = true
@@ -33,7 +34,7 @@ struct UndirectedGraph<Node> {
3334
/// - source: `Index` of a node to start traversing edges from.
3435
/// - destination: `Index` of a node to which a connection could exist via edges from `source`.
3536
/// - Returns: `true` if a path from `source` to `destination` exists, `false` otherwise.
36-
func areNodesConnected(source: Int, destination: Int) -> Bool {
37+
public func areNodesConnected(source: Int, destination: Int) -> Bool {
3738
var todo = Deque<Int>([source])
3839
var done = Set<Int>()
3940

Sources/Basics/Observability.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ public class ObservabilitySystem {
5656
self.underlying(scope, diagnostic)
5757
}
5858
}
59+
60+
public static var NOOP: ObservabilityScope {
61+
ObservabilitySystem { _, _ in }.topScope
62+
}
5963
}
6064

6165
public protocol ObservabilityHandlerProvider {

Sources/PackageGraph/ModulesGraph.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import protocol Basics.FileSystem
14+
import class Basics.ObservabilityScope
1315
import struct Basics.IdentifiableSet
1416
import OrderedCollections
1517
import PackageLoading
@@ -444,3 +446,50 @@ func topologicalSort<T: Identifiable>(
444446

445447
return result.reversed()
446448
}
449+
450+
@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly)
451+
public func loadModulesGraph(
452+
identityResolver: IdentityResolver = DefaultIdentityResolver(),
453+
fileSystem: FileSystem,
454+
manifests: [Manifest],
455+
binaryArtifacts: [PackageIdentity: [String: BinaryArtifact]] = [:],
456+
explicitProduct: String? = .none,
457+
shouldCreateMultipleTestProducts: Bool = false,
458+
createREPLProduct: Bool = false,
459+
useXCBuildFileRules: Bool = false,
460+
customXCTestMinimumDeploymentTargets: [PackageModel.Platform: PlatformVersion]? = .none,
461+
observabilityScope: ObservabilityScope
462+
) throws -> ModulesGraph {
463+
let rootManifests = manifests.filter(\.packageKind.isRoot).spm_createDictionary { ($0.path, $0) }
464+
let externalManifests = try manifests.filter { !$0.packageKind.isRoot }
465+
.reduce(
466+
into: OrderedCollections
467+
.OrderedDictionary<PackageIdentity, (manifest: Manifest, fs: FileSystem)>()
468+
) { partial, item in
469+
partial[try identityResolver.resolveIdentity(for: item.packageKind)] = (item, fileSystem)
470+
}
471+
472+
let packages = Array(rootManifests.keys)
473+
let input = PackageGraphRootInput(packages: packages)
474+
let graphRoot = PackageGraphRoot(
475+
input: input,
476+
manifests: rootManifests,
477+
explicitProduct: explicitProduct,
478+
observabilityScope: observabilityScope
479+
)
480+
481+
return try ModulesGraph.load(
482+
root: graphRoot,
483+
identityResolver: identityResolver,
484+
additionalFileRules: useXCBuildFileRules ? FileRuleDescription.xcbuildFileTypes : FileRuleDescription
485+
.swiftpmFileTypes,
486+
externalManifests: externalManifests,
487+
binaryArtifacts: binaryArtifacts,
488+
shouldCreateMultipleTestProducts: shouldCreateMultipleTestProducts,
489+
createREPLProduct: createREPLProduct,
490+
customXCTestMinimumDeploymentTargets: customXCTestMinimumDeploymentTargets,
491+
availableLibraries: [],
492+
fileSystem: fileSystem,
493+
observabilityScope: observabilityScope
494+
)
495+
}

Sources/SPMTestSupport/MockPackageGraphs.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313
import struct Basics.AbsolutePath
1414
import class Basics.ObservabilitySystem
1515
import class Basics.ObservabilityScope
16+
1617
import struct PackageGraph.ModulesGraph
18+
19+
@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly)
20+
import func PackageGraph.loadModulesGraph
21+
1722
import class PackageModel.Manifest
1823
import struct PackageModel.ProductDescription
1924
import struct PackageModel.TargetDescription

Sources/SPMTestSupport/Observability.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ extension ObservabilitySystem {
2424
let observabilitySystem = ObservabilitySystem(collector)
2525
return TestingObservability(collector: collector, topScope: observabilitySystem.topScope)
2626
}
27-
28-
package static var NOOP: ObservabilityScope {
29-
ObservabilitySystem { _, _ in }.topScope
30-
}
3127
}
3228

3329
package struct TestingObservability {

0 commit comments

Comments
 (0)