Skip to content

Commit c3d609e

Browse files
authored
Add NewTimedSlidingWindowI64 and a few other minor maths helpers (#23)
* add more maths helpers * Add NewTimedSlidingWindowI64 * comments
1 parent 44cb4fd commit c3d609e

File tree

6 files changed

+395
-0
lines changed

6 files changed

+395
-0
lines changed

maths/maths.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package maths
22

3+
import "math"
4+
35
// MaxInt returns the bigger value of the two input ints.
46
func MaxInt(x, y int) int {
57
if x > y {
@@ -16,6 +18,41 @@ func MinInt(x, y int) int {
1618
return y
1719
}
1820

21+
// AbsInt returns the absolute value of an int value.
22+
func AbsInt(a int) int {
23+
if a < 0 {
24+
return -a
25+
}
26+
return a
27+
}
28+
1929
// MaxIntValue is the max value for type int.
2030
// https://groups.google.com/forum/#!msg/golang-nuts/a9PitPAHSSU/ziQw1-QHw3EJ
2131
const MaxIntValue = int(^uint(0) >> 1)
32+
33+
// MaxI64 returns the bigger value of the two input int64s.
34+
func MaxI64(x, y int64) int64 {
35+
if x > y {
36+
return x
37+
}
38+
return y
39+
}
40+
41+
// MinI64 returns the smaller value of the two input int64s.
42+
func MinI64(x, y int64) int64 {
43+
if x < y {
44+
return x
45+
}
46+
return y
47+
}
48+
49+
// AbsI64 returns the absolute value of an int64 value.
50+
func AbsI64(a int64) int64 {
51+
if a < 0 {
52+
return -a
53+
}
54+
return a
55+
}
56+
57+
// MaxI64Value is the max value for type int64.
58+
const MaxI64Value = math.MaxInt64

maths/maths_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,71 @@ func TestMinMaxInt(t *testing.T) {
4343
})
4444
}
4545
}
46+
47+
func TestAbsIntAndInt64(t *testing.T) {
48+
tests := []struct {
49+
name string
50+
in int
51+
expected int
52+
}{
53+
{
54+
name: "in > 0",
55+
in: 1,
56+
expected: 1,
57+
},
58+
{
59+
name: "in == 0",
60+
in: 0,
61+
expected: 0,
62+
},
63+
{
64+
name: "in < 0",
65+
in: -4,
66+
expected: 4,
67+
},
68+
}
69+
for _, test := range tests {
70+
t.Run(test.name, func(t *testing.T) {
71+
assert.Equal(t, test.expected, AbsInt(test.in))
72+
assert.Equal(t, int64(test.expected), AbsI64(int64(test.in)))
73+
})
74+
}
75+
}
76+
77+
func TestMinMaxI64(t *testing.T) {
78+
tests := []struct {
79+
name string
80+
x int64
81+
y int64
82+
expectedMin int64
83+
expectedMax int64
84+
}{
85+
{
86+
name: "x less than y",
87+
x: 1,
88+
y: 2,
89+
expectedMin: 1,
90+
expectedMax: 2,
91+
},
92+
{
93+
name: "x greater than y",
94+
x: 2,
95+
y: 1,
96+
expectedMin: 1,
97+
expectedMax: 2,
98+
},
99+
{
100+
name: "x equal to y",
101+
x: 2,
102+
y: 2,
103+
expectedMin: 2,
104+
expectedMax: 2,
105+
},
106+
}
107+
for _, test := range tests {
108+
t.Run(test.name, func(t *testing.T) {
109+
assert.Equal(t, test.expectedMin, MinI64(test.x, test.y))
110+
assert.Equal(t, test.expectedMax, MaxI64(test.x, test.y))
111+
})
112+
}
113+
}

times/clock.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package times
2+
3+
import "time"
4+
5+
// Clock tells the current time.
6+
type Clock interface {
7+
Now() time.Time
8+
}
9+
10+
type osClock struct{}
11+
12+
func (*osClock) Now() time.Time {
13+
return time.Now()
14+
}
15+
16+
// NewOSClock returns a Clock interface implementation that uses time.Now.
17+
func NewOSClock() *osClock {
18+
return &osClock{}
19+
}

times/clock_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package times
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestOSClock(t *testing.T) {
11+
c := NewOSClock()
12+
cnow := c.Now()
13+
osnow := time.Now()
14+
assert.True(t, cnow.Before(osnow) || cnow.Equal(osnow))
15+
}

