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 }