lib: Added v.io/x/lib/timing package.

Adds the timing package, which provides utilities for collecting
trees of timing information and dumping it with a nice format.
An example of the output format:

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

Also add this support to the cmdline package via the -time flag,
and add some preliminary timings for "jiri update".

MultiPart: 3/4

Change-Id: Ic22476e7b75ad31756eec8b6554991dd885b7632
diff --git a/cmdline/.api b/cmdline/.api
index 3f3d2df..1d867d0 100644
--- a/cmdline/.api
+++ b/cmdline/.api
@@ -5,6 +5,8 @@
 pkg cmdline, func Main(*Command)
 pkg cmdline, func Parse(*Command, *Env, []string) (Runner, []string, error)
 pkg cmdline, func ParseAndRun(*Command, *Env, []string) error
+pkg cmdline, method (*Env) TimerPop()
+pkg cmdline, method (*Env) TimerPush(string)
 pkg cmdline, method (*Env) UsageErrorf(string, ...interface{}) error
 pkg cmdline, method (ErrExitCode) Error() string
 pkg cmdline, method (RunnerFunc) Run(*Env, []string) error
@@ -23,6 +25,7 @@
 pkg cmdline, type Env struct, Stderr io.Writer
 pkg cmdline, type Env struct, Stdin io.Reader
 pkg cmdline, type Env struct, Stdout io.Writer
+pkg cmdline, type Env struct, Timer timing.Timer
 pkg cmdline, type Env struct, Usage func(*Env, io.Writer)
 pkg cmdline, type Env struct, Vars map[string]string
 pkg cmdline, type ErrExitCode int
diff --git a/cmdline/cmdline.go b/cmdline/cmdline.go
index 1286f55..4194803 100644
--- a/cmdline/cmdline.go
+++ b/cmdline/cmdline.go
@@ -53,6 +53,7 @@
 
 	"v.io/x/lib/envvar"
 	_ "v.io/x/lib/metadata" // for the -metadata flag
+	"v.io/x/lib/timing"
 )
 
 // Command represents a single command in a command-line program.  A program
@@ -121,9 +122,16 @@
 func Main(root *Command) {
 	env := EnvFromOS()
 	err := ParseAndRun(root, env, os.Args[1:])
-	os.Exit(ExitCode(err, env.Stderr))
+	code := ExitCode(err, env.Stderr)
+	if *flagTime && env.Timer != nil {
+		env.Timer.Finish()
+		timing.IntervalPrinter{}.Print(env.Stderr, env.Timer.Root())
+	}
+	os.Exit(code)
 }
 
+var flagTime = flag.Bool("time", false, "Dump timing information to stderr before exiting the program.")
+
 // Parse parses args against the command tree rooted at root down to a leaf
 // command.  A single path through the command tree is traversed, based on the
 // sub-commands specified in args.  Global and command-specific flags are parsed
