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:
Muir Manders 2019-08-16 09:45:09 -07:00 committed by Rebecca Stambler
parent 85edb9ef32
commit f0be937dca
4 changed files with 88 additions and 37 deletions

View File

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

View File

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

View File

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

View File

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