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  }