mirror of https://github.com/golang/go.git
internal/lsp: speed up deep completion search
Optimize a few things to speed up deep completions: - item() is slow, so don't call it unless the candidate's name matches the input. - We only end up returning the top 3 deep candidates, so skip deep candidates early if they are not in the top 3 scores we have seen so far. This greatly reduces calls to item(), but also avoids a humongous sort in lsp/completion.go. - Get rid of error return value from found(). Nothing checked for this error, and we spent a lot of time allocating the only possible error "this candidate is not accessible", which is not unexpected to begin with. - Cache the call to types.NewMethodSet in methodsAndFields(). This is relatively expensive and can be called many times for the same type when searching for deep completions. - Avoid calling deepState.chainString() twice by calling it once and storing the result on the candidate. These optimizations sped up my slow completion from 1.5s to 0.5s. There were around 200k deep candidates examined for this one completion. The remaining time is dominated by the fuzzy matcher. Obviously 500ms is still unacceptable under any circumstances, so there will be subsequent improvements to limit the deep completion search scope to make sure we always return completions in a reasonable amount of time. I also made it so there is always a "matcher" set on the completer. This makes the matching logic a bit simpler. Change-Id: Id48ef7031ee1d4ea04515c828277384562b988a8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/190522 Run-TryBot: Rebecca Stambler <rstambler@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
parent
85edb9ef32
commit
f0be937dca
|
|
@ -52,10 +52,6 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara
|
|||
}, nil
|
||||
}
|
||||
|
||||
// Limit deep completion results because in some cases there are too many
|
||||
// to be useful.
|
||||
const maxDeepCompletions = 3
|
||||
|
||||
func (s *Server) toProtocolCompletionItems(ctx context.Context, view source.View, m *protocol.ColumnMapper, candidates []source.CompletionItem, pos protocol.Position, surrounding *source.Selection) []protocol.CompletionItem {
|
||||
// Sort the candidates by score, since that is not supported by LSP yet.
|
||||
sort.SliceStable(candidates, func(i, j int) bool {
|
||||
|
|
@ -92,7 +88,7 @@ func (s *Server) toProtocolCompletionItems(ctx context.Context, view source.View
|
|||
if !s.useDeepCompletions {
|
||||
continue
|
||||
}
|
||||
if numDeepCompletionsSeen >= maxDeepCompletions {
|
||||
if numDeepCompletionsSeen >= source.MaxDeepCompletions {
|
||||
continue
|
||||
}
|
||||
numDeepCompletionsSeen++
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
"golang.org/x/tools/internal/lsp/fuzzy"
|
||||
"golang.org/x/tools/internal/lsp/snippet"
|
||||
"golang.org/x/tools/internal/span"
|
||||
"golang.org/x/tools/internal/telemetry/log"
|
||||
"golang.org/x/tools/internal/telemetry/trace"
|
||||
errors "golang.org/x/xerrors"
|
||||
)
|
||||
|
|
@ -193,6 +194,11 @@ type completer struct {
|
|||
|
||||
// matcher matches the candidates against the surrounding prefix.
|
||||
matcher matcher
|
||||
|
||||
// methodSetCache caches the types.NewMethodSet call, which is relatively
|
||||
// expensive and can be called many times for the same type while searching
|
||||
// for deep completions.
|
||||
methodSetCache map[methodSetKey]*types.MethodSet
|
||||
}
|
||||
|
||||
type compLitInfo struct {
|
||||
|
|
@ -216,6 +222,11 @@ type compLitInfo struct {
|
|||
maybeInFieldName bool
|
||||
}
|
||||
|
||||
type methodSetKey struct {
|
||||
typ types.Type
|
||||
addressable bool
|
||||
}
|
||||
|
||||
// A Selection represents the cursor position and surrounding identifier.
|
||||
type Selection struct {
|
||||
Content string
|
||||
|
|
@ -243,9 +254,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.surrounding.Prefix() != "" {
|
||||
c.matcher = fuzzy.NewMatcher(c.surrounding.Prefix(), fuzzy.Symbol)
|
||||
}
|
||||
c.matcher = fuzzy.NewMatcher(c.surrounding.Prefix(), fuzzy.Symbol)
|
||||
} else {
|
||||
c.matcher = prefixMatcher(strings.ToLower(c.surrounding.Prefix()))
|
||||
}
|
||||
|
|
@ -253,9 +262,11 @@ func (c *completer) setSurrounding(ident *ast.Ident) {
|
|||
|
||||
// found adds a candidate completion. We will also search through the object's
|
||||
// members for more candidates.
|
||||
func (c *completer) found(obj types.Object, score float64, imp *imports.ImportInfo) error {
|
||||
func (c *completer) found(obj types.Object, score float64, imp *imports.ImportInfo) {
|
||||
if obj.Pkg() != nil && obj.Pkg() != c.types && !obj.Exported() {
|
||||
return errors.Errorf("%s is inaccessible from %s", obj.Name(), c.types.Path())
|
||||
// obj is not accessible because it lives in another package and is not
|
||||
// exported. Don't treat it as a completion candidate.
|
||||
return
|
||||
}
|
||||
|
||||
if c.inDeepCompletion() {
|
||||
|
|
@ -264,13 +275,13 @@ func (c *completer) found(obj types.Object, score float64, imp *imports.ImportIn
|
|||
// "bar.Baz" even though "Baz" is represented the same types.Object in both.
|
||||
for _, seenObj := range c.deepState.chain {
|
||||
if seenObj == obj {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// At the top level, dedupe by object.
|
||||
if c.seen[obj] {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
c.seen[obj] = true
|
||||
}
|
||||
|
|
@ -288,23 +299,23 @@ func (c *completer) found(obj types.Object, score float64, imp *imports.ImportIn
|
|||
// Favor shallow matches by lowering weight according to depth.
|
||||
cand.score -= stdScore * float64(len(c.deepState.chain))
|
||||
|
||||
item, err := c.item(cand)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.matcher == nil {
|
||||
c.items = append(c.items, item)
|
||||
} else {
|
||||
score := c.matcher.Score(item.Label)
|
||||
if score > 0 {
|
||||
item.Score *= float64(score)
|
||||
c.items = append(c.items, item)
|
||||
cand.name = c.deepState.chainString(obj.Name())
|
||||
matchScore := c.matcher.Score(cand.name)
|
||||
if matchScore > 0 {
|
||||
cand.score *= float64(matchScore)
|
||||
|
||||
// Avoid calling c.item() for deep candidates that wouldn't be in the top
|
||||
// MaxDeepCompletions anyway.
|
||||
if !c.inDeepCompletion() || c.deepState.isHighScore(cand.score) {
|
||||
if item, err := c.item(cand); err == nil {
|
||||
c.items = append(c.items, item)
|
||||
} else {
|
||||
log.Error(c.ctx, "error generating completion item", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.deepSearch(obj)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// candidate represents a completion candidate.
|
||||
|
|
@ -315,6 +326,9 @@ type candidate struct {
|
|||
// score is used to rank candidates.
|
||||
score float64
|
||||
|
||||
// name is the deep object name path, e.g. "foo.bar"
|
||||
name string
|
||||
|
||||
// expandFuncCall is true if obj should be invoked in the completion.
|
||||
// For example, expandFuncCall=true yields "foo()", expandFuncCall=false yields "foo".
|
||||
expandFuncCall bool
|
||||
|
|
@ -381,6 +395,9 @@ func Completion(ctx context.Context, view View, f GoFile, pos token.Pos, opts Co
|
|||
enclosingFunction: enclosingFunction(path, pos, pkg.GetTypesInfo()),
|
||||
enclosingCompositeLiteral: clInfo,
|
||||
opts: opts,
|
||||
// default to a matcher that always matches
|
||||
matcher: prefixMatcher(""),
|
||||
methodSetCache: make(map[methodSetKey]*types.MethodSet),
|
||||
}
|
||||
|
||||
c.deepState.enabled = opts.DeepComplete
|
||||
|
|
@ -498,14 +515,16 @@ func (c *completer) packageMembers(pkg *types.PkgName) {
|
|||
}
|
||||
|
||||
func (c *completer) methodsAndFields(typ types.Type, addressable bool) error {
|
||||
var mset *types.MethodSet
|
||||
|
||||
if addressable && !types.IsInterface(typ) && !isPointer(typ) {
|
||||
// Add methods of *T, which includes methods with receiver T.
|
||||
mset = types.NewMethodSet(types.NewPointer(typ))
|
||||
} else {
|
||||
// Add methods of T.
|
||||
mset = types.NewMethodSet(typ)
|
||||
mset := c.methodSetCache[methodSetKey{typ, addressable}]
|
||||
if mset == nil {
|
||||
if addressable && !types.IsInterface(typ) && !isPointer(typ) {
|
||||
// Add methods of *T, which includes methods with receiver T.
|
||||
mset = types.NewMethodSet(types.NewPointer(typ))
|
||||
} else {
|
||||
// Add methods of T.
|
||||
mset = types.NewMethodSet(typ)
|
||||
}
|
||||
c.methodSetCache[methodSetKey{typ, addressable}] = mset
|
||||
}
|
||||
|
||||
for i := 0; i < mset.Len(); i++ {
|
||||
|
|
|
|||
|
|
@ -26,11 +26,11 @@ func (c *completer) item(cand candidate) (CompletionItem, error) {
|
|||
|
||||
// Handle builtin types separately.
|
||||
if obj.Parent() == types.Universe {
|
||||
return c.formatBuiltin(cand)
|
||||
return c.formatBuiltin(cand), nil
|
||||
}
|
||||
|
||||
var (
|
||||
label = c.deepState.chainString(obj.Name())
|
||||
label = cand.name
|
||||
detail = types.TypeString(obj.Type(), c.qf)
|
||||
insert = label
|
||||
kind CompletionItemKind
|
||||
|
|
@ -172,7 +172,7 @@ func (c *completer) isParameter(v *types.Var) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (c *completer) formatBuiltin(cand candidate) (CompletionItem, error) {
|
||||
func (c *completer) formatBuiltin(cand candidate) CompletionItem {
|
||||
obj := cand.obj
|
||||
item := CompletionItem{
|
||||
Label: obj.Name(),
|
||||
|
|
@ -202,7 +202,7 @@ func (c *completer) formatBuiltin(cand candidate) (CompletionItem, error) {
|
|||
case *types.Nil:
|
||||
item.Kind = VariableCompletionItem
|
||||
}
|
||||
return item, nil
|
||||
return item
|
||||
}
|
||||
|
||||
var replacer = strings.NewReplacer(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// Limit deep completion results because in most cases there are too many
|
||||
// to be useful.
|
||||
const MaxDeepCompletions = 3
|
||||
|
||||
// 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.
|
||||
|
|
@ -23,6 +27,10 @@ type deepCompletionState struct {
|
|||
// chainNames holds the names of the chain objects. This allows us to
|
||||
// save allocations as we build many deep completion items.
|
||||
chainNames []string
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// push pushes obj onto our search stack.
|
||||
|
|
@ -45,6 +53,34 @@ func (s *deepCompletionState) chainString(finalName string) string {
|
|||
return chainStr
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (c *completer) inDeepCompletion() bool {
|
||||
return len(c.deepState.chain) > 0
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue