Skip to content

Commit e388fff

Browse files
committed
gopls/internal/server: don't reset views if configuration did not change
In CL 538796, options were made immutable on the View, meaning any change to options required a new View. As a result, the minorOptionsChange heuristic, which avoided recreating views on certain options changes, was removed. Unfortunately, in golang/go#66647, it appears that certain clients may send frequent didChangeConfiguration notifications. Presumably the configuration is unchanged, and yet gopls still reinitializes the view. This may be a cause of significant performance regression for these clients. Fix this by making didChangeConfiguration a no op if nothing changed, using reflect.DeepEqual to compare Options. Since Hooks are not comparable due to the GofumptFormat func value, they are excluded from comparison. A subsequent CL will remove hooks altogether. For golang/go#66647 Change-Id: I280059953d6b128461bef1001da3034f89ba3226 Reviewed-on: https://go-review.googlesource.com/c/tools/+/578037 LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent d034ae1 commit e388fff

File tree

11 files changed

+132
-38
lines changed

11 files changed

+132
-38
lines changed

gopls/doc/commands.md

+1
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,7 @@ Result:
703703

704704
```
705705
[]{
706+
"ID": string,
706707
"Type": string,
707708
"Root": string,
708709
"Folder": string,

gopls/internal/cache/session_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ replace (
351351
Dir: toURI(f.dir),
352352
Name: path.Base(f.dir),
353353
Options: opts,
354-
Env: env,
354+
Env: *env,
355355
})
356356
}
357357

gopls/internal/cache/view.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ type Folder struct {
5151
Dir protocol.DocumentURI
5252
Name string // decorative name for UI; not necessarily unique
5353
Options *settings.Options
54-
Env *GoEnv
54+
Env GoEnv
5555
}
5656

5757
// GoEnv holds the environment variables and data from the Go command that is
@@ -426,7 +426,7 @@ func viewEnv(v *View) string {
426426
v.root.Path(),
427427
strings.TrimRight(v.folder.Env.GoVersionOutput, "\n"),
428428
v.folder.Options.BuildFlags,
429-
*v.folder.Env,
429+
v.folder.Env,
430430
v.envOverlay,
431431
)
432432

gopls/internal/protocol/command/interface.go

+1
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,7 @@ type DiagnoseFilesArgs struct {
527527

528528
// A View holds summary information about a cache.View.
529529
type View struct {
530+
ID string // view ID (the index of this view among all views created)
530531
Type string // view type (via cache.ViewType.String)
531532
Root protocol.DocumentURI // root dir of the view (e.g. containing go.mod or go.work)
532533
Folder protocol.DocumentURI // workspace folder associated with the view

gopls/internal/server/command.go

+1
Original file line numberDiff line numberDiff line change
@@ -1430,6 +1430,7 @@ func (c *commandHandler) Views(ctx context.Context) ([]command.View, error) {
14301430
var summaries []command.View
14311431
for _, view := range c.s.session.Views() {
14321432
summaries = append(summaries, command.View{
1433+
ID: view.ID(),
14331434
Type: view.Type().String(),
14341435
Root: view.Root(),
14351436
Folder: view.Folder().Dir,

gopls/internal/server/general.go

+2-24
Original file line numberDiff line numberDiff line change
@@ -460,29 +460,7 @@ func (s *server) SetOptions(opts *settings.Options) {
460460
s.options = opts
461461
}
462462

463-
func (s *server) newFolder(ctx context.Context, folder protocol.DocumentURI, name string) (*cache.Folder, error) {
464-
opts := s.Options()
465-
if opts.ConfigurationSupported {
466-
scope := string(folder)
467-
configs, err := s.client.Configuration(ctx, &protocol.ParamConfiguration{
468-
Items: []protocol.ConfigurationItem{{
469-
ScopeURI: &scope,
470-
Section: "gopls",
471-
}},
472-
},
473-
)
474-
if err != nil {
475-
return nil, fmt.Errorf("failed to get workspace configuration from client (%s): %v", folder, err)
476-
}
477-
478-
opts = opts.Clone()
479-
for _, config := range configs {
480-
if err := s.handleOptionResults(ctx, settings.SetOptions(opts, config)); err != nil {
481-
return nil, err
482-
}
483-
}
484-
}
485-
463+
func (s *server) newFolder(ctx context.Context, folder protocol.DocumentURI, name string, opts *settings.Options) (*cache.Folder, error) {
486464
env, err := cache.FetchGoEnv(ctx, folder, opts)
487465
if err != nil {
488466
return nil, err
@@ -491,7 +469,7 @@ func (s *server) newFolder(ctx context.Context, folder protocol.DocumentURI, nam
491469
Dir: folder,
492470
Name: name,
493471
Options: opts,
494-
Env: env,
472+
Env: *env,
495473
}, nil
496474
}
497475

gopls/internal/server/workspace.go

+43-7
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ package server
77
import (
88
"context"
99
"fmt"
10+
"reflect"
1011
"sync"
1112

1213
"golang.org/x/tools/gopls/internal/cache"
1314
"golang.org/x/tools/gopls/internal/protocol"
15+
"golang.org/x/tools/gopls/internal/settings"
1416
"golang.org/x/tools/internal/event"
1517
)
1618

@@ -37,7 +39,11 @@ func (s *server) addView(ctx context.Context, name string, dir protocol.Document
3739
if state < serverInitialized {
3840
return nil, nil, fmt.Errorf("addView called before server initialized")
3941
}
40-
folder, err := s.newFolder(ctx, dir, name)
42+
opts, err := s.fetchFolderOptions(ctx, dir)
43+
if err != nil {
44+
return nil, nil, err
45+
}
46+
folder, err := s.newFolder(ctx, dir, name, opts)
4147
if err != nil {
4248
return nil, nil, err
4349
}
@@ -68,15 +74,45 @@ func (s *server) DidChangeConfiguration(ctx context.Context, _ *protocol.DidChan
6874
s.SetOptions(options)
6975

7076
// Collect options for all workspace folders.
71-
seen := make(map[protocol.DocumentURI]bool)
72-
var newFolders []*cache.Folder
73-
for _, view := range s.session.Views() {
77+
// If none have changed, this is a no op.
78+
folderOpts := make(map[protocol.DocumentURI]*settings.Options)
79+
changed := false
80+
// The set of views is implicitly guarded by the fact that gopls processes
81+
// didChange notifications synchronously.
82+
//
83+
// TODO(rfindley): investigate this assumption: perhaps we should hold viewMu
84+
// here.
85+
views := s.session.Views()
86+
for _, view := range views {
7487
folder := view.Folder()
75-
if seen[folder.Dir] {
88+
if folderOpts[folder.Dir] != nil {
7689
continue
7790
}
78-
seen[folder.Dir] = true
79-
newFolder, err := s.newFolder(ctx, folder.Dir, folder.Name)
91+
opts, err := s.fetchFolderOptions(ctx, folder.Dir)
92+
if err != nil {
93+
return err
94+
}
95+
96+
// Ignore hooks for the purposes of equality.
97+
sameOptions := reflect.DeepEqual(folder.Options.ClientOptions, opts.ClientOptions) &&
98+
reflect.DeepEqual(folder.Options.ServerOptions, opts.ServerOptions) &&
99+
reflect.DeepEqual(folder.Options.UserOptions, opts.UserOptions) &&
100+
reflect.DeepEqual(folder.Options.InternalOptions, opts.InternalOptions)
101+
102+
if !sameOptions {
103+
changed = true
104+
}
105+
folderOpts[folder.Dir] = opts
106+
}
107+
if !changed {
108+
return nil
109+
}
110+
111+
var newFolders []*cache.Folder
112+
for _, view := range views {
113+
folder := view.Folder()
114+
opts := folderOpts[folder.Dir]
115+
newFolder, err := s.newFolder(ctx, folder.Dir, folder.Name, opts)
80116
if err != nil {
81117
return err
82118
}

gopls/internal/settings/api_json.go

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gopls/internal/settings/settings_test.go

+9
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@
55
package settings
66

77
import (
8+
"reflect"
89
"testing"
910
"time"
1011
)
1112

13+
func TestDefaultsEquivalence(t *testing.T) {
14+
opts1 := DefaultOptions()
15+
opts2 := DefaultOptions()
16+
if !reflect.DeepEqual(opts1, opts2) {
17+
t.Fatal("default options are not equivalent using reflect.DeepEqual")
18+
}
19+
}
20+
1221
func TestSetOption(t *testing.T) {
1322
tests := []struct {
1423
name string

gopls/internal/test/integration/misc/configuration_test.go

+68-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ var FooErr = errors.New("foo")
3939
NoDiagnostics(ForFile("a/a.go")),
4040
)
4141
cfg := env.Editor.Config()
42-
cfg.Settings = map[string]interface{}{
42+
cfg.Settings = map[string]any{
4343
"staticcheck": true,
4444
}
4545
env.ChangeConfiguration(cfg)
@@ -49,6 +49,73 @@ var FooErr = errors.New("foo")
4949
})
5050
}
5151

52+
func TestIdenticalConfiguration(t *testing.T) {
53+
// This test checks that changing configuration does not cause views to be
54+
// recreated if there is no configuration change.
55+
const files = `
56+
-- a.go --
57+
package p
58+
59+
func _() {
60+
var x *int
61+
y := *x
62+
_ = y
63+
}
64+
`
65+
Run(t, files, func(t *testing.T, env *Env) {
66+
// Sanity check: before disabling the nilness analyzer, we should have a
67+
// diagnostic for the nil dereference.
68+
env.OpenFile("a.go")
69+
env.AfterChange(
70+
Diagnostics(
71+
ForFile("a.go"),
72+
WithMessage("nil dereference"),
73+
),
74+
)
75+
76+
// Collect the view ID before changing configuration.
77+
viewID := func() string {
78+
t.Helper()
79+
views := env.Views()
80+
if len(views) != 1 {
81+
t.Fatalf("got %d views, want 1", len(views))
82+
}
83+
return views[0].ID
84+
}
85+
before := viewID()
86+
87+
// Now disable the nilness analyzer.
88+
cfg := env.Editor.Config()
89+
cfg.Settings = map[string]any{
90+
"analyses": map[string]any{
91+
"nilness": false,
92+
},
93+
}
94+
95+
// This should cause the diagnostic to disappear...
96+
env.ChangeConfiguration(cfg)
97+
env.AfterChange(
98+
NoDiagnostics(),
99+
)
100+
// ...and we should be on the second view.
101+
after := viewID()
102+
if after == before {
103+
t.Errorf("after configuration change, got view %q (same as before), want new view", after)
104+
}
105+
106+
// Now change configuration again, this time with the same configuration as
107+
// before. We should still have no diagnostics...
108+
env.ChangeConfiguration(cfg)
109+
env.AfterChange(
110+
NoDiagnostics(),
111+
)
112+
// ...and we should still be on the second view.
113+
if got := viewID(); got != after {
114+
t.Errorf("after second configuration change, got view %q, want %q", got, after)
115+
}
116+
})
117+
}
118+
52119
// Test that clients can configure per-workspace configuration, which is
53120
// queried via the scopeURI of a workspace/configuration request.
54121
// (this was broken in golang/go#65519).

gopls/internal/test/integration/workspace/zero_config_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"testing"
1010

1111
"github.com/google/go-cmp/cmp"
12+
"github.com/google/go-cmp/cmp/cmpopts"
1213
"golang.org/x/tools/gopls/internal/cache"
1314
"golang.org/x/tools/gopls/internal/protocol/command"
1415

@@ -53,7 +54,7 @@ func main() {}
5354
}
5455
checkViews := func(want ...command.View) {
5556
got := env.Views()
56-
if diff := cmp.Diff(want, got); diff != "" {
57+
if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(command.View{}, "ID")); diff != "" {
5758
t.Errorf("SummarizeViews() mismatch (-want +got):\n%s", diff)
5859
}
5960
}
@@ -130,7 +131,7 @@ package a
130131
}
131132
checkViews := func(want ...command.View) {
132133
got := env.Views()
133-
if diff := cmp.Diff(want, got); diff != "" {
134+
if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(command.View{}, "ID")); diff != "" {
134135
t.Errorf("SummarizeViews() mismatch (-want +got):\n%s", diff)
135136
}
136137
}

0 commit comments

Comments
 (0)