summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--net/activesum/activesum.go80
-rw-r--r--net/activesum/activesum_test.go75
2 files changed, 155 insertions, 0 deletions
diff --git a/net/activesum/activesum.go b/net/activesum/activesum.go
new file mode 100644
index 000000000..d2815bfef
--- /dev/null
+++ b/net/activesum/activesum.go
@@ -0,0 +1,80 @@
+// Package activesum summarizes network activity into coarse event blocks.
+package activesum
+
+import (
+ "time"
+)
+
+// Event is a coarse (if all is well, at least half-minute) period of
+// network activity. Events end after a Idle time passes, or the network
+// interface changes.
+type Event struct {
+ Start time.Time // start of event
+ Duration time.Duration // duration of event
+ Bytes uint64 // total rx+tx bytes during event window
+ Interface string // network interface used for event
+}
+
+// Idle is the amount of time without data that marks the end of an event.
+const Idle = 30 * time.Second
+
+// ActiveSum stores activity summary state and generates Events.
+type ActiveSum struct {
+ // EventFunc is used to deliver complete Events.
+ EventFunc func(ev Event)
+
+ // Current event details.
+ start time.Time // beginning of current event
+ last time.Duration // nanos beyond start when last event was recorded
+ bytes uint64 // total rx+tx bytes so far
+ iface string // network interface of current event
+}
+
+// Variables for testing.
+var timeNow = time.Now
+var timeSince = time.Since
+
+// Record records bytes transferred.
+func (a *ActiveSum) Record(bytes uint64, iface string) {
+ if bytes == 0 {
+ return
+ }
+
+ // The function time.Since is faster than a typical time.Now call
+ // because a.start includes monotonic time, so it uses a fast path
+ // in the runtime that does clock_gettime via VDSO on linux.
+ since := timeSince(a.start)
+
+ // Clear previous event if Idle has passed or interface changed.
+ if a.start.IsZero() || a.iface != iface || (since-a.last) > Idle {
+ a.recordEvent()
+
+ // Calls to time.Now are relatively slow (in per-packet terms), but
+ // we only call it once per event, which lasts at least Idle.
+ a.start = timeNow()
+ a.iface = iface
+ a.bytes = 0
+ since = 0
+ }
+
+ a.bytes += bytes
+ a.last = since
+}
+
+func (a *ActiveSum) recordEvent() {
+ if a.start.IsZero() {
+ return
+ }
+ a.EventFunc(Event{
+ Start: a.start,
+ Duration: a.last,
+ Bytes: a.bytes,
+ Interface: a.iface,
+ })
+}
+
+// Close stops ActiveSum and records any remaining Event.
+func (a *ActiveSum) Close() {
+ a.recordEvent()
+ a.start = time.Time{}
+}
diff --git a/net/activesum/activesum_test.go b/net/activesum/activesum_test.go
new file mode 100644
index 000000000..5e350a3a9
--- /dev/null
+++ b/net/activesum/activesum_test.go
@@ -0,0 +1,75 @@
+package activesum
+
+import (
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+type testDatum struct {
+ offset time.Duration
+ bytes uint64
+ iface string
+}
+
+var tests = []struct {
+ name string
+ data []testDatum
+ want []Event
+}{
+ {
+ name: "basic",
+ data: []testDatum{
+ {offset: 0, bytes: 128, iface: "eth0"},
+ {offset: time.Millisecond, bytes: 512, iface: "eth0"},
+ {offset: 2 * time.Millisecond, bytes: 256, iface: "eth0"},
+ {offset: time.Second - 3*time.Millisecond, bytes: 128, iface: "eth0"},
+ {offset: 2 * Idle, bytes: 128, iface: "eth0"},
+ {offset: 2 * Idle, bytes: 50, iface: "eth0"},
+ {offset: 0, bytes: 50, iface: "lte0"},
+ {offset: time.Second, bytes: 50, iface: "eth0"},
+ {offset: Idle - 1*time.Second, bytes: 50, iface: "eth0"},
+ {offset: Idle - 1*time.Second, bytes: 50, iface: "eth0"},
+ {offset: Idle - 1*time.Second, bytes: 50, iface: "eth0"},
+ {offset: Idle - 1*time.Second, bytes: 50, iface: "eth0"},
+ },
+ want: []Event{
+ {Start: start, Duration: time.Second, Bytes: 1024, Interface: "eth0"},
+ {Start: start.Add(2*Idle + time.Second), Bytes: 128, Interface: "eth0"},
+ {Start: start.Add(4*Idle + time.Second), Bytes: 50, Interface: "eth0"},
+ {Start: start.Add(4*Idle + time.Second), Bytes: 50, Interface: "lte0"},
+ {Start: start.Add(4*Idle + 2*time.Second), Duration: 1*time.Minute + 56*time.Second, Bytes: 250, Interface: "eth0"},
+ },
+ },
+}
+
+var start = time.Date(1999, time.December, 31, 11, 11, 11, 0, time.UTC)
+
+func TestActiveSum(t *testing.T) {
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ now := start
+ timeNow = func() time.Time { return now }
+ timeSince = func(t time.Time) time.Duration { return now.Sub(t) }
+ t.Cleanup(func() {
+ timeNow = time.Now
+ timeSince = time.Since
+ })
+
+ var got []Event
+ a := &ActiveSum{EventFunc: func(ev Event) {
+ got = append(got, ev)
+ }}
+ for _, d := range test.data {
+ now = now.Add(d.offset)
+ a.Record(d.bytes, d.iface)
+ }
+ a.Close()
+ if !reflect.DeepEqual(got, test.want) {
+ t.Errorf("events mismatch (-got +want):\n%s", cmp.Diff(got, test.want))
+ }
+ })
+ }
+}