github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libfs/profilelist.go (about)

     1  // Copyright 2015-2016 Keybase Inc. All rights reserved.
     2  // Use of this source code is governed by a BSD
     3  // license that can be found in the LICENSE file.
     4  
     5  package libfs
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"io"
    11  	"os"
    12  	"regexp"
    13  	"runtime/pprof"
    14  	"runtime/trace"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/keybase/client/go/kbfs/libkbfs"
    19  	"github.com/pkg/errors"
    20  	billy "gopkg.in/src-d/go-billy.v4"
    21  )
    22  
    23  const (
    24  	// CPUProfilePrefix is the prefix to a CPU profile file (a
    25  	// duration should follow the prefix in the actual file name).
    26  	CPUProfilePrefix = "profile."
    27  
    28  	// TraceProfilePrefix is the prefix to a trace profile file (a
    29  	// duration should follow the prefix in the actual file name).
    30  	TraceProfilePrefix = "trace."
    31  )
    32  
    33  type timedProfile interface {
    34  	Start(w io.Writer) error
    35  	Stop()
    36  }
    37  
    38  type cpuProfile struct{}
    39  
    40  func (p cpuProfile) Start(w io.Writer) error {
    41  	return pprof.StartCPUProfile(w)
    42  }
    43  
    44  func (p cpuProfile) Stop() {
    45  	pprof.StopCPUProfile()
    46  }
    47  
    48  type traceProfile struct{}
    49  
    50  func (p traceProfile) Start(w io.Writer) error {
    51  	return trace.Start(w)
    52  }
    53  
    54  func (p traceProfile) Stop() {
    55  	trace.Stop()
    56  }
    57  
    58  // ProfileGet gets the relevant read function for the profile or nil if it doesn't exist.
    59  func ProfileGet(name string) func(context.Context) ([]byte, time.Time, error) {
    60  	p := pprof.Lookup(name)
    61  	if p == nil {
    62  		return nil
    63  	}
    64  
    65  	// See https://golang.org/pkg/runtime/pprof/#Profile.WriteTo
    66  	// for the meaning of debug.
    67  	debug := 1
    68  	if name == "goroutine" {
    69  		debug = 2
    70  	}
    71  	return profileRead(p, debug)
    72  }
    73  
    74  // profileRead reads from a Profile.
    75  func profileRead(p *pprof.Profile, debug int) func(context.Context) ([]byte, time.Time, error) {
    76  	return func(_ context.Context) ([]byte, time.Time, error) {
    77  		var b bytes.Buffer
    78  		err := p.WriteTo(&b, debug)
    79  		if err != nil {
    80  			return nil, time.Time{}, err
    81  		}
    82  
    83  		return b.Bytes(), time.Now(), nil
    84  	}
    85  }
    86  
    87  var profileNameRE = regexp.MustCompile("^[a-zA-Z0-9_]*$")
    88  
    89  // IsSupportedProfileName matches a string against allowed profile names.
    90  func IsSupportedProfileName(name string) bool {
    91  	// https://golang.org/pkg/runtime/pprof/#NewProfile recommends
    92  	// using an import path for profile names. But supporting that
    93  	// would require faking out sub-directories, too. For now,
    94  	// just support alphanumeric filenames.
    95  	return profileNameRE.MatchString(name)
    96  }
    97  
    98  // ProfileFS provides an easy way to browse the go profiles.
    99  type ProfileFS struct {
   100  	config libkbfs.Config
   101  }
   102  
   103  // NewProfileFS returns a read-only filesystem for browsing profiles.
   104  func NewProfileFS(config libkbfs.Config) ProfileFS {
   105  	return ProfileFS{config}
   106  }
   107  
   108  var _ libkbfs.NodeFSReadOnly = ProfileFS{}
   109  
   110  // Lstat implements the libkbfs.NodeFSReadOnly interface.
   111  func (pfs ProfileFS) Lstat(filename string) (os.FileInfo, error) {
   112  	if strings.HasPrefix(filename, CPUProfilePrefix) ||
   113  		strings.HasPrefix(filename, TraceProfilePrefix) {
   114  		// Set profile sizes to 0 because it's too expensive to read them
   115  		// ahead of time.
   116  		return &wrappedReadFileInfo{filename, 0, time.Now(), false}, nil
   117  	}
   118  
   119  	if !IsSupportedProfileName(filename) {
   120  		return nil, errors.Errorf("Unsupported profile %s", filename)
   121  	}
   122  
   123  	f := ProfileGet(filename)
   124  	// Get the data, just for the size.
   125  	b, t, err := f(context.Background())
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  	return &wrappedReadFileInfo{filename, int64(len(b)), t, false}, nil
   130  }
   131  
   132  // ListProfileNames returns the name of all profiles to list.
   133  func ListProfileNames() (res []string) {
   134  	profiles := pprof.Profiles()
   135  	res = make([]string, 0, len(profiles)+2)
   136  	for _, p := range profiles {
   137  		name := p.Name()
   138  		if !IsSupportedProfileName(name) {
   139  			continue
   140  		}
   141  		res = append(res, name)
   142  	}
   143  	res = append(res, CPUProfilePrefix+"30s")
   144  	res = append(res, TraceProfilePrefix+"30s")
   145  	return res
   146  }
   147  
   148  // ReadDir implements the libkbfs.NodeFSReadOnly interface.
   149  func (pfs ProfileFS) ReadDir(path string) ([]os.FileInfo, error) {
   150  	if path != "" && path != "." {
   151  		return nil, errors.New("Can't read subdirectories in profile list")
   152  	}
   153  
   154  	profiles := ListProfileNames()
   155  	res := make([]os.FileInfo, 0, len(profiles))
   156  	for _, p := range profiles {
   157  		fi, err := pfs.Lstat(p)
   158  		if err != nil {
   159  			return nil, err
   160  		}
   161  		res = append(res, fi)
   162  	}
   163  	return res, nil
   164  }
   165  
   166  // Readlink implements the libkbfs.NodeFSReadOnly interface.
   167  func (pfs ProfileFS) Readlink(_ string) (string, error) {
   168  	return "", errors.New("Readlink not supported")
   169  }
   170  
   171  func (pfs ProfileFS) openTimedProfile(
   172  	ctx context.Context, durationStr string, prof timedProfile) (
   173  	[]byte, error) {
   174  	duration, err := time.ParseDuration(durationStr)
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	// TODO: Blocking here until the profile is done is
   180  	// weird. Blocking on read is better.
   181  	//
   182  	// TODO: Maybe keep around a special last_profile file to be able
   183  	// to start capturing a profile and then interrupt when done,
   184  	// which would also be useful in general, since you'd be able to
   185  	// save a profile even if you open it up with a tool.
   186  	var buf bytes.Buffer
   187  	err = prof.Start(&buf)
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  
   192  	defer prof.Stop()
   193  
   194  	select {
   195  	case <-time.After(duration):
   196  	case <-ctx.Done():
   197  		return nil, ctx.Err()
   198  	}
   199  
   200  	prof.Stop()
   201  
   202  	return buf.Bytes(), nil
   203  }
   204  
   205  // OpenWithContext opens a profile, with a custom context.
   206  func (pfs ProfileFS) OpenWithContext(
   207  	ctx context.Context, filename string) (billy.File, error) {
   208  	var durationStr string
   209  	var prof timedProfile
   210  	if strings.HasPrefix(filename, CPUProfilePrefix) {
   211  		durationStr = strings.TrimPrefix(filename, CPUProfilePrefix)
   212  		prof = cpuProfile{}
   213  	} else if strings.HasPrefix(filename, TraceProfilePrefix) {
   214  		durationStr = strings.TrimPrefix(filename, TraceProfilePrefix)
   215  		prof = traceProfile{}
   216  	}
   217  	if durationStr != "" {
   218  		// Read and cache the timed profiles contents on open, so we
   219  		// don't re-do it on every read.
   220  		b, err := pfs.openTimedProfile(ctx, durationStr, prof)
   221  		if err != nil {
   222  			return nil, err
   223  		}
   224  		now := pfs.config.Clock().Now()
   225  		return &wrappedReadFile{
   226  			filename,
   227  			func(_ context.Context) ([]byte, time.Time, error) {
   228  				return b, now, nil
   229  			},
   230  			pfs.config.MakeLogger(""),
   231  			0}, nil
   232  	}
   233  
   234  	if !IsSupportedProfileName(filename) {
   235  		return nil, errors.Errorf("Unsupported profile %s", filename)
   236  	}
   237  
   238  	f := ProfileGet(filename)
   239  	return &wrappedReadFile{filename, f, pfs.config.MakeLogger(""), 0}, nil
   240  }
   241  
   242  // Open implements the libkbfs.NodeFSReadOnly interface.
   243  func (pfs ProfileFS) Open(filename string) (billy.File, error) {
   244  	// TODO: we should figure out some way to route the actual
   245  	// request context here if at all possible, so we can end
   246  	// early if it's canceled.
   247  	return pfs.OpenWithContext(context.TODO(), filename)
   248  }
   249  
   250  // OpenFile implements the libkbfs.NodeFSReadOnly interface.
   251  func (pfs ProfileFS) OpenFile(
   252  	filename string, flag int, _ os.FileMode) (billy.File, error) {
   253  	if flag&os.O_CREATE != 0 {
   254  		return nil, errors.New("read-only filesystem")
   255  	}
   256  
   257  	return pfs.Open(filename)
   258  }