github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/x/debug/triggering_profile.go (about)

     1  // Copyright (c) 2020 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package debug
    22  
    23  import (
    24  	"bytes"
    25  	"errors"
    26  	"fmt"
    27  	"os"
    28  	"runtime/pprof"
    29  	"text/template"
    30  	"time"
    31  
    32  	"github.com/m3db/m3/src/x/instrument"
    33  
    34  	"go.uber.org/zap"
    35  )
    36  
    37  const (
    38  	// ContinuousCPUProfileName is the name of continuous CPU profile.
    39  	ContinuousCPUProfileName = "cpu"
    40  )
    41  
    42  var (
    43  	defaultConditional     = func() bool { return true }
    44  	defaultInterval        = time.Second
    45  	errNoFilePathTemplate  = errors.New("no file path template")
    46  	errNoProfileName       = errors.New("no profile name")
    47  	errNoInstrumentOptions = errors.New("no instrument options")
    48  	errAlreadyOpen         = errors.New("already open")
    49  	errNotOpen             = errors.New("not open")
    50  )
    51  
    52  // ContinuousFileProfile is a profile that runs continously
    53  // to a file using a template for a file name.
    54  type ContinuousFileProfile struct {
    55  	filePathTemplate *template.Template
    56  	profileName      string
    57  	profileDuration  time.Duration
    58  	profileDebug     int
    59  	conditional      func() bool
    60  	interval         time.Duration
    61  
    62  	logger *zap.Logger
    63  
    64  	closeCh chan struct{}
    65  }
    66  
    67  // ContinuousFileProfileOptions is a set of continuous file profile options.
    68  type ContinuousFileProfileOptions struct {
    69  	FilePathTemplate  string
    70  	ProfileName       string
    71  	ProfileDuration   time.Duration
    72  	ProfileDebug      int
    73  	Conditional       func() bool
    74  	Interval          time.Duration
    75  	InstrumentOptions instrument.Options
    76  }
    77  
    78  // ContinuousFileProfilePathParams is the params used to construct
    79  // a file path.
    80  type ContinuousFileProfilePathParams struct {
    81  	ProfileName string
    82  	UnixTime    int64
    83  }
    84  
    85  // NewContinuousFileProfile returns a new continuous file profile.
    86  func NewContinuousFileProfile(
    87  	opts ContinuousFileProfileOptions,
    88  ) (*ContinuousFileProfile, error) {
    89  	if opts.FilePathTemplate == "" {
    90  		return nil, errNoFilePathTemplate
    91  	}
    92  	if opts.ProfileName == "" {
    93  		return nil, errNoProfileName
    94  	}
    95  	if opts.Conditional == nil {
    96  		opts.Conditional = defaultConditional
    97  	}
    98  	if opts.Interval == 0 {
    99  		opts.Interval = defaultInterval
   100  	}
   101  	if opts.InstrumentOptions == nil {
   102  		return nil, errNoInstrumentOptions
   103  	}
   104  
   105  	tmpl, err := template.New("fileName").Parse(opts.FilePathTemplate)
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  
   110  	return &ContinuousFileProfile{
   111  		filePathTemplate: tmpl,
   112  		profileName:      opts.ProfileName,
   113  		profileDuration:  opts.ProfileDuration,
   114  		profileDebug:     opts.ProfileDebug,
   115  		conditional:      opts.Conditional,
   116  		interval:         opts.Interval,
   117  		logger:           opts.InstrumentOptions.Logger(),
   118  	}, nil
   119  }
   120  
   121  // Start will start the continuous file profile.
   122  func (c *ContinuousFileProfile) Start() error {
   123  	if c.closeCh != nil {
   124  		return errAlreadyOpen
   125  	}
   126  
   127  	c.closeCh = make(chan struct{})
   128  	go c.run()
   129  	return nil
   130  }
   131  
   132  // Stop will stop the continuous file profile.
   133  func (c *ContinuousFileProfile) Stop() error {
   134  	if c.closeCh == nil {
   135  		return errNotOpen
   136  	}
   137  
   138  	close(c.closeCh)
   139  	c.closeCh = nil
   140  
   141  	return nil
   142  }
   143  
   144  func (c *ContinuousFileProfile) run() {
   145  	closeCh := c.closeCh
   146  	ticker := time.NewTicker(c.interval)
   147  	defer ticker.Stop()
   148  
   149  	for {
   150  		select {
   151  		case <-closeCh:
   152  			return
   153  		case <-ticker.C:
   154  			if !c.conditional() {
   155  				continue
   156  			}
   157  
   158  			err := c.profile()
   159  			if err != nil {
   160  				c.logger.Error("continuous profile error",
   161  					zap.String("name", c.profileName),
   162  					zap.Int("debug", c.profileDebug),
   163  					zap.Duration("interval", c.interval),
   164  					zap.Error(err))
   165  			}
   166  		}
   167  	}
   168  }
   169  
   170  func (c *ContinuousFileProfile) profile() error {
   171  	filePathBuffer := bytes.NewBuffer(nil)
   172  	filePathParams := ContinuousFileProfilePathParams{
   173  		ProfileName: c.profileName,
   174  		UnixTime:    time.Now().Unix(),
   175  	}
   176  	err := c.filePathTemplate.Execute(filePathBuffer, filePathParams)
   177  	if err != nil {
   178  		return err
   179  	}
   180  
   181  	w, err := os.Create(filePathBuffer.String())
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	success := false
   187  	defer func() {
   188  		if !success {
   189  			_ = w.Close()
   190  		}
   191  	}()
   192  
   193  	switch c.profileName {
   194  	case ContinuousCPUProfileName:
   195  		if err := pprof.StartCPUProfile(w); err != nil {
   196  			return err
   197  		}
   198  		time.Sleep(c.profileDuration)
   199  		pprof.StopCPUProfile()
   200  	default:
   201  		p := pprof.Lookup(c.profileName)
   202  		if p == nil {
   203  			return fmt.Errorf("unknown profile: %s", c.profileName)
   204  		}
   205  		if err := p.WriteTo(w, c.profileDebug); err != nil {
   206  			return err
   207  		}
   208  	}
   209  
   210  	success = true
   211  	return w.Close()
   212  }