service.go 3.13 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
package oppprof

import (
	"context"
	"io"
	"net"
	"net/http"
	httpPprof "net/http/pprof"
	"os"
	"path/filepath"
	"runtime"
	"runtime/pprof"
	"strconv"

	"github.com/ethereum-optimism/optimism/op-service/httputil"
	"github.com/ethereum/go-ethereum/log"
)

type Service struct {
	listenEnabled bool
	listenAddr    string
	listenPort    int

	profileType     string
	profileDir      string
	profileFilename string

	cpuFile    io.Closer
	httpServer *httputil.HTTPServer
}

func New(listenEnabled bool, listenAddr string, listenPort int, profType profileType, profileDir, profileFilename string) *Service {
	return &Service{
		listenEnabled:   listenEnabled,
		listenAddr:      listenAddr,
		listenPort:      listenPort,
		profileType:     string(profType),
		profileDir:      profileDir,
		profileFilename: profileFilename,
	}
}

func (s *Service) Start() error {
	switch s.profileType {
	case "cpu":
		if err := s.startCPUProfile(); err != nil {
			return err
		}
	case "block":
		runtime.SetBlockProfileRate(1)
	case "mutex":
		runtime.SetMutexProfileFraction(1)
	}
	if s.listenEnabled {
		if err := s.startServer(); err != nil {
			return err
		}
	}
	if s.profileType != "" {
		log.Info("start profiling to file", "profile_type", s.profileType, "profile_filepath", s.buildTargetFilePath())
	}
	return nil
}

func (s *Service) Stop(ctx context.Context) error {
	switch s.profileType {
	case "cpu":
		pprof.StopCPUProfile()
		if s.cpuFile != nil {
			if err := s.cpuFile.Close(); err != nil {
				return err
			}
		}
	case "heap":
		runtime.GC()
		fallthrough
	default:
		profile := pprof.Lookup(s.profileType)
		if profile == nil {
			break
		}
		filepath := s.buildTargetFilePath()
		log.Info("saving profile info", "profile_type", s.profileType, "profile_filepath", s.buildTargetFilePath())
		f, err := os.Create(filepath)
		if err != nil {
			return err
		}
		defer f.Close()
		_ = profile.WriteTo(f, 0)
	}
	if s.httpServer != nil {
		if err := s.httpServer.Stop(ctx); err != nil {
			return err
		}
	}
	return nil
}

func (s *Service) startServer() error {
	log.Debug("starting pprof server", "addr", net.JoinHostPort(s.listenAddr, strconv.Itoa(s.listenPort)))
	mux := http.NewServeMux()

	// have to do below to support multiple servers, since the
	// pprof import only uses DefaultServeMux
	mux.Handle("/debug/pprof/", http.HandlerFunc(httpPprof.Index))
	mux.Handle("/debug/pprof/profile", http.HandlerFunc(httpPprof.Profile))
	mux.Handle("/debug/pprof/symbol", http.HandlerFunc(httpPprof.Symbol))
	mux.Handle("/debug/pprof/trace", http.HandlerFunc(httpPprof.Trace))

	addr := net.JoinHostPort(s.listenAddr, strconv.Itoa(s.listenPort))

	var err error
	s.httpServer, err = httputil.StartHTTPServer(addr, mux)
	if err != nil {
		return err
	}

	log.Info("started pprof server", "addr", s.httpServer.Addr())
	return nil
}

func (s *Service) startCPUProfile() error {
	f, err := os.Create(s.buildTargetFilePath())
	if err != nil {
		return err
	}
	err = pprof.StartCPUProfile(f)
	s.cpuFile = f
	return err
}

func (s *Service) buildTargetFilePath() string {
	filename := s.profileType + ".prof"
	if s.profileFilename != "" {
		filename = s.profileFilename
	}
	return filepath.Join(s.profileDir, filename)
}