-
Couldn't load subscription status.
- Fork 213
[Incremental] Do incremental builds #329
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
[Incremental] Do incremental builds #329
Conversation
| @swift-ci please test |
| if jobs.contains(where: { $0.requiresInPlaceExecution }) | ||
| // Only one job and no cleanup required | ||
| || (jobs.count == 1 && !parsedOptions.hasArgument(.parseableOutput)) { | ||
| if jobs.contains(where: { $0.requiresInPlaceExecution }) { |
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.
This is the c++ driver logic. I believe there are lit tests that verify this optimization happens. If this significantly complicates testing it's probably still worth not having the optimization, but there will likely be a lit test regression.
// If we don't have to do any cleanup work, just exec the subprocess. if (Level < OutputLevel::Parseable && !ShowDriverTimeCompilation && (SaveTemps || TempFilePaths.empty()) && CompilationRecordPath.empty() && Jobs.size() == 1) { return performSingleCommand(Jobs.front().get()); } 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.
Thanks. Yes, it does complicate testing. We could add a boolean to pass in when creating the driver to disable this, just for testing. Or could fix the lit tests. What's best?
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.
Been a while since I've contributed to swift-driver, but I try to keep up on the changes. So I haven't run the lit tests in a while, but I think it's worth running them with and without this to see what the differences are first. @owenv Might have thoughts too as he has been continuing to update swift-driver to pass more lit tests.
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.
There aren't any lit tests for this behavior afaik, but we should try to keep it around. I don't think this change breaks anything outright, but, for example, -dump-ast disables color output because it assumes output is being redirected to a file if the frontend is launched as a subprocess. I think it does need to be disabled anytime a build record needs to be read or written.
| func populateOutOfDateBuildRecord() -> BuildRecord? { | ||
| func populateOutOfDateBuildRecord( | ||
| inputFiles: [TypedVirtualPath], | ||
| failed: (String) -> Void |
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.
Should these be Diagnostic.Messages? Seems like Strings will make it harder to support localization of diagnostics.
Can probably wait until this project has a plan for localization.
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.
I tried to ask folks about this issue, but missed you, sorry! The advantages of using diagnostics are that it is much easier to test, and it won't end up messing up the xcodebuild logs. The disadvantage I had thought of was the incompatibility. I hadn't though of localization. What do you think?
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.
Probably safe to just leave it as is for now, seeing as there is no plan for localization currently.
| .subtracting(firstWaveInputs) | ||
| for skippedInput in skippedInputs.sorted(by: {$0.file.name < $1.file.name}) { | ||
| reportIncrementalQueuing("Skipping:", skippedInput.file) | ||
| reportIncrementalDecision?("Skipping:", skippedInput) |
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.
Can do if let check for reportIncrementalDecision before the loop so the sort and iteration is skipped when it's nil.
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.
Thanks! Good catch. I'll fix it.
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.
Thanks! I'll fix it.
| func addPostCompileJobs(_ jobs: [Job]) { | ||
| for job in jobs { | ||
| for input in job.primaryInputs { | ||
| reportIncrementalDecision?("Delaying pending discovering delayed dependencies", input) |
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.
if let reportIncrementalDecision to skip loop if nil
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.
Thanks, fixed!
| | ||
| let secondWaveInputs = collectInputsToCompileIn2ndWave(job) | ||
| for input in secondWaveInputs { | ||
| reportIncrementalDecision?( |
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.
Can skip loop if nil (I wonder if the optimizer catches this?)
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.
Thanks, fixed!
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.
Yes, would be nice to just let the optimizer deal with it.
| | ||
| private func rememberJobFor2ndWave(_ j: Job) { | ||
| for input in j.primaryInputs { | ||
| reportIncrementalDecision?("Scheduling for 2nd wave", input) |
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.
Can skip loop if nil
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.
Thanks fixed
| @cltnschlosser I'm very grateful you put in the time to look at this large PR so quickly! Thank you. I'll get to your specific comments in a bit. |
| @swift-ci please test |
| @swift-ci please test |
OK, I now believe I've addressed the points that I can. Thank again! |
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.
Sorry, I haven't had a chance to look over this in detail yet, I'll try to set aside some time this weekend. I added a couple initial comments. It's really exciting to see this working!
| if jobs.contains(where: { $0.requiresInPlaceExecution }) | ||
| // Only one job and no cleanup required | ||
| || (jobs.count == 1 && !parsedOptions.hasArgument(.parseableOutput)) { | ||
| if jobs.contains(where: { $0.requiresInPlaceExecution }) { |
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.
There aren't any lit tests for this behavior afaik, but we should try to keep it around. I don't think this change breaks anything outright, but, for example, -dump-ast disables color output because it assumes output is being redirected to a file if the frontend is launched as a subprocess. I think it does need to be disabled anytime a build record needs to be read or written.
| let swiftInputFiles = inputFiles.filter { inputFile in | ||
| inputFile.type.isPartOfSwiftCompilation | ||
| } |
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.
This change seems to break using the driver as a linker, because object files passed on the command line are no longer added to the set of linkerInputs. It seems to be causing the Driver/unknown-inputs.swift lit test to fail. Other than that, I don't see any regressions in the lit tests
| var skippedCompileJobs = [Job]() | ||
| func addSkippedCompileJob(_ j: Job) { | ||
| skippedCompileJobs.append(j) | ||
| } | ||
| | ||
| try addPrecompileModuleDependenciesJobs(addJob: addJob) | ||
| try addPrecompileBridgingHeaderJob(addJob: addJob) | ||
| try addEmitModuleJob(addJob: addJob) | ||
| | ||
| let linkerInputs = try addJobsFeedingLinker(addJob: addJob) | ||
| let linkerInputs = try addJobsFeedingLinker( | ||
| addJob: addJob, | ||
| addSkippedCompileJob: addSkippedCompileJob) | ||
| | ||
| var postCompileJobs = [Job]() | ||
| func addPostCompileJob(_ j: Job) { postCompileJobs.append(j) } | ||
| try addLinkAndPostLinkJobs(linkerInputs: linkerInputs, | ||
| debugInfo: debugInfo, | ||
| addJob: addJob) | ||
| return jobs | ||
| addJob: skippedCompileJobs.isEmpty | ||
| ? addJob | ||
| : addPostCompileJob) | ||
| | ||
| incrementalCompilationState?.addSkippedCompileJobs(skippedCompileJobs) | ||
| incrementalCompilationState?.addPostCompileJobs(postCompileJobs) | ||
| return try formBatchedJobs(jobs) |
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.
I've been thinking about this a bit more, and I think we should try to avoid explicitly partitioning jobs into first wave, potentially-skippable and link/post-link during the planning stage. In the short term, it complicates the SPM implementation, which expects a complete, static build graph from Driver.planBuild, and in the long term I think it will make it harder to move more of the scheduling and synchronization out of Driver.run and into the llbuild-based executor.
What do you think about adding a potentiallySkippable flag to Job instead? Then, based on the new flag and Job.kind, Driver.run can do this partitioning just before execution and update the incremental state then. SPM (which always passes -incremental afaict) should keep working in that case because planBuild will continue to return the full set of jobs, and true incremental behavior would be 'opt-in' because clients would need to look at the potentiallySkippable flag and schedule jobs appropriately.
Edit: Alternatively, maybe Driver.planBuild could return a new BuildPlan type which contains the partitioning.
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.
Very interesting! Let me think about it for a while. At first glance, I'm not sure that that flag is appropriate for inclusion in the job. We could get the same effect by having the planning phase include all the jobs, and the run method could consult the incrementalBuildState. Does that sound reasonable?
Also, someday, we'll have to enhance the relationship between SPM and the driver, if we ever want SPM to have incremental capability. But that's a task for a later time, IMO.
I also need to go back into my notes to see how to run the lit tests, etc., on swift-driver.
I very much appreciate the time and thought you're putting in. Thank you!
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.
Very interesting! Let me think about it for a while. At first glance, I'm not sure that that flag is appropriate for inclusion in the job. We could get the same effect by having the planning phase include all the jobs, and the run method could consult the incrementalBuildState. Does that sound reasonable?
That's fair, and your proposed solution sounds good to me overall! I think as long as planBuild is returning all of the jobs in some form rather than only registering some of them with the incremental state, my concerns are mostly addressed.
Also, I haven't tested this change with SPM at all, but it might not be a bad idea to patch it so that it doesn't pass -incremental when using the integrated driver before landing this change.
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.
@owenv OK, I've addressed the issues. Would you be interested in taking another look? If you're interested in the current state of -driver-show-incremental or -driver-show-job-lifecycle check out testIncremental().
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.
This looks good to me, thanks! I'm probably not the best person to review the batching changes, but I didn't notice any issues except the one I mentioned above where something like swiftc test.o no longer produces any jobs.
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, @owenv . Nice catches!
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.
OK, I think it's fixed now. Tx again.
1. planBuild returns all jobs; run consults IncrementalBuildState. (for swiftpm compatibility) 2. Use job groups instead of jobs to accommodate bitcode emission. 3. Avoid potential race by keeping postCompile jobs separate
| @swift-ci please test |
| @swift-ci please test |
| @swift-ci please test linux platform |
| @swift-ci please test linux |
| @swift-ci please test macos platform |
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.
I'm happy with how this looks!
Excited to see how it evolves and about making SwiftPM do something similar, in-tandem.
| @@ -0,0 +1,64 @@ | |||
| //===--------------- JobQueue.swift - Incremental -----------===// | |||
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.
This filename got stale.
| // | ||
| // This source file is part of the Swift.org open source project | ||
| // | ||
| // Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors |
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.
2020 :)
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, @artemcm, for both of these catches and most of all, for taking the time to look over this large PR!
| @swift-ci please test |
EDIT: It has been redone. It returns all jobs from the planning method to preserve compatibility, but the
runfunction uses a side door for incremental execution.Here is a large PR (sorry!) that performs incremental builds (per our discussion the other day). It isn't optimal: it uses rebatching, and it uses llbuild in waves. It does not assume only two waves, though. Probably a bit rough in spots--all feedback welcome!
If you want to see it work, try
testIncremental.