{lib,services}/stats: expose time series values from counter.

Change-Id: I5b5622488a525377eee1a350af9b92096419d072
diff --git a/lib/stats/counter.go b/lib/stats/counter.go
index 6482149..f4646fa 100644
--- a/lib/stats/counter.go
+++ b/lib/stats/counter.go
@@ -27,6 +27,9 @@
 	addCounterChild(node, name+"/rate1h", cw, time.Hour, cw.Rate1h)
 	addCounterChild(node, name+"/rate10m", cw, 10*time.Minute, cw.Rate10m)
 	addCounterChild(node, name+"/rate1m", cw, time.Minute, cw.Rate1m)
+	addCounterChild(node, name+"/timeseries1h", cw, time.Hour, cw.TimeSeries1h)
+	addCounterChild(node, name+"/timeseries10m", cw, 10*time.Minute, cw.TimeSeries10m)
+	addCounterChild(node, name+"/timeseries1m", cw, time.Minute, cw.TimeSeries1m)
 	return c
 }
 
@@ -60,6 +63,15 @@
 func (cw counterWrapper) Rate1m() interface{} {
 	return cw.c.Rate1m()
 }
+func (cw counterWrapper) TimeSeries1h() interface{} {
+	return cw.c.TimeSeries1h()
+}
+func (cw counterWrapper) TimeSeries10m() interface{} {
+	return cw.c.TimeSeries10m()
+}
+func (cw counterWrapper) TimeSeries1m() interface{} {
+	return cw.c.TimeSeries1m()
+}
 
 type counterChild struct {
 	c      *counterWrapper
diff --git a/lib/stats/counter/counter.go b/lib/stats/counter/counter.go
index 947e1b7..f99b3d3 100644
--- a/lib/stats/counter/counter.go
+++ b/lib/stats/counter/counter.go
@@ -20,6 +20,8 @@
 import (
 	"sync"
 	"time"
+
+	"v.io/x/ref/services/stats"
 )
 
 var (
@@ -141,6 +143,31 @@
 	return c.ts[minute].rate()
 }
 
+// TimeSeries1h returns the time series data in the last hour.
+func (c *Counter) TimeSeries1h() stats.TimeSeries {
+	return c.timeseries(c.ts[hour])
+}
+
+// TimeSeries10m returns the time series data in the last 10 minutes.
+func (c *Counter) TimeSeries10m() stats.TimeSeries {
+	return c.timeseries(c.ts[tenminutes])
+}
+
+// TimeSeries1m returns the time series data in the last minute.
+func (c *Counter) TimeSeries1m() stats.TimeSeries {
+	return c.timeseries(c.ts[minute])
+}
+
+func (c *Counter) timeseries(ts *timeseries) stats.TimeSeries {
+	c.mu.RLock()
+	defer c.mu.RUnlock()
+	return stats.TimeSeries{
+		Values:     ts.values(),
+		Resolution: ts.resolution,
+		StartTime:  ts.time,
+	}
+}
+
 // Reset resets the counter to an empty state.
 func (c *Counter) Reset() {
 	c.mu.Lock()
diff --git a/lib/stats/counter/timeseries.go b/lib/stats/counter/timeseries.go
index 36dfbd8..3d46b5a 100644
--- a/lib/stats/counter/timeseries.go
+++ b/lib/stats/counter/timeseries.go
@@ -156,3 +156,19 @@
 	ts.stepCount = 1
 	ts.slots = make([]int64, ts.size)
 }
+
+// values returns timeseries values from oldest (tail) to newest (head).
+func (ts *timeseries) values() []int64 {
+	tail := 0
+	steps := ts.head + 1
+	if ts.stepCount > int64(ts.size) {
+		tail = (ts.head + 1) % ts.size
+		steps = ts.size
+	}
+	values := make([]int64, 0, steps)
+	for c := 0; c < steps; c++ {
+		i := (tail + c) % ts.size
+		values = append(values, ts.slots[i])
+	}
+	return values
+}
diff --git a/lib/stats/counter/timeseries_test.go b/lib/stats/counter/timeseries_test.go
index 8bd3bbd..a6eca0a 100644
--- a/lib/stats/counter/timeseries_test.go
+++ b/lib/stats/counter/timeseries_test.go
@@ -5,6 +5,7 @@
 package counter
 
 import (
+	"reflect"
 	"testing"
 	"time"
 )
@@ -121,3 +122,39 @@
 	}
 
 }
+
+func TestTimeSeriesValues(t *testing.T) {
+	now := time.Unix(1, 0)
+	// 6 time slots.
+	ts := newTimeSeries(now, 5*time.Second, time.Second)
+	// Add 3 values.
+	// slots: [0, 1, 2, 3, x, x]
+	// values: [0, 1, 2, 3]
+	now = addValue(1, 3, ts, now)
+	if expected, got := []int64{0, 1, 2, 3}, ts.values(); !reflect.DeepEqual(got, expected) {
+		t.Errorf("unexpected values. Got %v, want %v", got, expected)
+	}
+	// Add 2 more values.
+	// slots: [0, 1, 2, 3, 4, 5]
+	// values: [0, 1, 2, 3, 4, 5]
+	now = addValue(1, 2, ts, now)
+	if expected, got := []int64{0, 1, 2, 3, 4, 5}, ts.values(); !reflect.DeepEqual(got, expected) {
+		t.Errorf("unexpected values. Got %v, want %v", got, expected)
+	}
+	// Add 3 more values.
+	// slots: [6, 7, 8, 3, 4, 5]
+	// values: [3, 4, 5, 6, 7, 8]
+	now = addValue(1, 3, ts, now)
+	if expected, got := []int64{3, 4, 5, 6, 7, 8}, ts.values(); !reflect.DeepEqual(got, expected) {
+		t.Errorf("unexpected values. Got %v, want %v", got, expected)
+	}
+}
+
+func addValue(increment int64, count int, ts *timeseries, now time.Time) time.Time {
+	for i := 0; i < count; i++ {
+		now = now.Add(time.Second)
+		ts.advanceTime(now)
+		ts.incr(increment)
+	}
+	return now
+}
diff --git a/lib/stats/stats_test.go b/lib/stats/stats_test.go
index 4a9cfbd..fc67406 100644
--- a/lib/stats/stats_test.go
+++ b/lib/stats/stats_test.go
@@ -109,6 +109,21 @@
 		libstats.KeyValue{Key: "rpc/test/ddd/rate10m", Value: float64(0)},
 		libstats.KeyValue{Key: "rpc/test/ddd/rate1h", Value: float64(0)},
 		libstats.KeyValue{Key: "rpc/test/ddd/rate1m", Value: float64(0)},
+		libstats.KeyValue{Key: "rpc/test/ddd/timeseries10m", Value: s_stats.TimeSeries{
+			Values:     []int64{4},
+			Resolution: 10 * time.Second,
+			StartTime:  now.Truncate(10 * time.Second),
+		}},
+		libstats.KeyValue{Key: "rpc/test/ddd/timeseries1h", Value: s_stats.TimeSeries{
+			Values:     []int64{4},
+			Resolution: time.Minute,
+			StartTime:  now.Truncate(time.Minute),
+		}},
+		libstats.KeyValue{Key: "rpc/test/ddd/timeseries1m", Value: s_stats.TimeSeries{
+			Values:     []int64{4},
+			Resolution: time.Second,
+			StartTime:  now,
+		}},
 	}
 	if !reflect.DeepEqual(result, expected) {
 		t.Errorf("unexpected result. Got %#v, want %#v", result, expected)
@@ -146,6 +161,9 @@
 		libstats.KeyValue{Key: "rpc/test/ddd/rate10m"},
 		libstats.KeyValue{Key: "rpc/test/ddd/rate1h"},
 		libstats.KeyValue{Key: "rpc/test/ddd/rate1m"},
+		libstats.KeyValue{Key: "rpc/test/ddd/timeseries10m"},
+		libstats.KeyValue{Key: "rpc/test/ddd/timeseries1h"},
+		libstats.KeyValue{Key: "rpc/test/ddd/timeseries1m"},
 	}
 	if !reflect.DeepEqual(result, expected) {
 		t.Errorf("unexpected result. Got %#v, want %#v", result, expected)
@@ -165,6 +183,21 @@
 		libstats.KeyValue{Key: "rpc/test/ddd/rate10m", Value: float64(10.4)},
 		libstats.KeyValue{Key: "rpc/test/ddd/rate1h", Value: float64(0)},
 		libstats.KeyValue{Key: "rpc/test/ddd/rate1m", Value: float64(10.4)},
+		libstats.KeyValue{Key: "rpc/test/ddd/timeseries10m", Value: s_stats.TimeSeries{
+			Values:     []int64{4, 104},
+			Resolution: 10 * time.Second,
+			StartTime:  now.Truncate(10 * time.Second),
+		}},
+		libstats.KeyValue{Key: "rpc/test/ddd/timeseries1h", Value: s_stats.TimeSeries{
+			Values:     []int64{104},
+			Resolution: time.Minute,
+			StartTime:  now.Truncate(time.Minute),
+		}},
+		libstats.KeyValue{Key: "rpc/test/ddd/timeseries1m", Value: s_stats.TimeSeries{
+			Values:     []int64{4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 104},
+			Resolution: time.Second,
+			StartTime:  now,
+		}},
 	}
 	if !reflect.DeepEqual(result, expected) {
 		t.Errorf("unexpected result. Got %#v, want %#v", result, expected)
diff --git a/services/internal/statslib/stats_test.go b/services/internal/statslib/stats_test.go
index 3277fd7..4ef177e 100644
--- a/services/internal/statslib/stats_test.go
+++ b/services/internal/statslib/stats_test.go
@@ -76,6 +76,9 @@
 			"testing/foo/bar/rate10m",
 			"testing/foo/bar/rate1h",
 			"testing/foo/bar/rate1m",
+			"testing/foo/bar/timeseries10m",
+			"testing/foo/bar/timeseries1h",
+			"testing/foo/bar/timeseries1m",
 		}
 		sort.Strings(results)
 		sort.Strings(expected)
diff --git a/services/stats/stats.vdl.go b/services/stats/stats.vdl.go
index 725c16e..606eeec 100644
--- a/services/stats/stats.vdl.go
+++ b/services/stats/stats.vdl.go
@@ -9,7 +9,9 @@
 package stats
 
 import (
+	"time"
 	"v.io/v23/vdl"
+	vdltime "v.io/v23/vdlroot/time"
 )
 
 var _ = __VDLInit() // Must be first; see __VDLInit comments for details.
@@ -278,11 +280,172 @@
 	}
 }
 
+// TimeSeries records data of a single time series.
+type TimeSeries struct {
+	// Values holds the time series values (from oldest to newest).
+	Values []int64
+	// Resolution is the time resolution of the time series.
+	Resolution time.Duration
+	// StartTime is the time of the first value of the time series.
+	StartTime time.Time
+}
+
+func (TimeSeries) __VDLReflect(struct {
+	Name string `vdl:"v.io/x/ref/services/stats.TimeSeries"`
+}) {
+}
+
+func (x TimeSeries) VDLIsZero() bool {
+	if len(x.Values) != 0 {
+		return false
+	}
+	if x.Resolution != 0 {
+		return false
+	}
+	if !x.StartTime.IsZero() {
+		return false
+	}
+	return true
+}
+
+func (x TimeSeries) VDLWrite(enc vdl.Encoder) error {
+	if err := enc.StartValue(__VDLType_struct_4); err != nil {
+		return err
+	}
+	if len(x.Values) != 0 {
+		if err := enc.NextField(0); err != nil {
+			return err
+		}
+		if err := __VDLWriteAnon_list_2(enc, x.Values); err != nil {
+			return err
+		}
+	}
+	if x.Resolution != 0 {
+		if err := enc.NextField(1); err != nil {
+			return err
+		}
+		var wire vdltime.Duration
+		if err := vdltime.DurationFromNative(&wire, x.Resolution); err != nil {
+			return err
+		}
+		if err := wire.VDLWrite(enc); err != nil {
+			return err
+		}
+	}
+	if !x.StartTime.IsZero() {
+		if err := enc.NextField(2); err != nil {
+			return err
+		}
+		var wire vdltime.Time
+		if err := vdltime.TimeFromNative(&wire, x.StartTime); err != nil {
+			return err
+		}
+		if err := wire.VDLWrite(enc); err != nil {
+			return err
+		}
+	}
+	if err := enc.NextField(-1); err != nil {
+		return err
+	}
+	return enc.FinishValue()
+}
+
+func __VDLWriteAnon_list_2(enc vdl.Encoder, x []int64) error {
+	if err := enc.StartValue(__VDLType_list_5); err != nil {
+		return err
+	}
+	if err := enc.SetLenHint(len(x)); err != nil {
+		return err
+	}
+	for _, elem := range x {
+		if err := enc.NextEntryValueInt(vdl.Int64Type, elem); err != nil {
+			return err
+		}
+	}
+	if err := enc.NextEntry(true); err != nil {
+		return err
+	}
+	return enc.FinishValue()
+}
+
+func (x *TimeSeries) VDLRead(dec vdl.Decoder) error {
+	*x = TimeSeries{}
+	if err := dec.StartValue(__VDLType_struct_4); err != nil {
+		return err
+	}
+	decType := dec.Type()
+	for {
+		index, err := dec.NextField()
+		switch {
+		case err != nil:
+			return err
+		case index == -1:
+			return dec.FinishValue()
+		}
+		if decType != __VDLType_struct_4 {
+			index = __VDLType_struct_4.FieldIndexByName(decType.Field(index).Name)
+			if index == -1 {
+				if err := dec.SkipValue(); err != nil {
+					return err
+				}
+				continue
+			}
+		}
+		switch index {
+		case 0:
+			if err := __VDLReadAnon_list_2(dec, &x.Values); err != nil {
+				return err
+			}
+		case 1:
+			var wire vdltime.Duration
+			if err := wire.VDLRead(dec); err != nil {
+				return err
+			}
+			if err := vdltime.DurationToNative(wire, &x.Resolution); err != nil {
+				return err
+			}
+		case 2:
+			var wire vdltime.Time
+			if err := wire.VDLRead(dec); err != nil {
+				return err
+			}
+			if err := vdltime.TimeToNative(wire, &x.StartTime); err != nil {
+				return err
+			}
+		}
+	}
+}
+
+func __VDLReadAnon_list_2(dec vdl.Decoder, x *[]int64) error {
+	if err := dec.StartValue(__VDLType_list_5); err != nil {
+		return err
+	}
+	if len := dec.LenHint(); len > 0 {
+		*x = make([]int64, 0, len)
+	} else {
+		*x = nil
+	}
+	for {
+		switch done, elem, err := dec.NextEntryValueInt(64); {
+		case err != nil:
+			return err
+		case done:
+			return dec.FinishValue()
+		default:
+			*x = append(*x, elem)
+		}
+	}
+}
+
 // Hold type definitions in package-level variables, for better performance.
 var (
 	__VDLType_struct_1 *vdl.Type
 	__VDLType_struct_2 *vdl.Type
 	__VDLType_list_3   *vdl.Type
+	__VDLType_struct_4 *vdl.Type
+	__VDLType_list_5   *vdl.Type
+	__VDLType_struct_6 *vdl.Type
+	__VDLType_struct_7 *vdl.Type
 )
 
 var __VDLInitCalled bool
