github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/service/pprof.go (about) 1 // Copyright 2018 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package service 5 6 import ( 7 "fmt" 8 "io" 9 "os" 10 "path/filepath" 11 "runtime" 12 "runtime/pprof" 13 "runtime/trace" 14 "time" 15 16 "github.com/keybase/client/go/libkb" 17 "github.com/keybase/client/go/logger" 18 "github.com/keybase/client/go/protocol/keybase1" 19 "github.com/keybase/go-framed-msgpack-rpc/rpc" 20 "golang.org/x/net/context" 21 ) 22 23 type PprofHandler struct { 24 libkb.Contextified 25 *BaseHandler 26 } 27 28 func NewPprofHandler(xp rpc.Transporter, g *libkb.GlobalContext) *PprofHandler { 29 return &PprofHandler{ 30 BaseHandler: NewBaseHandler(g, xp), 31 Contextified: libkb.NewContextified(g), 32 } 33 } 34 35 func durationSecToDuration(s keybase1.DurationSec) time.Duration { 36 return time.Duration(float64(s) * float64(time.Second)) 37 } 38 39 type timedProfiler interface { 40 Name() string 41 42 MaxFileCount() int 43 MakeFilename(dir string, start time.Time, duration time.Duration) string 44 GetSortedFiles(dir string) ([]string, error) 45 46 Start(w io.Writer) error 47 Stop() 48 } 49 50 // doTimedProfile asynchronously runs a profile with the given 51 // timedProfiler and associated parameters. 52 func doTimedProfile(log libkb.LogUI, delayedLog logger.Logger, 53 profiler timedProfiler, outputFile string, 54 durationSeconds keybase1.DurationSec) (err error) { 55 if !filepath.IsAbs(outputFile) { 56 return fmt.Errorf("%q is not an absolute path", outputFile) 57 } 58 59 close := func(c io.Closer) { 60 err := c.Close() 61 if err != nil { 62 log.Warning("Failed to close %s: %s", outputFile, err) 63 } 64 } 65 66 name := profiler.Name() 67 68 defer func() { 69 if err != nil { 70 log.Warning("Failed to do %s profile to %s for %.2f second(s): %s", name, outputFile, durationSeconds, err) 71 } 72 }() 73 74 f, err := os.Create(outputFile) 75 if err != nil { 76 return err 77 } 78 79 err = profiler.Start(f) 80 if err != nil { 81 close(f) 82 return err 83 } 84 85 log.Info("Doing %s profile to %s for %.2f second(s)", name, outputFile, durationSeconds) 86 87 go func() { 88 time.Sleep(durationSecToDuration(durationSeconds)) 89 profiler.Stop() 90 close(f) 91 delayedLog.Info("%s profile to %s done", name, outputFile) 92 }() 93 94 return nil 95 } 96 97 func doTimedProfileInDir(log libkb.LogUI, delayedLog logger.Logger, 98 profiler timedProfiler, dir string, 99 durationSeconds keybase1.DurationSec) (err error) { 100 name := profiler.Name() 101 maxFileCount := profiler.MaxFileCount() 102 files, err := profiler.GetSortedFiles(dir) 103 if err != nil { 104 log.Warning("Error getting %s profile files in %q: %s", 105 name, dir, err) 106 } else if len(files)+1 > maxFileCount { 107 // Remove old trace files. 108 toRemove := files[:len(files)+1-maxFileCount] 109 for _, path := range toRemove { 110 log.Info("Removing old %s profile file %q", name, path) 111 err := os.Remove(path) 112 if err != nil { 113 log.Warning("Error on os.Remove(%q): %s", path, err) 114 } 115 } 116 } 117 118 outputFile := profiler.MakeFilename(dir, time.Now(), durationSecToDuration(durationSeconds)) 119 return doTimedProfile(log, delayedLog, profiler, outputFile, durationSeconds) 120 } 121 122 type cpuProfiler struct{} 123 124 func (cpuProfiler) Name() string { return "CPU" } 125 126 func (cpuProfiler) MaxFileCount() int { 127 return libkb.MaxCPUProfileFileCount 128 } 129 130 func (cpuProfiler) MakeFilename(dir string, start time.Time, duration time.Duration) string { 131 return libkb.MakeCPUProfileFilename(dir, start, duration) 132 } 133 134 func (cpuProfiler) GetSortedFiles(dir string) ([]string, error) { 135 return libkb.GetSortedCPUProfileFiles(dir) 136 } 137 138 func (cpuProfiler) Start(w io.Writer) error { 139 return pprof.StartCPUProfile(w) 140 } 141 142 func (cpuProfiler) Stop() { 143 pprof.StopCPUProfile() 144 } 145 146 func (c *PprofHandler) ProcessorProfile(_ context.Context, arg keybase1.ProcessorProfileArg) (err error) { 147 logui := c.getLogUI(arg.SessionID) 148 return doTimedProfile(logui, c.G().Log, cpuProfiler{}, arg.ProfileFile, arg.ProfileDurationSeconds) 149 } 150 151 func (c *PprofHandler) HeapProfile(ctx context.Context, arg keybase1.HeapProfileArg) (err error) { 152 runtime.GC() 153 f, err := os.Create(arg.ProfileFile) 154 if err != nil { 155 return err 156 } 157 defer f.Close() 158 return pprof.WriteHeapProfile(f) 159 } 160 161 func (c *PprofHandler) logDir(logDirForMobile string) string { 162 // Assume the returned directory already exists, i.e. we've 163 // already started logging. 164 if len(logDirForMobile) > 0 { 165 return logDirForMobile 166 } 167 return c.G().Env.GetLogDir() 168 } 169 170 func (c *PprofHandler) LogProcessorProfile(_ context.Context, arg keybase1.LogProcessorProfileArg) (err error) { 171 logui := c.getLogUI(arg.SessionID) 172 return doTimedProfileInDir(logui, c.G().Log, cpuProfiler{}, c.logDir(arg.LogDirForMobile), arg.ProfileDurationSeconds) 173 } 174 175 type traceProfiler struct{} 176 177 func (traceProfiler) Name() string { return "trace" } 178 179 func (traceProfiler) MaxFileCount() int { return libkb.MaxTraceFileCount } 180 181 func (traceProfiler) MakeFilename(dir string, start time.Time, duration time.Duration) string { 182 return libkb.MakeTraceFilename(dir, start, duration) 183 } 184 185 func (traceProfiler) GetSortedFiles(dir string) ([]string, error) { 186 return libkb.GetSortedTraceFiles(dir) 187 } 188 189 func (traceProfiler) Start(w io.Writer) error { 190 return trace.Start(w) 191 } 192 193 func (traceProfiler) Stop() { 194 trace.Stop() 195 } 196 197 func (c *PprofHandler) Trace(_ context.Context, arg keybase1.TraceArg) (err error) { 198 logui := c.getLogUI(arg.SessionID) 199 return doTimedProfile(logui, c.G().Log, traceProfiler{}, arg.TraceFile, arg.TraceDurationSeconds) 200 } 201 202 func (c *PprofHandler) LogTrace(_ context.Context, arg keybase1.LogTraceArg) (err error) { 203 logui := c.getLogUI(arg.SessionID) 204 return doTimedProfileInDir(logui, c.G().Log, traceProfiler{}, c.logDir(arg.LogDirForMobile), arg.TraceDurationSeconds) 205 }