internal/lsp/debug: show per-package memory usage

Calculate and display very crude memory usage statistics. This is
complicated by various levels of sharing and indirection, so the numbers
should be taken with *large* grains of salt and interpreted mostly by
experts.

Still, the results are interesting and helpful.

Change-Id: Ia9aff974c7d5fddd63df0cfd5cecc08ead33cf84
Reviewed-on: https://go-review.googlesource.com/c/tools/+/236163
Run-TryBot: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go
index 86b7e22..87da91f 100644
--- a/internal/lsp/cache/cache.go
+++ b/internal/lsp/cache/cache.go
@@ -8,10 +8,14 @@
 	"context"
 	"crypto/sha1"
 	"fmt"
+	"go/ast"
 	"go/token"
+	"go/types"
+	"html/template"
 	"io/ioutil"
 	"os"
 	"reflect"
+	"sort"
 	"strconv"
 	"sync/atomic"
 	"time"
@@ -155,3 +159,91 @@
 
 func (c *Cache) ID() string                     { return c.id }
 func (c *Cache) MemStats() map[reflect.Type]int { return c.store.Stats() }
+
+type packageStat struct {
+	id        packageID
+	mode      source.ParseMode
+	file      int64
+	ast       int64
+	types     int64
+	typesInfo int64
+	total     int64
+}
+
+func (c *Cache) PackageStats() template.HTML {
+	var packageStats []packageStat
+	c.store.DebugOnlyIterate(func(k, v interface{}) {
+		switch k.(type) {
+		case packageHandleKey:
+			v := v.(*packageData)
+			stat := packageStat{
+				id:        v.pkg.id,
+				mode:      v.pkg.mode,
+				types:     typesCost(v.pkg.types.Scope()),
+				typesInfo: typesInfoCost(v.pkg.typesInfo),
+			}
+			for _, f := range v.pkg.compiledGoFiles {
+				fvi := f.handle.Cached()
+				if fvi == nil {
+					continue
+				}
+				fv := fvi.(*parseGoData)
+				stat.file += int64(len(fv.src))
+				stat.ast += astCost(fv.ast)
+			}
+			stat.total = stat.file + stat.ast + stat.types + stat.typesInfo
+			packageStats = append(packageStats, stat)
+		}
+	})
+	var totalCost int64
+	for _, stat := range packageStats {
+		totalCost += stat.total
+	}
+	sort.Slice(packageStats, func(i, j int) bool {
+		return packageStats[i].total > packageStats[j].total
+	})
+	html := "<table><thead><td>Name</td><td>total = file + ast + types + types info</td></thead>\n"
+	human := func(n int64) string {
+		return fmt.Sprintf("%.2f", float64(n)/(1024*1024))
+	}
+	var printedCost int64
+	for _, stat := range packageStats {
+		html += fmt.Sprintf("<tr><td>%v (%v)</td><td>%v = %v + %v + %v + %v</td></tr>\n", stat.id, stat.mode,
+			human(stat.total), human(stat.file), human(stat.ast), human(stat.types), human(stat.typesInfo))
+		printedCost += stat.total
+		if float64(printedCost) > float64(totalCost)*.9 {
+			break
+		}
+	}
+	html += "</table>\n"
+	return template.HTML(html)
+}
+
+func astCost(f *ast.File) int64 {
+	var count int64
+	ast.Inspect(f, func(n ast.Node) bool {
+		count += 32 // nodes are pretty small.
+		return true
+	})
+	return count
+}
+
+func typesCost(scope *types.Scope) int64 {
+	cost := 64 + int64(scope.Len())*128 // types.object looks pretty big
+	for i := 0; i < scope.NumChildren(); i++ {
+		cost += typesCost(scope.Child(i))
+	}
+	return cost
+}
+
+func typesInfoCost(info *types.Info) int64 {
+	// Most of these refer to existing objects, with the exception of InitOrder, Selections, and Types.
+	cost := 24*len(info.Defs) +
+		32*len(info.Implicits) +
+		256*len(info.InitOrder) + // these are big, but there aren't many of them.
+		32*len(info.Scopes) +
+		128*len(info.Selections) + // wild guess
+		128*len(info.Types) + // wild guess
+		32*len(info.Uses)
+	return int64(cost)
+}
diff --git a/internal/lsp/debug/serve.go b/internal/lsp/debug/serve.go
index da38650..deb8bec 100644
--- a/internal/lsp/debug/serve.go
+++ b/internal/lsp/debug/serve.go
@@ -686,6 +686,8 @@
 {{define "body"}}
 <h2>memoize.Store entries</h2>
 <ul>{{range $k,$v := .MemStats}}<li>{{$k}} - {{$v}}</li>{{end}}</ul>
+<h2>Per-package usage - not accurate, for guidance only</h2>
+{{.PackageStats}}
 {{end}}
 `))
 
diff --git a/internal/memoize/memoize.go b/internal/memoize/memoize.go
index b05a216..30f4c0c 100644
--- a/internal/memoize/memoize.go
+++ b/internal/memoize/memoize.go
@@ -164,6 +164,27 @@
 	return result
 }
 
+// DebugOnlyIterate iterates through all live cache entries and calls f on them.
+// It should only be used for debugging purposes.
+func (s *Store) DebugOnlyIterate(f func(k, v interface{})) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	for k, e := range s.entries {
+		h := (*Handle)(unsafe.Pointer(e))
+		var v interface{}
+		h.mu.Lock()
+		if h.state == stateCompleted {
+			v = h.value
+		}
+		h.mu.Unlock()
+		if v == nil {
+			continue
+		}
+		f(k, v)
+	}
+}
+
 // Cached returns the value associated with a handle.
 //
 // It will never cause the value to be generated.