parent
a5e62bc7b2
commit
ef140c0b93
@ -0,0 +1,225 @@ |
|||||||
|
package pprof |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"runtime/pprof" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/rpc" |
||||||
|
"github.com/harmony-one/harmony/internal/utils" |
||||||
|
) |
||||||
|
|
||||||
|
// Config is the config for the pprof service
|
||||||
|
type Config struct { |
||||||
|
Enabled bool |
||||||
|
ListenAddr string |
||||||
|
Folder string |
||||||
|
ProfileNames []string |
||||||
|
ProfileIntervals []int |
||||||
|
ProfileDebugValues []int |
||||||
|
} |
||||||
|
|
||||||
|
type Profile struct { |
||||||
|
Name string |
||||||
|
Interval int |
||||||
|
Debug int |
||||||
|
ProfileRef *pprof.Profile |
||||||
|
} |
||||||
|
|
||||||
|
func (p Config) String() string { |
||||||
|
return fmt.Sprintf("%v, %v, %v, %v/%v/%v", p.Enabled, p.ListenAddr, p.Folder, p.ProfileNames, p.ProfileIntervals, p.ProfileDebugValues) |
||||||
|
} |
||||||
|
|
||||||
|
// Constants for profile names
|
||||||
|
const ( |
||||||
|
CPU = "cpu" |
||||||
|
) |
||||||
|
|
||||||
|
// Service provides access to pprof profiles via HTTP and can save them to local disk periodically as user settings.
|
||||||
|
type Service struct { |
||||||
|
config Config |
||||||
|
profiles map[string]Profile |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
initOnce sync.Once |
||||||
|
svc = &Service{} |
||||||
|
) |
||||||
|
|
||||||
|
// NewService creates the new pprof service
|
||||||
|
func NewService(cfg Config) *Service { |
||||||
|
initOnce.Do(func() { |
||||||
|
svc = newService(cfg) |
||||||
|
}) |
||||||
|
return svc |
||||||
|
} |
||||||
|
|
||||||
|
func newService(cfg Config) *Service { |
||||||
|
if !cfg.Enabled { |
||||||
|
utils.Logger().Info().Msg("Pprof service disabled...") |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
utils.Logger().Debug().Str("cfg", cfg.String()).Msg("Pprof") |
||||||
|
svc.config = cfg |
||||||
|
|
||||||
|
if profiles, err := cfg.unpackProfilesIntoMap(); err != nil { |
||||||
|
log.Fatal("Could not unpack pprof profiles into map") |
||||||
|
} else { |
||||||
|
svc.profiles = profiles |
||||||
|
} |
||||||
|
|
||||||
|
go func() { |
||||||
|
utils.Logger().Info().Str("address", cfg.ListenAddr).Msg("Starting pprof HTTP service") |
||||||
|
http.ListenAndServe(cfg.ListenAddr, nil) |
||||||
|
}() |
||||||
|
|
||||||
|
return svc |
||||||
|
} |
||||||
|
|
||||||
|
// Start start the service
|
||||||
|
func (s *Service) Start() error { |
||||||
|
dir, err := filepath.Abs(s.config.Folder) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
err = os.MkdirAll(dir, os.FileMode(0755)) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if cpuProfile, ok := s.profiles[CPU]; ok { |
||||||
|
go handleCpuProfile(cpuProfile, dir) |
||||||
|
} |
||||||
|
|
||||||
|
for _, profile := range s.profiles { |
||||||
|
scheduleProfile(profile, dir) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Stop stop the service
|
||||||
|
func (s *Service) Stop() error { |
||||||
|
dir, err := filepath.Abs(s.config.Folder) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
for _, profile := range s.profiles { |
||||||
|
if profile.Name == CPU { |
||||||
|
pprof.StopCPUProfile() |
||||||
|
} else { |
||||||
|
saveProfile(profile, dir) |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// APIs return all APIs of the service
|
||||||
|
func (s *Service) APIs() []rpc.API { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// scheduleProfile schedules the provided profile based on the specified interval (e.g. saves the profile every x seconds)
|
||||||
|
func scheduleProfile(profile Profile, dir string) { |
||||||
|
go func() { |
||||||
|
if profile.Interval > 0 { |
||||||
|
ticker := time.NewTicker(time.Second * time.Duration(profile.Interval)) |
||||||
|
defer ticker.Stop() |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-ticker.C: |
||||||
|
if profile.Name == CPU { |
||||||
|
handleCpuProfile(profile, dir) |
||||||
|
} else { |
||||||
|
saveProfile(profile, dir) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
} |
||||||
|
|
||||||
|
// saveProfile saves the provided profile in the specified directory
|
||||||
|
func saveProfile(profile Profile, dir string) error { |
||||||
|
f, err := newTempFile(dir, profile.Name, ".pb.gz") |
||||||
|
if err != nil { |
||||||
|
utils.Logger().Error().Err(err).Msg(fmt.Sprintf("Could not save profile: %s", profile.Name)) |
||||||
|
return err |
||||||
|
} |
||||||
|
defer f.Close() |
||||||
|
if profile.ProfileRef != nil { |
||||||
|
err = profile.ProfileRef.WriteTo(f, profile.Debug) |
||||||
|
if err != nil { |
||||||
|
utils.Logger().Error().Err(err).Msg(fmt.Sprintf("Could not write profile: %s", profile.Name)) |
||||||
|
return err |
||||||
|
} |
||||||
|
utils.Logger().Info().Msg(fmt.Sprintf("Saved profile in: %s", f.Name())) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// handleCpuProfile handles the provided CPU profile
|
||||||
|
func handleCpuProfile(profile Profile, dir string) { |
||||||
|
pprof.StopCPUProfile() |
||||||
|
f, err := newTempFile(dir, profile.Name, ".pb.gz") |
||||||
|
if err == nil { |
||||||
|
pprof.StartCPUProfile(f) |
||||||
|
utils.Logger().Info().Msg(fmt.Sprintf("Saved CPU profile in: %s", f.Name())) |
||||||
|
} else { |
||||||
|
utils.Logger().Error().Err(err).Msg("Could not start CPU profile") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// unpackProfilesIntoMap unpacks the profiles specified in the configuration into a map
|
||||||
|
func (config *Config) unpackProfilesIntoMap() (map[string]Profile, error) { |
||||||
|
result := make(map[string]Profile, len(config.ProfileNames)) |
||||||
|
if len(config.ProfileNames) == 0 { |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
for index, name := range config.ProfileNames { |
||||||
|
profile := Profile{ |
||||||
|
Name: name, |
||||||
|
Interval: 0, |
||||||
|
Debug: 0, |
||||||
|
} |
||||||
|
// Try set interval value
|
||||||
|
if len(config.ProfileIntervals) == len(config.ProfileNames) { |
||||||
|
profile.Interval = config.ProfileIntervals[index] |
||||||
|
} else if len(config.ProfileIntervals) > 0 { |
||||||
|
profile.Interval = config.ProfileIntervals[0] |
||||||
|
} |
||||||
|
// Try set debug value
|
||||||
|
if len(config.ProfileDebugValues) == len(config.ProfileNames) { |
||||||
|
profile.Debug = config.ProfileDebugValues[index] |
||||||
|
} else if len(config.ProfileDebugValues) > 0 { |
||||||
|
profile.Debug = config.ProfileDebugValues[0] |
||||||
|
} |
||||||
|
// Try set the profile reference
|
||||||
|
if profile.Name != CPU { |
||||||
|
if p := pprof.Lookup(profile.Name); p == nil { |
||||||
|
return nil, fmt.Errorf("Profile does not exist: %s", profile.Name) |
||||||
|
} else { |
||||||
|
profile.ProfileRef = p |
||||||
|
} |
||||||
|
} |
||||||
|
result[name] = profile |
||||||
|
} |
||||||
|
return result, nil |
||||||
|
} |
||||||
|
|
||||||
|
// newTempFile returns a new output file in dir with the provided prefix and suffix.
|
||||||
|
func newTempFile(dir, name, suffix string) (*os.File, error) { |
||||||
|
prefix := name + "." |
||||||
|
currentTime := time.Now().Unix() |
||||||
|
f, err := os.OpenFile(filepath.Join(dir, fmt.Sprintf("%s%d%s", prefix, currentTime, suffix)), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("could not create file of the form %s%d%s", prefix, currentTime, suffix) |
||||||
|
} |
||||||
|
return f, nil |
||||||
|
} |
@ -0,0 +1,80 @@ |
|||||||
|
package pprof |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"reflect" |
||||||
|
"strings" |
||||||
|
"testing" |
||||||
|
) |
||||||
|
|
||||||
|
func TestUnpackProfilesIntoMap(t *testing.T) { |
||||||
|
tests := []struct { |
||||||
|
input *Config |
||||||
|
expMap map[string]Profile |
||||||
|
expErr error |
||||||
|
}{ |
||||||
|
{ |
||||||
|
input: &Config{}, |
||||||
|
expMap: make(map[string]Profile), |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: &Config{ |
||||||
|
ProfileNames: []string{"test", "test"}, |
||||||
|
}, |
||||||
|
expMap: nil, |
||||||
|
expErr: errors.New("Pprof profile names contains duplicate: test"), |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: &Config{ |
||||||
|
ProfileNames: []string{"test"}, |
||||||
|
}, |
||||||
|
expMap: map[string]Profile{ |
||||||
|
"test": { |
||||||
|
Name: "test", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
input: &Config{ |
||||||
|
ProfileNames: []string{"test1", "test2"}, |
||||||
|
ProfileIntervals: []int{0, 60}, |
||||||
|
ProfileDebugValues: []int{1}, |
||||||
|
}, |
||||||
|
expMap: map[string]Profile{ |
||||||
|
"test1": { |
||||||
|
Name: "test1", |
||||||
|
Interval: 0, |
||||||
|
Debug: 1, |
||||||
|
}, |
||||||
|
"test2": { |
||||||
|
Name: "test2", |
||||||
|
Interval: 60, |
||||||
|
Debug: 1, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
for i, test := range tests { |
||||||
|
actual, err := test.input.unpackProfilesIntoMap() |
||||||
|
if assErr := assertError(err, test.expErr); assErr != nil { |
||||||
|
t.Fatalf("Test %v: %v", i, assErr) |
||||||
|
} |
||||||
|
if !reflect.DeepEqual(actual, test.expMap) { |
||||||
|
t.Errorf("Test %v: unexpected map\n\t%+v\n\t%+v", i, actual, test.expMap) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func assertError(gotErr, expErr error) error { |
||||||
|
if (gotErr == nil) != (expErr == nil) { |
||||||
|
return fmt.Errorf("error unexpected [%v] / [%v]", gotErr, expErr) |
||||||
|
} |
||||||
|
if gotErr == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
if !strings.Contains(gotErr.Error(), expErr.Error()) { |
||||||
|
return fmt.Errorf("error unexpected [%v] / [%v]", gotErr, expErr) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
Loading…
Reference in new issue