times/timedSlidingWindow.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package times
2+
3+
import (
4+
"time"
5+
6+
"github.com/jf-tech/go-corelib/maths"
7+
)
8+
9+
/*
10+
Until we move to golang generic, the interface{} based generic implementation is simply too
11+
slow, compared with raw type (int, int64, etc) implementation. For example, we compared this
12+
generic interface{} based implementation against an nearly identical but with direct int type
13+
implementation, the benchmark is not even close: too many int<->interface{} conversion induced
14+
heap escape:
15+
16+
BenchmarkTimedSlidingWindowIntRaw-8 100 11409978 ns/op 600 B/op 3 allocs/op
17+
BenchmarkTimedSlidingWindowIntIFace-8 31 37520116 ns/op 11837979 B/op 1479595 allocs/op
18+
19+
So the decision is to comment out the interface{} implementation for reference only.
20+
21+
type TimedSlidingWindowOp func(a, b interface{}) interface{}
22+
23+
type TimedSlidingWindowCfg struct {
24+
Clock Clock
25+
Window, Bucket time.Duration
26+
Adder, Subtracter TimedSlidingWindowOp
27+
}
28+
29+
type TimedSlidingWindow struct {
30+
cfg TimedSlidingWindowCfg
31+
n int
32+
buckets []interface{}
33+
start, end int
34+
startTime time.Time
35+
total interface{}
36+
}
37+
38+
func (s *TimedSlidingWindow) Add(amount interface{}) {
39+
now := s.cfg.Clock.Now()
40+
idx := int(now.Sub(s.startTime) / s.cfg.Bucket)
41+
e2 := s.end
42+
if s.end < s.start {
43+
e2 += s.n
44+
}
45+
if s.start+idx-e2 < s.n {
46+
for i := e2 + 1; i <= s.start+idx; i++ {
47+
s.total = s.cfg.Subtracter(s.total, s.buckets[i%s.n])
48+
s.buckets[i%s.n] = nil
49+
}
50+
s.end = (s.start + idx) % s.n
51+
newStart := maths.MaxInt(s.start+idx-s.n+1, s.start)
52+
s.startTime = s.startTime.Add(time.Duration(newStart-s.start) * s.cfg.Bucket)
53+
s.start = newStart
54+
s.buckets[s.end] = s.cfg.Adder(s.buckets[s.end], amount)
55+
s.total = s.cfg.Adder(s.total, amount)
56+
} else {
57+
for i := 0; i < s.n; i++ {
58+
s.buckets[i] = nil
59+
}
60+
s.start, s.end = 0, 0
61+
s.buckets[0] = amount
62+
s.total = amount
63+
s.startTime = now
64+
}
65+
}
66+
67+
func (s *TimedSlidingWindow) Total() interface{} {
68+
s.Add(nil)
69+
return s.total
70+
}
71+
72+
func NewTimedSlidingWindow(cfg TimedSlidingWindowCfg) *TimedSlidingWindow {
73+
if cfg.Window == 0 || cfg.Window%cfg.Bucket != 0 {
74+
panic("time window must be non-zero multiple of bucket")
75+
}
76+
n := int(cfg.Window / cfg.Bucket)
77+
return &TimedSlidingWindow{
78+
cfg: cfg,
79+
n: n,
80+
buckets: make([]interface{}, n),
81+
startTime: cfg.Clock.Now(),
82+
}
83+
}
84+
*/
85+
86+
// TimedSlidingWindowI64 offers a way to aggregate int64 values over a time-based sliding window.
87+
type TimedSlidingWindowI64 struct {
88+
clock Clock
89+
window, bucket time.Duration
90+
n int
91+
buckets []int64
92+
start, end int
93+
startTime time.Time
94+
total int64
95+
}
96+
97+
// Add adds a new int64 value into the current sliding window.
98+
func (t *TimedSlidingWindowI64) Add(amount int64) {
99+
now := t.clock.Now()
100+
idx := int(now.Sub(t.startTime) / t.bucket)
101+
e2 := t.end
102+
if t.end < t.start {
103+
e2 += t.n
104+
}
105+
if t.start+idx-e2 < t.n {
106+
for i := e2 + 1; i <= t.start+idx; i++ {
107+
t.total -= t.buckets[i%t.n]
108+
t.buckets[i%t.n] = 0
109+
}
110+
t.end = (t.start + idx) % t.n
111+
newStart := maths.MaxInt(t.start+idx-t.n+1, t.start)
112+
t.startTime = t.startTime.Add(time.Duration(newStart-t.start) * t.bucket)
113+
t.start = newStart
114+
t.buckets[t.end] += amount
115+
t.total += amount
116+
} else {
117+
for i := 0; i < t.n; i++ {
118+
t.buckets[i] = 0
119+
}
120+
t.start, t.end = 0, 0
121+
t.buckets[0] = amount
122+
t.total = amount
123+
t.startTime = now
124+
}
125+
}
126+
127+
// Total returns the aggregated int64 value over the current sliding window.
128+
func (t *TimedSlidingWindowI64) Total() int64 {
129+
t.Add(0)
130+
return t.total
131+
}
132+
133+
// Reset resets the sliding window and clear the existing aggregated value.
134+
func (t *TimedSlidingWindowI64) Reset() {
135+
for i := 0; i < t.n; i++ {
136+
t.buckets[i] = 0
137+
}
138+
t.start, t.end = 0, 0
139+
t.startTime = t.clock.Now()
140+
t.total = 0
141+
}
142+
143+
// NewTimedSlidingWindowI64 creates a new time-based sliding window for int64 value
144+
// aggregation. window is the sliding window "width", and bucket is the granularity of
145+
// how the window is divided. Both must be non-zero and window must be of an integer
146+
// multiple of bucket. Be careful of not making bucket too small as it would increase
147+
// the internal bucket memory allocation. If no clock is passed in, then os time.Now
148+
// clock will be used.
149+
func NewTimedSlidingWindowI64(window, bucket time.Duration, clock ...Clock) *TimedSlidingWindowI64 {
150+
if window == 0 || bucket == 0 || window%bucket != 0 {
151+
panic("window must be a non-zero multiple of non-zero bucket")
152+
}
153+
c := Clock(NewOSClock())
154+
if len(clock) > 0 {
155+
c = clock[0]
156+
}
157+
n := int(window / bucket)
158+
return &TimedSlidingWindowI64{
159+
clock: c,
160+
window: window,
161+
bucket: bucket,
162+
n: n,
163+
buckets: make([]int64, n),
164+
startTime: c.Now(),
165+
}
166+
}

0 commit comments

Comments
 (0)