internal/lsp/source: optimize enumeration of a type's fields

When searching for deep completions, we can end up enumerating struct
types' fields a lot. Optimize fieldSelections to reduce work:

- Wait until we see an embedded field before we create the "seen"
  map.
- Use a callback style to iterate over the struct's fields rather than
  returning a slice of fields.
- Change "seen" checking strategy back to track struct types rather
  than each individual field.

Struct with 5 non-embedded fields:

name       old time/op    new time/op    delta
Fields-16     293ns ± 1%      20ns ± 2%   -93.13%  (p=0.008 n=5+5)

name       old alloc/op   new alloc/op   delta
Fields-16      120B ± 0%        0B       -100.00%  (p=0.008 n=5+5)

name       old allocs/op  new allocs/op  delta
Fields-16      4.00 ± 0%      0.00       -100.00%  (p=0.008 n=5+5)

Same struct but add an embedded struct with 2 fields:

name       old time/op    new time/op    delta
Fields-16     389ns ± 1%     142ns ± 1%  -63.53%  (p=0.008 n=5+5)

name       old alloc/op   new alloc/op   delta
Fields-16      120B ± 0%      144B ± 0%  +20.00%  (p=0.008 n=5+5)

name       old allocs/op  new allocs/op  delta
Fields-16      4.00 ± 0%      2.00 ± 0%  -50.00%  (p=0.008 n=5+5)

I think the alloc/op went up because the "seen" map is no longer
allocated on the stack. There is more room for more optimization, but
it's probably not worth making things more complicated.

Change-Id: I6f9f2124334a8594ef9d6f9b5ac4b3a8aead5f49
Reviewed-on: https://go-review.googlesource.com/c/tools/+/223419
Run-TryBot: Muir Manders <muir@mnd.rs>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go
index d883be6..e103e26 100644
--- a/internal/lsp/source/completion.go
+++ b/internal/lsp/source/completion.go
@@ -869,14 +869,15 @@
 	}
 
 	// Add fields of T.
-	for _, f := range fieldSelections(typ) {
+	eachField(typ, func(v *types.Var) {
 		c.found(candidate{
-			obj:         f,
+			obj:         v,
 			score:       stdScore - 0.01,
 			imp:         imp,
 			addressable: addressable || isPointer(typ),
 		})
-	}
+	})
+
 	return nil
 }
 
diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go
index b691f7c..09f3569 100644
--- a/internal/lsp/source/util.go
+++ b/internal/lsp/source/util.go
@@ -272,34 +272,39 @@
 	return len(args)
 }
 
-// fieldSelections returns the set of fields that can
-// be selected from a value of type T.
-func fieldSelections(T types.Type) (fields []*types.Var) {
+// eachField invokes fn for each field that can be selected from a
+// value of type T.
+func eachField(T types.Type, fn func(*types.Var)) {
 	// TODO(adonovan): this algorithm doesn't exclude ambiguous
 	// selections that match more than one field/method.
 	// types.NewSelectionSet should do that for us.
 
-	seen := make(map[*types.Var]bool) // for termination on recursive types
+	// for termination on recursive types
+	var seen map[*types.Struct]bool
 
 	var visit func(T types.Type)
 	visit = func(T types.Type) {
 		if T, ok := deref(T).Underlying().(*types.Struct); ok {
+			if seen[T] {
+				return
+			}
+
 			for i := 0; i < T.NumFields(); i++ {
 				f := T.Field(i)
-				if seen[f] {
-					continue
-				}
-				seen[f] = true
-				fields = append(fields, f)
+				fn(f)
 				if f.Anonymous() {
+					if seen == nil {
+						// Lazily create "seen" since it is only needed for
+						// embedded structs.
+						seen = make(map[*types.Struct]bool)
+					}
+					seen[T] = true
 					visit(f.Type())
 				}
 			}
 		}
 	}
 	visit(T)
-
-	return fields
 }
 
 // typeIsValid reports whether typ doesn't contain any Invalid types.