Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign upGitHub is where the world builds software
Millions of developers and companies build, ship, and maintain their software on GitHub — the largest and most advanced development platform in the world.
compiler: use hash calculation for determining archive staleness #805
Conversation
@shurcooL @hajimehoshi this is ready for discussion and review. |
Basically what this PR does is to remove |
Currently staleness of an archive is determined using mod time of the transitive set of input files, the gopherjs binary and the archive itself. This PR switches from using modtimes (which can give false positives and incorrect answers) to using a hashing strategy similar to the build cache in Go 1.10 (although nowhere near as advanced - we still maintain just a single archive). It also has the advantage of allowing us to introduce other aspects into the staleness calculation, such as build tags which I have also done and this fixes #440. This PR also likely fixes a number of other staleness/caching issues that randomly get seen/reported. It also paves the way for fixing #804 by allowing us to zero the modification times. |
} | ||
|
||
binHash := sha256.New() | ||
io.Copy(binHash, binFile) |
myitcv
Apr 23, 2018
Author
Member
Thanks - yes, I'll check these copies because they are reads from a file.
} | ||
|
||
binHash := sha256.New() | ||
io.Copy(binHash, binFile) | ||
binFile.Close() |
hajimehoshi
Apr 23, 2018
Member
Actually (*File).Close
doesn't usually cause error (if error happens, this is a critical error), but checking error is not bad here?
myitcv
Apr 23, 2018
Author
Member
I'd agree; if that fails we're in serious trouble. I think I'll leave this.
@@ -596,20 +597,28 @@ func (s *Session) BuildPackage(pkg *PackageData) (*compiler.Archive, error) { | |||
return archive, nil | |||
} | |||
|
|||
pkgHash := sha256.New() |
return nil, err | ||
} | ||
archive, err := compiler.ReadArchive(pkg.PkgObj, pkg.ImportPath, objFile, s.Types) | ||
objFile.Close() |
myitcv
Apr 23, 2018
Author
Member
Because we end up opening a number of files as part of this hash calculation (recursive calls), I wanted to be clearer on when those open files will be closed.
hajimehoshi
Apr 23, 2018
Member
OK so could you comment that since this is not an usual case? I think defer objFile.Close()
is necessary anyway in case error happens.
myitcv
Apr 23, 2018
Author
Member
I've just pushed up a commit that refactors to use defer
; that way it's more consistent with other places, I agree.
@@ -298,11 +298,13 @@ func main() { | |||
run := cmdTest.Flags().String("run", "", "Run only those tests and examples matching the regular expression.") | |||
short := cmdTest.Flags().Bool("short", false, "Tell long-running tests to shorten their run time.") | |||
verbose := cmdTest.Flags().BoolP("verbose", "v", false, "Log all tests as they are run. Also print all text from Log and Logf calls even if the test succeeds.") | |||
buildVerbose := cmdTest.Flags().BoolP("bv", "", false, "Use a verbose build context.") |
myitcv
Apr 23, 2018
Author
Member
Not essential to have this... but helps to verify when we get cache misses or not in running tests.
Thanks for taking a look @hajimehoshi - addressed your feedback. PTAL. |
// determine staleness. Set hashDebug to see this in action. The format is: | ||
// | ||
// ## sync | ||
// gopherjs bin: 0x519d22c6ab65a950f5b6278e4d65cb75dbd3a7eb1cf16e976a40b9f1febc0446 |
myitcv
Apr 23, 2018
Author
Member
This is a hash of the https://godoc.org/os#Executable that is self, i.e. the gopherjs
binary.
hajimehoshi
Apr 23, 2018
Member
Hmm, there could be a better name but I don't have a good idea. At least, I feel like the keys should end with hash
for consistency?
myitcv
Apr 23, 2018
Author
Member
Ok I've made it gopherjs binary hash
to be explicit. I've also moved the calculation of this hash into a separate function because this need only be calculated once.
return nil, err | ||
} | ||
archive, err := compiler.ReadArchive(pkg.PkgObj, pkg.ImportPath, objFile, s.Types) | ||
objFile.Close() |
hajimehoshi
Apr 23, 2018
Member
OK so could you comment that since this is not an usual case? I think defer objFile.Close()
is necessary anyway in case error happens.
1b4cb77
to
f0ccf72
} | ||
|
||
fmt.Fprintf(hw, "import: %v\n", importedPkgPath) | ||
fmt.Fprintf(hw, " hash: %#x\n", importedArchive.Hash) | ||
} | ||
|
||
for _, name := range append(pkg.GoFiles, pkg.JSFiles...) { |
hajimehoshi
Apr 23, 2018
Member
Is this assured that pkg.GoFiles
and pkg.JSFiles
is ordered in alphabetical order?
sort.Strings(orderedBuildTags) | ||
|
||
fmt.Fprintf(hw, "build tags: %v\n", strings.Join(orderedBuildTags, ",")) | ||
|
||
for _, importedPkgPath := range pkg.Imports { |
// For non-main and test packages we build up a hash that will help | ||
// determine staleness. Set hashDebug to see this in action. The format is: | ||
// | ||
// ## sync |
myitcv
Apr 24, 2018
Author
Member
This is actually a package import path for sync
- it was an example. I've changed the example now to use placeholders instead of concrete examples, to hopefully make it clearer.
3ed84d1
to
b85c2e4
@hajimehoshi - I've tweaked a couple of things in my latest commit which also addresses the points you made. I've also moved from |
func init() { | ||
// We do this here because it will only fail in truly bad situations, i.e. | ||
// machine running out of resources. We also panic if there is a problem | ||
// because it's unlikely anything else will be useful/work |
flimzy
Apr 24, 2018
Member
Rather than panicking here, would it be useful to fallback to a cache-disabled state?
Maybe that would allow running the gopherjs compiler in unusual environments (such as within a browser?)? (That may already be impossible for other reasons, idk).
myitcv
Apr 24, 2018
Author
Member
That's a fair point. I think I'd like to understand that situation a bit better; because a whole load of logic in build
depends on the OS being available (writing archives etc).
The compiler is run within the browser by the playground, but that does not import build
, just compiler
.
lgtm @shurcooL, please take a look. |
// | ||
// file: <file path> | ||
// <file contents> | ||
// N bytes |
hajimehoshi
Apr 24, 2018
Member
Hmm, I feel like this format is a little bit arbitrary. Can we use JSON or YAML or something formatted? I don't have a strong opinion though...
flimzy
Apr 24, 2018
•
Member
A possible problem with JSON/YAML is that it's not inherently ordered. While we could find a way to ensure constant ordering, it wouldn't necessarily happen by default, which could lead to false negative cache hits.
myitcv
Apr 24, 2018
Author
Member
The goal here is to create a hash as quickly as possible in order to determine whether we have a cache miss or not. So yes, whilst the format is arbitrary, it is simple. And requires no additional processing in order to write to the hash. Using JSON/YAML adds more overhead in the middle, overhead that is unnecessary because ultimately the output is a 256 bit value. Humans will only ever read this whilst debugging (which itself will be a rare occurrence) with hashDebug = true
.
@@ -298,11 +298,13 @@ func main() { | |||
run := cmdTest.Flags().String("run", "", "Run only those tests and examples matching the regular expression.") | |||
short := cmdTest.Flags().Bool("short", false, "Tell long-running tests to shorten their run time.") | |||
verbose := cmdTest.Flags().BoolP("verbose", "v", false, "Log all tests as they are run. Also print all text from Log and Logf calls even if the test succeeds.") | |||
buildVerbose := cmdTest.Flags().BoolP("bv", "", false, "Use a verbose build context.") |
|
||
pkgHash := sha256.New() | ||
var hw io.Writer = pkgHash | ||
var hashDebugOut *bytes.Buffer |
hajimehoshi
Apr 24, 2018
Member
This is an optional suggestion: Instead of bytes.Buffer
, how about using text/template
?
@hajimehoshi thanks for taking a look. I've responded to your questions and removed the |
Sorry, but I don't have the bandwidth to properly review this change soon (maybe the coming weekend, but maybe not until the one after). I have the following concern and question: how can we gain enough confidence that this change will not introduce regressions in the build staleness calculations? It would be quite bad if the compiler starts to use old code and not rebuild in some situations, and there are many edge cases. Also, this PR changes code, but I would first like to see a high level description of the algorithm used, and review that. Afterwards, it would be easier to review the code and make sure it implements the algorithm. Thanks for working on this. I agree in general that using hash of content can be better than the modify time, as it avoids unnecessary rebuilds when the content hasn't changed but modtime has, but it needs to work in all cases that the previous approach handled. I see you also took on fixing #440 which is great, but that issue needs to be re-verified whether it still exists in latest |
Hi @shurcooL - very happy to add more detail. I pushed up the code to help with our discussion. What follows is based on my understanding of how archive staleness is currently, i.e. in Below,
Currently, an archive for a package P is deemed stale if its
Test programs and main packages are not archived. Here Far better is to follow an approach that uses the actual contents of those files to determine staleness. A hash-based approach does exactly this. The approach I've adopted here is similar to the build cache introduced in Go 1.10, but differs in one important respect: with the
The algorithm is therefore quite simple: we take all inputs that go into determining/building a package archive, hash them in a well defined order, compare that hash with the hash in the current archive. If the hashes are different then the archive on disk is stale, at which point we recompile it and rewrite it to disk. This approach allows us to include arbitrary data in the input hash, including build tags. So the inputs to the hash calculation are now:
We use
Because The one case that is neither handled by the hash-based nor the current
Based on my understand this will still be broken on
So to my mind we can have a high degree of confidence with this PR if we can be confident that we have the correct list of inputs; and I believe we do. The best way to see this in action (in the absence of specific tests at this point) is to:
Then try out something like the playground via http://localhost:8080/github.com/gopherjs/gopherjs.github.io/playground/. On the first load you should see the following output:
If you empty cache and hard reload the page you should see:
And that is because Indeed if you kill and restart If you then kill Would very much welcome feedback/questions on the above and this PR. Thanks. |
I've also just added a basic test to show this in action and working. |
796778c
to
1ed4e6a
Got hit by another staleness bug again this afternoon. Still unclear how the situation with mod times getting out of sync comes about, but the hash-based approach has been 100% good since I started using it. |
Fixes #827 . |
I've been using this for a while now without problems. Would be really cool to have this merged as the current mod staleness calculation can't be trusted (#827). |
Work towards #804 (will require a follow up to zero modification times)
Fixes #440 and likely other stale cache related issues.