veyron/lib/stats: Add Map object

Add a Map object to export a set of key/value pairs. The key/value pairs
can be updated atomically.

Export some system stats, including runtime.MemStats which uses the new
Map object.

Change-Id: Ib2cc9ca773df81751f60721115de08389188cfd0
diff --git a/lib/stats/map.go b/lib/stats/map.go
new file mode 100644
index 0000000..89312f6
--- /dev/null
+++ b/lib/stats/map.go
@@ -0,0 +1,165 @@
+package stats
+
+import (
+	"path"
+	"sort"
+	"sync"
+	"time"
+)
+
+// NewMap creates a new Map StatsObject with the given name and
+// returns a pointer to it.
+func NewMap(name string) *Map {
+	lock.Lock()
+	defer lock.Unlock()
+	node := findNodeLocked(name, true)
+	m := Map{name: name, value: make(map[string]mapValue)}
+	node.object = &m
+	return &m
+}
+
+// Map implements the StatsObject interface. The map keys are strings and the
+// values can be bool, int64, uint64, float64, or string.
+type Map struct {
+	mu    sync.RWMutex // ACQUIRED_BEFORE(stats.lock)
+	name  string
+	value map[string]mapValue // GUARDED_BY(mu)
+}
+
+type mapValue struct {
+	lastUpdate time.Time
+	value      interface{}
+}
+
+// Set sets the values of the given keys. There must be exactly one value for
+// each key.
+func (m *Map) Set(kvpairs []KeyValue) {
+	now := time.Now()
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	for _, kv := range kvpairs {
+		var v interface{}
+		switch value := kv.Value.(type) {
+		case bool:
+			v = bool(value)
+		case int:
+			v = int64(value)
+		case int8:
+			v = int64(value)
+		case int16:
+			v = int64(value)
+		case int32:
+			v = int64(value)
+		case int64:
+			v = int64(value)
+		case uint:
+			v = uint64(value)
+		case uint8:
+			v = uint64(value)
+		case uint16:
+			v = uint64(value)
+		case uint32:
+			v = uint64(value)
+		case uint64:
+			v = uint64(value)
+		case float32:
+			v = float64(value)
+		case float64:
+			v = float64(value)
+		case string:
+			v = string(value)
+		default:
+			panic("attempt to use an unsupported type as value")
+		}
+		m.value[kv.Key] = mapValue{now, v}
+	}
+	m.insertMissingNodes()
+}
+
+// Delete deletes the given keys from the map object.
+func (m *Map) Delete(keys []string) {
+	// The lock order is important.
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	lock.Lock()
+	defer lock.Unlock()
+	n := findNodeLocked(m.name, false)
+	for _, k := range keys {
+		delete(m.value, k)
+		if n != nil {
+			delete(n.children, k)
+		}
+	}
+}
+
+// Keys returns a sorted list of all the keys in the map.
+func (m *Map) Keys() []string {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+	keys := []string{}
+	for k, _ := range m.value {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+	return keys
+}
+
+// LastUpdate always returns a zero-value Time for a Map.
+func (m *Map) LastUpdate() time.Time {
+	return time.Time{}
+}
+
+// Value always returns nil for a Map.
+func (m *Map) Value() interface{} {
+	return nil
+}
+
+// insertMissingNodes inserts all the missing nodes.
+func (m *Map) insertMissingNodes() {
+	missing := []string{}
+	lock.RLock()
+	for key, _ := range m.value {
+		oName := path.Join(m.name, key)
+		if n := findNodeLocked(oName, false); n == nil {
+			missing = append(missing, key)
+		}
+	}
+	lock.RUnlock()
+	if len(missing) == 0 {
+		return
+	}
+
+	lock.Lock()
+	for _, key := range missing {
+		oName := path.Join(m.name, key)
+		if n := findNodeLocked(oName, true); n.object == nil {
+			n.object = &mapValueWrapper{m, key}
+		}
+	}
+	lock.Unlock()
+}
+
+type mapValueWrapper struct {
+	m   *Map
+	key string
+}
+
+// LastUpdate returns the time at which the parent map object was last updated.
+func (w *mapValueWrapper) LastUpdate() time.Time {
+	w.m.mu.RLock()
+	defer w.m.mu.RUnlock()
+	if v, ok := w.m.value[w.key]; ok {
+		return v.lastUpdate
+	}
+	return time.Time{}
+}
+
+// Value returns the current value for the map key.
+func (w *mapValueWrapper) Value() interface{} {
+	w.m.mu.RLock()
+	defer w.m.mu.RUnlock()
+	if v, ok := w.m.value[w.key]; ok {
+		return v.value
+	}
+	return nil
+}
diff --git a/lib/stats/stats.go b/lib/stats/stats.go
index e55947c..d06dabf 100644
--- a/lib/stats/stats.go
+++ b/lib/stats/stats.go
@@ -33,7 +33,7 @@
 
 var (
 	lock        sync.RWMutex
-	repository  *node
+	repository  *node // GUARDED_BY(lock)
 	ErrNotFound = errors.New("name not found")
 	ErrNoValue  = errors.New("object has no value")
 )
diff --git a/lib/stats/stats_test.go b/lib/stats/stats_test.go
index dc2c8c6..9a3f6b1 100644
--- a/lib/stats/stats_test.go
+++ b/lib/stats/stats_test.go
@@ -9,16 +9,13 @@
 	"veyron.io/veyron/veyron/lib/stats/counter"
 	"veyron.io/veyron/veyron/lib/stats/histogram"
 	istats "veyron.io/veyron/veyron/services/mgmt/stats"
-
-	"veyron.io/veyron/veyron2/rt"
 )
 
 func doGlob(root, pattern string, since time.Time, includeValues bool) ([]libstats.KeyValue, error) {
 	it := libstats.Glob(root, pattern, since, includeValues)
 	out := []libstats.KeyValue{}
 	for it.Advance() {
-		v := it.Value()
-		out = append(out, v)
+		out = append(out, it.Value())
 	}
 	if err := it.Err(); err != nil {
 		return nil, err
@@ -27,8 +24,6 @@
 }
 
 func TestStats(t *testing.T) {
-	rt.Init()
-
 	now := time.Unix(1, 0)
 	counter.Now = func() time.Time { return now }
 
@@ -273,6 +268,54 @@
 	}
 }
 
