Skip to content

Commit 8c0fcd2

Browse files
committed
gopls/internal/lsp/lru: extract LRU logic to a standalone package
Extract a simple LRU package from the parseCache implementation (which itself was extracted from an initial implementation of the filecache). In a subsequent CL, this package will be used to reduce I/O in the filecache package. For golang/go#57987 Change-Id: I307f397b71654226d4e0e1c532a81cfde49af831 Reviewed-on: https://go-review.googlesource.com/c/tools/+/494099 Reviewed-by: Alan Donovan <[email protected]> Run-TryBot: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 19d700c commit 8c0fcd2

File tree

3 files changed

+350
-0
lines changed

3 files changed

+350
-0
lines changed

gopls/internal/lsp/lru/lru.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// The lru package implements a fixed-size in-memory LRU cache.
6+
package lru
7+
8+
import (
9+
"container/heap"
10+
"fmt"
11+
"sync"
12+
)
13+
14+
type any = interface{} // TODO: remove once gopls only builds at go1.18+
15+
16+
// A Cache is a fixed-size in-memory LRU cache.
17+
type Cache struct {
18+
capacity int
19+
20+
mu sync.Mutex
21+
used int // used capacity, in user-specified units
22+
m map[any]*entry // k/v lookup
23+
lru queue // min-atime priority queue of *entry
24+
clock int64 // clock time, incremented whenever the cache is updated
25+
}
26+
27+
type entry struct {
28+
key any
29+
value any
30+
size int // caller-specified size
31+
atime int64 // last access / set time
32+
index int // index of entry in the heap slice
33+
}
34+
35+
// New creates a new Cache with the given capacity, which must be positive.
36+
//
37+
// The cache capacity uses arbitrary units, which are specified during the Set
38+
// operation.
39+
func New(capacity int) *Cache {
40+
if capacity == 0 {
41+
panic("zero capacity")
42+
}
43+
44+
return &Cache{
45+
capacity: capacity,
46+
m: make(map[any]*entry),
47+
}
48+
}
49+
50+
// Get retrieves the value for the specified key, or nil if the key is not
51+
// found.
52+
//
53+
// If the key is found, its access time is updated.
54+
func (c *Cache) Get(key any) any {
55+
c.mu.Lock()
56+
defer c.mu.Unlock()
57+
58+
c.clock++ // every access updates the clock
59+
60+
if e, ok := c.m[key]; ok { // cache hit
61+
e.atime = c.clock
62+
heap.Fix(&c.lru, e.index)
63+
return e.value
64+
}
65+
66+
return nil
67+
}
68+
69+
// Set stores a value for the specified key, using its given size to update the
70+
// current cache size, evicting old entries as necessary to fit in the cache
71+
// capacity.
72+
//
73+
// Size must be a non-negative value. If size is larger than the cache
74+
// capacity, the value is not stored and the cache is not modified.
75+
func (c *Cache) Set(key, value any, size int) {
76+
if size < 0 {
77+
panic(fmt.Sprintf("size must be non-negative, got %d", size))
78+
}
79+
if size > c.capacity {
80+
return // uncacheable
81+
}
82+
83+
c.mu.Lock()
84+
defer c.mu.Unlock()
85+
86+
c.clock++
87+
88+
// Remove the existing cache entry for key, if it exists.
89+
e, ok := c.m[key]
90+
if ok {
91+
c.used -= e.size
92+
heap.Remove(&c.lru, e.index)
93+
delete(c.m, key)
94+
}
95+
96+
// Evict entries until the new value will fit.
97+
newUsed := c.used + size
98+
if newUsed < 0 {
99+
return // integer overflow; return silently
100+
}
101+
c.used = newUsed
102+
for c.used > c.capacity {
103+
// evict oldest entry
104+
e = heap.Pop(&c.lru).(*entry)
105+
c.used -= e.size
106+
delete(c.m, e.key)
107+
}
108+
109+
// Store the new value.
110+
// Opt: e is evicted, so it can be reused to reduce allocation.
111+
if e == nil {
112+
e = new(entry)
113+
}
114+
e.key = key
115+
e.value = value
116+
e.size = size
117+
e.atime = c.clock
118+
c.m[e.key] = e
119+
heap.Push(&c.lru, e)
120+
121+
if len(c.m) != len(c.lru) {
122+
panic("map and LRU are inconsistent")
123+
}
124+
}
125+
126+
// -- priority queue boilerplate --
127+
128+
// queue is a min-atime priority queue of cache entries.
129+
type queue []*entry
130+
131+
func (q queue) Len() int { return len(q) }
132+
133+
func (q queue) Less(i, j int) bool { return q[i].atime < q[j].atime }
134+
135+
func (q queue) Swap(i, j int) {
136+
q[i], q[j] = q[j], q[i]
137+
q[i].index = i
138+
q[j].index = j
139+
}
140+
141+
func (q *queue) Push(x any) {
142+
e := x.(*entry)
143+
e.index = len(*q)
144+
*q = append(*q, e)
145+
}
146+
147+
func (q *queue) Pop() any {
148+
last := len(*q) - 1
149+
e := (*q)[last]
150+
(*q)[last] = nil // aid GC
151+
*q = (*q)[:last]
152+
return e
153+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.18
6+
// +build go1.18
7+
8+
package lru_test
9+
10+
import (
11+
"testing"
12+
13+
"golang.org/x/tools/gopls/internal/lsp/lru"
14+
)
15+
16+
// Simple fuzzing test for consistency.
17+
func FuzzCache(f *testing.F) {
18+
type op struct {
19+
set bool
20+
key, value byte
21+
}
22+
f.Fuzz(func(t *testing.T, data []byte) {
23+
var ops []op
24+
for len(data) >= 3 {
25+
ops = append(ops, op{data[0]%2 == 0, data[1], data[2]})
26+
data = data[3:]
27+
}
28+
cache := lru.New(100)
29+
var reference [256]byte
30+
for _, op := range ops {
31+
if op.set {
32+
reference[op.key] = op.value
33+
cache.Set(op.key, op.value, 1)
34+
} else {
35+
if v := cache.Get(op.key); v != nil && v != reference[op.key] {
36+
t.Fatalf("cache.Get(%d) = %d, want %d", op.key, v, reference[op.key])
37+
}
38+
}
39+
}
40+
})
41+
}

gopls/internal/lsp/lru/lru_test.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package lru_test
6+
7+
import (
8+
"bytes"
9+
cryptorand "crypto/rand"
10+
"fmt"
11+
"log"
12+
mathrand "math/rand"
13+
"strings"
14+
"testing"
15+
16+
"golang.org/x/sync/errgroup"
17+
"golang.org/x/tools/gopls/internal/lsp/lru"
18+
)
19+
20+
type any = interface{} // TODO: remove once gopls only builds at go1.18+
21+
22+
func TestCache(t *testing.T) {
23+
type get struct {
24+
key string
25+
want any
26+
}
27+
type set struct {
28+
key, value string
29+
}
30+
31+
tests := []struct {
32+
label string
33+
steps []any
34+
}{
35+
{"empty cache", []any{
36+
get{"a", nil},
37+
get{"b", nil},
38+
}},
39+
{"zero-length string", []any{
40+
set{"a", ""},
41+
get{"a", ""},
42+
}},
43+
{"under capacity", []any{
44+
set{"a", "123"},
45+
set{"b", "456"},
46+
get{"a", "123"},
47+
get{"b", "456"},
48+
}},
49+
{"over capacity", []any{
50+
set{"a", "123"},
51+
set{"b", "456"},
52+
set{"c", "78901"},
53+
get{"a", nil},
54+
get{"b", "456"},
55+
get{"c", "78901"},
56+
}},
57+
{"access ordering", []any{
58+
set{"a", "123"},
59+
set{"b", "456"},
60+
get{"a", "123"},
61+
set{"c", "78901"},
62+
get{"a", "123"},
63+
get{"b", nil},
64+
get{"c", "78901"},
65+
}},
66+
}
67+
68+
for _, test := range tests {
69+
t.Run(test.label, func(t *testing.T) {
70+
c := lru.New(10)
71+
for i, step := range test.steps {
72+
switch step := step.(type) {
73+
case get:
74+
if got := c.Get(step.key); got != step.want {
75+
t.Errorf("#%d: c.Get(%q) = %q, want %q", i, step.key, got, step.want)
76+
}
77+
case set:
78+
c.Set(step.key, step.value, len(step.value))
79+
}
80+
}
81+
})
82+
}
83+
}
84+
85+
// TestConcurrency exercises concurrent access to the same entry.
86+
//
87+
// It is a copy of TestConcurrency from the filecache package.
88+
func TestConcurrency(t *testing.T) {
89+
key := uniqueKey()
90+
const N = 100 // concurrency level
91+
92+
// Construct N distinct values, each larger
93+
// than a typical 4KB OS file buffer page.
94+
var values [N][8192]byte
95+
for i := range values {
96+
if _, err := mathrand.Read(values[i][:]); err != nil {
97+
t.Fatalf("rand: %v", err)
98+
}
99+
}
100+
101+
cache := lru.New(100 * 1e6) // 100MB cache
102+
103+
// get calls Get and verifies that the cache entry
104+
// matches one of the values passed to Set.
105+
get := func(mustBeFound bool) error {
106+
got := cache.Get(key)
107+
if got == nil {
108+
if !mustBeFound {
109+
return nil
110+
}
111+
return fmt.Errorf("Get did not return a value")
112+
}
113+
gotBytes := got.([]byte)
114+
for _, want := range values {
115+
if bytes.Equal(want[:], gotBytes) {
116+
return nil // a match
117+
}
118+
}
119+
return fmt.Errorf("Get returned a value that was never Set")
120+
}
121+
122+
// Perform N concurrent calls to Set and Get.
123+
// All sets must succeed.
124+
// All gets must return nothing, or one of the Set values;
125+
// there is no third possibility.
126+
var group errgroup.Group
127+
for i := range values {
128+
i := i
129+
v := values[i][:]
130+
group.Go(func() error {
131+
cache.Set(key, v, len(v))
132+
return nil
133+
})
134+
group.Go(func() error { return get(false) })
135+
}
136+
if err := group.Wait(); err != nil {
137+
if strings.Contains(err.Error(), "operation not supported") ||
138+
strings.Contains(err.Error(), "not implemented") {
139+
t.Skipf("skipping: %v", err)
140+
}
141+
t.Fatal(err)
142+
}
143+
144+
// A final Get must report one of the values that was Set.
145+
if err := get(true); err != nil {
146+
t.Fatalf("final Get failed: %v", err)
147+
}
148+
}
149+
150+
// uniqueKey returns a key that has never been used before.
151+
func uniqueKey() (key [32]byte) {
152+
if _, err := cryptorand.Read(key[:]); err != nil {
153+
log.Fatalf("rand: %v", err)
154+
}
155+
return
156+
}

0 commit comments

Comments
 (0)