@@ -156,6 +164,8 @@
 // Parse merges root flags into flag.CommandLine and sets ContinueOnError, so
 // that subsequent calls to flag.Parsed return true.
 func Parse(root *Command, env *Env, args []string) (Runner, []string, error) {
+	env.TimerPush("cmdline parse")
+	defer env.TimerPop()
 	if globalFlags == nil {
 		// Initialize our global flags to a cleaned copy.  We don't want the merging
 		// in parseFlags to contaminate the global flags, even if Parse is called
@@ -203,6 +213,8 @@
 	if err != nil {
 		return err
 	}
+	env.TimerPush("cmdline run")
+	defer env.TimerPop()
 	return runner.Run(env, args)
 }
 
@@ -337,7 +349,7 @@
 	if cmd.LookPath {
 		// Look for a matching executable in PATH.
 		subCmd := cmd.Name + "-" + subName
-		if lookPath(subCmd, env.pathDirs()) {
+		if lookPath(env, subCmd) {
 			return binaryRunner{subCmd, cmdPath}, subArgs, nil
 		}
 	}
@@ -464,6 +476,8 @@
 }
 
 func (b binaryRunner) Run(env *Env, args []string) error {
+	env.TimerPush("run " + b.subCmd)
+	defer env.TimerPop()
 	vars := envvar.CopyMap(env.Vars)
 	vars["CMDLINE_PREFIX"] = b.cmdPath
 	cmd := exec.Command(b.subCmd, args...)
@@ -481,10 +495,12 @@
 	return err
 }
 
-// lookPath returns a boolean that indicates whether executable <name>
-// can be found in any of the given directories.
-func lookPath(name string, dirs []string) bool {
-	for _, dir := range dirs {
+// lookPath returns true iff an executable with the given name can be found in
+// any of the PATH directories.
+func lookPath(env *Env, name string) bool {
+	env.TimerPush("lookpath " + name)
+	defer env.TimerPop()
+	for _, dir := range env.pathDirs() {
 		fileInfos, err := ioutil.ReadDir(dir)
 		if err != nil {
 			continue
@@ -501,11 +517,13 @@
 	return false
 }
 
-// lookPathAll returns a deduped list of all executables found in the given
-// directories whose name starts with "<name>-", and where the name doesn't
-// match the given seen set.  The seen set may be mutated by this function.
-func lookPathAll(name string, dirs []string, seen map[string]bool) (result []string) {
-	for _, dir := range dirs {
+// lookPathAll returns a deduped list of all executables found in the PATH
+// directories whose name starts with "name-", and where the name doesn't match
+// the given seen set.  The seen set may be mutated by this function.
+func lookPathAll(env *Env, name string, seen map[string]bool) (result []string) {
+	env.TimerPush("lookpathall " + name)
+	defer env.TimerPop()
+	for _, dir := range env.pathDirs() {
 		fileInfos, err := ioutil.ReadDir(dir)
 		if err != nil {
 			continue
diff --git a/cmdline/cmdline_test.go b/cmdline/cmdline_test.go
index dcc446e..2df7ea7 100644
--- a/cmdline/cmdline_test.go
+++ b/cmdline/cmdline_test.go
@@ -2770,6 +2770,8 @@
 The global flags are:
  -metadata=<just specify -metadata to activate>
    Displays metadata for the program and exits.
+ -time=false
+   Dump timing information to stderr before exiting the program.
 `,
 		},
 		{
@@ -2788,6 +2790,8 @@
 The global flags are:
  -metadata=<just specify -metadata to activate>
    Displays metadata for the program and exits.
+ -time=false
+   Dump timing information to stderr before exiting the program.
 `,
 		},
 		{
diff --git a/cmdline/env.go b/cmdline/env.go
index a669fd3..5a849f0 100644
--- a/cmdline/env.go
+++ b/cmdline/env.go
@@ -13,6 +13,7 @@
 
 	"v.io/x/lib/envvar"
 	"v.io/x/lib/textutil"
+	"v.io/x/lib/timing"
 )
 
 // EnvFromOS returns a new environment based on the operating system.
@@ -22,6 +23,7 @@
 		Stdout: os.Stdout,
 		Stderr: os.Stderr,
 		Vars:   envvar.SliceToMap(os.Environ()),
+		Timer:  timing.NewFullTimer("root"),
 	}
 }
 
@@ -33,6 +35,7 @@
 	Stdout io.Writer
 	Stderr io.Writer
 	Vars   map[string]string // Environment variables
+	Timer  timing.Timer
 
 	// Usage is a function that prints usage information to w.  Typically set by
 	// calls to Main or Parse to print usage of the leaf command.
@@ -46,7 +49,20 @@
 	return usageErrorf(e, e.Usage, format, args...)
 }
 
-// Clone creates a deep copy of Env.
+// TimerPush calls e.Timer.Push(name), only if the Timer is non-nil.
+func (e *Env) TimerPush(name string) {
+	if e.Timer != nil {
+		e.Timer.Push(name)
+	}
+}
+
+// TimerPop calls e.Timer.Pop(), only if the Timer is non-nil.
+func (e *Env) TimerPop() {
+	if e.Timer != nil {
+		e.Timer.Pop()
+	}
+}
+
 func (e *Env) clone() *Env {
 	return &Env{
 		Stdin:  e.Stdin,
@@ -54,6 +70,7 @@
 		Stderr: e.Stderr,
 		Vars:   envvar.CopyMap(e.Vars),
 		Usage:  e.Usage,
+		Timer:  e.Timer, // use the same timer for all operations
 	}
 }
 
diff --git a/cmdline/help.go b/cmdline/help.go
index 9619e5f..6ee2879 100644
--- a/cmdline/help.go
+++ b/cmdline/help.go
@@ -126,7 +126,7 @@
 	if cmd.LookPath {
 		// Look for a matching executable in PATH.
 		extName := cmd.Name + "-" + subName
-		if lookPath(extName, env.pathDirs()) {
+		if lookPath(env, extName) {
 			runner := binaryRunner{extName, cmdPath}
 			envCopy := env.clone()
 			envCopy.Vars["CMDLINE_STYLE"] = config.style.String()
@@ -225,7 +225,7 @@
 		usageAll(w, env, append(path, help), config, false)
 	}
 	if cmd.LookPath {
-		extNames := lookPathAll(cmd.Name, env.pathDirs(), cmd.subNames())
+		extNames := lookPathAll(env, cmd.Name, cmd.subNames())
 		for _, extName := range extNames {
 			runner := binaryRunner{extName, cmdPath}
 			var buffer bytes.Buffer
@@ -277,6 +277,8 @@
 // avoid printing redundant information (e.g. help command, global flags).
 func usage(w *textutil.LineWriter, env *Env, path []*Command, config *helpConfig, firstCall bool) {
 	cmd, cmdPath := path[len(path)-1], pathName(config.prefix, path)
+	env.TimerPush("usage " + cmdPath)
+	defer env.TimerPop()
 	if config.style == styleShort {
 		fmt.Fprintln(w, cmd.Short)
 		return
@@ -305,7 +307,7 @@
 	}
 	var extChildren []string
 	if cmd.LookPath {
-		extChildren = lookPathAll(cmd.Name, env.pathDirs(), cmd.subNames())
+		extChildren = lookPathAll(env, cmd.Name, cmd.subNames())
 	}
 	hasSubcommands := len(cmd.Children) > 0 || len(extChildren) > 0
 	if hasSubcommands {
diff --git a/timing/.api b/timing/.api
new file mode 100644
index 0000000..6356f10
--- /dev/null
+++ b/timing/.api
@@ -0,0 +1,45 @@
+pkg timing, func IntervalDuration(Interval, time.Time) time.Duration
+pkg timing, func NewCompactTimer(string) *CompactTimer
+pkg timing, func NewFullTimer(string) *FullTimer
+pkg timing, method (*CompactTimer) Finish()
+pkg timing, method (*CompactTimer) Pop()
+pkg timing, method (*CompactTimer) Push(string)
+pkg timing, method (*CompactTimer) Root() Interval
+pkg timing, method (*CompactTimer) String() string
+pkg timing, method (*FullTimer) Finish()
+pkg timing, method (*FullTimer) Pop()
+pkg timing, method (*FullTimer) Push(string)
+pkg timing, method (*FullTimer) Root() Interval
+pkg timing, method (*FullTimer) String() string
+pkg timing, method (FullInterval) Child(int) Interval
+pkg timing, method (FullInterval) End() time.Time
+pkg timing, method (FullInterval) Name() string
+pkg timing, method (FullInterval) NumChild() int
+pkg timing, method (FullInterval) Start() time.Time
+pkg timing, method (FullInterval) String() string
+pkg timing, method (IntervalPrinter) Print(io.Writer, Interval) error
+pkg timing, type CompactTimer struct
+pkg timing, type FullInterval struct
+pkg timing, type FullInterval struct, Children []FullInterval
+pkg timing, type FullInterval struct, EndTime time.Time
+pkg timing, type FullInterval struct, Label string
+pkg timing, type FullInterval struct, StartTime time.Time
+pkg timing, type FullTimer struct
+pkg timing, type FullTimer struct, FullRoot FullInterval
+pkg timing, type Interval interface { Child, End, Name, NumChild, Start, String }
+pkg timing, type Interval interface, Child(int) Interval
+pkg timing, type Interval interface, End() time.Time
+pkg timing, type Interval interface, Name() string
+pkg timing, type Interval interface, NumChild() int
+pkg timing, type Interval interface, Start() time.Time
+pkg timing, type Interval interface, String() string
+pkg timing, type IntervalPrinter struct
+pkg timing, type IntervalPrinter struct, Indent int
+pkg timing, type IntervalPrinter struct, MinGap time.Duration
+pkg timing, type IntervalPrinter struct, TimeFormat string
+pkg timing, type Timer interface { Finish, Pop, Push, Root, String }
+pkg timing, type Timer interface, Finish()
+pkg timing, type Timer interface, Pop()
+pkg timing, type Timer interface, Push(string)
+pkg timing, type Timer interface, Root() Interval
+pkg timing, type Timer interface, String() string
diff --git a/timing/compact_timer.go b/timing/compact_timer.go
new file mode 100644
index 0000000..6faf513
--- /dev/null
+++ b/timing/compact_timer.go
@@ -0,0 +1,173 @@
+// 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
+
+import (
+	"bytes"
+	"time"
+)
+
+// CompactTimer implements Timer with a memory-efficient implementation.
+// Timestamps are only recorded on calls to Push and Finish; the assumption is
+// that there is typically a very small delay between a call to Pop and a
+// subsequent call to Push or Finish, so the Pop time doesn't need to be
+// recorded.
+//
+// CompactTimer performs fewer memory allocations and is faster than FullTimer
+// for push and pop, but doesn't collect actual end times.
+type CompactTimer struct {
+	points []compactPoint
+	depth  int
+	zero   time.Time
+}
+
+// compactPoint represents a single interval, taking advantage of the fact that
+// all intervals are required to be disjoint, and also assuming that all
+// intervals are adjacent to each other.  Thus instead of holding both a start
+// and end time, it only holds a single NextStart time, represented as a delta
+// from the zero time for further space savings.
+//
+// The NextStart time represents the time that the next interval starts.  If the
+// next interval is at the same or smaller depth, this is also the end time for
+// the current interval.
+//
+// The interesting logic is in Push; we rely on each Push call to update
+// NextStart for the previous entry, and to create a new entry.  This also lets
+// us keep a single slice of points in CompactTimer, which is more memory
+// efficient than the tree of slices used in FullInterval.
+//
+// The trade-off we make for the memory efficiency and simple Push/Pop/Finish
+// logic is that we need to do more work in compactInterval to piece-together
+// the resulting time intervals.
+type compactPoint struct {
+	Label     string
+	Depth     int
+	NextStart time.Duration
+}
+
+const invalidNext = time.Duration(-1 << 63)
+
+// NewCompactTimer returns a new CompactTimer, with the root interval
+// initialized to the given name.
+func NewCompactTimer(name string) *CompactTimer {
+	return &CompactTimer{
+		points: []compactPoint{{
+			Label:     name,
+			Depth:     0,
+			NextStart: invalidNext,
+		}},
+		zero: nowFunc(),
+	}
+}
+
+// Push implements the Timer.Push method.
+func (t *CompactTimer) Push(name string) {
+	t.depth++
+	t.points[len(t.points)-1].NextStart = nowFunc().Sub(t.zero)
+	t.points = append(t.points, compactPoint{
+		Label:     name,
+		Depth:     t.depth,
+		NextStart: invalidNext,
+	})
+}
+
+// Pop implements the Timer.Pop method.
+func (t *CompactTimer) Pop() {
+	if t.depth > 0 {
+		t.depth--
+	}
+}
+
+// Finish implements the Timer.Finish method.
+func (t *CompactTimer) Finish() {
+	t.depth = 0
+	t.points[len(t.points)-1].NextStart = nowFunc().Sub(t.zero)
+}
+
+// String returns a formatted string describing the tree of time intervals.
+func (t *CompactTimer) String() string {
+	return t.Root().String()
+}
+
+// Root returns the root interval.
+func (t *CompactTimer) Root() Interval {
+	return compactInterval{
+		points:   t.points,
+		children: computeCompactChildren(t.points),
+		zero:     t.zero,
+		start:    t.zero,
+	}
+}
+
+// compactInterval implements Interval using the underlying slice of points
+// recorded by CompactTimer.  The interesting method is Child, where we
+// re-compute the points to use for the next compactInterval.
+type compactInterval struct {
+	points      []compactPoint
+	children    []int
+	zero, start time.Time
+}
+
+// computeCompactChildren returns the indices in points that are immediate
+// children of the first point.  Points must be a subtree rooted at the first
+// point; the depth of every point in points[1:] must be greater than the depth
+// of the first point.
+func computeCompactChildren(points []compactPoint) (children []int) {
+	if len(points) < 2 {
+		return
+	}
+	target := points[0].Depth + 1
+	for index := 1; index < len(points); index++ {
+		if point := points[index]; point.Depth == target {
+			children = append(children, index)
+		}
+	}
+	return
+}
+
+// Name returns the name of the interval.
+func (i compactInterval) Name() string { return i.points[0].Label }
+
+// Start returns the start time of the interval.
+func (i compactInterval) Start() time.Time { return i.start }
+
+// End returns the end time of the interval, or zero if the interval hasn't
+// ended yet (i.e. it's still open).
+func (i compactInterval) End() time.Time {
+	if next := i.points[len(i.points)-1].NextStart; next != invalidNext {
+		return i.zero.Add(next)
+	}
+	return time.Time{}
+}
+
+// NumChild returns the number of children contained in this interval.
+func (i compactInterval) NumChild() int { return len(i.children) }
+
+// Child returns the child interval at the given index.  Valid children are in
+// the range [0, NumChild).
+func (i compactInterval) Child(index int) Interval {
+	beg := i.children[index]
+	var end int
+	if index+1 < len(i.children) {
+		end = i.children[index+1]
+	} else {
+		end = len(i.points)
+	}
+	points := i.points[beg:end]
+	return compactInterval{
+		points:   points,
+		children: computeCompactChildren(points),
+		zero:     i.zero,
+		start:    i.zero.Add(i.points[beg-1].NextStart),
+	}
+}
+
+// String returns a formatted string describing the tree starting with the
+// given interval.
+func (i compactInterval) String() string {
+	var buf bytes.Buffer
+	IntervalPrinter{}.Print(&buf, i)
+	return buf.String()
+}
diff --git a/timing/full_timer.go b/timing/full_timer.go
new file mode 100644
index 0000000..20f18e0
--- /dev/null
+++ b/timing/full_timer.go
@@ -0,0 +1,117 @@
+// 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
+
+import (
+	"bytes"
+	"time"
+)
+
+// FullInterval implements Interval with a direct data mapping; each
+// FullInterval represents a node in the FullTimer tree.
+type FullInterval struct {
+	Label              string
+	StartTime, EndTime time.Time
+	Children           []FullInterval
+}
+
+// Name returns the name of the interval.
+func (i FullInterval) Name() string { return i.Label }
+
+// Start returns the start time of the interval.
+func (i FullInterval) Start() time.Time { return i.StartTime }
+
+// End returns the end time of the interval, or zero if the interval hasn't
+// ended yet (i.e. it's still open).
+func (i FullInterval) End() time.Time { return i.EndTime }
+
+// NumChild returns the number of children contained in this interval.
+func (i FullInterval) NumChild() int { return len(i.Children) }
+
+// Child returns the child interval at the given index.  Valid children are in
+// the range [0, NumChild).
+func (i FullInterval) Child(index int) Interval { return i.Children[index] }
+
+// String returns a formatted string describing the tree starting with the
+// given interval.
+func (i FullInterval) String() string {
+	var buf bytes.Buffer
+	IntervalPrinter{}.Print(&buf, i)
+	return buf.String()
+}
+
+// FullTimer implements Timer, using FullInterval to represent the tree of
+// intervals.  Timestamps are recorded on each call to Push, Pop and Finish.
+//
+// FullTimer performs more memory allocations and is slower than CompactTimer
+// for push and pop, but collects actual start and end times for all intervals.
+type FullTimer struct {
+	// FullRoot holds the root of the time interval tree.
+	FullRoot FullInterval
+
+	// 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.
+	//
+	// There is a subtlely here, since we're keeping a stack of pointers; we must
+	// be careful not to invalidate any pointers.  E.g. given an Interval X with a
+	// child Y, we will invalidate the pointer to Y when we append to X.Children,
+	// if a new underlying array is created.  This never occurs since the stack
+	// never contains a pointer to Y when appending to X.Children.
+	stack []*FullInterval
+}
+
+// NewFullTimer returns a new FullTimer, with the root interval initialized to
+// the given name.
+func NewFullTimer(name string) *FullTimer {
+	return &FullTimer{
+		FullRoot: FullInterval{Label: name, StartTime: nowFunc()},
+	}
+}
+
+// Push implements the Timer.Push method.
+func (t *FullTimer) Push(name string) {
+	var current *FullInterval
+	if len(t.stack) == 0 {
+		// Unset the root end time, to handle Push after Finish.
+		t.FullRoot.EndTime = time.Time{}
+		current = &t.FullRoot
+	} else {
+		current = t.stack[len(t.stack)-1]
+	}
+	push := FullInterval{Label: name, StartTime: nowFunc()}
+	current.Children = append(current.Children, push)
+	t.stack = append(t.stack, &current.Children[len(current.Children)-1])
+}
+
+// Pop implements the Timer.Pop method.
+func (t *FullTimer) Pop() {
+	if len(t.stack) == 0 {
+		return // Pop on the root does nothing.
+	}
+	last := len(t.stack) - 1
+	t.stack[last].EndTime = nowFunc()
+	t.stack = t.stack[:last]
+}
+
+// Finish implements the Timer.Finish method.
+func (t *FullTimer) Finish() {
+	end := nowFunc()
+	t.FullRoot.EndTime = end
+	for _, interval := range t.stack {
+		interval.EndTime = end
+	}
+	t.stack = t.stack[:0]
+}
+
+// Root returns the root interval.
+func (t *FullTimer) Root() Interval {
+	return t.FullRoot
+}
+
+// String returns a formatted string describing the tree of time intervals.
+func (t *FullTimer) String() string {
+	return t.FullRoot.String()
+}
diff --git a/timing/timer.go b/timing/timer.go
new file mode 100644
index 0000000..2819711
--- /dev/null
+++ b/timing/timer.go
@@ -0,0 +1,219 @@
+// 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)
+	}
+}
diff --git a/timing/timer_test.go b/timing/timer_test.go
new file mode 100644
index 0000000..122587d
--- /dev/null
+++ b/timing/timer_test.go
@@ -0,0 +1,522 @@
+// 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
+
+import (
+	"bytes"
+	"fmt"
+	"math/rand"
+	"strings"
+	"testing"
+	"time"
+)
+
+func sec(d int) time.Time {
+	return time.Time{}.Add(time.Second * time.Duration(d))
+}
+
+// fakeNow is a simulated clock where now is set manually.
+type fakeNow struct{ now int }
+
+func (f *fakeNow) Now() time.Time { return sec(f.now) }
+
+// stepNow is a simulated clock where now increments in 1 second steps.
+type stepNow struct{ now int }
+
+func (s *stepNow) Now() time.Time {
+	s.now++
+	return sec(s.now)
+}
+
+type (
+	// op represents the operations that can be performed on a Timer, with a fake
+	// clock.  This makes it easy to construct test cases.
+	op interface {
+		run(f *fakeNow, t Timer)
+	}
+	push struct {
+		now  int
+		name string
+	}
+	pop struct {
+		now int
+	}
+	finish struct {
+		now int
+	}
+)
+
+func (x push) run(f *fakeNow, t Timer) {
+	f.now = x.now
+	t.Push(x.name)
+}
+func (x pop) run(f *fakeNow, t Timer) {
+	f.now = x.now
+	t.Pop()
+}
+func (x finish) run(f *fakeNow, t Timer) {
+	f.now = x.now
+	t.Finish()
+}
+
+type kind int
+
+const (
+	kindFull kind = iota
+	kindCompact
+)
+
+func (k kind) NewTimer(name string) Timer {
+	switch k {
+	case kindFull:
+		return NewFullTimer(name)
+	default:
+		return NewCompactTimer(name)
+	}
+}
+
+func (k kind) String() string {
+	switch k {
+	case kindFull:
+		return "FullTimer"
+	default:
+		return "CompactTimer"
+	}
+}
+
+func TestTimer(t *testing.T) {
+	tests := []struct {
+		ops                 []op
+		full, compact       *FullInterval
+		fullStr, compactStr string
+	}{
+		{
+			nil,
+			&FullInterval{"root", sec(1), sec(0), nil}, nil,
+			`
+00:00:01.000 root 999.000s ---------now
+`, ``,
+		},
+		{
+			[]op{pop{123}},
+			&FullInterval{"root", sec(1), sec(0), nil}, nil,
+			`
+00:00:01.000 root 999.000s ---------now
+`, ``,
+		},
+		{
+			[]op{finish{99}},
+			&FullInterval{"root", sec(1), sec(99), nil}, nil,
+			`
+00:00:01.000 root 98.000s 00:01:39.000
+`, ``,
+		},
+		{
+			[]op{finish{99}, pop{123}},
+			&FullInterval{"root", sec(1), sec(99), nil}, nil,
+			`
+00:00:01.000 root 98.000s 00:01:39.000
+`, ``,
+		},
+		{
+			[]op{push{10, "abc"}},
+			&FullInterval{"root", sec(1), sec(0),
+				[]FullInterval{{"abc", sec(10), sec(0), nil}}}, nil,
+			`
+00:00:01.000 root   999.000s    ---------now
+00:00:01.000    *        9.000s 00:00:10.000
+00:00:10.000    abc    990.000s ---------now
+`, ``,
+		},
+		{
+			[]op{push{10, "abc"}, finish{99}},
+			&FullInterval{"root", sec(1), sec(99),
+				[]FullInterval{{"abc", sec(10), sec(99), nil}}}, nil,
+			`
+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    abc    89.000s 00:01:39.000
+`, ``,
+		},
+		{
+			[]op{push{10, "abc"}, pop{20}, finish{99}},
+			&FullInterval{"root", sec(1), sec(99),
+				[]FullInterval{{"abc", sec(10), sec(20), nil}}},
+			&FullInterval{"root", sec(1), sec(99),
+				[]FullInterval{{"abc", sec(10), sec(99), nil}}},
+			`
+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    abc    10.000s 00:00:20.000
+00:00:20.000    *      79.000s 00:01:39.000
+`,
+			`
+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    abc    89.000s 00:01:39.000
+`,
+		},
+		{
+			[]op{push{10, "A1"}, push{20, "A1_1"}},
+			&FullInterval{"root", sec(1), sec(0),
+				[]FullInterval{{"A1", sec(10), sec(0),
+					[]FullInterval{{"A1_1", sec(20), sec(0), nil}}}}}, nil,
+			`
+00:00:01.000 root       999.000s       ---------now
+00:00:01.000    *            9.000s    00:00:10.000
+00:00:10.000    A1         990.000s    ---------now
+00:00:10.000       *           10.000s 00:00:20.000
+00:00:20.000       A1_1       980.000s ---------now
+`, ``,
+		},
+		{
+			[]op{push{10, "A1"}, push{20, "A1_1"}, finish{99}},
+			&FullInterval{"root", sec(1), sec(99),
+				[]FullInterval{{"A1", sec(10), sec(99),
+					[]FullInterval{{"A1_1", sec(20), sec(99), nil}}}}}, nil,
+			`
+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    A1         89.000s    00:01:39.000
+00:00:10.000       *          10.000s 00:00:20.000
+00:00:20.000       A1_1       79.000s 00:01:39.000
+`, ``,
+		},
+		{
+			[]op{push{10, "A1"}, push{20, "A1_1"}, pop{30}, finish{99}},
+			&FullInterval{"root", sec(1), sec(99),
+				[]FullInterval{{"A1", sec(10), sec(99),
+					[]FullInterval{{"A1_1", sec(20), sec(30), nil}}}}},
+			&FullInterval{"root", sec(1), sec(99),
+				[]FullInterval{{"A1", sec(10), sec(99),
+					[]FullInterval{{"A1_1", sec(20), sec(99), nil}}}}},
+			`
+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    A1         89.000s    00:01:39.000
+00:00:10.000       *          10.000s 00:00:20.000
+00:00:20.000       A1_1       10.000s 00:00:30.000
+00:00:30.000       *          69.000s 00:01:39.000
+`,
+			`
+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    A1         89.000s    00:01:39.000
+00:00:10.000       *          10.000s 00:00:20.000
+00:00:20.000       A1_1       79.000s 00:01:39.000
+`,
+		},
+		{
+			[]op{push{10, "A1"}, push{20, "A1_1"}, pop{30}, pop{40}, finish{99}},
+			&FullInterval{"root", sec(1), sec(99),
+				[]FullInterval{{"A1", sec(10), sec(40),
+					[]FullInterval{{"A1_1", sec(20), sec(30), nil}}}}},
+			&FullInterval{"root", sec(1), sec(99),
+				[]FullInterval{{"A1", sec(10), sec(99),
+					[]FullInterval{{"A1_1", sec(20), sec(99), nil}}}}},
+			`
+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    A1         30.000s    00:00:40.000
+00:00:10.000       *          10.000s 00:00:20.000
+00:00:20.000       A1_1       10.000s 00:00:30.000
+00:00:30.000       *          10.000s 00:00:40.000
+00:00:40.000    *          59.000s    00:01:39.000
+`,
+			`
+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    A1         89.000s    00:01:39.000
+00:00:10.000       *          10.000s 00:00:20.000
+00:00:20.000       A1_1       79.000s 00:01:39.000
+`,
+		},
+		{
+			[]op{push{10, "A1"}, push{20, "A1_1"}, finish{30}, push{40, "B1"}},
+			&FullInterval{"root", sec(1), sec(0), []FullInterval{
+				{"A1", sec(10), sec(30), []FullInterval{
+					{"A1_1", sec(20), sec(30), nil}}},
+				{"B1", sec(40), sec(0), nil}}},
+			&FullInterval{"root", sec(1), sec(0), []FullInterval{
+				{"A1", sec(10), sec(40), []FullInterval{
+					{"A1_1", sec(20), sec(40), nil}}},
+				{"B1", sec(40), sec(0), nil}}},
+			`
+00:00:01.000 root       999.000s       ---------now
+00:00:01.000    *            9.000s    00:00:10.000
+00:00:10.000    A1          20.000s    00:00:30.000
+00:00:10.000       *           10.000s 00:00:20.000
+00:00:20.000       A1_1        10.000s 00:00:30.000
+00:00:30.000    *           10.000s    00:00:40.000
+00:00:40.000    B1         960.000s    ---------now
+`,
+			`
+00:00:01.000 root       999.000s       ---------now
+00:00:01.000    *            9.000s    00:00:10.000
+00:00:10.000    A1          30.000s    00:00:40.000
+00:00:10.000       *           10.000s 00:00:20.000
+00:00:20.000       A1_1        20.000s 00:00:40.000
+00:00:40.000    B1         960.000s    ---------now
+`,
+		},
+		{
+			[]op{push{10, "A1"}, push{20, "A1_1"}, finish{30}, push{40, "B1"}, finish{99}},
+			&FullInterval{"root", sec(1), sec(99), []FullInterval{
+				{"A1", sec(10), sec(30), []FullInterval{
+					{"A1_1", sec(20), sec(30), nil}}},
+				{"B1", sec(40), sec(99), nil}}},
+			&FullInterval{"root", sec(1), sec(99), []FullInterval{
+				{"A1", sec(10), sec(40), []FullInterval{
+					{"A1_1", sec(20), sec(40), nil}}},
+				{"B1", sec(40), sec(99), nil}}},
+			`
+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    A1         20.000s    00:00:30.000
+00:00:10.000       *          10.000s 00:00:20.000
+00:00:20.000       A1_1       10.000s 00:00:30.000
+00:00:30.000    *          10.000s    00:00:40.000
+00:00:40.000    B1         59.000s    00:01:39.000
+`,
+			`
+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    A1         30.000s    00:00:40.000
+00:00:10.000       *          10.000s 00:00:20.000
+00:00:20.000       A1_1       20.000s 00:00:40.000
+00:00:40.000    B1         59.000s    00:01:39.000
+`,
+		},
+		{
+			[]op{push{10, "foo"}, push{15, "foo1"}, pop{37}, push{37, "foo2"}, pop{55}, pop{55}, push{55, "bar"}, pop{80}, push{80, "baz"}, pop{99}, finish{99}},
+			&FullInterval{
+				"root", sec(1), sec(99), []FullInterval{
+					{"foo", sec(10), sec(55), []FullInterval{
+						{"foo1", sec(15), sec(37), nil},
+						{"foo2", sec(37), sec(55), nil},
+					}},
+					{"bar", sec(55), sec(80), nil},
+					{"baz", sec(80), sec(99), nil}}}, nil,
+			`
+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
+`, ``,
+		},
+		{
+			[]op{push{10, "foo"}, push{15, "foo1"}, pop{30}, push{37, "foo2"}, pop{50}, pop{53}, push{55, "bar"}, pop{75}, push{80, "baz"}, pop{90}, finish{99}},
+			&FullInterval{
+				"root", sec(1), sec(99), []FullInterval{
+					{"foo", sec(10), sec(53), []FullInterval{
+						{"foo1", sec(15), sec(30), nil},
+						{"foo2", sec(37), sec(50), nil},
+					}},
+					{"bar", sec(55), sec(75), nil},
+					{"baz", sec(80), sec(90), nil}}},
+			&FullInterval{
+				"root", sec(1), sec(99), []FullInterval{
+					{"foo", sec(10), sec(55), []FullInterval{
+						{"foo1", sec(15), sec(37), nil},
+						{"foo2", sec(37), sec(55), nil},
+					}},
+					{"bar", sec(55), sec(80), nil},
+					{"baz", sec(80), sec(99), nil}}},
+			`
+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        43.000s    00:00:53.000
+00:00:10.000       *           5.000s 00:00:15.000
+00:00:15.000       foo1       15.000s 00:00:30.000
+00:00:30.000       *           7.000s 00:00:37.000
+00:00:37.000       foo2       13.000s 00:00:50.000
+00:00:50.000       *           3.000s 00:00:53.000
+00:00:53.000    *           2.000s    00:00:55.000
+00:00:55.000    bar        20.000s    00:01:15.000
+00:01:15.000    *           5.000s    00:01:20.000
+00:01:20.000    baz        10.000s    00:01:30.000
+00:01:30.000    *           9.000s    00:01:39.000
+`,
+			`
+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
+`,
+		},
+	}
+	for _, test := range tests {
+		for _, kind := range []kind{kindFull, kindCompact} {
+			// Run all ops.
+			now := &fakeNow{1}
+			nowFunc = now.Now
+			timer := kind.NewTimer("root")
+			for _, op := range test.ops {
+				op.run(now, timer)
+			}
+			// Check all intervals.
+			wantRoot := test.full
+			if kind == kindCompact && test.compact != nil {
+				wantRoot = test.compact
+			}
+			name := fmt.Sprintf("%s %#v", kind.String(), test.ops)
+			expectEqualIntervals(t, name, timer.Root(), *wantRoot)
+			// Check string output.
+			now.now = 1000
+			wantStr := test.fullStr
+			if kind == kindCompact && test.compactStr != "" {
+				wantStr = test.compactStr
+			}
+			if got, want := timer.String(), strings.TrimLeft(wantStr, "\n"); got != want {
+				t.Errorf("%s GOT STRING\n%sWANT\n%s", name, got, want)
+			}
+			// Check print output hiding all gaps.
+			var buf bytes.Buffer
+			printer := IntervalPrinter{MinGap: time.Hour}
+			if err := printer.Print(&buf, timer.Root()); err != nil {
+				t.Errorf("%s got printer error: %v", name, err)
+			}
+			if got, want := buf.String(), stripGaps(wantStr); got != want {
+				t.Errorf("%s GOT PRINT\n%sWANT\n%s", name, got, want)
+			}
+		}
+	}
+	nowFunc = time.Now
+}
+
+func expectEqualIntervals(t *testing.T, name string, a, b Interval) {
+	if got, want := a.Name(), b.Name(); got != want {
+		t.Errorf("%s got name %q, want %q", name, got, want)
+	}
+	if got, want := a.Start(), b.Start(); !got.Equal(want) {
+		t.Errorf("%s got start %v, want %v", name, got, want)
+	}
+	if got, want := a.End(), b.End(); !got.Equal(want) {
+		t.Errorf("%s got end %v, want %v", name, got, want)
+	}
+	if got, want := a.NumChild(), b.NumChild(); got != want {
+		t.Errorf("%s got num child %v, want %v", name, got, want)
+		return
+	}
+	for child := 0; child < a.NumChild(); child++ {
+		expectEqualIntervals(t, name, a.Child(child), b.Child(child))
+	}
+}
+
+// stripGaps strips out leading newlines, and also strips any line with an
+// asterisk (*).  Asterisks appear in lines with gaps, as shown here:
+//    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    abc    89.000s 00:01:39.000
+func stripGaps(out string) string {
+	out = strings.TrimLeft(out, "\n")
+	var lines []string
+	for _, line := range strings.Split(out, "\n") {
+		if !strings.ContainsRune(line, '*') {
+			lines = append(lines, line)
+		}
+	}
+	return strings.Join(lines, "\n")
+}
+
+func BenchmarkFullTimerPush(b *testing.B) {
+	t := NewFullTimer("root")
+	for i := 0; i < b.N; i++ {
+		t.Push("child")
+	}
+}
+func BenchmarkCompactTimerPush(b *testing.B) {
+	t := NewCompactTimer("root")
+	for i := 0; i < b.N; i++ {
+		t.Push("child")
+	}
+}
+
+func BenchmarkFullTimerPushPop(b *testing.B) {
+	t := NewFullTimer("root")
+	for i := 0; i < b.N; i++ {
+		timerPushPop(t)
+	}
+}
+func BenchmarkCompactTimerPushPop(b *testing.B) {
+	t := NewCompactTimer("root")
+	for i := 0; i < b.N; i++ {
+		timerPushPop(t)
+	}
+}
+func timerPushPop(t Timer) {
+	t.Push("child1")
+	t.Pop()
+	t.Push("child2")
+	t.Push("child2_1")
+	t.Pop()
+	t.Push("child2_2")
+	t.Pop()
+	t.Pop()
+	t.Push("child3")
+	t.Pop()
+}
+
+var randSource = rand.NewSource(123)
+
+func BenchmarkFullTimerRandom(b *testing.B) {
+	t, rng := NewFullTimer("root"), rand.New(randSource)
+	for i := 0; i < b.N; i++ {
+		timerRandom(rng, t)
+	}
+}
+func BenchmarkCompactTimerRandom(b *testing.B) {
+	t, rng := NewCompactTimer("root"), rand.New(randSource)
+	for i := 0; i < b.N; i++ {
+		timerRandom(rng, t)
+	}
+}
+func timerRandom(rng *rand.Rand, t Timer) {
+	switch pct := rng.Intn(100); {
+	case pct < 60:
+		timerPushPop(t)
+	case pct < 90:
+		t.Push("foo")
+	case pct < 95:
+		t.Pop()
+	default:
+		t.Finish()
+	}
+}
+
+func BenchmarkFullTimerPrint(b *testing.B) {
+	now := &stepNow{0}
+	nowFunc = now.Now
+	benchTimerPrint(b, NewFullTimer("root"))
+	nowFunc = time.Now
+}
+func BenchmarkCompactTimerPrint(b *testing.B) {
+	now := &stepNow{0}
+	nowFunc = now.Now
+	benchTimerPrint(b, NewCompactTimer("root"))
+	nowFunc = time.Now
+}
+func benchTimerPrint(b *testing.B, t Timer) {
+	rng := rand.New(randSource)
+	for i := 0; i < 1000; i++ {
+		timerRandom(rng, t)
+	}
+	t.Finish() // Make sure all intervals are closed.
+	want := t.String()
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		if got := t.String(); got != want {
+			b.Fatalf("GOT\n%sWANT\n%s\nTIMER\n%#v", got, want, t)
+		}
+	}
+}