-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Conversation
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.
This comment was marked as outdated.
This comment was marked as outdated.
@swift-ci please smoke test |
@swift-ci please smoke test linux |
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. |
looks great, thank you @fcanas! is it possible to add a test to make sure this does not regress? |
@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. |
please prepare a 5.7 PR once this is merged, and we can look for an opportunity if and when such arise
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 |
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 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
try! -> try
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
@swift-ci please smoke test |
@swift-ci please smoke test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thank you @fcanas
I've created a pull request for the 5.7 branch |
Interesting error from the self-hosted job:
|
@swift-ci please smoke test macOS |
@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 |
@swift-ci please smoke test |
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 intocalls 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.