@@ -309,11 +472,16 @@
 	// Register types.
 	vdl.Register((*HistogramBucket)(nil))
 	vdl.Register((*HistogramValue)(nil))
+	vdl.Register((*TimeSeries)(nil))
 
 	// Initialize type definitions.
 	__VDLType_struct_1 = vdl.TypeOf((*HistogramBucket)(nil)).Elem()
 	__VDLType_struct_2 = vdl.TypeOf((*HistogramValue)(nil)).Elem()
 	__VDLType_list_3 = vdl.TypeOf((*[]HistogramBucket)(nil))
+	__VDLType_struct_4 = vdl.TypeOf((*TimeSeries)(nil)).Elem()
+	__VDLType_list_5 = vdl.TypeOf((*[]int64)(nil))
+	__VDLType_struct_6 = vdl.TypeOf((*vdltime.Duration)(nil)).Elem()
+	__VDLType_struct_7 = vdl.TypeOf((*vdltime.Time)(nil)).Elem()
 
 	return struct{}{}
 }
diff --git a/services/stats/types.vdl b/services/stats/types.vdl
index 1835f80..1243f99 100644
--- a/services/stats/types.vdl
+++ b/services/stats/types.vdl
@@ -5,6 +5,8 @@
 // Packages stats defines the non-native types exported by the stats service.
 package stats
 
+import "time"
+
 // HistogramValue is the value of Histogram objects.
 type HistogramValue struct {
 	// Count is the total number of values added to the histogram.
@@ -26,3 +28,13 @@
 	// Count is the number of values in the bucket.
 	Count int64
 }
+
+// TimeSeries records data of a single time series.
+type TimeSeries struct {
+	// Values holds the time series values (from oldest to newest).
+	Values []int64
+	// Resolution is the time resolution of the time series.
+	Resolution time.Duration
+	// StartTime is the time of the first value of the time series.
+	StartTime time.Time
+}