code.vegaprotocol.io/vega@v0.79.0/libs/pprof/pprof.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package pprof
    17  
    18  import (
    19  	"fmt"
    20  	"net/http"
    21  	"os"
    22  	"path/filepath"
    23  	"runtime"
    24  	"runtime/pprof"
    25  	"time"
    26  
    27  	"code.vegaprotocol.io/vega/libs/config/encoding"
    28  	vgfs "code.vegaprotocol.io/vega/libs/fs"
    29  	"code.vegaprotocol.io/vega/logging"
    30  
    31  	"github.com/felixge/fgprof"
    32  
    33  	// import pprof globally because it's used to init the package
    34  	// and this comment is mostly here as well in order to make
    35  	// golint very many much happy.
    36  	_ "net/http/pprof"
    37  )
    38  
    39  const (
    40  	pprofDir       = "pprof"
    41  	memprofileName = "mem"
    42  	cpuprofileName = "cpu"
    43  	profileExt     = ".pprof"
    44  
    45  	namedLogger = "pprof"
    46  )
    47  
    48  // Config represent the configuration of the pprof package.
    49  type Config struct {
    50  	Level       encoding.LogLevel `long:"level"`
    51  	Enabled     bool              `long:"enabled"`
    52  	Port        uint16            `long:"port"`
    53  	ProfilesDir string            `long:"profiles-dir"`
    54  	// To include every blocking event in the profile, pass rate = 1.
    55  	// To turn off profiling entirely, pass rate <= 0.
    56  	BlockProfileRate int `long:"block-profile-rate"`
    57  	// To turn off profiling entirely, pass rate 0.
    58  	// To just read the current rate, pass rate < 0.
    59  	// (For n>1 the details of sampling may change.)
    60  	MutexProfileFraction int `long:"mutex-profile-fraction"`
    61  	// Write the profiles to disk every WriteEvery interval
    62  	WriteEvery encoding.Duration `description:"write pprof files at this interval; if 0 only write on shutdown" long:"write-every"`
    63  }
    64  
    65  // Pprofhandler is handling pprof profile management.
    66  type Pprofhandler struct {
    67  	Config
    68  
    69  	log            *logging.Logger
    70  	stop           chan struct{}
    71  	done           chan struct{}
    72  	memprofilePath string
    73  	cpuprofilePath string
    74  }
    75  
    76  // NewDefaultConfig create a new default configuration for the pprof handler.
    77  func NewDefaultConfig() Config {
    78  	return Config{
    79  		Level:                encoding.LogLevel{Level: logging.InfoLevel},
    80  		Enabled:              false,
    81  		Port:                 6060,
    82  		ProfilesDir:          "/tmp",
    83  		BlockProfileRate:     0,
    84  		MutexProfileFraction: 0,
    85  		WriteEvery:           encoding.Duration{Duration: 15 * time.Minute},
    86  	}
    87  }
    88  
    89  // New creates a new pprof handler.
    90  func New(log *logging.Logger, config Config) (*Pprofhandler, error) {
    91  	// setup logger
    92  	log = log.Named(namedLogger)
    93  	log.SetLevel(config.Level.Get())
    94  
    95  	runtime.SetBlockProfileRate(config.BlockProfileRate)
    96  	runtime.SetMutexProfileFraction(config.MutexProfileFraction)
    97  
    98  	p := &Pprofhandler{
    99  		log:    log,
   100  		Config: config,
   101  		stop:   make(chan struct{}),
   102  		done:   make(chan struct{}),
   103  	}
   104  
   105  	// start the pprof http server
   106  	http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler())
   107  	go func() {
   108  		p.log.Error("pprof web server closed", logging.Error(http.ListenAndServe(fmt.Sprintf("localhost:%d", config.Port), nil)))
   109  	}()
   110  
   111  	// make sure profile dir exists
   112  	profDir := filepath.Join(config.ProfilesDir, pprofDir)
   113  	if err := vgfs.EnsureDir(profDir); err != nil {
   114  		p.log.Error("Could not create profile dir",
   115  			logging.String("path", profDir),
   116  			logging.Error(err),
   117  		)
   118  		return nil, err
   119  	}
   120  
   121  	go p.runProfiling()
   122  
   123  	return p, nil
   124  }
   125  
   126  func (p *Pprofhandler) runProfiling() error {
   127  	defer close(p.done)
   128  	// If WriteEvery is 0, make a ticker that never ticks
   129  	tick := make(<-chan time.Time)
   130  	if p.WriteEvery.Duration > 0 {
   131  		tick = time.NewTicker(p.WriteEvery.Duration).C
   132  	}
   133  
   134  	for {
   135  		if err := p.startProfiling(); err != nil {
   136  			return err
   137  		}
   138  
   139  		select {
   140  		case <-p.stop:
   141  			return p.stopProfiling()
   142  		case <-tick:
   143  		}
   144  
   145  		if err := p.stopProfiling(); err != nil {
   146  			return err
   147  		}
   148  	}
   149  }
   150  
   151  func (p *Pprofhandler) startProfiling() error {
   152  	t := time.Now()
   153  	memprofileFile := fmt.Sprintf("%s-%s%s", memprofileName, t.Format("2006-01-02-15-04-05"), profileExt)
   154  	cpuprofileFile := fmt.Sprintf("%s-%s%s", cpuprofileName, t.Format("2006-01-02-15-04-05"), profileExt)
   155  
   156  	p.memprofilePath = filepath.Join(p.ProfilesDir, pprofDir, memprofileFile)
   157  	p.cpuprofilePath = filepath.Join(p.ProfilesDir, pprofDir, cpuprofileFile)
   158  
   159  	profileFile, err := os.Create(p.cpuprofilePath)
   160  	if err != nil {
   161  		p.log.Error("Could not create CPU profile file",
   162  			logging.String("path", p.cpuprofilePath),
   163  			logging.Error(err),
   164  		)
   165  		return err
   166  	}
   167  	pprof.StartCPUProfile(profileFile)
   168  
   169  	return nil
   170  }
   171  
   172  // ReloadConf update the configuration of the pprof package.
   173  func (p *Pprofhandler) ReloadConf(cfg Config) {
   174  	p.log.Info("reloading configuration")
   175  	if p.log.GetLevel() != cfg.Level.Get() {
   176  		p.log.Info("updating log level",
   177  			logging.String("old", p.log.GetLevel().String()),
   178  			logging.String("new", cfg.Level.String()),
   179  		)
   180  		p.log.SetLevel(cfg.Level.Get())
   181  	}
   182  
   183  	// the config will not be used anyway, just use the log level in here
   184  	p.Config = cfg
   185  	runtime.SetBlockProfileRate(cfg.BlockProfileRate)
   186  	runtime.SetMutexProfileFraction(cfg.MutexProfileFraction)
   187  }
   188  
   189  // Stop is meant to be use to stop the pprof profile, should be use with defer probably.
   190  func (p *Pprofhandler) Stop() error {
   191  	close(p.stop)
   192  	<-p.done
   193  	return nil
   194  }
   195  
   196  func (p *Pprofhandler) stopProfiling() error {
   197  	// stop cpu profile once the memory profile is written
   198  	defer pprof.StopCPUProfile()
   199  
   200  	p.log.Info("saving pprof memory profile", logging.String("path", p.memprofilePath))
   201  	p.log.Info("saving pprof cpu profile", logging.String("path", p.cpuprofilePath))
   202  
   203  	// save memory profile
   204  	f, err := os.Create(p.memprofilePath)
   205  	if err != nil {
   206  		p.log.Error("Could not create memory profile file",
   207  			logging.String("path", p.memprofilePath),
   208  			logging.Error(err),
   209  		)
   210  		return err
   211  	}
   212  
   213  	runtime.GC()
   214  	if err := pprof.WriteHeapProfile(f); err != nil {
   215  		p.log.Error("Could not write memory profile",
   216  			logging.Error(err),
   217  		)
   218  		return err
   219  	}
   220  	f.Close()
   221  
   222  	return nil
   223  }