blob: 28197119c1e815cf0e7d258ce723edfd30284756 [file] [log] [blame]
// 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 (
"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
// Interval represents a named time interval and its children. The children are
// non-overlapping and ordered from earliest to latest. The start and end time
// of a given interval always completely cover all of its children. Put another
// way, intervals form a strictly hierarchical tree.
type Interval interface {
// Name returns the name of the interval.
Name() string
// Start returns the start time of the interval.
Start() time.Time
// End returns the end time of the interval, or zero if the interval hasn't
// ended yet (i.e. it's still open).
End() time.Time
// NumChild returns the number of children contained in this interval.
NumChild() int
// Child returns the child interval at the given index. Valid children are in
// the range [0, NumChild).
Child(index int) Interval
// String returns a formatted string describing the tree starting with the
// given interval.
String() string
}
// Timer provides support for tracking a tree of 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 interface {
// 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.
Push(name string)
// 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.
Pop()
// Finish finishes all timing, closing all intervals including the root.
Finish()
// Root returns the root interval.
Root() Interval
// String returns a formatted string describing the tree of time intervals.
String() string
}
// IntervalDuration returns the elapsed time between i.start and i.end if i.end
// is nonzero, otherwise it returns the elapsed time between i.start and now.
// Now is passed in explicitly to allow the caller to use the same now time when
// computing the duration of many intervals. E.g. the same now time is used by
// the IntervalPrinter to compute the duration of all intervals, to ensure
// consistent output.
func IntervalDuration(i Interval, now time.Time) time.Duration {
start, end := i.Start(), i.End()
if end.IsZero() {
return now.Sub(start)
}
return end.Sub(start)
}
// 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 {
// 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 tree rooted at i.
func (p IntervalPrinter) Print(w io.Writer, i Interval) error {
// 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 = 0
case p.MinGap == 0:
p.MinGap = time.Millisecond
}
return p.print(w, i, p.collectPrintStats(i), i.Start(), 0)
}
func (p IntervalPrinter) print(w io.Writer, i Interval, stats *printStats, prevEnd time.Time, depth int) error {
// Print gap before children, if a gap exists.
if gap := i.Start().Sub(prevEnd); gap >= p.MinGap {
if err := p.row(w, "*", prevEnd, i.Start(), gap, stats, depth); err != nil {
return err
}
}
// Print the current interval.
if err := p.row(w, i.Name(), i.Start(), i.End(), IntervalDuration(i, stats.Now), stats, depth); err != nil {
return err
}
// Print children recursively.
for child := 0; child < i.NumChild(); child++ {
prevEnd = i.Start()
if child > 0 {
prevEnd = i.Child(child - 1).End()
}
if err := p.print(w, i.Child(child), stats, prevEnd, depth+1); err != nil {
return err
}
}
// Print gap after children, if a gap exists.
if last := i.NumChild() - 1; last >= 0 {
lastChild := i.Child(last)
if gap := i.End().Sub(lastChild.End()); gap >= p.MinGap {
if err := p.row(w, "*", lastChild.End(), i.End(), gap, stats, depth+1); err != nil {
return err
}
}
}
return nil
}
func (p IntervalPrinter) row(w io.Writer, name string, start, end time.Time, dur time.Duration, stats *printStats, depth int) error {
pad := strings.Repeat(" ", p.Indent*depth)
pad2 := strings.Repeat(" ", p.Indent*(stats.MaxDepth-depth))
endStr := stats.NowLabel
if !end.IsZero() {
endStr = end.Format(p.TimeFormat)
}
_, err := fmt.Fprintf(w, "%s %-*s %s%*.3fs%s %s\n", start.Format(p.TimeFormat), stats.NameWidth, pad+name, pad, stats.DurationWidth, float64(dur)/float64(time.Second), pad2, endStr)
return err
}
// collectPrintStats performs a walk through the tree rooted at i, collecting
// statistics along the way, to help align columns in the output.
func (p IntervalPrinter) collectPrintStats(i Interval) *printStats {
stats := &printStats{
Now: nowFunc(),
NameWidth: 1,
NowLabel: strings.Repeat("-", len(p.TimeFormat)-3) + "now",
}
stats.collect(i, p.Indent, i.Start(), 0)
dur := fmt.Sprintf("%.3f", float64(stats.MaxDuration)/float64(time.Second))
stats.DurationWidth = len(dur)
return stats
}
type printStats struct {
Now time.Time
NowLabel string
NameWidth int
MaxDuration time.Duration
DurationWidth int
MaxDepth int
}
func (s *printStats) collect(i Interval, indent int, prevEnd time.Time, depth int) {
if x := len(i.Name()) + indent*depth; x > s.NameWidth {
s.NameWidth = x
}
if x := i.Start().Sub(prevEnd); x > s.MaxDuration {
s.MaxDuration = x
}
if x := IntervalDuration(i, s.Now); x > s.MaxDuration {
s.MaxDuration = x
}
if x := depth; x > s.MaxDepth {
s.MaxDepth = x
}
for child := 0; child < i.NumChild(); child++ {
prevEnd = i.Start()
if child > 0 {
prevEnd = i.Child(child - 1).End()
}
s.collect(i.Child(child), indent, prevEnd, depth+1)
}
}