Skip to content

Commit

Permalink
use ps.List for Sample storage, so it is immutable. Have to implement…
Browse files Browse the repository at this point in the history
… custom marshalling
  • Loading branch information
paulbellamy committed Nov 10, 2015
1 parent a7dbb31 commit ddc7dd9
Show file tree
Hide file tree
Showing 7 changed files with 490 additions and 293 deletions.
4 changes: 2 additions & 2 deletions probe/host/system_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ func TestGetLoad(t *testing.T) {
t.Fatalf("Expected 3 metrics, but got: %v", have)
}
for key, metric := range have {
if len(metric.Samples) != 1 {
t.Errorf("Expected metric %v to have 1 sample, but had: %d", key, len(metric.Samples))
if metric.Len() != 1 {
t.Errorf("Expected metric %v to have 1 sample, but had: %d", key, metric.Len())
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion render/detailed_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ func hostOriginTable(nmd report.Node) (Table, bool) {
)
for _, key := range []string{host.Load1, host.Load5, host.Load15} {
if metric, ok := nmd.Metrics[key]; ok {
if len(metric.Samples) == 0 {
if metric.Len() == 0 {
continue
}
if metric.Max > maxLoad {
Expand Down
6 changes: 3 additions & 3 deletions report/latest_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func (m LatestMap) toIntermediate() map[string]LatestEntry {
return intermediate
}

func fromIntermediate(in map[string]LatestEntry) LatestMap {
func (m LatestMap) fromIntermediate(in map[string]LatestEntry) LatestMap {
out := ps.NewMap()
for k, v := range in {
out = out.Set(k, v)
Expand All @@ -105,7 +105,7 @@ func (m *LatestMap) UnmarshalJSON(input []byte) error {
if err := json.NewDecoder(bytes.NewBuffer(input)).Decode(&in); err != nil {
return err
}
*m = fromIntermediate(in)
*m = LatestMap{}.fromIntermediate(in)
return nil
}

Expand All @@ -122,6 +122,6 @@ func (m *LatestMap) GobDecode(input []byte) error {
if err := gob.NewDecoder(bytes.NewBuffer(input)).Decode(&in); err != nil {
return err
}
*m = fromIntermediate(in)
*m = LatestMap{}.fromIntermediate(in)
return nil
}
265 changes: 265 additions & 0 deletions report/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
package report

import (
"bytes"
"encoding/gob"
"encoding/json"
"time"

"github.com/mndrix/ps"
)

// Metrics is a string->metric map.
type Metrics map[string]Metric

// Merge merges two sets maps into a fresh set, performing set-union merges as
// appropriate.
func (m Metrics) Merge(other Metrics) Metrics {
result := m.Copy()
for k, v := range other {
result[k] = result[k].Merge(v)
}
return result
}

// Copy returns a value copy of the sets map.
func (m Metrics) Copy() Metrics {
result := Metrics{}
for k, v := range m {
result[k] = v.Copy()
}
return result
}

// Metric is a list of timeseries data. Clients must use the Add
// method to add values.
type Metric struct {
Samples Samples `json:"samples"`
Min float64 `json:"min"`
Max float64 `json:"max"`
First time.Time `json:"first"`
Last time.Time `json:"last"`
}

// Sample is a single datapoint of a metric.
type Sample struct {
Timestamp time.Time `json:"date"`
Value float64 `json:"value"`
}

// MakeMetric makes a new Metric.
func MakeMetric() Metric {
return Metric{}
}

// WithFirst returns a fresh copy of m, with First set to t.
func (m Metric) WithFirst(t time.Time) Metric {
m.First = t
return m
}

// Len returns the number of samples in the metric.
func (m Metric) Len() int {
return m.Samples.Size()
}

// Add adds the sample to the Metric. Add is the only valid way to grow a
// Metric. Add returns the Metric to enable chaining.
func (m Metric) Add(t time.Time, v float64) Metric {
newSamples := m.Samples
popped := Samples{}
for !newSamples.IsNil() {
head := newSamples.Head()
if head.Timestamp.Equal(t) {
// The list already has the element.
return m
}
if head.Timestamp.Before(t) {
// Reached insertion point.
break
}
newSamples = newSamples.Tail()
popped = popped.Cons(head)
}
newSamples = newSamples.Cons(Sample{Timestamp: t, Value: v})
// Re-add any samples after this one.
popped.ForEach(func(s Sample) {
newSamples = newSamples.Cons(s)
})
m.Samples = newSamples
if v > m.Max {
m.Max = v
}
if v < m.Min {
m.Min = v
}
if m.First.IsZero() || t.Before(m.First) {
m.First = t
}
if m.Last.IsZero() || t.After(m.Last) {
m.Last = t
}
return m
}

// Merge combines the two Metrics and returns a new result.
func (m Metric) Merge(other Metric) Metric {
other.Samples.ForEach(func(s Sample) {
m = m.Add(s.Timestamp, s.Value)
})
if !other.First.IsZero() && other.First.Before(m.First) {
m.First = other.First
}
if !other.Last.IsZero() && other.Last.After(m.Last) {
m.Last = other.Last
}
if other.Min < m.Min {
m.Min = other.Min
}
if other.Max > m.Max {
m.Max = other.Max
}
return m
}

// Copy returns a value copy of the Metric. Metric is immutable, so we can skip
// this.
func (m Metric) Copy() Metric {
return m
}

// Div returns a new copy of the metric, with each value divided by n.
func (m Metric) Div(n float64) Metric {
oldSamples := m.Samples
m.Samples = Samples{}
oldSamples.ForEach(func(s Sample) {
m = m.Add(s.Timestamp, s.Value/n)
})
m.Max = m.Max / n
m.Min = m.Min / n
return m
}

// LastSample returns the last sample in the metric, or nil if there are no
// samples.
func (m Metric) LastSample() *Sample {
if m.Samples.IsNil() {
return nil
}
s := m.Samples.Head()
return &s
}

type Samples struct {
ps.List
}

// IsNil returns true if the list is empty. Unlike ps.List, this also works if
// the list is nil.
func (s Samples) IsNil() bool {
return s.List == nil || s.List.IsNil()
}

// Cons returns a new list with val as the head
func (s Samples) Cons(val Sample) Samples {
if s.List == nil {
s.List = ps.NewList()
}
return Samples{s.List.Cons(val)}
}

// Head returns the first element of the list;
// panics if the list is empty
func (s Samples) Head() Sample {
return s.List.Head().(Sample)
}

// Tail returns a list with all elements except the head;
// panics if the list is empty
func (s Samples) Tail() Samples {
return Samples{s.List.Tail()}
}

// Size returns the list's length. This takes O(1) time. Unlike ps.List, this
// also works if the list is nil
func (s Samples) Size() int {
if s.List == nil {
return 0
}
return s.List.Size()
}

// ForEach executes a callback for each value in the list.
func (s Samples) ForEach(f func(Sample)) {
if s.List == nil {
return
}
s.List.ForEach(func(s interface{}) {
f(s.(Sample))
})
}

// Reverse returns a list whose elements are in the opposite order as
// the original list.
func (s Samples) Reverse() Samples {
if s.List == nil {
return s
}
return Samples{s.List.Reverse()}
}

func (s Samples) toIntermediate() []Sample {
samples := []Sample{}
s.Reverse().ForEach(func(s Sample) {
samples = append(samples, s)
})
return samples
}

func (s Samples) fromIntermediate(in []Sample) Samples {
list := ps.NewList()
for _, sample := range in {
list = list.Cons(sample)
}
return Samples{list}
}

// MarshalJSON implements json.Marshaller
func (s Samples) MarshalJSON() ([]byte, error) {
buf := bytes.Buffer{}
var err error
if s.List == nil {
err = json.NewEncoder(&buf).Encode(nil)
return buf.Bytes(), err
}

err = json.NewEncoder(&buf).Encode(s.toIntermediate())
return buf.Bytes(), err
}

// UnmarshalJSON implements json.Unmarshaler
func (s *Samples) UnmarshalJSON(input []byte) error {
in := []Sample{}
if err := json.NewDecoder(bytes.NewBuffer(input)).Decode(&in); err != nil {
return err
}
*s = Samples{}.fromIntermediate(in)
return nil
}

// GobEncode implements gob.Marshaller
func (s Samples) GobEncode() ([]byte, error) {
buf := bytes.Buffer{}
err := gob.NewEncoder(&buf).Encode(s.toIntermediate())
return buf.Bytes(), err
}

// GobDecode implements gob.Unmarshaller
func (s *Samples) GobDecode(input []byte) error {
in := []Sample{}
if err := gob.NewDecoder(bytes.NewBuffer(input)).Decode(&in); err != nil {
return err
}
*s = Samples{}.fromIntermediate(in)
return nil
}
Loading

0 comments on commit ddc7dd9

Please sign in to comment.