github.com/whoyao/protocol@v0.0.0-20230519045905-2d8ace718ca5/utils/timeseries/timeseries_test.go (about) 1 package timeseries 2 3 import ( 4 "testing" 5 "time" 6 7 "github.com/stretchr/testify/require" 8 ) 9 10 func TestTimeSeries(t *testing.T) { 11 t.Run("ordering", func(t *testing.T) { 12 ts := NewTimeSeries[uint32](TimeSeriesParams{ 13 UpdateOp: TimeSeriesUpdateOpMax, 14 Window: time.Minute, 15 }) 16 17 now := time.Now() 18 expectedSamples := []TimeSeriesSample[uint32]{ 19 { 20 Value: 10, 21 At: now, 22 }, 23 { 24 Value: 20, 25 At: now.Add(time.Second), 26 }, 27 { 28 Value: 30, 29 At: now.Add(2 * time.Second), 30 }, 31 { 32 Value: 40, 33 At: now.Add(3 * time.Second), 34 }, 35 } 36 37 testCases := []struct { 38 name string 39 samples []TimeSeriesSample[uint32] 40 }{ 41 { 42 name: "regular", 43 samples: []TimeSeriesSample[uint32]{ 44 {10, now}, 45 {20, now.Add(time.Second)}, 46 {30, now.Add(2 * time.Second)}, 47 {40, now.Add(3 * time.Second)}, 48 }, 49 }, 50 { 51 name: "reverse", 52 samples: []TimeSeriesSample[uint32]{ 53 {40, now.Add(3 * time.Second)}, 54 {30, now.Add(2 * time.Second)}, 55 {20, now.Add(time.Second)}, 56 {10, now}, 57 }, 58 }, 59 { 60 name: "jumbled 1", 61 samples: []TimeSeriesSample[uint32]{ 62 {20, now.Add(time.Second)}, 63 {40, now.Add(3 * time.Second)}, 64 {30, now.Add(2 * time.Second)}, 65 {10, now}, 66 }, 67 }, 68 { 69 name: "jumbled 2", 70 samples: []TimeSeriesSample[uint32]{ 71 {10, now}, 72 {40, now.Add(3 * time.Second)}, 73 {30, now.Add(2 * time.Second)}, 74 {20, now.Add(time.Second)}, 75 }, 76 }, 77 } 78 79 for _, tc := range testCases { 80 t.Run(tc.name, func(t *testing.T) { 81 ts.ClearSamples() 82 83 for _, tss := range tc.samples { 84 ts.AddSampleAt(tss.Value, tss.At) 85 } 86 87 require.Equal(t, expectedSamples, ts.GetSamples()) 88 }) 89 } 90 }) 91 92 t.Run("get samples after", func(t *testing.T) { 93 ts := NewTimeSeries[uint32](TimeSeriesParams{ 94 UpdateOp: TimeSeriesUpdateOpMax, 95 Window: 2 * time.Minute, 96 }) 97 98 expectedSamples := make([]TimeSeriesSample[uint32], 0, 4) 99 100 now := time.Now() 101 for val := uint32(0); val < 10; val++ { 102 at := now.Add(time.Duration(val) * time.Second) 103 ts.AddSampleAt(val, at) 104 if val > 5 { 105 expectedSamples = append(expectedSamples, TimeSeriesSample[uint32]{ 106 Value: val, 107 At: at, 108 }) 109 } 110 } 111 require.Equal(t, expectedSamples, ts.GetSamplesAfter(now.Add(5*time.Second))) 112 }) 113 114 t.Run("sum", func(t *testing.T) { 115 ts := NewTimeSeries[uint32](TimeSeriesParams{ 116 UpdateOp: TimeSeriesUpdateOpMax, 117 Window: 2 * time.Minute, 118 }) 119 120 ts.UpdateSample(10) 121 ts.UpdateSample(20) 122 ts.CommitActiveSampleAt(time.Now()) 123 require.Equal(t, float64(20.0), ts.Sum()) 124 125 ts.AddSample(30) 126 require.Equal(t, float64(50.0), ts.Sum()) 127 }) 128 129 t.Run("min", func(t *testing.T) { 130 ts := NewTimeSeries[uint32](TimeSeriesParams{ 131 UpdateOp: TimeSeriesUpdateOpLatest, 132 Window: 2 * time.Minute, 133 }) 134 135 ts.UpdateSample(10) 136 ts.UpdateSample(20) 137 ts.UpdateSample(15) 138 ts.CommitActiveSampleAt(time.Now()) 139 require.Equal(t, uint32(15), ts.Min()) 140 141 ts.AddSample(30) 142 require.Equal(t, uint32(15), ts.Min()) 143 }) 144 145 t.Run("max", func(t *testing.T) { 146 ts := NewTimeSeries[uint32](TimeSeriesParams{ 147 UpdateOp: TimeSeriesUpdateOpAdd, 148 Window: 2 * time.Minute, 149 }) 150 151 ts.UpdateSample(10) 152 ts.UpdateSample(20) 153 ts.CommitActiveSampleAt(time.Now()) 154 require.Equal(t, uint32(30), ts.Max()) 155 156 ts.AddSample(20) 157 require.Equal(t, uint32(30), ts.Max()) 158 }) 159 160 t.Run("current_run", func(t *testing.T) { 161 ts := NewTimeSeries[uint32](TimeSeriesParams{ 162 UpdateOp: TimeSeriesUpdateOpMax, 163 Window: time.Minute, 164 }) 165 166 testCases := []struct { 167 name string 168 values []uint32 169 timeStep time.Duration 170 compareOp TimeSeriesCompareOp 171 threshold uint32 172 expectedResult time.Duration 173 }{ 174 { 175 name: "eq_run", 176 values: []uint32{ 177 10, 178 20, 179 30, 180 40, 181 40, 182 40, 183 }, 184 timeStep: time.Second, 185 compareOp: TimeSeriesCompareOpEQ, 186 threshold: 40, 187 expectedResult: 2 * time.Second, 188 }, 189 { 190 name: "eq_no_run", 191 values: []uint32{ 192 10, 193 20, 194 30, 195 40, 196 40, 197 40, 198 }, 199 timeStep: time.Second, 200 compareOp: TimeSeriesCompareOpEQ, 201 threshold: 50, 202 expectedResult: 0, 203 }, 204 { 205 name: "ne", 206 values: []uint32{ 207 10, 208 20, 209 30, 210 40, 211 40, 212 40, 213 }, 214 timeStep: time.Second, 215 compareOp: TimeSeriesCompareOpNE, 216 threshold: 50, 217 expectedResult: 5 * time.Second, 218 }, 219 { 220 name: "gt", 221 values: []uint32{ 222 10, 223 20, 224 30, 225 40, 226 40, 227 40, 228 }, 229 timeStep: time.Second, 230 compareOp: TimeSeriesCompareOpGT, 231 threshold: 20, 232 expectedResult: 3 * time.Second, 233 }, 234 { 235 name: "gte", 236 values: []uint32{ 237 10, 238 20, 239 30, 240 40, 241 40, 242 40, 243 }, 244 timeStep: time.Second, 245 compareOp: TimeSeriesCompareOpGTE, 246 threshold: 20, 247 expectedResult: 4 * time.Second, 248 }, 249 { 250 name: "lt", 251 values: []uint32{ 252 50, 253 20, 254 30, 255 40, 256 40, 257 40, 258 }, 259 timeStep: time.Second, 260 compareOp: TimeSeriesCompareOpLT, 261 threshold: 50, 262 expectedResult: 4 * time.Second, 263 }, 264 { 265 name: "lte", 266 values: []uint32{ 267 10, 268 20, 269 30, 270 40, 271 40, 272 40, 273 }, 274 timeStep: time.Second, 275 compareOp: TimeSeriesCompareOpLTE, 276 threshold: 40, 277 expectedResult: 5 * time.Second, 278 }, 279 } 280 281 for _, tc := range testCases { 282 t.Run(tc.name, func(t *testing.T) { 283 ts.ClearSamples() 284 285 now := time.Now() 286 for idx, value := range tc.values { 287 ts.AddSampleAt(value, now.Add(time.Duration(idx)*tc.timeStep)) 288 } 289 290 require.Equal(t, tc.expectedResult, ts.CurrentRun(tc.threshold, tc.compareOp)) 291 }) 292 } 293 }) 294 295 t.Run("online", func(t *testing.T) { 296 ts := NewTimeSeries[uint32](TimeSeriesParams{ 297 UpdateOp: TimeSeriesUpdateOpMax, 298 Window: time.Minute, 299 }) 300 301 now := time.Now() 302 for val := uint32(1); val <= 10; val++ { 303 ts.AddSampleAt(val, now.Add(time.Duration(val)*time.Second)) 304 } 305 306 require.Equal(t, float64(5.5), ts.OnlineAverage()) 307 onlineVariance := ts.OnlineVariance() 308 require.Condition(t, func() bool { return onlineVariance > 9.16 && onlineVariance < 9.17 }, "online variance out of range") 309 onlineStdDev := ts.OnlineStdDev() 310 require.Condition(t, func() bool { return onlineStdDev > 3.02 && onlineStdDev < 3.03 }, "online std dev out of range") 311 }) 312 313 t.Run("collapse", func(t *testing.T) { 314 ts := NewTimeSeries[uint32](TimeSeriesParams{ 315 UpdateOp: TimeSeriesUpdateOpMax, 316 Window: time.Minute, 317 CollapseDuration: 2 * time.Second, 318 }) 319 320 // add same value spaced apart by half the collapse duration, should add only five to the list 321 now := time.Now() 322 for i := 0; i < 10; i++ { 323 ts.AddSampleAt(42, now.Add(time.Duration(i)*time.Second)) 324 } 325 samples := ts.GetSamples() 326 require.Equal(t, 5, len(samples)) 327 require.Equal(t, uint32(42), samples[0].Value) // spot check 328 require.Equal(t, uint32(42), samples[3].Value) // spot check 329 330 // add a sample of different value within the collapse window, it should get added 331 ts.AddSampleAt(43, now.Add(time.Duration(9)*time.Second)) // same time offset as last sample to keep within collapse window 332 samples = ts.GetSamples() 333 require.Equal(t, 6, len(samples)) 334 require.Equal(t, uint32(42), samples[0].Value) // spot check 335 require.Equal(t, uint32(42), samples[3].Value) // spot check 336 require.Equal(t, uint32(43), samples[5].Value) 337 338 // add a sample with same value as initial burst within the collapse window, it should get added 339 ts.AddSampleAt(42, now.Add(time.Duration(10)*time.Second)) 340 samples = ts.GetSamples() 341 require.Equal(t, 7, len(samples)) 342 require.Equal(t, uint32(42), samples[0].Value) // spot check 343 require.Equal(t, uint32(42), samples[3].Value) // spot check 344 require.Equal(t, uint32(43), samples[5].Value) 345 require.Equal(t, uint32(42), samples[6].Value) 346 }) 347 348 t.Run("slope", func(t *testing.T) { 349 ts := NewTimeSeries[float64](TimeSeriesParams{ 350 UpdateOp: TimeSeriesUpdateOpMax, 351 Window: time.Minute, 352 }) 353 354 // increasing values 355 now := time.Now() 356 for val := 1; val <= 10; val++ { 357 ts.AddSampleAt(float64(val)/10.0, now.Add(time.Duration(val)*time.Second)) 358 } 359 slope := ts.Slope() 360 require.Condition(t, func() bool { return slope > 5.71 && slope < 5.72 }, "slope out of range") 361 362 ts.ClearSamples() 363 364 // decreasing values 365 now = time.Now() 366 for val := 1; val <= 10; val++ { 367 ts.AddSampleAt(float64(11-val)/10.0, now.Add(time.Duration(val)*time.Second)) 368 } 369 slope = ts.Slope() 370 require.Condition(t, func() bool { return slope > -5.72 && slope < -5.71 }, "slope out of range") 371 372 ts.ClearSamples() 373 374 // see-saw values, slope should be 0.0 375 now = time.Now() 376 for val := 1; val <= 11; val++ { 377 if val&0x1 == 1 { 378 ts.AddSampleAt(1.0, now.Add(time.Duration(val)*time.Second)) 379 } else { 380 ts.AddSampleAt(10.0, now.Add(time.Duration(val)*time.Second)) 381 } 382 } 383 require.Equal(t, float64(0.0), ts.Slope()) 384 }) 385 386 t.Run("linear extrapolate to", func(t *testing.T) { 387 ts := NewTimeSeries[float64](TimeSeriesParams{ 388 UpdateOp: TimeSeriesUpdateOpMax, 389 Window: time.Minute, 390 }) 391 392 // increasing values 393 now := time.Now() 394 for val := 1; val <= 10; val++ { 395 ts.AddSampleAt(float64(val)/10.0, now.Add(time.Duration(val)*time.Second)) 396 } 397 398 // try to extrapolate using more than available samples 399 y, err := ts.LinearExtrapolateTo(11, 1*time.Second) 400 require.Error(t, err) 401 require.Equal(t, float64(0.0), y) 402 403 y, err = ts.LinearExtrapolateTo(10, 1*time.Second) 404 require.NoError(t, err) 405 require.Equal(t, float64(1.1), y) 406 407 ts.ClearSamples() 408 409 // decreasing values 410 now = time.Now() 411 for val := 1; val <= 10; val++ { 412 ts.AddSampleAt(float64(11-val)/10.0, now.Add(time.Duration(val)*time.Second)) 413 } 414 415 y, err = ts.LinearExtrapolateTo(10, 1*time.Second) 416 require.NoError(t, err) 417 // this picks up a value of -5.55 * 10^-17, probably due to float64 implementation, so check for smaller than some value very close to 0.0 418 // NOTE: printing the value still shows 0.000000, only require.Equal checking for 0.0 failed with that small value 419 require.Greater(t, float64(0.0000000000001), y) 420 }) 421 422 t.Run("kendall's tau", func(t *testing.T) { 423 ts := NewTimeSeries[int64](TimeSeriesParams{ 424 UpdateOp: TimeSeriesUpdateOpMax, 425 Window: time.Minute, 426 }) 427 428 // increasing values 429 now := time.Now() 430 for val := int64(1); val <= 10; val++ { 431 ts.AddSampleAt(val, now.Add(time.Duration(val)*time.Second)) 432 } 433 434 // asking to use more samples than available should return 0.0 435 tau, err := ts.KendallsTau(11) 436 require.Error(t, err) 437 require.Equal(t, float64(0.0), tau) 438 439 // ever increasing should return 1.0 440 tau, err = ts.KendallsTau(8) 441 require.NoError(t, err) 442 require.Equal(t, float64(1.0), tau) 443 444 ts.ClearSamples() 445 446 // decreasing values 447 now = time.Now() 448 for val := int64(1); val <= 10; val++ { 449 ts.AddSampleAt(11-val, now.Add(time.Duration(val)*time.Second)) 450 } 451 452 // ever decreasing should return -1.0 453 tau, err = ts.KendallsTau(8) 454 require.NoError(t, err) 455 require.Equal(t, float64(-1.0), tau) 456 457 ts.ClearSamples() 458 459 // overall increasing 460 now = time.Now() 461 for val := int64(1); val <= 10; val++ { 462 if val&0x1 == 0 { 463 ts.AddSampleAt(2*val, now.Add(time.Duration(val)*time.Second)) 464 } else { 465 ts.AddSampleAt(val, now.Add(time.Duration(val)*time.Second)) 466 } 467 } 468 469 // increasing envelope should trend positive 470 tau, err = ts.KendallsTau(8) 471 require.NoError(t, err) 472 require.Less(t, float64(0.0), tau) 473 474 // overall decreasing 475 now = time.Now() 476 for val := int64(1); val <= 10; val++ { 477 if val&0x1 == 0 { 478 ts.AddSampleAt(2*(11-val), now.Add(time.Duration(val)*time.Second)) 479 } else { 480 ts.AddSampleAt(11-val, now.Add(time.Duration(val)*time.Second)) 481 } 482 } 483 484 // decreasing envelope should trend negative 485 tau, err = ts.KendallsTau(8) 486 require.NoError(t, err) 487 require.Greater(t, float64(0.0), tau) 488 }) 489 }