blob: f48ac93699b816a3f43de3e527a45350f74bb2ba [file] [log] [blame]
// Copyright 2016 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 base
import (
"fmt"
"io"
"strings"
"time"
)
var Timer Timings
// Timings collects the execution times of labeled phases
// which are added through a sequence of Start/Stop calls.
// Events may be associated with each phase via AddEvent.
type Timings struct {
list []timestamp
events map[int][]*event // lazily allocated
}
type timestamp struct {
time time.Time
label string
start bool
}
type event struct {
size int64 // count or amount of data processed (allocations, data size, lines, funcs, ...)
unit string // unit of size measure (count, MB, lines, funcs, ...)
}
func (t *Timings) append(labels []string, start bool) {
t.list = append(t.list, timestamp{time.Now(), strings.Join(labels, ":"), start})
}
// Start marks the beginning of a new phase and implicitly stops the previous phase.
// The phase name is the colon-separated concatenation of the labels.
func (t *Timings) Start(labels ...string) {
t.append(labels, true)
}
// Stop marks the end of a phase and implicitly starts a new phase.
// The labels are added to the labels of the ended phase.
func (t *Timings) Stop(labels ...string) {
t.append(labels, false)
}
// AddEvent associates an event, i.e., a count, or an amount of data,
// with the most recently started or stopped phase; or the very first
// phase if Start or Stop hasn't been called yet. The unit specifies
// the unit of measurement (e.g., MB, lines, no. of funcs, etc.).
func (t *Timings) AddEvent(size int64, unit string) {
m := t.events
if m == nil {
m = make(map[int][]*event)
t.events = m
}
i := len(t.list)
if i > 0 {
i--
}
m[i] = append(m[i], &event{size, unit})
}
// Write prints the phase times to w.
// The prefix is printed at the start of each line.
func (t *Timings) Write(w io.Writer, prefix string) {
if len(t.list) > 0 {
var lines lines
// group of phases with shared non-empty label prefix
var group struct {
label string // label prefix
tot time.Duration // accumulated phase time
size int // number of phases collected in group
}
// accumulated time between Stop/Start timestamps
var unaccounted time.Duration
// process Start/Stop timestamps
pt := &t.list[0] // previous timestamp
tot := t.list[len(t.list)-1].time.Sub(pt.time)
for i := 1; i < len(t.list); i++ {
qt := &t.list[i] // current timestamp
dt := qt.time.Sub(pt.time)
var label string
var events []*event
if pt.start {
// previous phase started
label = pt.label
events = t.events[i-1]
if qt.start {
// start implicitly ended previous phase; nothing to do
} else {
// stop ended previous phase; append stop labels, if any
if qt.label != "" {
label += ":" + qt.label
}
// events associated with stop replace prior events
if e := t.events[i]; e != nil {
events = e
}
}
} else {
// previous phase stopped
if qt.start {
// between a stopped and started phase; unaccounted time
unaccounted += dt
} else {
// previous stop implicitly started current phase
label = qt.label
events = t.events[i]
}
}
if label != "" {
// add phase to existing group, or start a new group
l := commonPrefix(group.label, label)
if group.size == 1 && l != "" || group.size > 1 && l == group.label {
// add to existing group
group.label = l
group.tot += dt
group.size++
} else {
// start a new group
if group.size > 1 {
lines.add(prefix+group.label+"subtotal", 1, group.tot, tot, nil)
}
group.label = label
group.tot = dt
group.size = 1
}
// write phase
lines.add(prefix+label, 1, dt, tot, events)
}
pt = qt
}
if group.size > 1 {
lines.add(prefix+group.label+"subtotal", 1, group.tot, tot, nil)
}
if unaccounted != 0 {
lines.add(prefix+"unaccounted", 1, unaccounted, tot, nil)
}
lines.add(prefix+"total", 1, tot, tot, nil)
lines.write(w)
}
}
func commonPrefix(a, b string) string {
i := 0
for i < len(a) && i < len(b) && a[i] == b[i] {
i++
}
return a[:i]
}
type lines [][]string
func (lines *lines) add(label string, n int, dt, tot time.Duration, events []*event) {
var line []string
add := func(format string, args ...interface{}) {
line = append(line, fmt.Sprintf(format, args...))
}
add("%s", label)
add(" %d", n)
add(" %d ns/op", dt)
add(" %.2f %%", float64(dt)/float64(tot)*100)
for _, e := range events {
add(" %d", e.size)
add(" %s", e.unit)
add(" %d", int64(float64(e.size)/dt.Seconds()+0.5))
add(" %s/s", e.unit)
}
*lines = append(*lines, line)
}
func (lines lines) write(w io.Writer) {
// determine column widths and contents
var widths []int
var number []bool
for _, line := range lines {
for i, col := range line {
if i < len(widths) {
if len(col) > widths[i] {
widths[i] = len(col)
}
} else {
widths = append(widths, len(col))
number = append(number, isnumber(col)) // first line determines column contents
}
}
}
// make column widths a multiple of align for more stable output
const align = 1 // set to a value > 1 to enable
if align > 1 {
for i, w := range widths {
w += align - 1
widths[i] = w - w%align
}
}
// print lines taking column widths and contents into account
for _, line := range lines {
for i, col := range line {
format := "%-*s"
if number[i] {
format = "%*s" // numbers are right-aligned
}
fmt.Fprintf(w, format, widths[i], col)
}
fmt.Fprintln(w)
}
}
func isnumber(s string) bool {
for _, ch := range s {
if ch <= ' ' {
continue // ignore leading whitespace
}
return '0' <= ch && ch <= '9' || ch == '.' || ch == '-' || ch == '+'
}
return false
}