_content/blog: gopls scalability blog post

Add a new blog post describing some of the scalability work we've been
doing for gopls.

Change-Id: Id9a9a328b2e29499d635d36e96f753f9a4787978
Reviewed-on: https://go-review.googlesource.com/c/website/+/520995
Reviewed-by: Russ Cox <rsc@golang.org>
Reviewed-by: Alan Donovan <adonovan@google.com>
Auto-Submit: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Rob Findley 2023-08-18 15:31:27 -04:00 коммит произвёл Gopher Robot
Родитель 70cf2cb772
Коммит 45145578b7
6 изменённых файлов: 962 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,241 @@
---
title: Scaling gopls for the growing Go ecosystem
date: 2023-09-08
by:
- Robert Findley
- Alan Donovan
summary: As the Go ecosystem gets bigger, gopls must get smaller
---
<style type="text/css" scoped>
.chart {
width: 100%;
}
@media (prefers-color-scheme: dark) {
.chart {
border-radius: 8px;
}
}
</style>
Earlier this summer, the Go team released version [v0.12](https://go.dev/s/gopls-v0.12)
of [gopls](https://pkg.go.dev/golang.org/x/tools/gopls),
the [language server](https://microsoft.github.io/language-server-protocol/) for Go, featuring a rewrite of its core that allows
it to scale to larger codebases.
This is the culmination of a year-long effort,
and we're excited to share our progress, as well as talk a little bit about
the new architecture and what it means for the future of gopls.
Since the v0.12 release, we've fine-tuned the new design,
focusing on making interactive queries (such as auto-completion or finding
references) as fast as they were with v0.11,
despite holding so much less state in memory.
If you haven't already, we hope you'll try it out:
```
$ go install golang.org/x/tools/gopls@latest
```
We'd love to hear about your experience with it via this [brief survey](https://google.qualtrics.com/jfe/form/SV_4SnGxpcSKN33WZw?s=blog).
## Reductions in memory use and startup time {#results}
Before we dive into the details, let's look at the results!
The chart below shows the change to startup time and memory usage for 28
of the most popular Go repositories on GitHub.
These measurements were taken after opening a randomly selected Go file
and waiting for gopls to fully load its state,
and since we assume that the initial indexing is amortized over many editing sessions,
we take these measurements the _second_ time we open the file.
<div class="image">
<img src="gopls-scalability/performance-improvements.svg" alt="Relative savings
in memory and startup time" class="chart"/>
</div>
Across these repos, the savings average around 75%,
but memory reductions are non-linear:
as projects get larger, so does the relative decrease in memory usage.
We'll explain this in more detail below.
## Gopls and the evolving Go ecosystem {#background}
Gopls provides language-agnostic editors with IDE-like features such as auto-completion,
formatting, cross-references, and refactoring.
Since its beginnings in 2018, gopls has consolidated many disparate command-line
tools such as [guru](https://pkg.go.dev/golang.org/x/tools/cmd/guru),
[gorename](https://pkg.go.dev/golang.org/x/tools/cmd/gorename),
and [goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports) and
has become the [default backend for the VS Code Go extension](https://go.dev/blog/gopls-vscode-go)
as well as many other editors and LSP plugins.
Perhaps youve been using gopls through your editor without even knowing
it---thats the goal!
Five years ago, gopls offered improved performance merely by maintaining a stateful session.
Whereas older command-line tools had to start from scratch each time they executed,
gopls could save intermediate results to significantly reduce latency.
But all that state came with a cost, and over time we increasingly [heard from users](https://github.com/golang/go/issues?q=is%3Aissue+is%3Aclosed+in%3Atitle+gopls+memory)
that gopls's high memory usage was barely tolerable.
Meanwhile, the Go ecosystem was growing, with more code being written in
larger repositories.
[Go workspaces](https://go.dev/blog/get-familiar-with-workspaces) allowed
developers to work on multiple modules simultaneously,
and [containerized development](https://code.visualstudio.com/docs/devcontainers/containers)
put language servers in increasingly resource-constrained environments.
Codebases were getting larger, and developer environments were getting smaller.
We needed to change the way gopls scaled in order to keep up.
## Revisiting gopls's compiler origins {#origins}
In many ways, gopls resembles a compiler:
it has to read, parse, type-check, and analyze Go source files,
for which it uses many of the compiler [building blocks](https://github.com/golang/example/tree/master/gotypes#introduction)
provided by the [Go standard library](https://pkg.go.dev/go) and [golang.org/x/tools](https://pkg.go.dev/golang.org/x/tools) module.
These building blocks use the technique of "symbolic programming":
in a running compiler there is a single object or "symbol" that stands for
each function such as `fmt.Println`.
Any reference to a function is represented as a pointer to its symbol.
To test whether two references are talking about the same symbol,
you dont need to think about names.
You just compare pointers. A pointer is much smaller than a string,
and pointer comparison is very cheap, so symbols are an efficient way to
represent a structure as complex as a program.
In order to respond quickly to requests, gopls v0.11 held all these symbols in memory,
as though gopls was **compiling your entire program at once**.
The result was a memory footprint that was proportional to and much larger
than the source code being edited (for example,
typed syntax trees are typically 30x larger than the source text!).
<!-- deps(gopls) = 18.5MB source, 542MB RAM -->
## Separate compilation {#separate-compilation}
The designers of the first compilers in the 1950s quickly discovered the
limits of monolithic compilation.
Their solution was to break the program into units and compile each unit separately.
Separate compilation makes it possible to build a program that does not fit in memory,
by doing it in small pieces.
In Go, the units are packages. Compilation of different packages cannot
be completely separated:
when compiling a package P, the compiler still needs information about what's
provided by the packages that P imports.
To arrange this, the Go build system compiles all of P's imported packages before P itself,
and the Go compiler writes a compact summary of each package's exported API.
The summaries of P's imported packages are provided as inputs to the compilation of P itself.
Gopls v0.12 brings separate compilation to gopls,
reusing the same package summary format used by the compiler.
The idea is simple, but theres subtlety in the details.
We rewrote each algorithm that previously inspected the data structure representing the entire program,
so that it now works on one package at a time and saves per-package results to files,
just like a compiler emitting object code.
For example, finding all references to a function used to be as easy as
searching the program data structure for all occurrences of a particular pointer value.
Now, when gopls processes each package, it must construct and save an index
that associates each identifier location in the source code with the name
of the symbol to which it refers.
At query time, gopls loads and searches these indexes.
Other global queries, such as "find implementations",
use similar techniques.
Like the `go build` command, gopls now uses a [file-based cache](https://cs.opensource.google/go/x/tools/+/master:gopls/internal/lsp/filecache/filecache.go;l=5;drc=6f567c8090cb88f13a71b19595bf88c6b27dbeed)
store to record summaries of information computed from each package,
including the type of each declaration, the index of cross-references,
and the method set of each type.
Since the cache is persisted across processes,
youll notice that the second time you start gopls in your workspace,
it becomes ready to serve much more quickly,
and if you run two gopls instances, they work together synergistically.
<div class="image">
<img src="gopls-scalability/separate-compilation.png" alt="separate compilation" class="chart"/>
</div>
The result of this change is that gopls's memory use is proportional to
the number of open packages and their direct imports.
This is why we observe sublinear scaling in the chart above:
as repositories get larger, the fraction of the project observed by any
one open package gets smaller.
## Fine-grained invalidation {#invalidation}
When you make a change in one package, it's only necessary to recompile
the packages that import that one,
directly or indirectly.
This idea is the basis of all incremental build systems since Make in the 1970s,
and gopls has been using it since its inception.
In effect, every keystroke in your LSP-enabled editor starts an incremental build!
However, in a large project, indirect dependencies add up,
making these incremental rebuilds too slow.
It turns out that a lot of this work isn't strictly necessary,
because most changes, such as adding a statement within an existing function,
don't affect the import summaries.
If you make a small change in one file, we have to recompile its package,
but if the change does not affect the import summary, we don't have to compile any other packages.
The effect of the change is "pruned". A change that does affect the import
summary requires recompiling the packages that directly import that package,
but most such changes won't affect the import summaries of _those_ packages,
in which case the effect is still pruned and avoids recompiling indirect importers.
Thanks to this pruning, it is rare for a change in a low-level package to
require recompiling _all_ the packages that indirectly depend on that package.
Pruned incremental rebuilds make the amount of work proportional to the
scope of each change.
This is not a new idea: it was introduced by [Vesta](https://www.hpl.hp.com/techreports/Compaq-DEC/SRC-RR-177.pdf)
and also used in [`go build`](https://go.dev/doc/go1.10#build).
The v0.12 release introduces a similar pruning technique to gopls,
going one step further to implement a faster pruning heuristic based on syntactic analysis.
By keeping a simplified graph of symbol references in memory,
gopls can quickly determine whether a change in package `c` can possibly
affect package `a` through a chain of references.
<div class="image">
<img src="gopls-scalability/precise-pruning.png" alt="fine-grained invalidation" class="chart"/>
</div>
In the example above, there's no chain of references from `a` to `c`,
so a is not exposed to changes in c even though it indirectly depends on it.
## New possibilities {#new-possibilities}
While we're happy with the performance improvements we've achieved,
we're also excited about several gopls features that are feasible now that
gopls is no longer constrained by memory.
The first is robust static analysis. Previously,
our static analysis driver had to operate on gopls's in-memory representation of packages,
so it couldn't analyze dependencies:
doing so would pull in too much additional code.
With that requirement removed, we were able to include a new analysis driver
in gopls v0.12 that analyzes all dependencies,
resulting in greater precision.
For example, gopls now reports diagnostics for `Printf` formatting mistakes
even in your user-defined wrappers around `fmt.Printf`.
Notably, `go vet` has provided this level of precision for years,
but gopls was unable to do this in real time after each edit. Now it can.
The second is [simpler workspace configuration](https://go.dev/issue/57979)
and [improved handling for build tags](https://go.dev/issue/29202).
These two features both amount to gopls "doing the right thing" when you
open any Go file on your machine,
but both were infeasible without the optimization work because (for example)
each build configuration multiplies the memory footprint!
## Try it out! {#try}
In addition to scalability and performance improvements,
we've also fixed [numerous](https://github.com/golang/go/milestone/282?closed=1)
[reported bugs](https://github.com/golang/go/milestone/318?closed=1) and
many unreported ones that we discovered while improving test coverage during the transition.
To install the latest gopls:
```
$ go install golang.org/x/tools/gopls@latest
```
Please try it out and fill out the [survey](https://google.qualtrics.com/jfe/form/SV_4SnGxpcSKN33WZw?s=blog) ---
and if you should encounter a bug,
[report it](https://github.com/golang/go/issues/new/choose) and we will fix it.

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

После

Ширина:  |  Высота:  |  Размер: 79 KiB

Просмотреть файл

@ -0,0 +1,467 @@
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"type": "text",
"version": 131,
"versionNonce": 1812852806,
"isDeleted": false,
"id": "KLTqqKKdKPvv_uyDvIH56",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 708.5,
"y": 226,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"width": 117.1875,
"height": 192,
"seed": 1764650250,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1694183553513,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 3,
"text": "package a\n\nimport \"b\"\n\nfunc A() {\n b.B1()\n}\n",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "package a\n\nimport \"b\"\n\nfunc A() {\n b.B1()\n}\n",
"lineHeight": 1.2,
"baseline": 187
},
{
"type": "text",
"version": 382,
"versionNonce": 198113875,
"isDeleted": false,
"id": "PScOU98GRKKPi_JFEXVOd",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 927.5,
"y": 225.5,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"width": 152.34375,
"height": 168,
"seed": 1266662166,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [
{
"id": "dlPrM6ifKO8CYIQGgVSjT",
"type": "arrow"
}
],
"updated": 1692713327603,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 3,
"text": "package b\n\nimport \"c\"\n\nfunc B1() {}\n\nfunc B2(C) {}",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "package b\n\nimport \"c\"\n\nfunc B1() {}\n\nfunc B2(C) {}",
"lineHeight": 1.2,
"baseline": 163
},
{
"type": "text",
"version": 245,
"versionNonce": 410175581,
"isDeleted": false,
"id": "qdvLpJXYgRj1saqwYi-Ff",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1152.2400512695312,
"y": 227.5,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"width": 175.78125,
"height": 72,
"seed": 343723210,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [
{
"id": "dlPrM6ifKO8CYIQGgVSjT",
"type": "arrow"
}
],
"updated": 1692713327603,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 3,
"text": "package c\n\ntype C struct{}",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "package c\n\ntype C struct{}",
"lineHeight": 1.2,
"baseline": 67
},
{
"type": "arrow",
"version": 565,
"versionNonce": 61253530,
"isDeleted": false,
"id": "Jo5-4OWytSqsWKyCCv0zs",
"fillStyle": "hachure",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 774,
"y": 373,
"strokeColor": "#1971c2",
"backgroundColor": "transparent",
"width": 208.5,
"height": 68.5,
"seed": 662000458,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1694183684530,
"link": null,
"locked": false,
"startBinding": null,
"endBinding": null,
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
35.5,
9
],
[
168.5,
-59.5
],
[
208.5,
-49
]
]
},
{
"type": "arrow",
"version": 1142,
"versionNonce": 1980630022,
"isDeleted": false,
"id": "dlPrM6ifKO8CYIQGgVSjT",
"fillStyle": "hachure",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 1034.8165343534565,
"y": 397.5,
"strokeColor": "#1971c2",
"backgroundColor": "transparent",
"width": 176.6140607076302,
"height": 106.6081098471862,
"seed": 371482890,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1694183691116,
"link": null,
"locked": false,
"startBinding": {
"elementId": "PScOU98GRKKPi_JFEXVOd",
"focus": 0.5314594691269521,
"gap": 4
},
"endBinding": {
"elementId": "qdvLpJXYgRj1saqwYi-Ff",
"focus": -0.035506896849099974,
"gap": 5
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
22.480751639833215,
13.6081098471862
],
[
91.90307013873098,
-13
],
[
154.18346564654348,
-65
],
[
176.6140607076302,
-93
]
]
},
{
"type": "text",
"version": 591,
"versionNonce": 995219209,
"isDeleted": false,
"id": "0e_RgDukEA7a4s-hPUdUD",
"fillStyle": "hachure",
"strokeWidth": 2,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 856,
"y": 176,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"width": 272.5196838378906,
"height": 25,
"seed": 442549770,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1693423437323,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 1,
"text": "Package a doesn't reach c.",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "Package a doesn't reach c.",
"lineHeight": 1.25,
"baseline": 18
},
{
"type": "arrow",
"version": 260,
"versionNonce": 116791514,
"isDeleted": false,
"id": "zUxFciaTErQ7nvkqFE4xZ",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dotted",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 994.6198891769479,
"y": 367.4258069066833,
"strokeColor": "#1971c2",
"backgroundColor": "transparent",
"width": 0.17108894564739785,
"height": 21.464261588495276,
"seed": 390419155,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1694183723060,
"link": null,
"locked": false,
"startBinding": null,
"endBinding": null,
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": null,
"points": [
[
0,
0
],
[
0.17108894564739785,
-21.464261588495276
]
]
},
{
"type": "text",
"version": 245,
"versionNonce": 838129114,
"isDeleted": false,
"id": "lwbMT4vlzQR_SD-RiLk_N",
"fillStyle": "hachure",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 2,
"opacity": 100,
"angle": 0,
"x": 788.7196044921875,
"y": 416,
"strokeColor": "#1971c2",
"backgroundColor": "transparent",
"width": 148.08029174804688,
"height": 20,
"seed": 89416659,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [
{
"id": "J_bngpKx7t6uKubZgsPUc",
"type": "arrow"
}
],
"updated": 1694183688957,
"link": null,
"locked": false,
"fontSize": 16,
"fontFamily": 1,
"text": "Note: no edge here",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "Note: no edge here",
"lineHeight": 1.25,
"baseline": 14
},
{
"type": "arrow",
"version": 295,
"versionNonce": 1976297030,
"isDeleted": false,
"id": "J_bngpKx7t6uKubZgsPUc",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 869.1993031227533,
"y": 408.50000000000006,
"strokeColor": "#1971c2",
"backgroundColor": "transparent",
"width": 109.02030136943415,
"height": 51.00000000000006,
"seed": 1824236701,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1694183686932,
"link": null,
"locked": false,
"startBinding": {
"elementId": "lwbMT4vlzQR_SD-RiLk_N",
"focus": -0.1187385614826156,
"gap": 7.499999999999943
},
"endBinding": null,
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
29.304081675061752,
-31.38513730898279
],
[
62.87164228631707,
-47.22297253820352
],
[
109.02030136943415,
-51.00000000000006
]
]
},
{
"type": "line",
"version": 250,
"versionNonce": 471171581,
"isDeleted": false,
"id": "N9ei1d7la2SGB-r_dfxam",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 851.7196044921875,
"y": 203.5,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"width": 288,
"height": 0.5,
"seed": 953081427,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1692713333701,
"link": null,
"locked": false,
"startBinding": null,
"endBinding": null,
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": null,
"points": [
[
0,
0
],
[
288,
-0.5
]
]
}
],
"appState": {
"gridSize": null,
"viewBackgroundColor": "#ffffff"
},
"files": {}
}

Двоичные данные
_content/blog/gopls-scalability/precise-pruning.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 82 KiB

Просмотреть файл

@ -0,0 +1,253 @@
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"type": "text",
"version": 514,
"versionNonce": 1931030537,
"isDeleted": false,
"id": "8vCu0eNhvFjFZlYF6qMKK",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 2,
"opacity": 100,
"angle": 0,
"x": 634.2309083984003,
"y": 256.521234050657,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"width": 304.6875,
"height": 216,
"seed": 588319446,
"groupIds": [
"catUVsDqTBTKpq-jyjQOz"
],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1693404698186,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 3,
"text": "package foo\n\nimport \"fmt\"\n\ntype Bar struct{}\n\nfunc (Bar) Baz(s string) {\n fmt.Println(s)\n}",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "package foo\n\nimport \"fmt\"\n\ntype Bar struct{}\n\nfunc (Bar) Baz(s string) {\n fmt.Println(s)\n}",
"lineHeight": 1.2,
"baseline": 211
},
{
"type": "rectangle",
"version": 466,
"versionNonce": 584127881,
"isDeleted": false,
"id": "OIDpNpialGaYan-IDYsYp",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 2,
"opacity": 100,
"angle": 0,
"x": 607,
"y": 211,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"width": 348.79070653208913,
"height": 312.7659493429823,
"seed": 160776214,
"groupIds": [
"catUVsDqTBTKpq-jyjQOz"
],
"frameId": null,
"roundness": {
"type": 3
},
"boundElements": [
{
"id": "YfmaTvV7nBEdCO5v-yiWr",
"type": "arrow"
}
],
"updated": 1693403971747,
"link": null,
"locked": false
},
{
"type": "rectangle",
"version": 479,
"versionNonce": 2124129159,
"isDeleted": false,
"id": "3OdPvUKZKpvbzhl_B5fid",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 2,
"opacity": 100,
"angle": 0,
"x": 1030.5,
"y": 186.5,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"width": 342,
"height": 341.4999999999999,
"seed": 1665264074,
"groupIds": [
"3Jg1OxEHWdnyfhavtSLUi"
],
"frameId": null,
"roundness": {
"type": 3
},
"boundElements": [
{
"id": "YfmaTvV7nBEdCO5v-yiWr",
"type": "arrow"
}
],
"updated": 1693403971747,
"link": null,
"locked": false
},
{
"type": "text",
"version": 518,
"versionNonce": 1221397097,
"isDeleted": false,
"id": "eeZNpC4B2KUVuWE7MReMW",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 2,
"opacity": 100,
"angle": 0,
"x": 1051,
"y": 207,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"width": 288.3397216796875,
"height": 275,
"seed": 1807097994,
"groupIds": [
"3Jg1OxEHWdnyfhavtSLUi"
],
"frameId": null,
"roundness": null,
"boundElements": [
{
"id": "YfmaTvV7nBEdCO5v-yiWr",
"type": "arrow"
}
],
"updated": 1693423357961,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 1,
"text": "Summary\n package foo\n import \"fmt\"\n type Bar struct{}\n func (Bar) Baz(s string)\n\nReferences:\n fmt.Println: line 8, column 6\n\nMethods:\n Baz(string): Bar",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "Summary\n package foo\n import \"fmt\"\n type Bar struct{}\n func (Bar) Baz(s string)\n\nReferences:\n fmt.Println: line 8, column 6\n\nMethods:\n Baz(string): Bar",
"lineHeight": 1.25,
"baseline": 268
},
{
"type": "arrow",
"version": 1132,
"versionNonce": 1317771943,
"isDeleted": false,
"id": "YfmaTvV7nBEdCO5v-yiWr",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 2,
"opacity": 100,
"angle": 0,
"x": 971.7907065320892,
"y": 367.1869308196924,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"width": 44.20929346791081,
"height": 1.1750564695992125,
"seed": 1753119510,
"groupIds": [],
"frameId": null,
"roundness": {
"type": 2
},
"boundElements": [],
"updated": 1693403971747,
"link": null,
"locked": false,
"startBinding": {
"elementId": "OIDpNpialGaYan-IDYsYp",
"gap": 16,
"focus": -0.03876047338293693
},
"endBinding": {
"elementId": "3OdPvUKZKpvbzhl_B5fid",
"gap": 14.5,
"focus": -0.0915169218121322
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "triangle",
"points": [
[
0,
0
],
[
44.20929346791081,
1.1750564695992125
]
]
},
{
"type": "text",
"version": 215,
"versionNonce": 1703806793,
"isDeleted": false,
"id": "Ca7svLx55Vj_MCQV-LfdJ",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 2,
"opacity": 100,
"angle": 0.17623928971477731,
"x": 1254.5398740640808,
"y": 209.57474426703484,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"width": 99.139892578125,
"height": 25,
"seed": 1290026070,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1693403971747,
"link": null,
"locked": false,
"fontSize": 20,
"fontFamily": 1,
"text": "File Cache",
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "File Cache",
"lineHeight": 1.25,
"baseline": 18
}
],
"appState": {
"gridSize": null,
"viewBackgroundColor": "#ffffff"
},
"files": {}
}

Двоичные данные
_content/blog/gopls-scalability/separate-compilation.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 147 KiB