-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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
x/tools/gopls: optimize didChange handling #45686
Comments
Change https://golang.org/cl/312689 mentions this issue: |
Working on unnecessary URI.Filename() usage in snapshot reduction |
The existing implementation uses a lot of URI.Filename() calls, which are pretty expensive. Moreover, these calls are not necessary, as long as all the actions could be done with the raw URI string. This patch removes such calls and uses simple string casts. Updates golang/go#45686
Change https://golang.org/cl/312809 mentions this issue: |
The existing implementation uses a lot of URI.Filename() calls, which are pretty expensive. Moreover, these calls are not necessary, as long as all the actions could be done with the raw URI string. This patch removes such calls and uses simple string casts. Updates golang/go#45686
Extending this more generally to didChange handling, as snapshot.clone is only one of the problems. There is some other stuff in e.g. didModifyFiles that's quite slow. @justplesh thanks very much for your contribution, and interest. Right now we're still trying to figure out the best path forward for some of these optimizations. Hopefully we'll have a better sense of what needs to be prioritized next week. If you'd like, we can keep you in mind for anything that is relatively self-contained. |
@findleyr Sure, please mention me or assign any lsp issue that I may work on. I'll be happy to work on it on some +- regular basis. |
@findleyr will we merge my PR then or will we wait for some more fundamental approach? |
For large codebases, the cost of copying these maps can be fairly high, especially when it needs to repeatedly grow the map's underlying storage. Preallocate these to the size of the original snapshot maps to prevent the need to grow the storage during the clone. Updates golang/go#45686 Change-Id: I4cfcd5b7cba8110e4f7e706fd9ea968aaeb6ff0c Reviewed-on: https://go-review.googlesource.com/c/tools/+/312689 Trust: Robert Findley <[email protected]> Run-TryBot: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]>
@justplesh we can proceed with your CL; it should be an improvement. |
The existing implementation uses a lot of URI.Filename() calls, which are pretty expensive. Moreover, these calls are not necessary, as long as all the actions could be done with the raw URI string. This patch removes such calls and uses simple string casts. Updates golang/go#45686 Change-Id: Ibe11735969eaf0cfe33024f08418e14bf71e7fc4 GitHub-Last-Rev: 67a3ccd GitHub-Pull-Request: #306 Reviewed-on: https://go-review.googlesource.com/c/tools/+/312809 Reviewed-by: Rebecca Stambler <[email protected]> Trust: Rebecca Stambler <[email protected]> Trust: Suzy Mueller <[email protected]> Run-TryBot: Rebecca Stambler <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]>
Change https://golang.org/cl/317292 mentions this issue: |
Add a benchmark for the processing of workspace/didChange notifications, attempting to isolate the synchronous change processing from asynchronous diagnostics. To enable this, add a new type of expectation that asserts on work that has been _started_, but not necessarily completed. Of course, what we really want to know is whether the current notification has been processed, but that's ~equivalent to knowing whether the next one has been started. Really, it's off-by-one, but amortized over e.g. the 100 iterations of a benchmark we get approximately the right results. Also change some functions to accept testing.TB, because in a first pass at this I modified the regtest framework to operate on testing.B in addition to testing.T... but that didn't work out as IWL is just too slow to execute the benchmarks outside of the environment -- even though we can ResetTimer, the benchmark execution is just too slow to be usable. It seems like a fine change to accept testing.TB is some places, though. For golang/go#45686 Change-Id: I8894444b01177dc947bbed56ec7df80a15a2eae9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/317292 Trust: Robert Findley <[email protected]> Run-TryBot: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]>
Change https://golang.org/cl/317410 mentions this issue: |
A lot of the time spent for every file change is recomputing the set of known subdirectories in the workspace. We can easily memoize these known subdirectories and avoid recomputing them on every file change. Do that here and update the set as file creations and deletions come in. Updates golang/go#45686 Fixes golang/go#45974 Change-Id: Ide07f7c90f0cafc3a3cc7b89ba14ab82d4e3ab28 Reviewed-on: https://go-review.googlesource.com/c/tools/+/317410 Trust: Rebecca Stambler <[email protected]> Run-TryBot: Rebecca Stambler <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Robert Findley <[email protected]> TryBot-Result: Go Bot <[email protected]>
We will probably focus next on:
|
I spent some time analyzing our didChange performance last night. I've come to the opinion that using a map datastructure that is optimized for cloning is really just working around the lack of modularity in the snapshot. I think we should instead try to:
I think doing these three things will take a huge chunk of CPU (and complexity) out of clone. There are still more optimizations to be had. |
I'm currently working on this, and think I'll be able to significantly reduce the cost via the following two changes:
These two improvements take care of the majority of change processing CPU time; there are other improvements to be made, but they are all of second-order. |
Change https://golang.org/cl/340735 mentions this issue: |
Change https://golang.org/cl/340853 mentions this issue: |
Change https://golang.org/cl/340852 mentions this issue: |
Change https://go.dev/cl/410697 mentions this issue: |
1. Avoid unnecessary intermediate map updates. 2. Avoid accumulating defers in a loop when the control is simple. Yield: -10% CPU, -37% allocs. Typical results: $ go test -v ./gopls/internal/regtest/bench -run=TestBenchmarkDidChange -didchange_dir=$HOME/w/kubernetes -didchange_file=pkg/util/hash/hash.go Before: BenchmarkStatistics 100 25932206 ns/op 11684109 B/op 75458 allocs/op After: BenchmarkStatistics 100 23294195 ns/op 11293472 B/op 47299 allocs/op Also, move profiling logic outside the loop so that later runs don't overwrite earlier runs. (This doesn't appear to be a problem in practice, presumably because the last run is the big one.) Updates golang/go#45686 Change-Id: I538ca6bb88cc18f1eefe35d2db29a62e5190280e Reviewed-on: https://go-review.googlesource.com/c/tools/+/410697 Run-TryBot: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Robert Findley <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
In preparation for making metadata immutable, move metadata-related fields to a new MetadataGraph type. Other than instantiating this type when cloning, this CL contains no functional changes. For golang/go#45686 Change-Id: I7ad29d1f331ba7e53dad3f012ad7ecdae4f7d4b7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/340730 Run-TryBot: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Alan Donovan <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
Change https://go.dev/cl/410176 mentions this issue: |
Change https://go.dev/cl/340851 mentions this issue: |
This change replaces the single large map used for snapshot.goFiles by a map of 256 stripes, each of which becomes immutable once shared. This optimizes the common case in which the copy is nearly identical to the original. We still need to visit each map entry to see whether it needs to be deleted (which is rare) and to inherit the handle in the usual case. This is now done concurrently. Also, share the (logically immutable) []PackageIDs slices across old and new snapshots. This was worth 5% of CPU and 1/3 of allocations (all small). Benchmark on darwin/arm64 shows a 29% reduction for DidChange. $ go test -v ./gopls/internal/regtest/bench -run=TestBenchmarkDidChange -didchange_dir=$HOME/w/kubernetes -didchange_file=pkg/util/hash/hash.go Before: BenchmarkStatistics 100 22955469 ns/op 11308095 B/op 47412 allocs/op BenchmarkStatistics 100 23454630 ns/op 11226742 B/op 46882 allocs/op BenchmarkStatistics 100 23618532 ns/op 11258619 B/op 47068 allocs/op After goFilesMap: BenchmarkStatistics 100 16643972 ns/op 8770787 B/op 46238 allocs/op BenchmarkStatistics 100 17805864 ns/op 8862926 B/op 46762 allocs/op BenchmarkStatistics 100 18618255 ns/op 9308864 B/op 49776 allocs/op After goFilesMap and ids sharing: BenchmarkStatistics 100 16703623 ns/op 8772626 B/op 33812 allocs/op BenchmarkStatistics 100 16927378 ns/op 8529491 B/op 32328 allocs/op BenchmarkStatistics 100 16632762 ns/op 8557533 B/op 32497 allocs/op Also: - Add comments documenting findings of profiling. - preallocate slice for knownSubdirs. - remove unwanted loop over slice in Generation.Inherit Updates golang/go#45686 Change-Id: Id953699191b8404cf36ba3a7ab9cd78b1d19c0a2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/410176 TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> Run-TryBot: Alan Donovan <[email protected]>
Change https://go.dev/cl/411554 mentions this issue: |
Now that we preserve stale metadata, we can derive workspace packages entirely from known metadata and files. This consolidates the logic to compute workspace packages into a single location, which can be invoked whenever metadata changes (via load or invalidation in clone). Additionally: - Precompute 'HasWorkspaceFiles' when loading metadata. This value should never change for a given Metadata, and our view.contains func is actually quite slow due to evaluating symlinks. - Track 'PkgFilesChanged' on KnownMetadata, since we don't include packages whose package name has changed in our workspace. Also introduce a few debug helpers, so that we can leave some instrumentation in critical functions. For golang/go#45686 Change-Id: I2c994a1e8ca05c3c42f67bd2f4519bea5095c54c Reviewed-on: https://go-review.googlesource.com/c/tools/+/340735 Run-TryBot: Robert Findley <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Alan Donovan <[email protected]> gopls-CI: kokoro <[email protected]>
Introduce a metadataGraph.Clone method that can be used to clone a metadata graph, applying a set of updates. During clone, ids and imports are recomputed from scratch based on the known metadata. Also refine the check for "real" packages when determining whether a command-line-arguments package should be kept as a workspace package: if all other packages are invalid, but the command-line-arguments package is valid, we should keep the command-line-arguments package. Updates golang/go#45686 Change-Id: Iea8d4f19c1d1c5a2b0582b9dda5f9143482a34af Reviewed-on: https://go-review.googlesource.com/c/tools/+/340851 Reviewed-by: Alan Donovan <[email protected]> TryBot-Result: Gopher Robot <[email protected]> gopls-CI: kokoro <[email protected]> Run-TryBot: Robert Findley <[email protected]>
Rather than updating metadata directly in snapshot.clone, build a set of updates to apply and call metadata.Clone. After this change, metadata is only updated by cloning, so we can eliminate some code that works with mutable metadata. In the next CL we'll only update the metadata if something changed, but this is intentionally left out of this CL to isolate the change. Benchmark (didChange in kubernetes): ~55ms->65ms, because it is now more work to compute uris. For golang/go#45686 Change-Id: I048bed65760b266a209f67111c57fae29bd3e6f0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/340852 gopls-CI: kokoro <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
We with immutable metadata, we don't need to clone if nothing was invalidated. Benchmark (didChange in k8s): 65ms->45ms For golang/go#45686 Change-Id: I6b5e764c53a35784fd8c7b43bc26361f4ee8d928 Reviewed-on: https://go-review.googlesource.com/c/tools/+/340853 Run-TryBot: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
Since ids is derived from metadata, we should not have to walk ids to see which metadata is still active. Just compute metadata updates directly. Benchmark (didChange in k8s): ~45ms->41ms For golang/go#45686 Change-Id: Id557ed3f2e05c903e4bb3f3f6a4af864751c4546 Reviewed-on: https://go-review.googlesource.com/c/tools/+/340854 TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]> gopls-CI: kokoro <[email protected]>
Now that the entire metadata graph and workspace packages are derived from metadata, there should be no need to validate coherency. This results in a small improvement to didChange benchmarking (within statistical noise). For golang/go#45686 Change-Id: I32683e025f42d768d62864683e55d4c00146a31c Reviewed-on: https://go-review.googlesource.com/c/tools/+/340855 TryBot-Result: Gopher Robot <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Run-TryBot: Robert Findley <[email protected]>
…pshot Use treap (https://en.wikipedia.org/wiki/Treap) as a persistent map to avoid copying s.goFiles across generations. Maintain an additional s.parseKeysByURIMap to avoid scanning s.goFiles on individual file's content invalidation. This on average reduces didChange latency on internal codebase from 160ms to 150ms. In a followup the same approach can be generically extended to avoid copying s.files and s.packages. Updates golang/go#45686 Change-Id: Ic4a9b3c8fb2b66256f224adf9896ddcaaa6865b1 GitHub-Last-Rev: b211d6c GitHub-Pull-Request: golang#382
…pshot Use treap (https://en.wikipedia.org/wiki/Treap) as a persistent map to avoid copying s.goFiles across generations. Maintain an additional s.parseKeysByURIMap to avoid scanning s.goFiles on individual file's content invalidation. This on average reduces didChange latency on internal codebase from 160ms to 150ms. In a followup the same approach can be generically extended to avoid copying s.files and s.packages. Updates golang/go#45686 Change-Id: Ic4a9b3c8fb2b66256f224adf9896ddcaaa6865b1 GitHub-Last-Rev: b211d6c GitHub-Pull-Request: golang#382
…pshot Use treap (https://en.wikipedia.org/wiki/Treap) as a persistent map to avoid copying s.goFiles across generations. Maintain an additional s.parseKeysByURIMap to avoid scanning s.goFiles on individual file's content invalidation. This on average reduces didChange latency on internal codebase from 160ms to 150ms. In a followup the same approach can be used to avoid copying s.files, s.packages, and s.knownSubdirs. Updates golang/go#45686 Change-Id: Ic4a9b3c8fb2b66256f224adf9896ddcaaa6865b1 GitHub-Last-Rev: 0abd257 GitHub-Pull-Request: #382 Reviewed-on: https://go-review.googlesource.com/c/tools/+/411554 Reviewed-by: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Run-TryBot: Robert Findley <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
Change https://go.dev/cl/413657 mentions this issue: |
Change https://go.dev/cl/413654 mentions this issue: |
Change https://go.dev/cl/413655 mentions this issue: |
This on average reduces latency from 34ms to 25ms on internal codebase. Updates golang/go#45686 Change-Id: I57b05e5679620d8481b1f1a051645cf1cc00aca5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/413654 TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Run-TryBot: Alan Donovan <[email protected]>
I'm going to tentatively mark this issue as complete. There's more we can do here (and in fact, more CLs still pending), but we've largely succeeded 🎉 The benchmark I use for didChange latency (typing in Kubernetes) has already decreased from ~55ms->1.5ms. Since repositories significantly larger than Kubernetes are going to bump up against other limitations of gopls, I think we can reasonably say that didChange processing time is no longer a major issue. (for record keeping we can continue to submit CLs against this issue, even though I am closing it now). |
…apshot This on average reduces latency from 25ms to 12ms on internal codebase. Updates golang/go#45686 Change-Id: I49c8f09f8e54b7b486d7ff7eb8f4ba9f0d90b278 Reviewed-on: https://go-review.googlesource.com/c/tools/+/413655 gopls-CI: kokoro <[email protected]> Run-TryBot: Robert Findley <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
This on average reduces latency from 12ms to 4ms on internal codebase. Updates golang/go#45686 Change-Id: Id376fcd97ce375210f2ad8b88e42f6ca283d29d3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/413657 Reviewed-by: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
Recent profiling has shown that, particularly on large codebases, snapshot cloning can significantly impact gopls' responsiveness.
Snapshot cloning is not optimized: there are lots of opportunities for performance improvement. For example:
Not sure which of these we'll do. Filing this as an umbrella issue to track improving performance.
CC @heschi @stamblerre
The text was updated successfully, but these errors were encountered: