github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/storage/segment/fuzz_test.go (about) 1 package segment 2 3 import ( 4 "log" 5 "math/big" 6 "math/rand" 7 "sync" 8 "time" 9 10 . "github.com/onsi/ginkgo/v2" 11 . "github.com/onsi/gomega" 12 13 "github.com/pyroscope-io/pyroscope/pkg/testing" 14 ) 15 16 type datapoint struct { 17 t time.Time 18 samples uint64 19 r *big.Rat 20 } 21 22 type storageMock struct { 23 resolution time.Duration 24 data []datapoint 25 } 26 27 func newMock(resolution time.Duration) *storageMock { 28 return &storageMock{ 29 resolution: resolution, 30 data: []datapoint{}, 31 } 32 } 33 34 func (sm *storageMock) Put(st, et time.Time, samples uint64) { 35 st, et = normalize(st, et) 36 fullDur := et.Sub(st) / sm.resolution 37 for t := st; t.Before(et); t = t.Add(sm.resolution) { 38 d := datapoint{ 39 t: t, 40 samples: samples, 41 r: big.NewRat(int64(samples), int64(fullDur)), 42 } 43 44 sm.data = append(sm.data, d) 45 } 46 } 47 48 func (sm *storageMock) Get(st, et time.Time, cb func(depth int, samples, writes uint64, t time.Time, r *big.Rat)) { 49 st, et = normalize(st, et) 50 for _, d := range sm.data { 51 if !d.t.Before(st) && !d.t.Add(sm.resolution).After(et) { 52 cb(0, 1, 1, d.t, d.r) 53 } 54 } 55 } 56 57 // if you change something in this test make sure it doesn't change test coverage. 58 func fuzzTest(testWrites bool, writeSize func() int) { 59 s := New() 60 m := newMock(10 * time.Second) 61 62 r := rand.New(rand.NewSource(1213)) 63 64 for k := 0; k < 20; k++ { 65 maxStartTime := r.Intn(5000) 66 // for i := 0; i < 10; i++ { 67 for i := 0; i < r.Intn(200); i++ { 68 sti := r.Intn(maxStartTime) * 10 69 st := testing.SimpleTime(sti) 70 et := testing.SimpleTime(sti + writeSize()) 71 dur := et.Sub(st) 72 73 // samples := uint64(1+r.Intn(10)) * uint64(dur/(10*time.Second)) 74 samples := uint64(20) 75 76 m.Put(st, et, samples) 77 s.Put(st, et, samples, func(depth int, t time.Time, r *big.Rat, addons []Addon) { 78 log.Println(depth, r, dur) 79 }) 80 } 81 mSum := big.NewRat(0, 1) 82 mWrites := big.NewRat(0, 1) 83 sSum := big.NewRat(0, 1) 84 sWrites := big.NewRat(0, 1) 85 for i := 0; i < r.Intn(100); i++ { 86 sti := r.Intn(100) * 10 87 st := testing.SimpleTime(sti) 88 et := testing.SimpleTime(sti + r.Intn(100)*10) 89 90 m.Get(st, et, func(depth int, samples, writes uint64, t time.Time, r *big.Rat) { 91 rClone := big.NewRat(r.Num().Int64(), r.Denom().Int64()) 92 mSum.Add(mSum, rClone.Mul(rClone, big.NewRat(int64(samples), 1))) 93 log.Println("mWrites", samples, writes, r) 94 // if r.Num().Int64() > 0 { 95 // r = r.Inv(r) 96 w := big.NewRat(int64(writes), 1) 97 // mWrites.Add(mWrites, r.Mul(r, w)) 98 mWrites.Add(mWrites, w) 99 // } 100 }) 101 102 s.Get(st, et, func(depth int, samples, writes uint64, t time.Time, r *big.Rat) { 103 rClone := big.NewRat(r.Num().Int64(), r.Denom().Int64()) 104 sSum.Add(sSum, rClone.Mul(rClone, big.NewRat(int64(samples), 1))) 105 log.Println("sWrites", samples, writes, r) 106 // if r.Num().Int64() > 0 { 107 // r = r.Inv(r) 108 w := big.NewRat(int64(writes), 1) 109 // sWrites.Add(sWrites, r.Mul(r, w)) 110 sWrites.Add(sWrites, w) 111 // } 112 }) 113 } 114 mSumF, _ := mSum.Float64() 115 mWritesF, _ := mWrites.Float64() 116 log.Println("m:", mSum, mSumF, mWrites, mWritesF) 117 118 sSumF, _ := sSum.Float64() 119 sWritesF, _ := sWrites.Float64() 120 log.Println("s:", sSum, sSumF, sWrites, sWritesF) 121 122 Expect(mSum.Cmp(sSum)).To(Equal(0)) 123 if testWrites { 124 Expect(mWrites.Cmp(sWrites)).To(Equal(0)) 125 } 126 } 127 } 128 129 // See https://github.com/pyroscope-io/pyroscope/issues/28 for more context 130 var _ = Describe("segment", func() { 131 Context("fuzz tests", func() { 132 Context("writes are 10 second long", func() { 133 It("works as expected", func() { 134 done := make(chan interface{}) 135 go func() { 136 fuzzTest(true, func() int { 137 return 10 138 }) 139 close(done) 140 }() 141 Eventually(done, 5).Should(BeClosed()) 142 }) 143 }) 144 Context("writes are different lengths", func() { 145 It("works as expected", func() { 146 done := make(chan interface{}) 147 go func() { 148 fuzzTest(false, func() int { 149 return 20 150 // return 1 + rand.Intn(10)*10 151 }) 152 close(done) 153 }() 154 Eventually(done, 5).Should(BeClosed()) 155 }) 156 }) 157 Context("retention and sampling randomized test", func() { 158 It("works as expected", func() { 159 var ( 160 seed = 7332 161 n = 1 162 wg sync.WaitGroup 163 ) 164 wg.Add(n) 165 for i := 0; i < n; i++ { 166 go func(i int) { 167 fuzzDeleteNodesBefore(seed + i) 168 wg.Done() 169 }(i) 170 } 171 wg.Wait() 172 }) 173 }) 174 }) 175 }) 176 177 func fuzzDeleteNodesBefore(seed int) { 178 defer GinkgoRecover() 179 180 s := New() 181 r := rand.New(rand.NewSource(int64(seed))) 182 w := testSegWriter{ 183 n: 10e3, // Number of writes 184 r: r, 185 186 samplesPerWrite: 100, 187 writeTimeSpanSec: 10, 188 startTimeMin: randInt(1000, 3000), 189 startTimeMax: randInt(7000, 100000), 190 191 buckets: make([]*bucket, 10), 192 } 193 194 w.write(s) 195 196 for _, b := range w.buckets { 197 // Delete samples that fall within the time span of the bucket. 198 removed, err := s.DeleteNodesBefore(&RetentionPolicy{AbsoluteTime: b.time}) 199 Expect(err).ToNot(HaveOccurred()) 200 Expect(removed).To(BeFalse()) 201 // Ensure we have removed expected number of samples from the segment. 202 samples, writes := totalSamplesWrites(s, time.Time{}, testing.SimpleTime(w.startTimeMax*10)) 203 Expect(samples).To(Equal(b.samples)) 204 Expect(writes).To(Equal(b.writes)) 205 // Ensure no samples left outside the retention period. 206 samples, writes = totalSamplesWrites(s, b.time, testing.SimpleTime(w.startTimeMax*10)) 207 Expect(samples).To(Equal(b.samples)) 208 Expect(writes).To(Equal(b.writes)) 209 } 210 211 st := testing.SimpleTime(w.startTimeMax * 10) 212 samples, writes := totalSamplesWrites(s, st, st.Add(time.Hour)) 213 Expect(samples).To(BeZero()) 214 Expect(writes).To(BeZero()) 215 } 216 217 // testSegWriter inserts randomized data into the segment recording the 218 // samples distribution by time. Every bucket indicates the number of 219 // writes and samples that had been written before the bucket time mark. 220 type testSegWriter struct { 221 r *rand.Rand 222 n int 223 224 samplesPerWrite int 225 writeTimeSpanSec int 226 expectedWrites int 227 228 startTimeMin int 229 startTimeMax int 230 231 buckets []*bucket 232 } 233 234 type bucket struct { 235 time time.Time 236 samples int 237 writes int 238 } 239 240 func (f testSegWriter) putStartEndTime() (st time.Time, et time.Time) { 241 st = testing.SimpleTime(randInt(f.startTimeMin, f.startTimeMax) * 10) 242 et = st.Add(time.Second * time.Duration(f.writeTimeSpanSec)) 243 return st, et 244 } 245 246 func randInt(min, max int) int { return rand.Intn(max-min) + min } 247 248 func (f testSegWriter) expectedSamples() int { return f.n * f.samplesPerWrite } 249 250 func (f testSegWriter) write(s *Segment) { 251 // Initialize time buckets, if required: the whole time 252 // span is divided proportionally to the number of buckets. 253 if len(f.buckets) > 0 { 254 step := (f.startTimeMax - f.startTimeMin) / len(f.buckets) * 10 255 for i := 0; i < len(f.buckets); i++ { 256 f.buckets[i] = &bucket{time: testing.SimpleTime(f.startTimeMin + step*i)} 257 } 258 } 259 for i := 0; i < f.n; i++ { 260 st, et := f.putStartEndTime() 261 err := s.Put(st, et, uint64(f.samplesPerWrite), putNoOp) 262 Expect(err).ToNot(HaveOccurred()) 263 for _, b := range f.buckets { 264 if et.After(b.time) { 265 b.samples += f.samplesPerWrite 266 b.writes++ 267 } 268 } 269 } 270 } 271 272 func totalSamplesWrites(s *Segment, st, et time.Time) (samples, writes int) { 273 v := big.NewRat(0, 1) 274 s.Get(st, et, func(depth int, s, w uint64, t time.Time, r *big.Rat) { 275 x := big.NewRat(r.Num().Int64(), r.Denom().Int64()) 276 v.Add(v, x.Mul(x, big.NewRat(int64(s), 1))) 277 writes += int(w) 278 }) 279 return int(v.Num().Int64()), writes 280 }