Merge "veyron/tools/mgmt,veyron/security/agent/agentd: node mgr integration test."
diff --git a/lib/stats/counter/counter.go b/lib/stats/counter/counter.go
index 4aa9db3..0dc4ec9 100644
--- a/lib/stats/counter/counter.go
+++ b/lib/stats/counter/counter.go
@@ -20,7 +20,7 @@
 
 var (
 	// Used for testing.
-	Now func() time.Time = time.Now
+	TimeNow func() time.Time = time.Now
 )
 
 const (
@@ -39,7 +39,7 @@
 
 // New returns a new Counter.
 func New() *Counter {
-	now := Now()
+	now := TimeNow()
 	c := &Counter{}
 	c.ts[hour] = newTimeSeries(now, time.Hour, time.Minute)
 	c.ts[tenminutes] = newTimeSeries(now, 10*time.Minute, 10*time.Second)
@@ -48,7 +48,7 @@
 }
 
 func (c *Counter) advance() time.Time {
-	now := Now()
+	now := TimeNow()
 	for _, ts := range c.ts {
 		ts.advanceTime(now)
 	}
@@ -141,7 +141,7 @@
 func (c *Counter) Reset() {
 	c.mu.Lock()
 	defer c.mu.Unlock()
-	now := Now()
+	now := TimeNow()
 	for _, ts := range c.ts {
 		ts.reset(now)
 	}
diff --git a/lib/stats/counter/counter_test.go b/lib/stats/counter/counter_test.go
index 03a3951..fc943b3 100644
--- a/lib/stats/counter/counter_test.go
+++ b/lib/stats/counter/counter_test.go
@@ -10,7 +10,7 @@
 
 func TestCounter(t *testing.T) {
 	now := time.Unix(1, 0)
-	counter.Now = func() time.Time { return now }
+	counter.TimeNow = func() time.Time { return now }
 	c := counter.New()
 
 	// Time 1, value=1
@@ -107,7 +107,7 @@
 
 func TestCounterRate(t *testing.T) {
 	now := time.Unix(1, 0)
-	counter.Now = func() time.Time { return now }
+	counter.TimeNow = func() time.Time { return now }
 	c := counter.New()
 
 	// No data, rate is 0.
diff --git a/lib/stats/counter/timeseries.go b/lib/stats/counter/timeseries.go
index 06b5981..3573d57 100644
--- a/lib/stats/counter/timeseries.go
+++ b/lib/stats/counter/timeseries.go
@@ -29,10 +29,10 @@
 	}
 }
 
-// advanceTime moves the timeseries forward to time t and fills in any slots
-// that get skipped in the process. Values older than the timeseries period are
-// lost.
-func (ts *timeseries) advanceTime(t time.Time) {
+// advanceTimeWithFill moves the timeseries forward to time t and fills in any
+// slots that get skipped in the process with the given value. Values older than
+// the timeseries period are lost.
+func (ts *timeseries) advanceTimeWithFill(t time.Time, value int64) {
 	advanceTo := t.Truncate(ts.resolution)
 	if !advanceTo.After(ts.time) {
 		// This is shortcut for the most common case of a busy counter
@@ -45,7 +45,6 @@
 	if steps > ts.size {
 		steps = ts.size
 	}
-	value := ts.slots[ts.head]
 	for steps > 0 {
 		ts.head = (ts.head + 1) % ts.size
 		ts.slots[ts.head] = value
@@ -54,6 +53,13 @@
 	ts.time = advanceTo
 }
 
+// advanceTime moves the timeseries forward to time t and fills in any slots
+// that get skipped in the process with the head value. Values older than the
+// timeseries period are lost.
+func (ts *timeseries) advanceTime(t time.Time) {
+	ts.advanceTimeWithFill(t, ts.slots[ts.head])
+}
+
 // set sets the current value of the timeseries.
 func (ts *timeseries) set(value int64) {
 	ts.slots[ts.head] = value
@@ -113,9 +119,10 @@
 	if ts.stepCount < int64(ts.size) {
 		to = ts.head + 1
 	}
+	tail := (ts.head + 1) % ts.size
 	min := int64(math.MaxInt64)
 	for b := 0; b < to; b++ {
-		if ts.slots[b] < min {
+		if b != tail && ts.slots[b] < min {
 			min = ts.slots[b]
 		}
 	}
@@ -128,9 +135,10 @@
 	if ts.stepCount < int64(ts.size) {
 		to = ts.head + 1
 	}
+	tail := (ts.head + 1) % ts.size
 	max := int64(math.MinInt64)
 	for b := 0; b < to; b++ {
-		if ts.slots[b] > max {
+		if b != tail && ts.slots[b] > max {
 			max = ts.slots[b]
 		}
 	}
diff --git a/lib/stats/counter/timeseries_test.go b/lib/stats/counter/timeseries_test.go
index a05191e..9a3700d 100644
--- a/lib/stats/counter/timeseries_test.go
+++ b/lib/stats/counter/timeseries_test.go
@@ -69,6 +69,28 @@
 	if expected, got := int64(345), ts.tailValue(); got != expected {
 		t.Errorf("unexpected value. Got %v, want %v", got, expected)
 	}
+	if expected, got := int64(111), ts.min(); got != expected {
+		t.Errorf("unexpected value. Got %v, want %v", got, expected)
+	}
+	if expected, got := int64(345), ts.max(); got != expected {
+		t.Errorf("unexpected value. Got %v, want %v", got, expected)
+	}
+
+	// Time 8
+	now = now.Add(time.Second)
+	ts.advanceTime(now)
+	if expected, got := int64(111), ts.headValue(); got != expected {
+		t.Errorf("unexpected value. Got %v, want %v", got, expected)
+	}
+	if expected, got := int64(345), ts.tailValue(); got != expected {
+		t.Errorf("unexpected value. Got %v, want %v", got, expected)
+	}
+	if expected, got := int64(111), ts.min(); got != expected {
+		t.Errorf("unexpected value. Got %v, want %v", got, expected)
+	}
+	if expected, got := int64(111), ts.max(); got != expected {
+		t.Errorf("unexpected value. Got %v, want %v", got, expected)
+	}
 
 	// Time 27
 	now = now.Add(20 * time.Second)
diff --git a/lib/stats/counter/tracker.go b/lib/stats/counter/tracker.go
new file mode 100644
index 0000000..30cc114
--- /dev/null
+++ b/lib/stats/counter/tracker.go
@@ -0,0 +1,159 @@
+package counter
+
+import (
+	"math"
+	"sync"
+	"time"
+)
+
+// Tracker is a min/max value tracker that keeps track of its min/max values
+// over a given period of time, and with a given resolution. The initial min
+// and max values are math.MaxInt64 and math.MinInt64 respectively.
+type Tracker struct {
+	mu           sync.RWMutex
+	min, max     int64 // All time min/max.
+	minTS, maxTS [3]*timeseries
+	lastUpdate   time.Time
+}
+
+// NewTracker returns a new Tracker.
+func NewTracker() *Tracker {
+	now := TimeNow()
+	t := &Tracker{}
+	t.minTS[hour] = newTimeSeries(now, time.Hour, time.Minute)
+	t.minTS[tenminutes] = newTimeSeries(now, 10*time.Minute, 10*time.Second)
+	t.minTS[minute] = newTimeSeries(now, time.Minute, time.Second)
+	t.maxTS[hour] = newTimeSeries(now, time.Hour, time.Minute)
+	t.maxTS[tenminutes] = newTimeSeries(now, 10*time.Minute, 10*time.Second)
+	t.maxTS[minute] = newTimeSeries(now, time.Minute, time.Second)
+	t.init()
+	return t
+}
+
+func (t *Tracker) init() {
+	t.min = math.MaxInt64
+	t.max = math.MinInt64
+	for _, ts := range t.minTS {
+		ts.set(math.MaxInt64)
+	}
+	for _, ts := range t.maxTS {
+		ts.set(math.MinInt64)
+	}
+}
+
+func (t *Tracker) advance() time.Time {
+	now := TimeNow()
+	for _, ts := range t.minTS {
+		ts.advanceTimeWithFill(now, math.MaxInt64)
+	}
+	for _, ts := range t.maxTS {
+		ts.advanceTimeWithFill(now, math.MinInt64)
+	}
+	return now
+}
+
+// LastUpdate returns the last update time of the range.
+func (t *Tracker) LastUpdate() time.Time {
+	t.mu.RLock()
+	defer t.mu.RUnlock()
+	return t.lastUpdate
+}
+
+// Push adds a new value if it is a new minimum or maximum.
+func (t *Tracker) Push(value int64) {
+	t.mu.Lock()
+	defer t.mu.Unlock()
+	t.lastUpdate = t.advance()
+	if t.min > value {
+		t.min = value
+	}
+	if t.max < value {
+		t.max = value
+	}
+	for _, ts := range t.minTS {
+		if ts.headValue() > value {
+			ts.set(value)
+		}
+	}
+	for _, ts := range t.maxTS {
+		if ts.headValue() < value {
+			ts.set(value)
+		}
+	}
+}
+
+// Min returns the minimum value of the tracker
+func (t *Tracker) Min() int64 {
+	t.mu.RLock()
+	defer t.mu.RUnlock()
+	return t.min
+}
+
+// Max returns the maximum value of the tracker.
+func (t *Tracker) Max() int64 {
+	t.mu.RLock()
+	defer t.mu.RUnlock()
+	return t.max
+}
+
+// Min1h returns the minimum value for the last hour.
+func (t *Tracker) Min1h() int64 {
+	t.mu.Lock()
+	defer t.mu.Unlock()
+	t.advance()
+	return t.minTS[hour].min()
+}
+
+// Max1h returns the maximum value for the last hour.
+func (t *Tracker) Max1h() int64 {
+	t.mu.Lock()
+	defer t.mu.Unlock()
+	t.advance()
+	return t.maxTS[hour].max()
+}
+
+// Min10m returns the minimum value for the last 10 minutes.
+func (t *Tracker) Min10m() int64 {
+	t.mu.Lock()
+	defer t.mu.Unlock()
+	t.advance()
+	return t.minTS[tenminutes].min()
+}
+
+// Max10m returns the maximum value for the last 10 minutes.
+func (t *Tracker) Max10m() int64 {
+	t.mu.Lock()
+	defer t.mu.Unlock()
+	t.advance()
+	return t.maxTS[tenminutes].max()
+}
+
+// Min1m returns the minimum value for the last 1 minute.
+func (t *Tracker) Min1m() int64 {
+	t.mu.Lock()
+	defer t.mu.Unlock()
+	t.advance()
+	return t.minTS[minute].min()
+}
+
+// Max1m returns the maximum value for the last 1 minute.
+func (t *Tracker) Max1m() int64 {
+	t.mu.Lock()
+	defer t.mu.Unlock()
+	t.advance()
+	return t.maxTS[minute].max()
+}
+
+// Reset resets the range to an empty state.
+func (t *Tracker) Reset() {
+	t.mu.Lock()
+	defer t.mu.Unlock()
+	now := TimeNow()
+	for _, ts := range t.minTS {
+		ts.reset(now)
+	}
+	for _, ts := range t.maxTS {
+		ts.reset(now)
+	}
+	t.init()
+}
diff --git a/lib/stats/counter/tracker_test.go b/lib/stats/counter/tracker_test.go
new file mode 100644
index 0000000..c2e152c
--- /dev/null
+++ b/lib/stats/counter/tracker_test.go
@@ -0,0 +1,210 @@
+package counter_test
+
+import (
+	"fmt"
+	"math"
+	"math/rand"
+	"sync"
+	"testing"
+	"time"
+
+	"veyron.io/veyron/veyron/lib/stats/counter"
+)
+
+var trackerTests = []struct {
+	after      time.Duration
+	push       int64   // 0 = none, -1 = reset
+	mins, maxs []int64 // min, 10min, hour, all time
+}{
+	{ // T0
+		after: 0,
+		push:  0,
+		mins:  []int64{math.MaxInt64, math.MaxInt64, math.MaxInt64, math.MaxInt64},
+		maxs:  []int64{math.MinInt64, math.MinInt64, math.MinInt64, math.MinInt64},
+	},
+	{ // T1
+		after: 1 * time.Second,
+		push:  5,
+		mins:  []int64{5, 5, 5, 5},
+		maxs:  []int64{5, 5, 5, 5},
+	},
+	{ // T2
+		after: 1 * time.Second,
+		push:  10,
+		mins:  []int64{5, 5, 5, 5},
+		maxs:  []int64{10, 10, 10, 10},
+	},
+	{ // T3
+		after: 1 * time.Second,
+		push:  1,
+		mins:  []int64{1, 1, 1, 1},
+		maxs:  []int64{10, 10, 10, 10},
+	},
+	{ // T4
+		after: 1 * time.Minute,
+		push:  0,
+		mins:  []int64{math.MaxInt64, 1, 1, 1},
+		maxs:  []int64{math.MinInt64, 10, 10, 10},
+	},
+	{ // T5
+		after: 10 * time.Minute,
+		push:  0,
+		mins:  []int64{math.MaxInt64, math.MaxInt64, 1, 1},
+		maxs:  []int64{math.MinInt64, math.MinInt64, 10, 10},
+	},
+	{ // T6
+		after: 1 * time.Hour,
+		push:  0,
+		mins:  []int64{math.MaxInt64, math.MaxInt64, math.MaxInt64, 1},
+		maxs:  []int64{math.MinInt64, math.MinInt64, math.MinInt64, 10},
+	},
+	{ // T7
+		after: 1 * time.Second,
+		push:  5,
+		mins:  []int64{5, 5, 5, 1},
+		maxs:  []int64{5, 5, 5, 10},
+	},
+	{ // T8
+		after: 1 * time.Minute,
+		push:  20,
+		mins:  []int64{20, 5, 5, 1},
+		maxs:  []int64{20, 20, 20, 20},
+	},
+	{ // T9
+		after: 10 * time.Minute,
+		push:  15,
+		mins:  []int64{15, 15, 5, 1},
+		maxs:  []int64{15, 15, 20, 20},
+	},
+	{ // T10
+		after: 1 * time.Hour,
+		push:  10,
+		mins:  []int64{10, 10, 10, 1},
+		maxs:  []int64{10, 10, 10, 20},
+	},
+	{ // T11
+		after: 1 * time.Second,
+		push:  -1,
+		mins:  []int64{math.MaxInt64, math.MaxInt64, math.MaxInt64, math.MaxInt64},
+		maxs:  []int64{math.MinInt64, math.MinInt64, math.MinInt64, math.MinInt64},
+	},
+	{ // T12
+		after: 1 * time.Second,
+		push:  5,
+		mins:  []int64{5, 5, 5, 5},
+		maxs:  []int64{5, 5, 5, 5},
+	},
+}
+
+func TestTracker(t *testing.T) {
+	now := time.Unix(1, 0)
+	counter.TimeNow = func() time.Time { return now }
+
+	tracker := counter.NewTracker()
+	for i, tt := range trackerTests {
+		now = now.Add(tt.after)
+		name := fmt.Sprintf("[T%d] %s:", i, now.Format("15:04:05"))
+		if tt.push > 0 {
+			tracker.Push(tt.push)
+			t.Logf("%s pushed %d\n", name, tt.push)
+		} else if tt.push < 0 {
+			tracker.Reset()
+			t.Log(name, "reset")
+		} else {
+			t.Log(name, "none")
+		}
+
+		if expected, got := tt.mins[0], tracker.Min1m(); got != expected {
+			t.Errorf("%s Min1m returned %d, want %v", name, got, expected)
+		}
+		if expected, got := tt.maxs[0], tracker.Max1m(); got != expected {
+			t.Errorf("%s Max1m returned %d, want %v", name, got, expected)
+		}
+		if expected, got := tt.mins[1], tracker.Min10m(); got != expected {
+			t.Errorf("%s Min10m returned %d, want %v", name, got, expected)
+		}
+		if expected, got := tt.maxs[1], tracker.Max10m(); got != expected {
+			t.Errorf("%s Max10m returned %d, want %v", name, got, expected)
+		}
+		if expected, got := tt.mins[2], tracker.Min1h(); got != expected {
+			t.Errorf("%s Min1h returned %d, want %v", name, got, expected)
+		}
+		if expected, got := tt.maxs[2], tracker.Max1h(); got != expected {
+			t.Errorf("%s Max1h returned %d, want %v", name, got, expected)
+		}
+		if expected, got := tt.mins[3], tracker.Min(); got != expected {
+			t.Errorf("%s Min returned %d, want %v", name, got, expected)
+		}
+		if expected, got := tt.maxs[3], tracker.Max(); got != expected {
+			t.Errorf("%s Max returned %d, want %v", name, got, expected)
+		}
+	}
+}
+
+func min(a, b int64) int64 {
+	if a > b {
+		return b
+	}
+	return a
+}
+
+func max(a, b int64) int64 {
+	if a < b {
+		return b
+	}
+	return a
+}
+
+func TestTrackerConcurrent(t *testing.T) {
+	rand.Seed(time.Now().UnixNano())
+
+	const numGoRoutines = 100
+	const numPushPerGoRoutine = 100000
+	tracker := counter.NewTracker()
+
+	var mins, maxs [numGoRoutines]int64
+	var wg sync.WaitGroup
+	wg.Add(numGoRoutines)
+	for i := 0; i < numGoRoutines; i++ {
+		go func(i int) {
+			var localMin, localMax int64 = math.MaxInt64, math.MinInt64
+			for j := 0; j < numPushPerGoRoutine; j++ {
+				v := rand.Int63()
+				tracker.Push(v)
+				localMin, localMax = min(localMin, v), max(localMax, v)
+			}
+
+			mins[i], maxs[i] = localMin, localMax
+			wg.Done()
+		}(i)
+	}
+	wg.Wait()
+
+	var expectedMin, expectedMax int64 = math.MaxInt64, math.MinInt64
+	for _, v := range mins {
+		expectedMin = min(expectedMin, v)
+	}
+	for _, v := range maxs {
+		expectedMax = max(expectedMax, v)
+	}
+
+	if got := tracker.Min(); got != expectedMin {
+		t.Errorf("Min returned %d, want %v", got, expectedMin)
+	}
+	if got := tracker.Max(); got != expectedMax {
+		t.Errorf("Max returned %d, want %v", got, expectedMax)
+	}
+}
+
+func BenchmarkTrackerPush(b *testing.B) {
+	const numVals = 10000
+	vals := rand.Perm(numVals)
+	tracker := counter.NewTracker()
+
+	b.SetParallelism(100)
+	b.RunParallel(func(pb *testing.PB) {
+		for i := 0; pb.Next(); i++ {
+			tracker.Push(int64(vals[i%numVals]))
+		}
+	})
+}
diff --git a/lib/stats/histogram/histogram.go b/lib/stats/histogram/histogram.go
index 44c3b07..2c0ccef 100644
--- a/lib/stats/histogram/histogram.go
+++ b/lib/stats/histogram/histogram.go
@@ -17,8 +17,9 @@
 type Histogram struct {
 	opts    Options
 	buckets []bucketInternal
-	sum     *counter.Counter
 	count   *counter.Counter
+	sum     *counter.Counter
+	tracker *counter.Tracker
 }
 
 // Options contains the parameters that define the histogram's buckets.
@@ -54,8 +55,9 @@
 	h := Histogram{
 		opts:    opts,
 		buckets: make([]bucketInternal, opts.NumBuckets),
-		sum:     counter.New(),
 		count:   counter.New(),
+		sum:     counter.New(),
+		tracker: counter.NewTracker(),
 	}
 	low := opts.MinValue
 	delta := opts.SmallestBucketSize
@@ -82,6 +84,7 @@
 	h.buckets[bucket].count.Incr(1)
 	h.count.Incr(1)
 	h.sum.Incr(value)
+	h.tracker.Push(value)
 	return nil
 }
 
@@ -103,6 +106,8 @@
 	v := stats.HistogramValue{
 		Count:   h.count.Value(),
 		Sum:     h.sum.Value(),
+		Min:     h.tracker.Min(),
+		Max:     h.tracker.Max(),
 		Buckets: b,
 	}
 	return v
@@ -121,6 +126,8 @@
 	v := stats.HistogramValue{
 		Count:   h.count.Delta1h(),
 		Sum:     h.sum.Delta1h(),
+		Min:     h.tracker.Min1h(),
+		Max:     h.tracker.Max1h(),
 		Buckets: b,
 	}
 	return v
@@ -139,6 +146,8 @@
 	v := stats.HistogramValue{
 		Count:   h.count.Delta10m(),
 		Sum:     h.sum.Delta10m(),
+		Min:     h.tracker.Min10m(),
+		Max:     h.tracker.Max10m(),
 		Buckets: b,
 	}
 	return v
@@ -157,6 +166,8 @@
 	v := stats.HistogramValue{
 		Count:   h.count.Delta1m(),
 		Sum:     h.sum.Delta1m(),
+		Min:     h.tracker.Min1m(),
+		Max:     h.tracker.Max1m(),
 		Buckets: b,
 	}
 	return v
diff --git a/lib/stats/histogram/histogram_test.go b/lib/stats/histogram/histogram_test.go
index e184142..538e619 100644
--- a/lib/stats/histogram/histogram_test.go
+++ b/lib/stats/histogram/histogram_test.go
@@ -8,11 +8,11 @@
 
 func TestHistogram(t *testing.T) {
 	// This creates a histogram with the following buckets:
-	//  [1, 2[
-	//  [2, 4[
-	//  [4, 8[
-	//  [8, 16[
-	//  [16, Inf
+	//  [1, 2)
+	//  [2, 4)
+	//  [4, 8)
+	//  [8, 16)
+	//  [16, Inf)
 	opts := histogram.Options{
 		NumBuckets:         5,
 		GrowthFactor:       1.0,
@@ -22,21 +22,35 @@
 	h := histogram.New(opts)
 	// Trying to add a value that's less than MinValue, should return an error.
 	if err := h.Add(0); err == nil {
-		t.Errorf("unexpected return value for Add(0.0). Want != nil, Got nil")
+		t.Errorf("unexpected return value for Add(0.0). Want != nil, got nil")
 	}
 	// Adding good values. Expect no errors.
 	for i := 1; i <= 50; i++ {
 		if err := h.Add(int64(i)); err != nil {
-			t.Errorf("unexpected return value for Add(%d). Want nil, Got %v", i, err)
+			t.Errorf("unexpected return value for Add(%d). Want nil, got %v", i, err)
 		}
 	}
 	expectedCount := []int64{1, 2, 4, 8, 35}
 	buckets := h.Value().Buckets
 	for i := 0; i < opts.NumBuckets; i++ {
 		if buckets[i].Count != expectedCount[i] {
-			t.Errorf("unexpected count for bucket[%d]. Want %d, Got %v", i, expectedCount[i], buckets[i].Count)
+			t.Errorf("unexpected count for bucket[%d]. Want %d, got %v", i, expectedCount[i], buckets[i].Count)
 		}
 	}
+
+	v := h.Value()
+	if expected, got := int64(50), v.Count; got != expected {
+		t.Errorf("unexpected count in histogram value. Want %d, got %v", expected, got)
+	}
+	if expected, got := int64(50*(1+50)/2), v.Sum; got != expected {
+		t.Errorf("unexpected sum in histogram value. Want %d, got %v", expected, got)
+	}
+	if expected, got := int64(1), v.Min; got != expected {
+		t.Errorf("unexpected min in histogram value. Want %d, got %v", expected, got)
+	}
+	if expected, got := int64(50), v.Max; got != expected {
+		t.Errorf("unexpected max in histogram value. Want %d, got %v", expected, got)
+	}
 }
 
 func BenchmarkHistogram(b *testing.B) {
diff --git a/lib/stats/stats_test.go b/lib/stats/stats_test.go
index 0773280..e7b28a4 100644
--- a/lib/stats/stats_test.go
+++ b/lib/stats/stats_test.go
@@ -25,7 +25,7 @@
 
 func TestStats(t *testing.T) {
 	now := time.Unix(1, 0)
-	counter.Now = func() time.Time { return now }
+	counter.TimeNow = func() time.Time { return now }
 
 	a := libstats.NewInteger("ipc/test/aaa")
 	b := libstats.NewFloat("ipc/test/bbb")
@@ -224,6 +224,8 @@
 			Value: istats.HistogramValue{
 				Count: 2,
 				Sum:   3,
+				Min:   1,
+				Max:   2,
 				Buckets: []istats.HistogramBucket{
 					istats.HistogramBucket{LowBound: 0, Count: 0},
 					istats.HistogramBucket{LowBound: 1, Count: 1},
@@ -253,6 +255,8 @@
 			Value: istats.HistogramValue{
 				Count: 2,
 				Sum:   5,
+				Min:   2,
+				Max:   3,
 				Buckets: []istats.HistogramBucket{
 					istats.HistogramBucket{LowBound: 0, Count: 0},
 					istats.HistogramBucket{LowBound: 1, Count: 0},
diff --git a/services/identity/googleoauth/template.go b/services/identity/googleoauth/template.go
index 7a06956..a3dc4ee 100644
--- a/services/identity/googleoauth/template.go
+++ b/services/identity/googleoauth/template.go
@@ -185,18 +185,17 @@
   <label class="col-sm-2" for="required-caveat">Expiration</label>
   <div class="col-sm-10" class="input-group" name="required-caveat">
     <div class="radio">
-      <div class="input-group">
-        <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Expiry" checked>
-        <input type="datetime-local" id="expiry" name="expiry">
-      </div>
-    </div>
-    <div class="radio">
       <label>
-      <!-- TODO(suharshs): Enable this after ThirdPartyCaveats are fixed. -->
-      <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Revocation" disabled>
+      <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Revocation" checked>
       When explicitly revoked
       </label>
     </div>
+    <div class="radio">
+      <div class="input-group">
+        <input type="radio" name="requiredCaveat" id="requiredCaveat" value="Expiry">
+        <input type="datetime-local" id="expiry" name="expiry">
+      </div>
+    </div>
   </div>
 </div>
 <h4 class="form-signin-heading">Additional caveats</h4>
diff --git a/services/mgmt/stats/impl/stats_test.go b/services/mgmt/stats/impl/stats_test.go
index 5555d2c..fe502c4 100644
--- a/services/mgmt/stats/impl/stats_test.go
+++ b/services/mgmt/stats/impl/stats_test.go
@@ -179,6 +179,8 @@
 		want := istats.HistogramValue{
 			Count: 10,
 			Sum:   45,
+			Min:   0,
+			Max:   9,
 			Buckets: []istats.HistogramBucket{
 				istats.HistogramBucket{LowBound: 0, Count: 1},
 				istats.HistogramBucket{LowBound: 1, Count: 2},
diff --git a/services/mgmt/stats/types.go b/services/mgmt/stats/types.go
new file mode 100644
index 0000000..1791465
--- /dev/null
+++ b/services/mgmt/stats/types.go
@@ -0,0 +1,51 @@
+package stats
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+)
+
+// Print writes textual output of the histogram values.
+func (v HistogramValue) Print(w io.Writer) {
+	avg := float64(v.Sum) / float64(v.Count)
+	fmt.Fprintf(w, "Count: %d  Min: %d  Max: %d  Avg: %.2f\n", v.Count, v.Min, v.Max, avg)
+	fmt.Fprintf(w, "%s\n", strings.Repeat("-", 60))
+	if v.Count <= 0 {
+		return
+	}
+
+	maxBucketDigitLen := len(strconv.FormatInt(v.Buckets[len(v.Buckets)-1].LowBound, 10))
+	if maxBucketDigitLen < 3 {
+		// For "inf".
+		maxBucketDigitLen = 3
+	}
+	maxCountDigitLen := len(strconv.FormatInt(v.Count, 10))
+	percentMulti := 100 / float64(v.Count)
+
+	accCount := int64(0)
+	for i, b := range v.Buckets {
+		fmt.Fprintf(w, "[%*d, ", maxBucketDigitLen, b.LowBound)
+		if i+1 < len(v.Buckets) {
+			fmt.Fprintf(w, "%*d)", maxBucketDigitLen, v.Buckets[i+1].LowBound)
+		} else {
+			fmt.Fprintf(w, "%*s)", maxBucketDigitLen, "inf")
+		}
+
+		accCount += b.Count
+		fmt.Fprintf(w, "  %*d  %5.1f%%  %5.1f%%", maxCountDigitLen, b.Count, float64(b.Count)*percentMulti, float64(accCount)*percentMulti)
+
+		const barScale = 0.1
+		barLength := int(float64(b.Count)*percentMulti*barScale + 0.5)
+		fmt.Fprintf(w, "  %s\n", strings.Repeat("#", barLength))
+	}
+}
+
+// String returns the textual output of the histogram values as string.
+func (v HistogramValue) String() string {
+	var b bytes.Buffer
+	v.Print(&b)
+	return b.String()
+}
diff --git a/services/mgmt/stats/types.vdl b/services/mgmt/stats/types.vdl
index 8aa3ec9..d11aa13 100644
--- a/services/mgmt/stats/types.vdl
+++ b/services/mgmt/stats/types.vdl
@@ -7,6 +7,10 @@
 	Count int64
 	// Sum is the sum of all the values added to the histogram.
 	Sum int64
+	// Min is the minimum of all the values added to the histogram.
+	Min int64
+	// Max is the maximum of all the values added to the histogram.
+	Max int64
 	// Buckets contains all the buckets of the histogram.
 	Buckets []HistogramBucket
 }
diff --git a/services/mgmt/stats/types.vdl.go b/services/mgmt/stats/types.vdl.go
index cc41c75..4884f69 100644
--- a/services/mgmt/stats/types.vdl.go
+++ b/services/mgmt/stats/types.vdl.go
@@ -15,6 +15,10 @@
 	Count int64
 	// Sum is the sum of all the values added to the histogram.
 	Sum int64
+	// Min is the minimum of all the values added to the histogram.
+	Min int64
+	// Max is the maximum of all the values added to the histogram.
+	Max int64
 	// Buckets contains all the buckets of the histogram.
 	Buckets []HistogramBucket
 }
diff --git a/tools/debug/impl.go b/tools/debug/impl.go
index 9c5effc..6a5a1fa 100644
--- a/tools/debug/impl.go
+++ b/tools/debug/impl.go
@@ -3,7 +3,6 @@
 import (
 	"bytes"
 	"fmt"
-	"io"
 	"os"
 	"os/exec"
 	"regexp"
@@ -423,40 +422,13 @@
 	}
 	switch v := value.(type) {
 	case istats.HistogramValue:
-		writeASCIIHistogram(&buf, v)
+		v.Print(&buf)
 	default:
 		fmt.Fprintf(&buf, "%v", v)
 	}
 	return buf.String()
 }
 
-func writeASCIIHistogram(w io.Writer, h istats.HistogramValue) {
-	scale := h.Count
-	if scale > 100 {
-		scale = 100
-	}
-	var avg float64
-	if h.Count > 0 {
-		avg = float64(h.Sum) / float64(h.Count)
-	}
-	fmt.Fprintf(w, "Count: %d Sum: %d Avg: %f\n", h.Count, h.Sum, avg)
-	for i, b := range h.Buckets {
-		var r string
-		if i+1 < len(h.Buckets) {
-			r = fmt.Sprintf("[%d,%d[", b.LowBound, h.Buckets[i+1].LowBound)
-		} else {
-			r = fmt.Sprintf("[%d,Inf", b.LowBound)
-		}
-		fmt.Fprintf(w, "%-18s: ", r)
-		if b.Count > 0 && h.Count > 0 {
-			fmt.Fprintf(w, "%s %d (%.1f%%)", strings.Repeat("*", int(b.Count*scale/h.Count)), b.Count, 100*float64(b.Count)/float64(h.Count))
-		}
-		if i+1 < len(h.Buckets) {
-			fmt.Fprintln(w)
-		}
-	}
-}
-
 var cmdPProfRun = &cmdline.Command{
 	Run:      runPProf,
 	Name:     "run",
diff --git a/tools/debug/test.sh b/tools/debug/test.sh
index c460c21..17a016b 100755
--- a/tools/debug/test.sh
+++ b/tools/debug/test.sh
@@ -85,7 +85,7 @@
     "${DEBUG_BIN}" stats watch -raw "${EP}/__debug/stats/ipc/server/routing-id/*/methods/ReadLog/latency-ms")
   shell::timed_wait_for "${shell_test_DEFAULT_MESSAGE_TIMEOUT}" "${TMP}" "ReadLog/latency-ms"
   kill "${DEBUG_PID}"
-  grep -q "Count:1 " "${TMP}" || (dumplogs "${TMP}"; shell_test::fail "line ${LINENO}: failed to find expected output")
+  grep -q "Count: 1 " "${TMP}" || (dumplogs "${TMP}"; shell_test::fail "line ${LINENO}: failed to find expected output")
 
   # Test pprof.
   if ! "${DEBUG_BIN}" pprof run "${EP}/__debug/pprof" heap --text &> "${DBGLOG}"; then