-
Notifications
You must be signed in to change notification settings - Fork 17
/
timer.go
311 lines (290 loc) · 9.54 KB
/
timer.go
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
// Copyright 2015 The Vanadium 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 timing implements utilities for tracking timing information.
package timing
import (
"bytes"
"fmt"
"io"
"strings"
"time"
)
// nowFunc is used rather than direct calls to time.Now to allow tests to inject
// different clock functions.
var nowFunc = time.Now
// InvalidDuration is set on Interval.End to indicate that the end hasn't been
// set yet; i.e. the interval is open.
const InvalidDuration = time.Duration(-1 << 63)
// Interval represents a named time interval. The start and end time of the
// interval are represented as durations from a fixed "zero" time, rather than
// time.Time, for a more compact and faster implementation.
type Interval struct {
Name string
Depth int
Start, End time.Duration
}
// Timer provides support for tracking a tree of strictly hierarchical time
// intervals. If you need to track overlapping time intervals, simply use
// separate Timers.
//
// Timer maintains a notion of a current interval, initialized to the root. The
// tree of intervals is constructed by push and pop operations, which add and
// update intervals to the tree, while updating the currently referenced
// interval. Finish should be called to finish all timing.
type Timer struct {
Zero time.Time // Absolute start time of the timer.
Intervals []Interval // List of intervals, in depth-first order.
// The stack holds the path through the interval tree leading to the current
// interval. This makes it easy to determine the current interval, as well as
// pop up to the parent interval. The root is never held in the stack.
stack []int
}
// NewTimer returns a new Timer, with the root interval set to the given name.
func NewTimer(name string) *Timer {
return &Timer{
Intervals: []Interval{{
Name: name,
End: InvalidDuration,
}},
Zero: nowFunc(),
}
}
// Push appends a child with the given name and an open interval to current, and
// updates the current interval to refer to the newly created child.
func (t *Timer) Push(name string) {
depth := len(t.stack)
if depth == 0 {
// Unset the root end time, to handle Push after Finish.
t.Intervals[0].End = InvalidDuration
}
t.Intervals = append(t.Intervals, Interval{
Name: name,
Depth: depth + 1,
Start: t.Now(),
End: InvalidDuration,
})
t.stack = append(t.stack, len(t.Intervals)-1)
}
// Pop closes the current interval, and updates the current interval to refer to
// its parent. Pop does nothing if the current interval is the root.
func (t *Timer) Pop() {
if last := len(t.stack) - 1; last >= 0 {
t.Intervals[t.stack[last]].End = t.Now()
t.stack = t.stack[:last]
}
}
// Finish finishes all timing, closing all intervals including the root.
func (t *Timer) Finish() {
end := t.Now()
t.Intervals[0].End = end
for _, index := range t.stack {
t.Intervals[index].End = end
}
t.stack = t.stack[:0]
}
// Now returns the time now relative to timer.Zero.
func (t *Timer) Now() time.Duration {
return nowFunc().Sub(t.Zero)
}
// String returns a formatted string describing the tree of time intervals.
func (t *Timer) String() string {
var buf bytes.Buffer
IntervalPrinter{Zero: t.Zero}.Print(&buf, t.Intervals, t.Now())
return buf.String()
}
// IntervalPrinter is a pretty-printer for Intervals. Example output:
//
// 00:00:01.000 root 98.000s 00:01:39.000
// 00:00:01.000 * 9.000s 00:00:10.000
// 00:00:10.000 foo 45.000s 00:00:55.000
// 00:00:10.000 * 5.000s 00:00:15.000
// 00:00:15.000 foo1 22.000s 00:00:37.000
// 00:00:37.000 foo2 18.000s 00:00:55.000
// 00:00:55.000 bar 25.000s 00:01:20.000
// 00:01:20.000 baz 19.000s 00:01:39.000
type IntervalPrinter struct {
// Zero is the absolute start time to use for printing; all interval times are
// computed relative to the zero time. Typically this is set to Timer.Zero to
// print actual timestamps, or time.Time{} to print relative timestamps
// starting at the root.
Zero time.Time
// TimeFormat is passed to time.Format to format the start and end times.
// Defaults to "15:04:05.000" if the value is empty.
TimeFormat string
// Indent is the number of spaces to indent each successive depth in the tree.
// Defaults to 3 spaces if the value is 0; set to a negative value for no
// indent.
Indent int
// MinGap is the minimum duration for gaps to be shown between successive
// entries; only gaps that are larger than this threshold will be shown.
// Defaults to 1 millisecond if the value is 0; set to a negative duration to
// show all gaps.
MinGap time.Duration
}
// Print writes formatted output to w representing the given intervals. The
// intervals must be in depth-first order; i.e. any slice or subslice of
// intervals produced by Timer are valid.
//
// The time now is used as the end time for any open intervals, and is
// represented as a duration from the zero time for the intervals;
// e.g. use Timer.Now() for intervals collected by the Timer.
func (p IntervalPrinter) Print(w io.Writer, intervals []Interval, now time.Duration) error {
if len(intervals) == 0 {
return nil
}
// Set default options for zero fields.
if p.TimeFormat == "" {
p.TimeFormat = "15:04:05.000"
}
switch {
case p.Indent < 0:
p.Indent = 0
case p.Indent == 0:
p.Indent = 3
}
switch {
case p.MinGap < 0:
p.MinGap = InvalidDuration
case p.MinGap == 0:
p.MinGap = time.Millisecond
}
printer := printer{
IntervalPrinter: p,
w: w,
now: now,
intervals: intervals,
}
return printer.print()
}
type printer struct {
IntervalPrinter
w io.Writer
now time.Duration
intervals []Interval
stack []int
// Stats collected to help formatting.
nowLabel string
depthMin int
depthRange int
nameWidth int
durWidth int
}
func (p *printer) print() error {
p.stack = make([]int, 1)
p.collectStats()
return p.walkIntervals(p.printRow)
}
// nolint: gocyclo
func (p *printer) walkIntervals(fn func(name string, start, end time.Duration, depth int) error) error {
stack := p.stack[:1]
stack[0] = 0
prev := Interval{"", p.intervals[0].Depth, 0, 0}
for index, i := range p.intervals {
for i.Depth < prev.Depth && len(stack) > 1 {
// Handle the normal case for gaps based on pops, where we have a full
// subtree. The gap end is based on the end of the parent, which is the
// new previous interval.
parent := p.intervals[stack[len(stack)-2]]
start, end := prev.End, parent.End
if gap := end - start; gap >= p.MinGap {
if err := fn("*", start, end, prev.Depth); err != nil {
return err
}
}
stack = stack[:len(stack)-1]
prev = parent
}
switch {
case i.Depth < prev.Depth && len(stack) == 1:
// Handle a corner-case for gaps based on pops, where we have a partial
// subtree. The gap end is based on the start of the current interval,
// and we update the stack to start with the current interval.
start, end := prev.End, i.Start
if gap := end - start; gap >= p.MinGap {
if err := fn("*", start, end, prev.Depth); err != nil {
return err
}
}
stack[0] = index
case i.Depth == prev.Depth:
// Handle the regular case for gaps based on pop/push siblings.
start, end := prev.End, i.Start
if gap := end - start; gap >= p.MinGap {
if err := fn("*", start, end, i.Depth); err != nil {
return err
}
}
stack[len(stack)-1] = index
case i.Depth > prev.Depth:
// Handle the regular case for gaps based on push children.
start, end := prev.Start, i.Start
if gap := end - start; gap >= p.MinGap {
if err := fn("*", start, end, i.Depth); err != nil {
return err
}
}
stack = append(stack, index)
}
// Visit the current interval.
if err := fn(i.Name, i.Start, i.End, i.Depth); err != nil {
return err
}
prev = i
}
// Handle leftover gaps (based on pops), if any exist.
for len(stack) > 1 {
parent := p.intervals[stack[len(stack)-2]]
start, end := prev.End, parent.End
if gap := end - start; gap >= p.MinGap {
if err := fn("*", start, end, prev.Depth); err != nil {
return err
}
}
stack = stack[:len(stack)-1]
prev = parent
}
p.stack = stack[:0]
return nil
}
func (p *printer) printRow(name string, start, end time.Duration, depth int) error {
depth -= p.depthMin
pad := strings.Repeat(" ", p.Indent*depth)
pad2 := strings.Repeat(" ", p.Indent*(p.depthRange-depth))
startStr := p.Zero.Add(start).Format(p.TimeFormat)
endStr, dur := p.nowLabel, p.now-start
if end != InvalidDuration {
endStr, dur = p.Zero.Add(end).Format(p.TimeFormat), end-start
}
_, err := fmt.Fprintf(p.w, "%s %-*s %s%*.3fs%s %s\n", startStr, p.nameWidth, pad+name, pad, p.durWidth, float64(dur)/float64(time.Second), pad2, endStr)
return err
}
func (p *printer) collectStats() {
p.nowLabel = strings.Repeat("-", len(p.TimeFormat)-3) + "now"
depthMin, depthMax := p.intervals[0].Depth, p.intervals[0].Depth
for _, i := range p.intervals[1:] {
if x := i.Depth; x < depthMin {
depthMin = x
}
if x := i.Depth; x > depthMax {
depthMax = x
}
}
p.depthMin = depthMin
p.depthRange = depthMax - depthMin
var durMax time.Duration
p.walkIntervals(func(name string, start, end time.Duration, depth int) error {
if x := len(name) + p.Indent*(depth-p.depthMin); x > p.nameWidth {
p.nameWidth = x
}
dur := p.now - start
if end != InvalidDuration {
dur = end - start
}
if dur > durMax {
durMax = dur
}
return nil
})
p.durWidth = len(fmt.Sprintf("%.3f", float64(durMax)/float64(time.Second)))
}