This repository has been archived by the owner on Sep 30, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
resolve major search perf regression in v3.2.0 (add redis cache to gi…
…t commit OID resolution) (#3685) * refactor OID resolution logic (review with whitespace off) * add oid resolution cache * add observability * CHANGELOG: update * custom cache implementation * enterprise/dev/Procfile: fix bug where zoekt is not listening * remove redundant return stmt * resolveCommitIODUncached * faster test * Update CHANGELOG.md
- Loading branch information
Showing
5 changed files
with
279 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
cmd/frontend/graphqlbackend/git_commit_resolution_cache.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package graphqlbackend | ||
|
||
import ( | ||
"sync" | ||
"time" | ||
|
||
"github.com/prometheus/client_golang/prometheus" | ||
) | ||
|
||
type resolutionCacheEntry struct { | ||
t time.Time | ||
value string | ||
} | ||
|
||
type resolutionCache struct { | ||
// ttl indicates how long before cache entries expire. There is no limit on | ||
// the size of the cache except the effective # of repositories on the | ||
// Sourcegraph instance. | ||
ttl time.Duration | ||
|
||
// cacheEntries, if non-nil, is used to record the number of entries in the cache. | ||
cacheEntries prometheus.Histogram | ||
|
||
// workerInterval, if non-zero, specifies the interval at which the worker | ||
// checks for entries to evict. Defaults ttl / 2. | ||
workerInterval time.Duration | ||
|
||
// mockSleep, if non-nil can be used to mock time.Sleep for testing purposes. | ||
mockSleep func(d time.Duration) | ||
|
||
m sync.Map | ||
} | ||
|
||
func (r *resolutionCache) Set(k, v string) { | ||
r.m.Store(k, resolutionCacheEntry{ | ||
t: time.Now(), | ||
value: v, | ||
}) | ||
} | ||
|
||
func (r *resolutionCache) Get(k string) (string, bool) { | ||
v, ok := r.m.Load(k) | ||
if !ok { | ||
return "", false | ||
} | ||
e := v.(resolutionCacheEntry) | ||
if time.Since(e.t) >= r.ttl { | ||
// entry has expired | ||
r.m.Delete(k) | ||
return "", false | ||
} | ||
return e.value, true | ||
} | ||
|
||
func (r *resolutionCache) startWorker() *resolutionCache { | ||
if r.workerInterval == 0 { | ||
r.workerInterval = r.ttl / 2 | ||
} | ||
sleep := time.Sleep | ||
if r.mockSleep != nil { | ||
sleep = r.mockSleep | ||
} | ||
go func() { | ||
for { | ||
sleep(r.workerInterval) | ||
size := 0 | ||
r.m.Range(func(key, value interface{}) bool { | ||
size++ | ||
e := value.(resolutionCacheEntry) | ||
if time.Since(e.t) >= r.ttl { | ||
// entry has expired | ||
r.m.Delete(key) | ||
} | ||
return true | ||
}) | ||
r.cacheEntries.Observe(float64(size)) | ||
} | ||
}() | ||
return r | ||
} | ||
|
||
var ( | ||
// oidResolutionCache is used to cache Git commit OID resolution. This is | ||
// used because OID resolution happens extremely often (e.g. multiple times | ||
// per search result). | ||
oidResolutionCache = (&resolutionCache{ | ||
ttl: 60 * time.Second, | ||
cacheEntries: prometheus.NewHistogram(prometheus.HistogramOpts{ | ||
Namespace: "src", | ||
Subsystem: "graphql", | ||
Name: "git_commit_oid_resolution_cache_entries", | ||
Help: "Total number of entries in the in-memory Git commit OID resolution cache.", | ||
}), | ||
}).startWorker() | ||
|
||
oidResolutionCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ | ||
Namespace: "src", | ||
Subsystem: "graphql", | ||
Name: "git_commit_oid_resolution_cache_hit", | ||
Help: "Counts cache hits and misses for Git commit OID resolution.", | ||
}, []string{"type"}) | ||
|
||
oidResolutionDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ | ||
Namespace: "src", | ||
Subsystem: "graphql", | ||
Name: "git_commit_oid_resolution_duration_seconds", | ||
Help: "Total time spent performing uncached Git commit OID resolution.", | ||
}) | ||
|
||
oidResolutionCacheLookupDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ | ||
Namespace: "src", | ||
Subsystem: "graphql", | ||
Name: "git_commit_oid_resolution_cache_lookup_duration_seconds", | ||
Help: "Total time spent performing cache lookups for Git commit OID resolution.", | ||
}) | ||
) | ||
|
||
func init() { | ||
prometheus.MustRegister(oidResolutionCache.cacheEntries) | ||
prometheus.MustRegister(oidResolutionCounter) | ||
prometheus.MustRegister(oidResolutionDuration) | ||
prometheus.MustRegister(oidResolutionCacheLookupDuration) | ||
} |
96 changes: 96 additions & 0 deletions
96
cmd/frontend/graphqlbackend/git_commit_resolution_cache_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package graphqlbackend | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
"time" | ||
|
||
"github.com/prometheus/client_golang/prometheus" | ||
) | ||
|
||
func TestGitCommitResolutionCache(t *testing.T) { | ||
doneSleep := make(chan struct{}) | ||
cache := (&resolutionCache{ | ||
ttl: 15 * time.Millisecond, | ||
cacheEntries: prometheus.NewHistogram(prometheus.HistogramOpts{ | ||
Namespace: "src", | ||
Subsystem: "graphql", | ||
Name: "git_commit_oid_resolution_cache_entries", | ||
Help: "Total number of entries in the in-memory Git commit OID resolution cache.", | ||
}), | ||
mockSleep: func(d time.Duration) { | ||
<-doneSleep | ||
}, | ||
}).startWorker() | ||
|
||
cache.Set("repo-1", "commit-1") | ||
cache.Set("repo-2", "commit-2") | ||
if v, ok := cache.Get("repo-1"); !ok || v != "commit-1" { | ||
t.Fatal("expected cache get to succeed") | ||
} | ||
if v, ok := cache.Get("repo-2"); !ok || v != "commit-2" { | ||
t.Fatal("expected cache get to succeed") | ||
} | ||
doneSleep <- struct{}{} // cause worker to begin a single iteration | ||
time.Sleep(30 * time.Millisecond) // wait long enough for TTL | ||
if _, ok := cache.Get("repo-1"); ok { | ||
t.Fatal("expected cache entry to have expired") | ||
} | ||
if _, ok := cache.Get("repo-2"); ok { | ||
t.Fatal("expected cache entry to have expired") | ||
} | ||
} | ||
|
||
// Merely shows that the cache can support a very high concurrent read rate | ||
// with a low write rate. Run it like: | ||
// | ||
// $ go test -bench BenchmarkGitCommitResolutionCache -benchtime=30s ./cmd/frontend/graphqlbackend/ | ||
// BenchmarkGitCommitResolutionCache/8-8 200000000 202 ns/op | ||
// BenchmarkGitCommitResolutionCache/16-8 100000000 410 ns/op | ||
// BenchmarkGitCommitResolutionCache/32-8 50000000 879 ns/op | ||
// BenchmarkGitCommitResolutionCache/64-8 30000000 1709 ns/op | ||
// BenchmarkGitCommitResolutionCache/128-8 20000000 3345 ns/op | ||
// BenchmarkGitCommitResolutionCache/256-8 20000000 6177 ns/op | ||
// | ||
// The last one shows that while 256 concurrent cache reads are occurring we | ||
// can perform 8 cache reads in 6177ns. | ||
func BenchmarkGitCommitResolutionCache(b *testing.B) { | ||
cache := (&resolutionCache{ | ||
ttl: 10 * time.Minute, | ||
cacheEntries: prometheus.NewHistogram(prometheus.HistogramOpts{ | ||
Namespace: "src", | ||
Subsystem: "graphql", | ||
Name: "git_commit_oid_resolution_cache_entries", | ||
Help: "Total number of entries in the in-memory Git commit OID resolution cache.", | ||
}), | ||
}).startWorker() | ||
|
||
cache.Set("repo-1", "commit-1") | ||
|
||
for _, concurrentReads := range []int{8, 16, 32, 64, 128, 256} { | ||
b.Run(fmt.Sprint(concurrentReads), func(b *testing.B) { | ||
done := make(chan bool) | ||
defer close(done) | ||
for i := 0; i < concurrentReads; i++ { | ||
go func() { | ||
for { | ||
select { | ||
case <-done: | ||
return | ||
default: | ||
cache.Get("repo-1") | ||
} | ||
} | ||
}() | ||
} | ||
time.Sleep(1 * time.Second) // Time for goroutines to start running. | ||
b.ResetTimer() | ||
|
||
for n := 0; n < b.N; n++ { | ||
if v, ok := cache.Get("repo-1"); !ok || v != "commit-1" { | ||
b.Fatal("expected cache get to succeed") | ||
} | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters