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  }