// Copyright 2020 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package benchmark provides a Metrics object that enables memory and CPU // profiling for the linker. The Metrics objects can be used to mark stages // of the code, and name the measurements during that stage. There is also // optional GCs that can be performed at the end of each stage, so you // can get an accurate measurement of how each stage changes live memory. package benchmark import ( "fmt" "io" "os" "runtime" "runtime/pprof" "time" "unicode" ) type Flags int const ( GC = 1 << iota NoGC Flags = 0 ) type Metrics struct { gc Flags marks []*mark curMark *mark filebase string pprofFile *os.File } type mark struct { name string startM, endM, gcM runtime.MemStats startT, endT time.Time } // New creates a new Metrics object. // // Typical usage should look like: // // func main() { // filename := "" // Set to enable per-phase pprof file output. // bench := benchmark.New(benchmark.GC, filename) // defer bench.Report(os.Stdout) // // etc // bench.Start("foo") // foo() // bench.Start("bar") // bar() // } // // Note that a nil Metrics object won't cause any errors, so one could write // code like: // // func main() { // enableBenchmarking := flag.Bool("enable", true, "enables benchmarking") // flag.Parse() // var bench *benchmark.Metrics // if *enableBenchmarking { // bench = benchmark.New(benchmark.GC) // } // bench.Start("foo") // // etc. // } func New(gc Flags, filebase string) *Metrics { if gc == GC { runtime.GC() } return &Metrics{gc: gc, filebase: filebase} } // Report reports the metrics. // Closes the currently Start(ed) range, and writes the report to the given io.Writer. func (m *Metrics) Report(w io.Writer) { if m == nil { return } m.closeMark() gcString := "" if m.gc == GC { gcString = "_GC" } var totTime time.Duration for _, curMark := range m.marks { dur := curMark.endT.Sub(curMark.startT) totTime += dur fmt.Fprintf(w, "%s 1 %d ns/op", makeBenchString(curMark.name+gcString), dur.Nanoseconds()) fmt.Fprintf(w, "\t%d B/op", curMark.endM.TotalAlloc-curMark.startM.TotalAlloc) fmt.Fprintf(w, "\t%d allocs/op", curMark.endM.Mallocs-curMark.startM.Mallocs) if m.gc == GC { fmt.Fprintf(w, "\t%d live-B", curMark.gcM.HeapAlloc) } else { fmt.Fprintf(w, "\t%d heap-B", curMark.endM.HeapAlloc) } fmt.Fprintf(w, "\n") } fmt.Fprintf(w, "%s 1 %d ns/op\n", makeBenchString("total time"+gcString), totTime.Nanoseconds()) } // Starts marks the beginning of a new measurement phase. // Once a metric is started, it continues until either a Report is issued, or another Start is called. func (m *Metrics) Start(name string) { if m == nil { return } m.closeMark() m.curMark = &mark{name: name} // Unlikely we need to a GC here, as one was likely just done in closeMark. if m.shouldPProf() { f, err := os.Create(makePProfFilename(m.filebase, name, "cpuprof")) if err != nil { panic(err) } m.pprofFile = f if err = pprof.StartCPUProfile(m.pprofFile); err != nil { panic(err) } } runtime.ReadMemStats(&m.curMark.startM) m.curMark.startT = time.Now() } func (m *Metrics) closeMark() { if m == nil || m.curMark == nil { return } m.curMark.endT = time.Now() if m.shouldPProf() { pprof.StopCPUProfile() m.pprofFile.Close() m.pprofFile = nil } runtime.ReadMemStats(&m.curMark.endM) if m.gc == GC { runtime.GC() runtime.ReadMemStats(&m.curMark.gcM) if m.shouldPProf() { // Collect a profile of the live heap. Do a // second GC to force sweep completion so we // get a complete snapshot of the live heap at // the end of this phase. runtime.GC() f, err := os.Create(makePProfFilename(m.filebase, m.curMark.name, "memprof")) if err != nil { panic(err) } err = pprof.WriteHeapProfile(f) if err != nil { panic(err) } err = f.Close() if err != nil { panic(err) } } } m.marks = append(m.marks, m.curMark) m.curMark = nil } // shouldPProf returns true if we should be doing pprof runs. func (m *Metrics) shouldPProf() bool { return m != nil && len(m.filebase) > 0 } // makeBenchString makes a benchmark string consumable by Go's benchmarking tools. func makeBenchString(name string) string { needCap := true ret := []rune("Benchmark") for _, r := range name { if unicode.IsSpace(r) { needCap = true continue } if needCap { r = unicode.ToUpper(r) needCap = false } ret = append(ret, r) } return string(ret) } func makePProfFilename(filebase, name, typ string) string { return fmt.Sprintf("%s_%s.%s", filebase, makeBenchString(name), typ) }