go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/runtime/profiling/profiler.go (about)

     1  // Copyright 2016 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package profiling provides a tool to profile various aspects of the process.
    16  package profiling
    17  
    18  import (
    19  	"flag"
    20  	"fmt"
    21  	"net"
    22  	"net/http"
    23  	httpProf "net/http/pprof"
    24  	"os"
    25  	"path/filepath"
    26  	"runtime"
    27  	"runtime/pprof"
    28  	"runtime/trace"
    29  	"sync/atomic"
    30  	"time"
    31  
    32  	"go.chromium.org/luci/common/clock"
    33  	"go.chromium.org/luci/common/errors"
    34  	"go.chromium.org/luci/common/logging"
    35  )
    36  
    37  // Profiler helps setup and manage profiling
    38  type Profiler struct {
    39  	// BindHTTP, if not empty, is the HTTP address to bind to.
    40  	//
    41  	// Can also be configured with "-profile-bind-http" flag.
    42  	BindHTTP string
    43  
    44  	// Dir, if set, is the path where profiling data will be written to.
    45  	//
    46  	// Can also be configured with "-profile-output-dir" flag.
    47  	Dir string
    48  
    49  	// ProfileCPU, if true, indicates that the profiler should profile the CPU.
    50  	//
    51  	// Requires Dir to be set, since it's where the profiler output is dumped.
    52  	//
    53  	// Can also be set with "-profile-cpu".
    54  	ProfileCPU bool
    55  
    56  	// ProfileTrace, if true, indicates that the profiler should enable
    57  	// runtime tracing.
    58  	//
    59  	// Requires Dir to be set, since it's where the profiler output is dumped.
    60  	//
    61  	// Can also be set with "-profile-trace".
    62  	ProfileTrace bool
    63  
    64  	// ProfileHeap, if true, indicates that the profiler should profile heap
    65  	// allocations.
    66  	//
    67  	// Requires Dir to be set, since it's where the profiler output is dumped.
    68  	//
    69  	// Can also be set with "-profile-heap".
    70  	ProfileHeap bool
    71  
    72  	// ProfileHeapFrequency, if set non-zero, instructs the profiler to
    73  	// periodically dump heap profiler snapshots.
    74  	//
    75  	// Requires Dir to be set, since it's where the profiler output is dumped.
    76  	//
    77  	// Can also be set with "-profile-heap-frequency".
    78  	ProfileHeapFrequency time.Duration
    79  
    80  	// Logger, if not nil, will be used to log events and errors. If nil, no
    81  	// logging will be used.
    82  	Logger logging.Logger
    83  	// Clock is the clock instance to use. If nil, the system clock will be used.
    84  	Clock clock.Clock
    85  
    86  	// listener is the active listener instance. It is set when Start is called.
    87  	listener net.Listener
    88  
    89  	// pathCounter is an atomic counter used to ensure non-conflicting paths.
    90  	pathCounter uint32
    91  
    92  	// profilingCPU is true if 'Start' successfully launched CPU profiling.
    93  	profilingCPU bool
    94  
    95  	// profilingTrace is true if 'Start' successfully launched runtime tracing.
    96  	profilingTrace bool
    97  }
    98  
    99  // AddFlags adds command line flags to common Profiler fields.
   100  func (p *Profiler) AddFlags(fs *flag.FlagSet) {
   101  	fs.StringVar(&p.BindHTTP, "profile-bind-http", "",
   102  		"If specified, run a runtime profiler HTTP server bound to this [address][:port].")
   103  	fs.StringVar(&p.Dir, "profile-output-dir", "",
   104  		"If specified, allow generation of profiling artifacts, which will be written here.")
   105  	fs.BoolVar(&p.ProfileCPU, "profile-cpu", false, "If specified, enables CPU profiling.")
   106  	fs.BoolVar(&p.ProfileTrace, "profile-trace", false, "If specified, enables runtime tracing.")
   107  	fs.BoolVar(&p.ProfileHeap, "profile-heap", false, "If specified, enables heap profiling.")
   108  	fs.DurationVar(&p.ProfileHeapFrequency, "profile-heap-frequency", 0, "If specified non-zero, enables periodic heap profiler snapshots dump.")
   109  }
   110  
   111  // Start starts the Profiler's configured operations.  On success, returns a
   112  // function that can be called to shutdown the profiling server.
   113  //
   114  // Calling Stop is not necessary, but will enable end-of-operation profiling
   115  // to be gathered.
   116  func (p *Profiler) Start() error {
   117  	if p.Dir == "" {
   118  		if p.ProfileCPU {
   119  			return errors.New("-profile-cpu requires -profile-output-dir to be set")
   120  		}
   121  		if p.ProfileTrace {
   122  			return errors.New("-profile-trace requires -profile-output-dir to be set")
   123  		}
   124  		if p.ProfileHeap {
   125  			return errors.New("-profile-heap requires -profile-output-dir to be set")
   126  		}
   127  
   128  		if p.ProfileHeapFrequency > 0 {
   129  			return errors.New("-profile-heap-frequency requires -profile-output-dir to be set")
   130  		}
   131  	}
   132  
   133  	if p.ProfileHeapFrequency < 0 {
   134  		return errors.New("-profile-heap-frequency should be positive if set")
   135  	}
   136  
   137  	if p.ProfileHeapFrequency > 0 && !p.ProfileHeap {
   138  		return errors.New("-profile-heap-frequency requires -profile-heap")
   139  	}
   140  
   141  	if p.ProfileCPU {
   142  		out, err := os.Create(p.generateOutPath("cpu"))
   143  		if err != nil {
   144  			return errors.Annotate(err, "failed to create CPU profile output file").Err()
   145  		}
   146  		pprof.StartCPUProfile(out)
   147  		p.profilingCPU = true
   148  	}
   149  
   150  	if p.ProfileTrace {
   151  		out, err := os.Create(p.generateOutPath("trace"))
   152  		if err != nil {
   153  			return errors.Annotate(err, "failed to create runtime trace output file").Err()
   154  		}
   155  		trace.Start(out)
   156  		p.profilingTrace = true
   157  	}
   158  
   159  	if p.ProfileHeapFrequency > 0 {
   160  		go func() {
   161  			for {
   162  				time.Sleep(p.ProfileHeapFrequency)
   163  				if err := p.dumpHeapProfile(); err != nil {
   164  					p.getLogger().Errorf("Error dump heap profile: %v", err)
   165  				}
   166  			}
   167  		}()
   168  	}
   169  
   170  	if p.BindHTTP != "" {
   171  		if err := p.startHTTP(); err != nil {
   172  			return errors.Annotate(err, "failed to start HTTP server").Err()
   173  		}
   174  	}
   175  
   176  	return nil
   177  }
   178  
   179  func (p *Profiler) startHTTP() error {
   180  	// Register paths: https://golang.org/src/net/http/pprof/pprof.go
   181  	router := http.NewServeMux()
   182  	router.HandleFunc("/debug/pprof/", httpProf.Index)
   183  	router.HandleFunc("/debug/pprof/cmdline", httpProf.Cmdline)
   184  	router.HandleFunc("/debug/pprof/profile", httpProf.Profile)
   185  	router.HandleFunc("/debug/pprof/symbol", httpProf.Symbol)
   186  	router.HandleFunc("/debug/pprof/trace", httpProf.Trace)
   187  	for _, p := range pprof.Profiles() {
   188  		name := p.Name()
   189  		router.Handle(fmt.Sprintf("/debug/pprof/%s", name), httpProf.Handler(name))
   190  	}
   191  
   192  	// Bind to our profiling port.
   193  	l, err := net.Listen("tcp4", p.BindHTTP)
   194  	if err != nil {
   195  		return errors.Annotate(err, "failed to bind to TCP4 address: %q", p.BindHTTP).Err()
   196  	}
   197  
   198  	server := http.Server{
   199  		Handler: router,
   200  	}
   201  	go func() {
   202  		if err := server.Serve(l); err != nil {
   203  			p.getLogger().Errorf("Error serving profile HTTP: %s", err)
   204  		}
   205  	}()
   206  	return nil
   207  }
   208  
   209  // Stop stops the Profiler's operations.
   210  func (p *Profiler) Stop() {
   211  	if p.profilingCPU {
   212  		pprof.StopCPUProfile()
   213  		p.profilingCPU = false
   214  	}
   215  
   216  	if p.profilingTrace {
   217  		trace.Stop()
   218  		p.profilingTrace = false
   219  	}
   220  
   221  	if p.listener != nil {
   222  		if err := p.listener.Close(); err != nil {
   223  			p.getLogger().Warningf("Failed to stop profile HTTP server: %s", err)
   224  		}
   225  		p.listener = nil
   226  	}
   227  
   228  	// Take one final snapshot.
   229  	p.DumpSnapshot()
   230  }
   231  
   232  // DumpSnapshot dumps a profile snapshot to the configured output directory. If
   233  // no output directory is configured, nothing will happen.
   234  func (p *Profiler) DumpSnapshot() error {
   235  	if p.Dir == "" {
   236  		return nil
   237  	}
   238  
   239  	if p.ProfileHeap {
   240  		if err := p.dumpHeapProfile(); err != nil {
   241  			return errors.Annotate(err, "failed to dump heap profile").Err()
   242  		}
   243  	}
   244  
   245  	return nil
   246  }
   247  
   248  func (p *Profiler) dumpHeapProfile() error {
   249  	fd, err := os.Create(p.generateOutPath("memory"))
   250  	if err != nil {
   251  		return errors.Annotate(err, "failed to create output file").Err()
   252  	}
   253  	defer fd.Close()
   254  
   255  	// Get up-to-date statistics.
   256  	runtime.GC()
   257  	if err := pprof.WriteHeapProfile(fd); err != nil {
   258  		return errors.Annotate(err, "failed to write heap profile").Err()
   259  	}
   260  	return nil
   261  }
   262  
   263  func (p *Profiler) generateOutPath(base string) string {
   264  	clk := p.Clock
   265  	if clk == nil {
   266  		clk = clock.GetSystemClock()
   267  	}
   268  	now := clk.Now()
   269  	counter := atomic.AddUint32(&p.pathCounter, 1) - 1
   270  	return filepath.Join(p.Dir, fmt.Sprintf("%s_%d_%d.prof", base, now.Unix(), counter))
   271  }
   272  
   273  func (p *Profiler) getLogger() logging.Logger {
   274  	if p.Logger != nil {
   275  		return p.Logger
   276  	}
   277  	return logging.Null
   278  }