golang.org/x/tools/gopls@v0.15.3/internal/cmd/stats.go (about) 1 // Copyright 2023 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package cmd 6 7 import ( 8 "context" 9 "encoding/json" 10 "flag" 11 "fmt" 12 "go/token" 13 "io/fs" 14 "os" 15 "path/filepath" 16 "reflect" 17 "runtime" 18 "strings" 19 "sync" 20 "time" 21 22 "golang.org/x/tools/gopls/internal/filecache" 23 "golang.org/x/tools/gopls/internal/protocol" 24 "golang.org/x/tools/gopls/internal/protocol/command" 25 "golang.org/x/tools/gopls/internal/server" 26 "golang.org/x/tools/gopls/internal/settings" 27 bugpkg "golang.org/x/tools/gopls/internal/util/bug" 28 versionpkg "golang.org/x/tools/gopls/internal/version" 29 "golang.org/x/tools/internal/event" 30 ) 31 32 type stats struct { 33 app *Application 34 35 Anon bool `flag:"anon" help:"hide any fields that may contain user names, file names, or source code"` 36 } 37 38 func (s *stats) Name() string { return "stats" } 39 func (r *stats) Parent() string { return r.app.Name() } 40 func (s *stats) Usage() string { return "" } 41 func (s *stats) ShortHelp() string { return "print workspace statistics" } 42 43 func (s *stats) DetailedHelp(f *flag.FlagSet) { 44 fmt.Fprint(f.Output(), ` 45 Load the workspace for the current directory, and output a JSON summary of 46 workspace information relevant to performance. As a side effect, this command 47 populates the gopls file cache for the current workspace. 48 49 By default, this command may include output that refers to the location or 50 content of user code. When the -anon flag is set, fields that may refer to user 51 code are hidden. 52 53 Example: 54 $ gopls stats -anon 55 `) 56 printFlagDefaults(f) 57 } 58 59 func (s *stats) Run(ctx context.Context, args ...string) error { 60 if s.app.Remote != "" { 61 // stats does not work with -remote. 62 // Other sessions on the daemon may interfere with results. 63 // Additionally, the type assertions in below only work if progress 64 // notifications bypass jsonrpc2 serialization. 65 return fmt.Errorf("the stats subcommand does not work with -remote") 66 } 67 68 if !s.app.Verbose { 69 event.SetExporter(nil) // don't log errors to stderr 70 } 71 72 stats := GoplsStats{ 73 GOOS: runtime.GOOS, 74 GOARCH: runtime.GOARCH, 75 GOPLSCACHE: os.Getenv("GOPLSCACHE"), 76 GoVersion: runtime.Version(), 77 GoplsVersion: versionpkg.Version(), 78 GOPACKAGESDRIVER: os.Getenv("GOPACKAGESDRIVER"), 79 } 80 81 opts := s.app.options 82 s.app.options = func(o *settings.Options) { 83 if opts != nil { 84 opts(o) 85 } 86 o.VerboseWorkDoneProgress = true 87 } 88 var ( 89 iwlMu sync.Mutex 90 iwlToken protocol.ProgressToken 91 iwlDone = make(chan struct{}) 92 ) 93 94 onProgress := func(p *protocol.ProgressParams) { 95 switch v := p.Value.(type) { 96 case *protocol.WorkDoneProgressBegin: 97 if v.Title == server.DiagnosticWorkTitle(server.FromInitialWorkspaceLoad) { 98 iwlMu.Lock() 99 iwlToken = p.Token 100 iwlMu.Unlock() 101 } 102 case *protocol.WorkDoneProgressEnd: 103 iwlMu.Lock() 104 tok := iwlToken 105 iwlMu.Unlock() 106 107 if p.Token == tok { 108 close(iwlDone) 109 } 110 } 111 } 112 113 // do executes a timed section of the stats command. 114 do := func(name string, f func() error) (time.Duration, error) { 115 start := time.Now() 116 fmt.Fprintf(os.Stderr, "%-30s", name+"...") 117 if err := f(); err != nil { 118 return time.Since(start), err 119 } 120 d := time.Since(start) 121 fmt.Fprintf(os.Stderr, "done (%v)\n", d) 122 return d, nil 123 } 124 125 var conn *connection 126 iwlDuration, err := do("Initializing workspace", func() error { 127 var err error 128 conn, err = s.app.connect(ctx, onProgress) 129 if err != nil { 130 return err 131 } 132 select { 133 case <-iwlDone: 134 case <-ctx.Done(): 135 return ctx.Err() 136 } 137 return nil 138 }) 139 stats.InitialWorkspaceLoadDuration = fmt.Sprint(iwlDuration) 140 if err != nil { 141 return err 142 } 143 defer conn.terminate(ctx) 144 145 // Gather bug reports produced by any process using 146 // this executable and persisted in the cache. 147 do("Gathering bug reports", func() error { 148 stats.CacheDir, stats.BugReports = filecache.BugReports() 149 if stats.BugReports == nil { 150 stats.BugReports = []bugpkg.Bug{} // non-nil for JSON 151 } 152 return nil 153 }) 154 155 if _, err := do("Querying memstats", func() error { 156 memStats, err := conn.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ 157 Command: command.MemStats.ID(), 158 }) 159 if err != nil { 160 return err 161 } 162 stats.MemStats = memStats.(command.MemStatsResult) 163 return nil 164 }); err != nil { 165 return err 166 } 167 168 if _, err := do("Querying workspace stats", func() error { 169 wsStats, err := conn.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ 170 Command: command.WorkspaceStats.ID(), 171 }) 172 if err != nil { 173 return err 174 } 175 stats.WorkspaceStats = wsStats.(command.WorkspaceStatsResult) 176 return nil 177 }); err != nil { 178 return err 179 } 180 181 if _, err := do("Collecting directory info", func() error { 182 var err error 183 stats.DirStats, err = findDirStats() 184 if err != nil { 185 return err 186 } 187 return nil 188 }); err != nil { 189 return err 190 } 191 192 // Filter JSON output to fields that are consistent with s.Anon. 193 okFields := make(map[string]interface{}) 194 { 195 v := reflect.ValueOf(stats) 196 t := v.Type() 197 for i := 0; i < t.NumField(); i++ { 198 f := t.Field(i) 199 if !token.IsExported(f.Name) { 200 continue 201 } 202 vf := v.FieldByName(f.Name) 203 if s.Anon && f.Tag.Get("anon") != "ok" && !vf.IsZero() { 204 // Fields that can be served with -anon must be explicitly marked as OK. 205 // But, if it's zero value, it's ok to print. 206 continue 207 } 208 okFields[f.Name] = vf.Interface() 209 } 210 } 211 data, err := json.MarshalIndent(okFields, "", " ") 212 if err != nil { 213 return err 214 } 215 216 os.Stdout.Write(data) 217 fmt.Println() 218 return nil 219 } 220 221 // GoplsStats holds information extracted from a gopls session in the current 222 // workspace. 223 // 224 // Fields that should be printed with the -anon flag should be explicitly 225 // marked as `anon:"ok"`. Only fields that cannot refer to user files or code 226 // should be marked as such. 227 type GoplsStats struct { 228 GOOS, GOARCH string `anon:"ok"` 229 GOPLSCACHE string 230 GoVersion string `anon:"ok"` 231 GoplsVersion string `anon:"ok"` 232 GOPACKAGESDRIVER string 233 InitialWorkspaceLoadDuration string `anon:"ok"` // in time.Duration string form 234 CacheDir string 235 BugReports []bugpkg.Bug 236 MemStats command.MemStatsResult `anon:"ok"` 237 WorkspaceStats command.WorkspaceStatsResult `anon:"ok"` 238 DirStats dirStats `anon:"ok"` 239 } 240 241 type dirStats struct { 242 Files int 243 TestdataFiles int 244 GoFiles int 245 ModFiles int 246 Dirs int 247 } 248 249 // findDirStats collects information about the current directory and its 250 // subdirectories. 251 func findDirStats() (dirStats, error) { 252 var ds dirStats 253 filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error { 254 if err != nil { 255 return err 256 } 257 if d.IsDir() { 258 ds.Dirs++ 259 } else { 260 ds.Files++ 261 slashed := filepath.ToSlash(path) 262 switch { 263 case strings.Contains(slashed, "/testdata/") || strings.HasPrefix(slashed, "testdata/"): 264 ds.TestdataFiles++ 265 case strings.HasSuffix(path, ".go"): 266 ds.GoFiles++ 267 case strings.HasSuffix(path, ".mod"): 268 ds.ModFiles++ 269 } 270 } 271 return nil 272 }) 273 return ds, nil 274 }