Skip to content

Commit 887727a

Browse files
committed
gopls: Measure the efficacy of completions
This CL introduces code to count how often proposed completions are accepted. The results of the latest completion request are stored and compared with the next DidChange notification. If the change is at the same position, more than one character long, and matches a completion item, the code counts the completion as accepted. If, for some reason, DidChange returns multiple changes, only the first is checked. If the whole file is replaced there is no check for using any proposed completion. The test does not exercise the multi-change counter. Change-Id: I08e9e39f651e4975a86c2dda9f6f5ceab7787e53 Reviewed-on: https://go-review.googlesource.com/c/tools/+/562248 Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent ca94c96 commit 887727a

File tree

6 files changed

+162
-2
lines changed

6 files changed

+162
-2
lines changed

gopls/internal/server/completion.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ func (s *server) Completion(ctx context.Context, params *protocol.CompletionPara
4040
var surrounding *completion.Selection
4141
switch snapshot.FileKind(fh) {
4242
case file.Go:
43+
complCnt.Inc() // completion requests for Go programs
4344
candidates, surrounding, err = completion.Completion(ctx, snapshot, fh, params.Position, params.Context)
4445
case file.Mod:
4546
candidates, surrounding = nil, nil
@@ -61,6 +62,7 @@ func (s *server) Completion(ctx context.Context, params *protocol.CompletionPara
6162
event.Error(ctx, "no completions found", err, tag.Position.Of(params.Position))
6263
}
6364
if candidates == nil {
65+
complEmpty.Inc()
6466
return &protocol.CompletionList{
6567
IsIncomplete: true,
6668
Items: []protocol.CompletionItem{},
@@ -78,13 +80,28 @@ func (s *server) Completion(ctx context.Context, params *protocol.CompletionPara
7880
incompleteResults := options.DeepCompletion || options.Matcher == settings.Fuzzy
7981

8082
items := toProtocolCompletionItems(candidates, rng, options)
83+
if snapshot.FileKind(fh) == file.Go {
84+
s.saveLastCompletion(fh.URI(), fh.Version(), items, params.Position)
85+
}
8186

87+
if len(items) > 10 {
88+
// TODO(pjw): long completions are ok for field lists
89+
complLong.Inc()
90+
}
8291
return &protocol.CompletionList{
8392
IsIncomplete: incompleteResults,
8493
Items: items,
8594
}, nil
8695
}
8796

97+
func (s *server) saveLastCompletion(uri protocol.DocumentURI, version int32, items []protocol.CompletionItem, pos protocol.Position) {
98+
s.efficacyMu.Lock()
99+
defer s.efficacyMu.Unlock()
100+
s.efficacyURI = uri
101+
s.efficacyPos = pos
102+
s.efficacyItems = items
103+
}
104+
88105
func toProtocolCompletionItems(candidates []completion.CompletionItem, rng protocol.Range, options *settings.Options) []protocol.CompletionItem {
89106
var (
90107
items = make([]protocol.CompletionItem, 0, len(candidates))

gopls/internal/server/counters.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package server
2+
3+
import "golang.org/x/telemetry/counter"
4+
5+
// Proposed counters for evaluating gopls code completion.
6+
var (
7+
complCnt = counter.New("gopls/completion/cnt") // for Go programs
8+
complEmpty = counter.New("gopls/completion/empty") // count empty responses
9+
complLong = counter.New("gopls/completion/long") // returning more than 10 items
10+
11+
changeMulti = counter.New("gopls/completion/multi-change") // multiple changes in didChange
12+
changeFull = counter.New("gopls/completion/full-change") // full file change in didChange
13+
14+
complUsed = counter.New("gopls/completion/used") // used a completion
15+
16+
// exported so tests can verify that counters are incrementd
17+
CompletionCounters = []*counter.Counter{
18+
complCnt,
19+
complEmpty,
20+
complLong,
21+
changeMulti,
22+
changeFull,
23+
complUsed,
24+
}
25+
)

gopls/internal/server/server.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ type server struct {
115115
optionsMu sync.Mutex
116116
options *settings.Options
117117

118+
// Track the most recent completion results, for measuring completion efficacy
119+
efficacyMu sync.Mutex
120+
efficacyURI protocol.DocumentURI
121+
efficacyVersion int32
122+
efficacyItems []protocol.CompletionItem
123+
efficacyPos protocol.Position
124+
118125
// # Modification tracking and diagnostics
119126
//
120127
// For the purpose of tracking diagnostics, we need a monotonically

gopls/internal/server/text_synchronization.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"errors"
1111
"fmt"
1212
"path/filepath"
13+
"strings"
1314
"sync"
1415

1516
"golang.org/x/tools/gopls/internal/cache"
@@ -313,6 +314,7 @@ func (s *server) changedText(ctx context.Context, uri protocol.DocumentURI, chan
313314
// Check if the client sent the full content of the file.
314315
// We accept a full content change even if the server expected incremental changes.
315316
if len(changes) == 1 && changes[0].Range == nil && changes[0].RangeLength == 0 {
317+
changeFull.Inc()
316318
return []byte(changes[0].Text), nil
317319
}
318320
return s.applyIncrementalChanges(ctx, uri, changes)
@@ -327,7 +329,10 @@ func (s *server) applyIncrementalChanges(ctx context.Context, uri protocol.Docum
327329
if err != nil {
328330
return nil, fmt.Errorf("%w: file not found (%v)", jsonrpc2.ErrInternal, err)
329331
}
330-
for _, change := range changes {
332+
if len(changes) > 1 {
333+
changeMulti.Inc()
334+
}
335+
for i, change := range changes {
331336
// TODO(adonovan): refactor to use diff.Apply, which is robust w.r.t.
332337
// out-of-order or overlapping changes---and much more efficient.
333338

@@ -348,10 +353,51 @@ func (s *server) applyIncrementalChanges(ctx context.Context, uri protocol.Docum
348353
buf.WriteString(change.Text)
349354
buf.Write(content[end:])
350355
content = buf.Bytes()
356+
if i == 0 { // only look at the first change if there are seversl
357+
// TODO(pjw): understand multi-change)
358+
s.checkEfficacy(fh.URI(), fh.Version(), change)
359+
}
351360
}
352361
return content, nil
353362
}
354363

364+
// increment counters if any of the completions look like there were used
365+
func (s *server) checkEfficacy(uri protocol.DocumentURI, version int32, change protocol.TextDocumentContentChangePartial) {
366+
s.efficacyMu.Lock()
367+
defer s.efficacyMu.Unlock()
368+
if s.efficacyURI != uri {
369+
return
370+
}
371+
// gopls increments the version, the test client does not
372+
if version != s.efficacyVersion && version != s.efficacyVersion+1 {
373+
return
374+
}
375+
// does any change at pos match a proposed completion item?
376+
for _, item := range s.efficacyItems {
377+
if item.TextEdit == nil {
378+
continue
379+
}
380+
if item.TextEdit.Range.Start == change.Range.Start {
381+
// the change and the proposed completion start at the same
382+
if change.RangeLength == 0 && len(change.Text) == 1 {
383+
// a single character added it does not count as a completion
384+
continue
385+
}
386+
ix := strings.Index(item.TextEdit.NewText, "$")
387+
if ix < 0 && strings.HasPrefix(change.Text, item.TextEdit.NewText) {
388+
// not a snippet, suggested completion is a prefix of the change
389+
complUsed.Inc()
390+
return
391+
}
392+
if ix > 1 && strings.HasPrefix(change.Text, item.TextEdit.NewText[:ix]) {
393+
// a snippet, suggested completion up to $ marker is a prefix of the change
394+
complUsed.Inc()
395+
return
396+
}
397+
}
398+
}
399+
}
400+
355401
func changeTypeToFileAction(ct protocol.FileChangeType) file.Action {
356402
switch ct {
357403
case protocol.Changed:

gopls/internal/telemetry/telemetry_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func addForwardedCounters(env *Env, names []string, values []int64) {
125125
env.ExecuteCommand(&protocol.ExecuteCommandParams{
126126
Command: command.AddTelemetryCounters.ID(),
127127
Arguments: args,
128-
}, res)
128+
}, &res)
129129
if res != nil {
130130
env.T.Errorf("%v failed - %v", command.AddTelemetryCounters.ID(), res)
131131
}

gopls/internal/test/integration/completion/completion_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@ package completion
66

77
import (
88
"fmt"
9+
"log"
910
"sort"
1011
"strings"
1112
"testing"
1213
"time"
1314

1415
"github.com/google/go-cmp/cmp"
16+
"golang.org/x/telemetry/counter"
17+
"golang.org/x/telemetry/counter/countertest"
1518
"golang.org/x/tools/gopls/internal/hooks"
1619
"golang.org/x/tools/gopls/internal/protocol"
20+
"golang.org/x/tools/gopls/internal/server"
1721
. "golang.org/x/tools/gopls/internal/test/integration"
1822
"golang.org/x/tools/gopls/internal/test/integration/fake"
1923
"golang.org/x/tools/gopls/internal/util/bug"
@@ -256,6 +260,11 @@ func compareCompletionLabels(want []string, gotItems []protocol.CompletionItem)
256260
return ""
257261
}
258262

263+
func init() {
264+
// useful while debugging
265+
log.SetFlags(log.Lshortfile)
266+
}
267+
259268
func TestUnimportedCompletion(t *testing.T) {
260269
const mod = `
261270
-- go.mod --
@@ -999,3 +1008,59 @@ func Join() {}
9991008
}
10001009
})
10011010
}
1011+
1012+
func TestCounters(t *testing.T) {
1013+
const files = `
1014+
-- go.mod --
1015+
module foo
1016+
go 1.21
1017+
-- x.go --
1018+
package foo
1019+
1020+
func main() {
1021+
}
1022+
1023+
`
1024+
WithOptions(
1025+
Modes(Default),
1026+
).Run(t, files, func(t *testing.T, env *Env) {
1027+
cts := func() map[*counter.Counter]uint64 {
1028+
ans := make(map[*counter.Counter]uint64)
1029+
for _, c := range server.CompletionCounters {
1030+
ans[c], _ = countertest.ReadCounter(c)
1031+
}
1032+
return ans
1033+
}
1034+
env.OpenFile("x.go")
1035+
env.Await(env.DoneWithOpen())
1036+
saved := env.BufferText("x.go")
1037+
lines := strings.Split(saved, "\n")
1038+
loc := env.RegexpSearch("x.go", "p")
1039+
before := cts()
1040+
// all the action is after 4 characters on line 2 (counting from 0)
1041+
// (doing the whole file is too expensive)
1042+
for i := 2; i < len(lines); i++ {
1043+
l := lines[i]
1044+
loc.Range.Start.Line = uint32(i)
1045+
for j := 5; j < len(l); j++ {
1046+
loc.Range.Start.Character = uint32(j)
1047+
loc.Range.End = loc.Range.Start
1048+
res := env.Completion(loc)
1049+
for _, r := range res.Items {
1050+
env.AcceptCompletion(loc, r)
1051+
env.SetBufferContent("x.go", saved)
1052+
}
1053+
}
1054+
}
1055+
after := cts()
1056+
for c := range after {
1057+
if c.Name() == "gopls/completion/multi-change" {
1058+
// don't know how to force multi-change
1059+
continue
1060+
}
1061+
if after[c] <= before[c] {
1062+
t.Errorf("%s did not increase", c.Name())
1063+
}
1064+
}
1065+
})
1066+
}

0 commit comments

Comments
 (0)