github.com/livekit/protocol@v1.16.1-0.20240517185851-47e4c6bba773/utils/timeseries/timeseries_test.go (about)

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