| // 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 trough 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 | |
| } |