1
0
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:
Muir Manders 2019-08-18 12:55:34 -07:00 committed by Rebecca Stambler
parent 062dbaebb6
commit b29f5f60c3
2 changed files with 74 additions and 5 deletions

View File

@ -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 {

View File

@ -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
}