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"
)