mirror of
https://github.com/golang/go
synced 2024-11-18 16:44:43 -07:00
internal/lsp: limit deep completion search scope
Deep completions can take a long time (500ms+) if there are many large, deeply nested structs in scope. To make sure we return completion results in a timely manner we now notice if we have spent "too long" searching for deep completions and reduce the search scope. In particular, our overall completion budget is 100ms. This value is often cited as the longest latency that still feels instantaneous to most people. As we spend 25%, 50%, and 75% of our budget we limit our deep completion candidate search depth to 4, 3, and 2, respectively. If we hit 90% of our budget, we disable deep completions entirely. In my testing, limiting the search scope to 4 normally makes even enormous searches finish in a few milliseconds. Of course, you can have arbitrarily many objects in scope with arbitrarily many fields, so to cover our bases we continue to dial down the search depth as needed. I replaced the "enabled" field with a "maxDepth" field that disables deep search when set to 0. Change-Id: I9b5a07de70709895c065503ae6082d1ea615d1af Reviewed-on: https://go-review.googlesource.com/c/tools/+/190978 Run-TryBot: Rebecca Stambler <rstambler@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Ian Cottrell <iancottrell@google.com>
This commit is contained in:
parent
062dbaebb6
commit
b29f5f60c3
@ -10,6 +10,7 @@ import (
|
||||
"go/token"
|
||||
"go/types"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/go/ast/astutil"
|
||||
"golang.org/x/tools/internal/imports"
|
||||
@ -122,6 +123,12 @@ const (
|
||||
|
||||
// lowScore indicates an irrelevant or not useful completion item.
|
||||
lowScore float64 = 0.01
|
||||
|
||||
// completionBudget is the soft latency goal for completion requests. Most
|
||||
// requests finish in a couple milliseconds, but in some cases deep
|
||||
// completions can take much longer. As we use up our budget we dynamically
|
||||
// reduce the search scope to ensure we return timely results.
|
||||
completionBudget = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
// matcher matches a candidate's label against the user input.
|
||||
@ -202,6 +209,10 @@ type completer struct {
|
||||
|
||||
// mapper converts the positions in the file from which the completion originated.
|
||||
mapper *protocol.ColumnMapper
|
||||
|
||||
// startTime is when we started processing this completion request. It does
|
||||
// not include any time the request spent in the queue.
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
type compLitInfo struct {
|
||||
@ -259,7 +270,7 @@ func (c *completer) setSurrounding(ident *ast.Ident) {
|
||||
|
||||
// Fuzzy matching shares the "useDeepCompletions" config flag, so if deep completions
|
||||
// are enabled then also enable fuzzy matching.
|
||||
if c.deepState.enabled {
|
||||
if c.deepState.maxDepth != 0 {
|
||||
c.matcher = fuzzy.NewMatcher(c.surrounding.Prefix(), fuzzy.Symbol)
|
||||
} else {
|
||||
c.matcher = prefixMatcher(strings.ToLower(c.surrounding.Prefix()))
|
||||
@ -306,6 +317,12 @@ func (c *completer) found(obj types.Object, score float64, imp *imports.ImportIn
|
||||
c.seen[obj] = true
|
||||
}
|
||||
|
||||
// If we are running out of budgeted time we must limit our search for deep
|
||||
// completion candidates.
|
||||
if c.shouldPrune() {
|
||||
return
|
||||
}
|
||||
|
||||
cand := candidate{
|
||||
obj: obj,
|
||||
score: score,
|
||||
@ -375,6 +392,8 @@ func Completion(ctx context.Context, view View, f GoFile, pos protocol.Position,
|
||||
ctx, done := trace.StartSpan(ctx, "source.Completion")
|
||||
defer done()
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
pkg, err := f.GetPackage(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@ -443,9 +462,12 @@ func Completion(ctx context.Context, view View, f GoFile, pos protocol.Position,
|
||||
matcher: prefixMatcher(""),
|
||||
methodSetCache: make(map[methodSetKey]*types.MethodSet),
|
||||
mapper: m,
|
||||
startTime: startTime,
|
||||
}
|
||||
|
||||
c.deepState.enabled = opts.DeepComplete
|
||||
if opts.DeepComplete {
|
||||
c.deepState.maxDepth = -1
|
||||
}
|
||||
|
||||
// Set the filter surrounding.
|
||||
if ident, ok := path[0].(*ast.Ident); ok {
|
||||
|
@ -7,6 +7,7 @@ package source
|
||||
import (
|
||||
"go/types"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Limit deep completion results because in most cases there are too many
|
||||
@ -17,8 +18,9 @@ const MaxDeepCompletions = 3
|
||||
// "deep completion" refers to searching into objects' fields and methods to
|
||||
// find more completion candidates.
|
||||
type deepCompletionState struct {
|
||||
// enabled is true if deep completions are enabled.
|
||||
enabled bool
|
||||
// maxDepth limits the deep completion search depth. 0 means
|
||||
// disabled and -1 means unlimited.
|
||||
maxDepth int
|
||||
|
||||
// chain holds the traversal path as we do a depth-first search through
|
||||
// objects' members looking for exact type matches.
|
||||
@ -31,6 +33,10 @@ type deepCompletionState struct {
|
||||
// highScores tracks the highest deep candidate scores we have found
|
||||
// so far. This is used to avoid work for low scoring deep candidates.
|
||||
highScores [MaxDeepCompletions]float64
|
||||
|
||||
// candidateCount is the count of unique deep candidates encountered
|
||||
// so far.
|
||||
candidateCount int
|
||||
}
|
||||
|
||||
// push pushes obj onto our search stack.
|
||||
@ -85,10 +91,51 @@ func (c *completer) inDeepCompletion() bool {
|
||||
return len(c.deepState.chain) > 0
|
||||
}
|
||||
|
||||
// shouldPrune returns whether we should prune the current deep
|
||||
// candidate search to reduce the overall search scope. The
|
||||
// maximum search depth is reduced gradually as we use up our
|
||||
// completionBudget.
|
||||
func (c *completer) shouldPrune() bool {
|
||||
if !c.inDeepCompletion() {
|
||||
return false
|
||||
}
|
||||
|
||||
c.deepState.candidateCount++
|
||||
|
||||
// Check our remaining budget every 1000 candidates.
|
||||
if c.deepState.candidateCount%1000 == 0 {
|
||||
spent := float64(time.Since(c.startTime)) / float64(completionBudget)
|
||||
|
||||
switch {
|
||||
case spent >= 0.90:
|
||||
// We are close to exhausting our budget. Disable deep completions.
|
||||
c.deepState.maxDepth = 0
|
||||
case spent >= 0.75:
|
||||
// We are running out of budget, reduce max depth again.
|
||||
c.deepState.maxDepth = 2
|
||||
case spent >= 0.5:
|
||||
// We have used half our budget, reduce max depth again.
|
||||
c.deepState.maxDepth = 3
|
||||
case spent >= 0.25:
|
||||
// We have used a good chunk of our budget, so start limiting our search.
|
||||
// By default the search depth is unlimited, so this limit, while still
|
||||
// generous, is normally a huge reduction in search scope that will result
|
||||
// in our search completing very soon.
|
||||
c.deepState.maxDepth = 4
|
||||
}
|
||||
}
|
||||
|
||||
if c.deepState.maxDepth >= 0 {
|
||||
return len(c.deepState.chain) >= c.deepState.maxDepth
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// deepSearch searches through obj's subordinate objects for more
|
||||
// completion items.
|
||||
func (c *completer) deepSearch(obj types.Object) {
|
||||
if !c.deepState.enabled {
|
||||
if c.deepState.maxDepth == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user