Skip to content

Avoid redundant checks in cycle detection #5839

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Oct 28, 2022
Merged

Conversation

fcanas
Copy link
Contributor

@fcanas fcanas commented Oct 25, 2022

Dramatically improve speed of cycle detection for large or well-connected manifest graphs.

Motivation:

Loading a project with a large or well-connected package graph in Xcode can be very slow.

Xcode uses Workspace.loadPackageGraph(rootPath:...), which calls into
calls into findCycle(). findCycle has a bug leading to slower than linear cycle detection.

Modifications:

There is a note to convert to use an algorithm tracking traversal with a stack, which would allow for linear time cycle detection. But given the output of findCycle is the path to first the cycle, and the cycle itself, not all the elements in a strongly connected subgraph, it's enough to memoize fully explored nodes.

Once all of a Manifests outbound edges have been traversed for cycles, it's not possible for it to participate in a cycle. So if a fully-explored Manifest is encountered again in a cycle check, exit early.

Without such an early exit, cycle detection is quadratic. This change makes it linear.

Result:

I made a tool to generate and profile the loading of package graphs through the same path as Xcode here.

These are the results of profiling package loading a small range of number of packages.

Number of packages apple/spm memoized
22 17.9s 0.2s
23 35.8s 0.2s
24 88.1s 0.2s
25 160.5s 0.2s
26 323.4s 0.2s

Once all of a Manifests outbound edges have been traversed for cycles, it's not possible for it to participate in a cycle. So if a fully-explored Manifest is encountered again in a cycle check, exit early.

Without such an early exit, cycle detection is quadratic. This change makes it linear.
@MaxDesiatov

This comment was marked as outdated.

@MaxDesiatov
Copy link
Contributor

@swift-ci please smoke test

@neonichu
Copy link
Contributor

@swift-ci please smoke test linux

@fcanas
Copy link
Contributor Author

fcanas commented Oct 25, 2022

This change can also apply cleanly to the 5.7 branch. Given the impact, I would hope this would be considered for consideration in a future release of Xcode.

@tomerd
Copy link
Contributor

tomerd commented Oct 25, 2022

looks great, thank you @fcanas!

is it possible to add a test to make sure this does not regress?

@fcanas
Copy link
Contributor Author

fcanas commented Oct 25, 2022

@tomerd I'm happy to add tests.

I'm guessing I should add fixtures somewhere in Fixtures/DependencyResolution. Do you have any guidance as to whether this is "External" or "Internal"? And do you have any preference on the naming?

The tests will otherwise look like what I've shared in https://github.com/fcanas/manifest-cycle-bug, except with fixed fixtures to follow the existing pattern. Unless you'd like me to share the fixture generation as well.

@tomerd
Copy link
Contributor

tomerd commented Oct 25, 2022

This change can also apply cleanly to the 5.7 branch. Given the impact, I would hope this would be considered for consideration in a future release of Xcode.

please prepare a 5.7 PR once this is merged, and we can look for an opportunity if and when such arise

I'm guessing I should add fixtures somewhere in Fixtures/DependencyResolution. Do you have any guidance as to whether this is "External" or "Internal"? And do you have any preference on the naming?

The tests will otherwise look like what I've shared in https://github.com/fcanas/manifest-cycle-bug, except with fixed fixtures to follow the existing pattern. Unless you'd like me to share the fixture generation as well.

Fixtures is one way, probably under "Miscellaneous".

Alternatively you can synthesize a resolution unit test (see WorkspaceTests), assuming there are clear side effects we can use to validate this

@fcanas
Copy link
Contributor Author

fcanas commented Oct 25, 2022

I've added a performance test following as many of the patterns I could see for such tests, including putting the test in a subclass of XCTestCasePerf, and using the fixture loading utility.

I'm not sure how this project manages baselines for performance measurements. If this test were to run on master, the time would be several minutes on almost any machine. On an M1 MacBook Air, it's just under 5 seconds.

I'm happy to reshape or move what I've added.

let N = 1
measure {
for _ in 1..<N {
try! workspace.loadPackageGraph(rootPath: rootPackagePath, observabilityScope: observability.topScope)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try! -> try

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the other uses of measure in the codebase use try!, so I was following that pattern. But I personally do disagree with crashing a test process when we can fail and complete the test suite.

Done.

@@ -12355,6 +12355,25 @@ final class WorkspaceTests: XCTestCase {
}
}

final class WorkspacePerformanceTests: XCTestCasePerf {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps we should move this to PackageGraphPerfTests? @neonichu opinions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whatever you prefer.

The XCTestCasePerf seems to be used for excluding certain performance tests unless specified. This case could be rolled up into the same class in this file and would just be run regardless of being specifically a performance test run.

If it's sufficiently standardized, this test function could check for the TSC_ENABLE_PERF_TESTS environment variable and succeed early if it's not present. But reaching into TSC for arbitrary constants isn't something I would do without being asked to.

Or this new test case class could be in a new file, Tests/WorkspaceTests/WorkspacePerformanceTests.swift.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yah, I think testing that at the package graph rather than the workspace level sounds right to me, but I am not sure we have infra there to load from fixtures.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixture is in SPMTestSupport so should be fine? if not, we can leave it here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once I looked at testing the package graph more directly instead of via the workspace, doing it with generated Manifests rather than fixtures seemed viable, and more in keeping with the other Package tests.

Updated.

@abertelrud
Copy link
Contributor

@swift-ci please smoke test

@neonichu
Copy link
Contributor

@swift-ci please smoke test

Copy link
Contributor

@tomerd tomerd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you @fcanas

@fcanas
Copy link
Contributor Author

fcanas commented Oct 27, 2022

I've created a pull request for the 5.7 branch

@neonichu
Copy link
Contributor

Interesting error from the self-hosted job:

Undefined symbols for architecture x86_64:
  "_$s12SPMBuildCore23ProductBuildDescriptionMp", referenced from:
      _symbolic ______p 12SPMBuildCore23ProductBuildDescriptionP in BuildOperationBuildSystemDelegateHandler.swift.o
      _symbolic Say______pG 12SPMBuildCore23ProductBuildDescriptionP in BuildOperationBuildSystemDelegateHandler.swift.o
  "_$s12SPMBuildCore23ProductBuildDescriptionPAAE6binary8TSCBasic12AbsolutePathVvg", referenced from:
      _$s5Build0A11DescriptionV4plan13swiftCommands0d8FrontendE0013testDiscoveryE00g10EntryPointE004copyE018pluginDescriptionsAcA0A4PlanC_SDySS15LLBuildManifest17SwiftCompilerToolVGSDySSAM0qfS0VGSDySSAM04TesthS0VGSDySSAM0tijS0VGSDySSAM04CopyS0VGSayAA06PluginB0CGtKcfc12SPMBuildCore05BuiltT7ProductVA4_0zaB0_pXEfU3_ in BuildOperationBuildSystemDelegateHandler.swift.o

@neonichu
Copy link
Contributor

@swift-ci please smoke test macOS

@tomerd
Copy link
Contributor

tomerd commented Oct 27, 2022

/Users/ec2-user/jenkins/workspace/swift-package-manager-with-xcode-self-hosted-PR-osx/branch-main/swiftpm/Tests/PackageGraphPerformanceTests/PackageGraphPerfTests.swift:97:61: error: unable to infer complex closure return type; add explicit type to disambiguate
        let packageSequence = try packageNumberSequence.map { sequenceNumber in
                                                            ^
                                                                             -> <#Result#> 
[18/66] Compiling PackageGraphPerformanceTests DependencyResolverPerfTests.swift
[19/67] Merging module PackagePluginAPITests
[20/67] Merging module PackageLoadingPerformanceTests
[2

@fcanas some of the CI machines use older compiler which needs a bit of help with type inference. should be easy to fix by adding the type signature

@neonichu
Copy link
Contributor

@swift-ci please smoke test

@tomerd tomerd merged commit 5639797 into swiftlang:main Oct 28, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants