2019-06-27 11:50:01 -06:00
|
|
|
// Copyright 2019 The Go Authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package source
|
|
|
|
|
|
|
|
import (
|
2020-04-05 23:18:15 -06:00
|
|
|
"context"
|
2019-06-27 11:50:01 -06:00
|
|
|
"go/types"
|
|
|
|
"strings"
|
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>
2019-08-18 13:55:34 -06:00
|
|
|
"time"
|
2019-06-27 11:50:01 -06:00
|
|
|
)
|
|
|
|
|
2020-04-02 15:30:46 -06:00
|
|
|
// MaxDeepCompletions limits deep completion results because in most cases
|
|
|
|
// there are too many to be useful.
|
2019-08-16 10:45:09 -06:00
|
|
|
const MaxDeepCompletions = 3
|
|
|
|
|
2019-06-27 11:50:01 -06:00
|
|
|
// deepCompletionState stores our state as we search for deep completions.
|
|
|
|
// "deep completion" refers to searching into objects' fields and methods to
|
|
|
|
// find more completion candidates.
|
|
|
|
type deepCompletionState struct {
|
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>
2019-08-18 13:55:34 -06:00
|
|
|
// maxDepth limits the deep completion search depth. 0 means
|
|
|
|
// disabled and -1 means unlimited.
|
|
|
|
maxDepth int
|
2019-06-27 11:50:01 -06:00
|
|
|
|
|
|
|
// chain holds the traversal path as we do a depth-first search through
|
|
|
|
// objects' members looking for exact type matches.
|
|
|
|
chain []types.Object
|
|
|
|
|
|
|
|
// chainNames holds the names of the chain objects. This allows us to
|
|
|
|
// save allocations as we build many deep completion items.
|
|
|
|
chainNames []string
|
2019-08-16 10:45:09 -06:00
|
|
|
|
|
|
|
// 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
|
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>
2019-08-18 13:55:34 -06:00
|
|
|
|
|
|
|
// candidateCount is the count of unique deep candidates encountered
|
|
|
|
// so far.
|
|
|
|
candidateCount int
|
2019-06-27 11:50:01 -06:00
|
|
|
}
|
|
|
|
|
2019-10-01 14:36:34 -06:00
|
|
|
// push pushes obj onto our search stack. If invoke is true then
|
|
|
|
// invocation parens "()" will be appended to the object name.
|
|
|
|
func (s *deepCompletionState) push(obj types.Object, invoke bool) {
|
2019-06-27 11:50:01 -06:00
|
|
|
s.chain = append(s.chain, obj)
|
2019-10-01 14:36:34 -06:00
|
|
|
|
|
|
|
name := obj.Name()
|
|
|
|
if invoke {
|
|
|
|
name += "()"
|
|
|
|
}
|
|
|
|
s.chainNames = append(s.chainNames, name)
|
2019-06-27 11:50:01 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// pop pops the last object off our search stack.
|
|
|
|
func (s *deepCompletionState) pop() {
|
|
|
|
s.chain = s.chain[:len(s.chain)-1]
|
|
|
|
s.chainNames = s.chainNames[:len(s.chainNames)-1]
|
|
|
|
}
|
|
|
|
|
|
|
|
// chainString joins the chain of objects' names together on ".".
|
|
|
|
func (s *deepCompletionState) chainString(finalName string) string {
|
|
|
|
s.chainNames = append(s.chainNames, finalName)
|
|
|
|
chainStr := strings.Join(s.chainNames, ".")
|
|
|
|
s.chainNames = s.chainNames[:len(s.chainNames)-1]
|
|
|
|
return chainStr
|
|
|
|
}
|
|
|
|
|
2019-08-16 10:45:09 -06:00
|
|
|
// isHighScore returns whether score is among the top MaxDeepCompletions
|
|
|
|
// deep candidate scores encountered so far. If so, it adds score to
|
|
|
|
// highScores, possibly displacing an existing high score.
|
|
|
|
func (s *deepCompletionState) isHighScore(score float64) bool {
|
|
|
|
// Invariant: s.highScores is sorted with highest score first. Unclaimed
|
|
|
|
// positions are trailing zeros.
|
|
|
|
|
|
|
|
// First check for an unclaimed spot and claim if available.
|
|
|
|
for i, deepScore := range s.highScores {
|
|
|
|
if deepScore == 0 {
|
|
|
|
s.highScores[i] = score
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, if we beat an existing score then take its spot and scoot
|
|
|
|
// all lower scores down one position.
|
|
|
|
for i, deepScore := range s.highScores {
|
|
|
|
if score > deepScore {
|
|
|
|
copy(s.highScores[i+1:], s.highScores[i:])
|
|
|
|
s.highScores[i] = score
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-03-05 22:46:49 -07:00
|
|
|
// scorePenalty computes a deep candidate score penalty. A candidate
|
|
|
|
// is penalized based on depth to favor shallower candidates. We also
|
|
|
|
// give a slight bonus to unexported objects and a slight additional
|
|
|
|
// penalty to function objects.
|
|
|
|
func (s *deepCompletionState) scorePenalty() float64 {
|
|
|
|
var deepPenalty float64
|
|
|
|
for _, dc := range s.chain {
|
|
|
|
deepPenalty += 1
|
|
|
|
|
|
|
|
if !dc.Exported() {
|
|
|
|
deepPenalty -= 0.1
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, isSig := dc.Type().Underlying().(*types.Signature); isSig {
|
|
|
|
deepPenalty += 0.1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Normalize penalty to a max depth of 10.
|
|
|
|
return deepPenalty / 10
|
|
|
|
}
|
|
|
|
|
2019-06-27 11:50:01 -06:00
|
|
|
func (c *completer) inDeepCompletion() bool {
|
|
|
|
return len(c.deepState.chain) > 0
|
|
|
|
}
|
|
|
|
|
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>
2019-08-18 13:55:34 -06:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2019-09-14 10:29:08 -06:00
|
|
|
// Check our remaining budget every 100 candidates.
|
2019-12-29 00:22:12 -07:00
|
|
|
if c.opts.budget > 0 && c.deepState.candidateCount%100 == 0 {
|
|
|
|
spent := float64(time.Since(c.startTime)) / float64(c.opts.budget)
|
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>
2019-08-18 13:55:34 -06:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-14 10:29:08 -06:00
|
|
|
c.deepState.candidateCount++
|
|
|
|
|
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>
2019-08-18 13:55:34 -06:00
|
|
|
if c.deepState.maxDepth >= 0 {
|
|
|
|
return len(c.deepState.chain) >= c.deepState.maxDepth
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-01-20 15:50:36 -07:00
|
|
|
// deepSearch searches through cand's subordinate objects for more
|
2019-06-27 11:50:01 -06:00
|
|
|
// completion items.
|
2020-04-05 23:18:15 -06:00
|
|
|
func (c *completer) deepSearch(ctx context.Context, cand candidate) {
|
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>
2019-08-18 13:55:34 -06:00
|
|
|
if c.deepState.maxDepth == 0 {
|
2019-06-27 11:50:01 -06:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-12-21 16:39:02 -07:00
|
|
|
obj := cand.obj
|
|
|
|
|
2019-08-13 11:35:13 -06:00
|
|
|
// If we are definitely completing a struct field name, deep completions
|
|
|
|
// don't make sense.
|
|
|
|
if c.wantStructFieldCompletions() && c.enclosingCompositeLiteral.inKey {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-06-27 11:50:01 -06:00
|
|
|
// Don't search into type names.
|
|
|
|
if isTypeName(obj) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-11-01 11:50:21 -06:00
|
|
|
if obj.Type() == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-06-27 11:50:01 -06:00
|
|
|
// Don't search embedded fields because they were already included in their
|
|
|
|
// parent's fields.
|
|
|
|
if v, ok := obj.(*types.Var); ok && v.Embedded() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-10-01 14:36:34 -06:00
|
|
|
if sig, ok := obj.Type().Underlying().(*types.Signature); ok {
|
|
|
|
// If obj is a function that takes no arguments and returns one
|
|
|
|
// value, keep searching across the function call.
|
|
|
|
if sig.Params().Len() == 0 && sig.Results().Len() == 1 {
|
|
|
|
// Pass invoke=true since the function needs to be invoked in
|
|
|
|
// the deep chain.
|
|
|
|
c.deepState.push(obj, true)
|
|
|
|
// The result of a function call is not addressable.
|
2020-04-05 23:18:15 -06:00
|
|
|
c.methodsAndFields(ctx, sig.Results().At(0).Type(), false, cand.imp)
|
2019-10-01 14:36:34 -06:00
|
|
|
c.deepState.pop()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-27 11:50:01 -06:00
|
|
|
// Push this object onto our search stack.
|
2019-10-01 14:36:34 -06:00
|
|
|
c.deepState.push(obj, false)
|
2019-06-27 11:50:01 -06:00
|
|
|
|
|
|
|
switch obj := obj.(type) {
|
|
|
|
case *types.PkgName:
|
2020-04-05 23:18:15 -06:00
|
|
|
c.packageMembers(ctx, obj.Imported(), stdScore, cand.imp)
|
2019-06-27 11:50:01 -06:00
|
|
|
default:
|
2020-04-05 23:18:15 -06:00
|
|
|
c.methodsAndFields(ctx, obj.Type(), cand.addressable, cand.imp)
|
2019-06-27 11:50:01 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// Pop the object off our search stack.
|
|
|
|
c.deepState.pop()
|
|
|
|
}
|