Skip to content

Commit 8e85a28

Browse files
committed
internal/lsp: adopt bcmills' suggestion for an improved debouncer API
The debounce API becomes both more testable and more elegant when using channels rather than callbacks to signal events, as suggested by bcmills in CL 333349. Adopt these suggestions. Fixes golang/go#45085 Change-Id: Ic1843f582d514af8aa109c24f5e3311536e54a60 Reviewed-on: https://go-review.googlesource.com/c/tools/+/334252 Trust: Robert Findley <[email protected]> Run-TryBot: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]>
1 parent ae0deb7 commit 8e85a28

File tree

4 files changed

+61
-121
lines changed

4 files changed

+61
-121
lines changed

internal/lsp/debounce.go

Lines changed: 36 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,73 +9,63 @@ import (
99
"time"
1010
)
1111

12-
type debounceFunc struct {
12+
type debounceEvent struct {
1313
order uint64
1414
done chan struct{}
1515
}
1616

1717
type debouncer struct {
18-
mu sync.Mutex
19-
funcs map[string]*debounceFunc
18+
mu sync.Mutex
19+
events map[string]*debounceEvent
2020
}
2121

2222
func newDebouncer() *debouncer {
2323
return &debouncer{
24-
funcs: make(map[string]*debounceFunc),
24+
events: make(map[string]*debounceEvent),
2525
}
2626
}
2727

28-
// debounce waits timeout before running f, if no subsequent call is made with
29-
// the same key in the intervening time. If a later call to debounce with the
30-
// same key occurs while the original call is blocking, the original call will
31-
// return immediately without running its f.
32-
//
33-
// If order is specified, it will be used to order calls logically, so calls
34-
// with lesser order will not cancel calls with greater order.
35-
func (d *debouncer) debounce(key string, order uint64, timeout time.Duration, f func()) {
36-
if timeout == 0 {
37-
// Degenerate case: no debouncing.
38-
f()
39-
return
40-
}
28+
// debounce returns a channel that receives a boolean reporting whether,
29+
// by the time the delay channel receives a value, this call is (or will be)
30+
// the most recent call with the highest order number for its key.
31+
func (d *debouncer) debounce(key string, order uint64, delay <-chan time.Time) <-chan bool {
32+
okc := make(chan bool, 1)
4133

42-
// First, atomically acquire the current func, cancel it, and insert this
43-
// call into d.funcs.
4434
d.mu.Lock()
45-
current, ok := d.funcs[key]
46-
if ok && current.order > order {
47-
// If we have a logical ordering of events (as is the case for snapshots),
48-
// don't overwrite a later event with an earlier event.
49-
d.mu.Unlock()
50-
return
51-
}
52-
if ok {
53-
close(current.done)
35+
if prev, ok := d.events[key]; ok {
36+
if prev.order > order {
37+
// If we have a logical ordering of events (as is the case for snapshots),
38+
// don't overwrite a later event with an earlier event.
39+
d.mu.Unlock()
40+
okc <- false
41+
return okc
42+
}
43+
close(prev.done)
5444
}
5545
done := make(chan struct{})
56-
next := &debounceFunc{
46+
next := &debounceEvent{
5747
order: order,
5848
done: done,
5949
}
60-
d.funcs[key] = next
50+
d.events[key] = next
6151
d.mu.Unlock()
6252

63-
// Next, wait to be cancelled or for our wait to expire. There is a race here
64-
// that we must handle: our timer could expire while another goroutine holds
65-
// d.mu.
66-
select {
67-
case <-done:
68-
case <-time.After(timeout):
69-
d.mu.Lock()
70-
if d.funcs[key] != next {
71-
// We lost the race: another event has arrived for the key and started
72-
// waiting. We could reasonably choose to run f at this point, but doing
73-
// nothing is simpler.
53+
go func() {
54+
ok := false
55+
select {
56+
case <-delay:
57+
d.mu.Lock()
58+
if d.events[key] == next {
59+
ok = true
60+
delete(d.events, key)
61+
} else {
62+
// The event was superseded before we acquired d.mu.
63+
}
7464
d.mu.Unlock()
75-
return
65+
case <-done:
7666
}
77-
delete(d.funcs, key)
78-
d.mu.Unlock()
79-
f()
80-
}
67+
okc <- ok
68+
}()
69+
70+
return okc
8171
}

internal/lsp/debounce_test.go

Lines changed: 15 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,16 @@
55
package lsp
66

77
import (
8-
"fmt"
9-
"strings"
10-
"sync"
118
"testing"
129
"time"
13-
14-
errors "golang.org/x/xerrors"
1510
)
1611

1712
func TestDebouncer(t *testing.T) {
1813
t.Parallel()
1914

20-
const evtWait = 30 * time.Millisecond
21-
const initialDelay = 400 * time.Millisecond
22-
2315
type event struct {
2416
key string
2517
order uint64
26-
fired bool
2718
wantFired bool
2819
}
2920
tests := []struct {
@@ -65,72 +56,26 @@ func TestDebouncer(t *testing.T) {
6556
for _, test := range tests {
6657
test := test
6758
t.Run(test.label, func(t *testing.T) {
68-
try := func(delay time.Duration) (bool, error) {
69-
d := newDebouncer()
70-
var wg sync.WaitGroup
71-
var validMu sync.Mutex
72-
valid := true
73-
prev := -1
74-
for i, e := range test.events {
75-
wg.Add(1)
76-
go func(i int, e *event) {
77-
// Check if goroutines were not scheduled in the intended order.
78-
validMu.Lock()
79-
if prev != i-1 {
80-
valid = false
81-
}
82-
prev = i
83-
validMu.Unlock()
59+
d := newDebouncer()
8460

85-
d.debounce(e.key, e.order, delay, func() {
86-
e.fired = true
87-
})
88-
wg.Done()
89-
}(i, e)
90-
// For a bit more fidelity, sleep to try to make things actually
91-
// execute in order. This shouldn't have to be perfect: as long as we
92-
// don't have extreme pauses the test should still pass.
93-
if i < len(test.events)-1 {
94-
time.Sleep(evtWait)
95-
}
96-
}
97-
wg.Wait()
61+
delays := make([]chan time.Time, len(test.events))
62+
okcs := make([]<-chan bool, len(test.events))
9863

99-
var errs []string
100-
for _, event := range test.events {
101-
if event.fired != event.wantFired {
102-
msg := fmt.Sprintf("(key: %q, order: %d): fired = %t, want %t",
103-
event.key, event.order, event.fired, event.wantFired)
104-
errs = append(errs, msg)
105-
}
106-
}
107-
var err error
108-
if len(errs) > 0 {
109-
err = errors.New(strings.Join(errs, "\n"))
110-
}
111-
return valid, err
64+
// Register the events in deterministic order, synchronously.
65+
for i, e := range test.events {
66+
delays[i] = make(chan time.Time, 1)
67+
okcs[i] = d.debounce(e.key, e.order, delays[i])
11268
}
11369

114-
if err := retryInvalid(initialDelay, try); err != nil {
115-
t.Error(err)
70+
// Now see which event fired.
71+
for i, okc := range okcs {
72+
event := test.events[i]
73+
delays[i] <- time.Now()
74+
fired := <-okc
75+
if fired != event.wantFired {
76+
t.Errorf("[key: %q, order: %d]: fired = %t, want %t", event.key, event.order, fired, event.wantFired)
77+
}
11678
}
11779
})
11880
}
11981
}
120-
121-
// retryInvalid runs the try func up to three times with exponential back-off
122-
// in its duration argument, starting with the provided initial duration. Calls
123-
// to try are retried if their first result indicates that they may be invalid,
124-
// and their second result is a non-nil error.
125-
func retryInvalid(initial time.Duration, try func(time.Duration) (valid bool, err error)) (err error) {
126-
delay := initial
127-
for attempts := 3; attempts > 0; attempts-- {
128-
var valid bool
129-
valid, err = try(delay)
130-
if err == nil || valid {
131-
return err
132-
}
133-
delay += delay
134-
}
135-
return err
136-
}

internal/lsp/diagnostics.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"path/filepath"
1313
"strings"
1414
"sync"
15+
"time"
1516

1617
"golang.org/x/tools/internal/event"
1718
"golang.org/x/tools/internal/lsp/debug/log"
@@ -119,10 +120,10 @@ func (s *Server) diagnoseSnapshot(snapshot source.Snapshot, changedURIs []span.U
119120
// delay.
120121
s.diagnoseChangedFiles(ctx, snapshot, changedURIs, onDisk)
121122
s.publishDiagnostics(ctx, false, snapshot)
122-
s.diagDebouncer.debounce(snapshot.View().Name(), snapshot.ID(), delay, func() {
123+
if ok := <-s.diagDebouncer.debounce(snapshot.View().Name(), snapshot.ID(), time.After(delay)); ok {
123124
s.diagnose(ctx, snapshot, false)
124125
s.publishDiagnostics(ctx, true, snapshot)
125-
})
126+
}
126127
return
127128
}
128129

internal/lsp/text_synchronization.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"context"
1010
"fmt"
1111
"path/filepath"
12+
"time"
1213

1314
"golang.org/x/tools/internal/event"
1415
"golang.org/x/tools/internal/jsonrpc2"
@@ -241,7 +242,11 @@ func (s *Server) didModifyFiles(ctx context.Context, modifications []source.File
241242
// process changes in order.
242243
s.pendingOnDiskChanges = append(s.pendingOnDiskChanges, pending)
243244
ctx = xcontext.Detach(ctx)
244-
delayed := func() {
245+
okc := s.watchedFileDebouncer.debounce("", 0, time.After(delay))
246+
go func() {
247+
if ok := <-okc; !ok {
248+
return
249+
}
245250
s.fileChangeMu.Lock()
246251
var allChanges []source.FileModification
247252
// For accurate progress notifications, we must notify all goroutines
@@ -263,8 +268,7 @@ func (s *Server) didModifyFiles(ctx context.Context, modifications []source.File
263268
for _, done := range dones {
264269
close(done)
265270
}
266-
}
267-
go s.watchedFileDebouncer.debounce("", 0, delay, delayed)
271+
}()
268272
return nil
269273
}
270274

0 commit comments

Comments
 (0)