Skip to content
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

build: mtime cache for patches to speed up build #25881

Merged
merged 5 commits into from Oct 20, 2020

Conversation

@dsanders11
Copy link
Contributor

@dsanders11 dsanders11 commented Oct 11, 2020

Description of Change

Motivation

Due to how patches are applied in the build system, a gclient sync will cause a subsequent build to rebuild a lot of code unnecessarily, because Ninja thinks it has changed as a result of the patching, even if it is identical to what it was before the sync.

This causes me a lot of unnecessary grief around gclient sync, since on the machine I'm using (which is a slow 4-core) the rebuilding will take 1-2 hours. That kind of downtime makes me weary of syncing due to how much it disrupts the workflow.

Since most of the time the patches haven't changed due to the sync, and the patched files still have the same content post-sync, I looked for a way to prevent the unnecessary rebuilding and cut down the rebuild time after a sync.

Implementation

The implementation is fairly simple. Since Ninja is an mtime-based build system (and seems to have no desire to include file hashing), mtime is all that matters for preventing unnecessary rebuilding.

The goal is to carry forward the mtime for patched files across gclient sync as long as they didn't change as a result of the sync. This lets them appear unchanged (to Ninja) by the sync like other files, even though they were reverted and patched again.

Thankfully GN has pre-DEPS hooks, which made the implementation pretty easy. I've added a pre-DEPS hook which will run before gclient sync starts changing files, and this hook walks all patched files and creates a cache of their contents hash and their current mtime and atime. It dumps this into a file to be accessed by the hook which runs after the sync.

After sync completes and the patches are applied, there's a hook which reads the cache file and checks the files listed. As long as their hash has not changed, this hook will apply the mtime and atime from before the sync began. The only shortcoming with the current implementation is it doesn't change the ctime (couldn't see a way to do it in Python), so the file metadata isn't quite identical to what it was before the sync, but that doesn't affect the build. I believe Ninja will only look at the mtime - I only included atime because the Python command to change mtime also took atime, and it was easy enough to throw in.

Due to potential for unintended side effects (I think it's pretty safe, but would like to solicit feedback) I made the feature opt-in under the use_mtime_cache flag in DEPS.

Results

Builds after a gclient sync are significantly faster. Before after any sync I would be looking at a rebuild with 16,000 tasks or so, and it would take over an hour. Now when doing a gclient sync when there's nothing new to pull down I see Ninja shows 127 tasks and only takes about 5 minutes to build. This makes it "safe" for me to do gclient sync to my heart's content without worrying about having hours of rebuilding as a result, so it's a major improvement to workflow.

cc @codebytere @MarshallOfSound

Checklist

  • PR description included and stakeholders cc'd
  • relevant documentation is changed or added
  • PR title follows semantic commit guidelines

Release Notes

Notes: none

@dsanders11 dsanders11 requested a review from electron/wg-upgrades as a code owner Oct 11, 2020
@codebytere
Copy link
Member

@codebytere codebytere commented Oct 11, 2020

@dsanders11 the failure is owing to a small issue fixed in master recently - rebase and it should be resolved :)

And this looks really exciting! I'll give it a more thorough review come Monday 😁

@dsanders11 dsanders11 force-pushed the dsanders11:patches-mtime-cache branch 2 times, most recently from 5b00b5f to a91a856 Oct 11, 2020
@dsanders11 dsanders11 force-pushed the dsanders11:patches-mtime-cache branch from a91a856 to 2da5656 Oct 11, 2020
@dsanders11
Copy link
Contributor Author

@dsanders11 dsanders11 commented Oct 11, 2020

@codebytere, thanks! I've rebased it on master and added a few changes. Looks like there's still some failures though. Since the flag for the feature is false in the PR, there should be no changes, so I presume the failures are unrelated. That does bring up a question about should I flip the feature on for the PR (and have it flipped back to off before merging) so that the build checks actually use the code. Although if the builds are doing a clean build it won't really exercise it (but would at least show that it doesn't break it).

Looking forward to the more thorough review.

Copy link
Member

@codebytere codebytere left a comment

@dsanders11 had some more time to read into this and pull it down to play with it today - conceptually this looks good to me (and resulted in significant local speedup 🎉), although i'd like to run CI in this PR with it enabled and see if it causes any issues i wouldn't be able to preemptively spot.

I would also be potentially open to running with this by default and/or in testing CI but would like a second opinion from @nornagon!

@codebytere codebytere requested a review from nornagon Oct 13, 2020
@dsanders11
Copy link
Contributor Author

@dsanders11 dsanders11 commented Oct 13, 2020

@codebytere, flipped the feature on, the CI builds look good, just a test failure on macos which seems unlikely to be related.

Copy link
Member

@ckerr ckerr left a comment

Nice writeup in the description! This looks like a good idea and the code looks reasonable.

CC'ing resident gclient wizard @nornagon in case he sees any pitfalls that I don't; but other than that, LGTM.

script/patches-mtime-cache.py Outdated Show resolved Hide resolved
script/patches-mtime-cache.py Outdated Show resolved Hide resolved
Copy link
Member

@nornagon nornagon left a comment

Thanks for this; this is an idea I've had in the back of my head for a while and never got around to implementing. I think this approach is fairly sound.

The main concerns I can think of are:

  1. Can we end up with a stale mtime cache that causes difficult-to-diagnose build failures?
  2. Would it be possible instead to detect whether the patches need to be reapplied, and if not, skip the patching step?

I think we're relatively ok with (1) because of the sha1 step. However, as an additional precaution we should probably delete the mtime-cache.json file after applying it.

I suspect (2) is not possible, because gclient will always want to reset repos to the version listed in DEPS. I'm not aware of any good solutions in that direction.

Requesting changes for the cache file cleanup.

@dsanders11
Copy link
Contributor Author

@dsanders11 dsanders11 commented Oct 13, 2020

Can we end up with a stale mtime cache that causes difficult-to-diagnose build failures?

Agreed on this concern, and the more nefarious situation where a build works but it's not what you expected it to be. That's why I've initially made it opt-in by defaulting the flag to false, since I didn't want to be responsible for causing cryptic build issues. 😄 However, like you said, the SHA-256 step should make this safe, outside of logic errors in this script.

However, as an additional precaution we should probably delete the mtime-cache.json file after applying it.

This seems reasonable, and something I'd considered during implementation. It is a slight trade-off in the "difficult to debug" direction since it leaves no trace. I think I'll add a flag --preserve-cache for applying so that nothing has to be hacked up if you want to keep the cache around to inspect.

I suspect (2) is not possible, because gclient will always want to reset repos to the version listed in DEPS. I'm not aware of any good solutions in that direction.

I came to the same conclusion, after initially investigating how patches were applied since I wasn't familiar.

@MarshallOfSound
Copy link
Member

@MarshallOfSound MarshallOfSound commented Oct 13, 2020

RE (2) in theory you could git tag THING when the patches are applied and then do git checkout THING instead of applying all the patches. Not sure how much faster that is though

@nornagon
Copy link
Member

@nornagon nornagon commented Oct 13, 2020

@MarshallOfSound applying the patches is pretty quick, I don't think it's worth the complexity of creating and managing new refs.

git checkout modifies the files so mtimes wouldn't be preserved anyway.

@dsanders11
Copy link
Contributor Author

@dsanders11 dsanders11 commented Oct 14, 2020

Made a new commit. Doing some more local testing I was seeing weird results from gclient sync if the pre-DEPS hook exited with an error code. Instead of just stopping the sync there where the error happened it continued on and in doing so seemed to thrash the src tree. That made me weary and so I made the script never exit with an error code other than 0, just logging the error. This also ensures the apply step doesn't stop the sync if the generate step failed, or if --noprehooks was used (which you can currently use as a quick way to disable the feature, although if any future pre-DEPS hooks exist that might not be a good idea).

So, the latest commit changed:

  • Better use of argparse for keeping the arguments between the commands separate
  • Avoids exiting with a non-zero exit code
  • Refactored a little to not apply the mtime until all files had been checked in case something errored out. This hopefully avoids a half-applied situation, although even that situation should be safe.
  • Delete the cache after applying (as @nornagon requested)
  • Added a set command which is only intended for advanced use, letting you set the mtime for all patched files to a specific value. During testing this let me recover from a sync with the mtime cache feature not in use. Since I know the files haven't changed I could set them back to an mtime before that sync and save myself the hour of rebuilding. Not intended for use by anyone who doesn't know what it does, and it includes a warning as such.

@ckerr, @nornagon, please review the latest commit. Apologies for the thrash, but the no non-zero exit code change was important for robustness. I should be done touching it now that's done.

@ckerr ckerr dismissed their stale review Oct 14, 2020

updated PR

@ckerr
Copy link
Member

@ckerr ckerr commented Oct 15, 2020

I'm still 👍 on this PR and am going to use it locally for a few days to see if anything catches fire

@ckerr
Copy link
Member

@ckerr ckerr commented Oct 20, 2020

I'm still on this PR and am going to use it locally for a few days to see if anything catches fire

I've been moving back and forth between branches and have no problems to report so far!

@ckerr
ckerr approved these changes Oct 20, 2020
@nornagon nornagon merged commit f7945ad into electron:master Oct 20, 2020
7 of 9 checks passed
7 of 9 checks passed
appveyor: win-x64-testing-pr AppVeyor build failed
Details
build-mac Workflow: build-mac
Details
Artifact Comparison No Changes
Details
Semantic Pull Request ready to be squashed
Details
WIP Ready for review
Details
appveyor: win-ia32-testing-pr AppVeyor build succeeded
Details
build-linux Workflow: build-linux
Details
lint Workflow: lint
Details
release-notes Release notes found
@release-clerk
Copy link

@release-clerk release-clerk bot commented Oct 20, 2020

No Release Notes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

5 participants
You can’t perform that action at this time.