+func TestMap(t *testing.T) {
+	m := libstats.NewMap("testing/foo")
+	m.Set([]libstats.KeyValue{{"a", 1}, {"b", 2}})
+
+	// Test the Value of the map.
+	{
+		got, err := libstats.Value("testing/foo")
+		if err != nil {
+			t.Errorf("unexpected error: %v", err)
+		}
+		if expected := interface{}(nil); got != expected {
+			t.Errorf("unexpected result. Got %v, want %v", got, expected)
+		}
+	}
+	// Test Glob on the map object.
+	{
+		got, err := doGlob("testing", "foo/...", time.Time{}, true)
+		if err != nil {
+			t.Errorf("unexpected error: %v", err)
+		}
+		expected := []libstats.KeyValue{
+			libstats.KeyValue{Key: "foo", Value: nil},
+			libstats.KeyValue{Key: "foo/a", Value: int64(1)},
+			libstats.KeyValue{Key: "foo/b", Value: int64(2)},
+		}
+		if !reflect.DeepEqual(got, expected) {
+			t.Errorf("unexpected result. Got %#v, want %#v", got, expected)
+		}
+	}
+
+	m.Delete([]string{"a"})
+
+	// Test Glob on the map object.
+	{
+		got, err := doGlob("testing", "foo/...", time.Time{}, true)
+		if err != nil {
+			t.Errorf("unexpected error: %v", err)
+		}
+		expected := []libstats.KeyValue{
+			libstats.KeyValue{Key: "foo", Value: nil},
+			libstats.KeyValue{Key: "foo/b", Value: int64(2)},
+		}
+		if !reflect.DeepEqual(got, expected) {
+			t.Errorf("unexpected result. Got %#v, want %#v", got, expected)
+		}
+	}
+}
+
 func TestDelete(t *testing.T) {
 	_ = libstats.NewInteger("a/b/c/d")
 	if _, err := libstats.GetStatsObject("a/b/c/d"); err != nil {
diff --git a/lib/stats/sysstats/sysstats.go b/lib/stats/sysstats/sysstats.go
new file mode 100644
index 0000000..ead2de1
--- /dev/null
+++ b/lib/stats/sysstats/sysstats.go
@@ -0,0 +1,73 @@
+// Package sysstats exports system statistics, and updates them periodically.
+// The package does not export any symbols, but needs to be imported for its
+// side-effects.
+package sysstats
+
+import (
+	"os"
+	"reflect"
+	"runtime"
+	"strings"
+	"time"
+
+	"veyron.io/veyron/veyron/lib/stats"
+)
+
+func init() {
+	now := time.Now()
+	stats.NewInteger("system/start-time-unix").Set(now.Unix())
+	stats.NewString("system/start-time-rfc1123").Set(now.Format(time.RFC1123))
+	stats.NewString("system/cmdline").Set(strings.Join(os.Args, " "))
+	stats.NewInteger("system/num-cpu").Set(int64(runtime.NumCPU()))
+	stats.NewInteger("system/num-goroutine").Set(int64(runtime.NumGoroutine()))
+	stats.NewString("system/version").Set(runtime.Version())
+	if hostname, err := os.Hostname(); err == nil {
+		stats.NewString("system/hostname").Set(hostname)
+	}
+	exportEnv()
+	exportMemStats()
+}
+
+func exportEnv() {
+	kv := make([]stats.KeyValue, len(os.Environ()))
+	for i, v := range os.Environ() {
+		if parts := strings.SplitN(v, "=", 2); len(parts) == 2 {
+			kv[i] = stats.KeyValue{parts[0], parts[1]}
+		}
+	}
+	stats.NewMap("system/environ").Set(kv)
+}
+
+func exportMemStats() {
+	mstats := stats.NewMap("system/memstats")
+
+	// Get field names to export.
+	var memstats runtime.MemStats
+	fieldNames := []string{}
+	v := reflect.ValueOf(memstats)
+	v.FieldByNameFunc(func(name string) bool {
+		switch v.FieldByName(name).Kind() {
+		case reflect.Bool, reflect.Uint32, reflect.Uint64:
+			fieldNames = append(fieldNames, name)
+		}
+		return false
+	})
+	updateStats := func() {
+		var memstats runtime.MemStats
+		runtime.ReadMemStats(&memstats)
+		v := reflect.ValueOf(memstats)
+		kv := make([]stats.KeyValue, len(fieldNames))
+		for i, name := range fieldNames {
+			kv[i] = stats.KeyValue{name, v.FieldByName(name).Interface()}
+		}
+		mstats.Set(kv)
+	}
+	// Update stats now and every 10 seconds afterwards.
+	updateStats()
+	go func() {
+		for {
+			time.Sleep(10 * time.Second)
+			updateStats()
+		}
+	}()
+}
diff --git a/lib/stats/sysstats/sysstats_test.go b/lib/stats/sysstats/sysstats_test.go
new file mode 100644
index 0000000..e341c1e
--- /dev/null
+++ b/lib/stats/sysstats/sysstats_test.go
@@ -0,0 +1,33 @@
+package sysstats_test
+
+import (
+	"os"
+	"testing"
+
+	"veyron.io/veyron/veyron/lib/stats"
+	_ "veyron.io/veyron/veyron/lib/stats/sysstats"
+)
+
+func TestHostname(t *testing.T) {
+	obj, err := stats.GetStatsObject("system/hostname")
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+	expected, err := os.Hostname()
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+	if got := obj.Value(); got != expected {
+		t.Errorf("unexpected result. Got %q, want %q", got, expected)
+	}
+}
+
+func TestMemStats(t *testing.T) {
+	alloc, err := stats.GetStatsObject("system/memstats/Alloc")
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+	if v := alloc.Value(); v == uint64(0) {
+		t.Errorf("unexpected Alloc value. Got %v, want != 0", v)
+	}
+}
diff --git a/runtimes/google/rt/rt.go b/runtimes/google/rt/rt.go
index 4cd8779..a7c13e8 100644
--- a/runtimes/google/rt/rt.go
+++ b/runtimes/google/rt/rt.go
@@ -16,6 +16,7 @@
 	"veyron.io/veyron/veyron2/vlog"
 
 	"veyron.io/veyron/veyron/lib/exec"
+	_ "veyron.io/veyron/veyron/lib/stats/sysstats"
 	"veyron.io/veyron/veyron/profiles"
 	"veyron.io/veyron/veyron/runtimes/google/naming/namespace"
 )