@@ -15,6 +15,7 @@ package silence
1515
1616import (
1717 "strconv"
18+ "sync"
1819 "testing"
1920 "time"
2021
@@ -153,3 +154,257 @@ func benchmarkQuery(b *testing.B, numSilences int) {
153154 require .Len (b , sils , numSilences / 10 )
154155 }
155156}
157+
158+ // BenchmarkQueryParallel benchmarks concurrent queries to demonstrate
159+ // the performance improvement from using read locks (RLock) instead of
160+ // write locks (Lock). With the pre-compiled matcher cache, multiple
161+ // queries can now execute in parallel.
162+ func BenchmarkQueryParallel (b * testing.B ) {
163+ b .Run ("100 silences, 1 goroutine" , func (b * testing.B ) {
164+ benchmarkQueryParallel (b , 100 , 1 )
165+ })
166+ b .Run ("100 silences, 2 goroutines" , func (b * testing.B ) {
167+ benchmarkQueryParallel (b , 100 , 2 )
168+ })
169+ b .Run ("100 silences, 4 goroutines" , func (b * testing.B ) {
170+ benchmarkQueryParallel (b , 100 , 4 )
171+ })
172+ b .Run ("100 silences, 8 goroutines" , func (b * testing.B ) {
173+ benchmarkQueryParallel (b , 100 , 8 )
174+ })
175+ b .Run ("1000 silences, 1 goroutine" , func (b * testing.B ) {
176+ benchmarkQueryParallel (b , 1000 , 1 )
177+ })
178+ b .Run ("1000 silences, 2 goroutines" , func (b * testing.B ) {
179+ benchmarkQueryParallel (b , 1000 , 2 )
180+ })
181+ b .Run ("1000 silences, 4 goroutines" , func (b * testing.B ) {
182+ benchmarkQueryParallel (b , 1000 , 4 )
183+ })
184+ b .Run ("1000 silences, 8 goroutines" , func (b * testing.B ) {
185+ benchmarkQueryParallel (b , 1000 , 8 )
186+ })
187+ b .Run ("10000 silences, 1 goroutine" , func (b * testing.B ) {
188+ benchmarkQueryParallel (b , 10000 , 1 )
189+ })
190+ b .Run ("10000 silences, 2 goroutines" , func (b * testing.B ) {
191+ benchmarkQueryParallel (b , 10000 , 2 )
192+ })
193+ b .Run ("10000 silences, 4 goroutines" , func (b * testing.B ) {
194+ benchmarkQueryParallel (b , 10000 , 4 )
195+ })
196+ b .Run ("10000 silences, 8 goroutines" , func (b * testing.B ) {
197+ benchmarkQueryParallel (b , 10000 , 8 )
198+ })
199+ }
200+
201+ func benchmarkQueryParallel (b * testing.B , numSilences , numGoroutines int ) {
202+ s , err := New (Options {})
203+ require .NoError (b , err )
204+
205+ clock := quartz .NewMock (b )
206+ s .clock = clock
207+ now := clock .Now ()
208+
209+ lset := model.LabelSet {"aaaa" : "AAAA" , "bbbb" : "BBBB" , "cccc" : "CCCC" }
210+
211+ // Create silences with pre-compiled matchers
212+ for i := 0 ; i < numSilences ; i ++ {
213+ id := strconv .Itoa (i )
214+ patA := "A{4}|" + id
215+ patB := id
216+ if i % 10 == 0 {
217+ patB = "B(B|C)B.|" + id
218+ }
219+
220+ sil := & silencepb.Silence {
221+ Matchers : []* silencepb.Matcher {
222+ {Type : silencepb .Matcher_REGEXP , Name : "aaaa" , Pattern : patA },
223+ {Type : silencepb .Matcher_REGEXP , Name : "bbbb" , Pattern : patB },
224+ },
225+ StartsAt : now .Add (- time .Minute ),
226+ EndsAt : now .Add (time .Hour ),
227+ UpdatedAt : now .Add (- time .Hour ),
228+ }
229+ require .NoError (b , s .Set (sil ))
230+ }
231+
232+ // Verify initial query works
233+ sils , _ , err := s .Query (
234+ QState (types .SilenceStateActive ),
235+ QMatches (lset ),
236+ )
237+ require .NoError (b , err )
238+ require .Len (b , sils , numSilences / 10 )
239+
240+ b .ResetTimer ()
241+
242+ // Run queries in parallel across multiple goroutines
243+ b .RunParallel (func (pb * testing.PB ) {
244+ for pb .Next () {
245+ sils , _ , err := s .Query (
246+ QState (types .SilenceStateActive ),
247+ QMatches (lset ),
248+ )
249+ if err != nil {
250+ b .Error (err )
251+ }
252+ if len (sils ) != numSilences / 10 {
253+ b .Errorf ("expected %d silences, got %d" , numSilences / 10 , len (sils ))
254+ }
255+ }
256+ })
257+ }
258+
259+ // BenchmarkQueryWithConcurrentAdds benchmarks the behavior when queries
260+ // are running concurrently with silence additions. This demonstrates how
261+ // the system handles read-heavy workloads with occasional writes.
262+ func BenchmarkQueryWithConcurrentAdds (b * testing.B ) {
263+ b .Run ("1000 initial silences, 10% add rate" , func (b * testing.B ) {
264+ benchmarkQueryWithConcurrentAdds (b , 1000 , 0.1 )
265+ })
266+ b .Run ("1000 initial silences, 1% add rate" , func (b * testing.B ) {
267+ benchmarkQueryWithConcurrentAdds (b , 1000 , 0.01 )
268+ })
269+ b .Run ("1000 initial silences, 0.1% add rate" , func (b * testing.B ) {
270+ benchmarkQueryWithConcurrentAdds (b , 1000 , 0.001 )
271+ })
272+ b .Run ("10000 initial silences, 1% add rate" , func (b * testing.B ) {
273+ benchmarkQueryWithConcurrentAdds (b , 10000 , 0.01 )
274+ })
275+ b .Run ("10000 initial silences, 0.1% add rate" , func (b * testing.B ) {
276+ benchmarkQueryWithConcurrentAdds (b , 10000 , 0.001 )
277+ })
278+ }
279+
280+ func benchmarkQueryWithConcurrentAdds (b * testing.B , initialSilences int , addRatio float64 ) {
281+ s , err := New (Options {})
282+ require .NoError (b , err )
283+
284+ clock := quartz .NewMock (b )
285+ s .clock = clock
286+ now := clock .Now ()
287+
288+ lset := model.LabelSet {"aaaa" : "AAAA" , "bbbb" : "BBBB" , "cccc" : "CCCC" }
289+
290+ // Create initial silences
291+ for i := 0 ; i < initialSilences ; i ++ {
292+ id := strconv .Itoa (i )
293+ patA := "A{4}|" + id
294+ patB := id
295+ if i % 10 == 0 {
296+ patB = "B(B|C)B.|" + id
297+ }
298+
299+ sil := & silencepb.Silence {
300+ Matchers : []* silencepb.Matcher {
301+ {Type : silencepb .Matcher_REGEXP , Name : "aaaa" , Pattern : patA },
302+ {Type : silencepb .Matcher_REGEXP , Name : "bbbb" , Pattern : patB },
303+ },
304+ StartsAt : now .Add (- time .Minute ),
305+ EndsAt : now .Add (time .Hour ),
306+ UpdatedAt : now .Add (- time .Hour ),
307+ }
308+ require .NoError (b , s .Set (sil ))
309+ }
310+
311+ var addCounter int
312+ var mu sync.Mutex
313+
314+ b .ResetTimer ()
315+
316+ // Run parallel operations
317+ b .RunParallel (func (pb * testing.PB ) {
318+ for pb .Next () {
319+ // Determine if this iteration should add a silence
320+ mu .Lock ()
321+ shouldAdd := float64 (addCounter ) < float64 (b .N )* addRatio
322+ if shouldAdd {
323+ addCounter ++
324+ }
325+ localCounter := addCounter + initialSilences
326+ mu .Unlock ()
327+
328+ if shouldAdd {
329+ // Add a new silence
330+ id := strconv .Itoa (localCounter )
331+ patA := "A{4}|" + id
332+ patB := "B(B|C)B.|" + id
333+
334+ sil := & silencepb.Silence {
335+ Matchers : []* silencepb.Matcher {
336+ {Type : silencepb .Matcher_REGEXP , Name : "aaaa" , Pattern : patA },
337+ {Type : silencepb .Matcher_REGEXP , Name : "bbbb" , Pattern : patB },
338+ },
339+ StartsAt : now .Add (- time .Minute ),
340+ EndsAt : now .Add (time .Hour ),
341+ UpdatedAt : now .Add (- time .Hour ),
342+ }
343+ if err := s .Set (sil ); err != nil {
344+ b .Error (err )
345+ }
346+ } else {
347+ // Query silences (the common operation)
348+ _ , _ , err := s .Query (
349+ QState (types .SilenceStateActive ),
350+ QMatches (lset ),
351+ )
352+ if err != nil {
353+ b .Error (err )
354+ }
355+ }
356+ }
357+ })
358+ }
359+
360+ // BenchmarkMutesParallel benchmarks concurrent Mutes calls to demonstrate
361+ // the performance improvement from parallel query execution.
362+ func BenchmarkMutesParallel (b * testing.B ) {
363+ b .Run ("100 silences, 4 goroutines" , func (b * testing.B ) {
364+ benchmarkMutesParallel (b , 100 , 4 )
365+ })
366+ b .Run ("1000 silences, 4 goroutines" , func (b * testing.B ) {
367+ benchmarkMutesParallel (b , 1000 , 4 )
368+ })
369+ b .Run ("10000 silences, 4 goroutines" , func (b * testing.B ) {
370+ benchmarkMutesParallel (b , 10000 , 4 )
371+ })
372+ b .Run ("10000 silences, 8 goroutines" , func (b * testing.B ) {
373+ benchmarkMutesParallel (b , 10000 , 8 )
374+ })
375+ }
376+
377+ func benchmarkMutesParallel (b * testing.B , numSilences , numGoroutines int ) {
378+ silences , err := New (Options {})
379+ require .NoError (b , err )
380+
381+ clock := quartz .NewMock (b )
382+ silences .clock = clock
383+ now := clock .Now ()
384+
385+ // Create silences that will match the alert
386+ for i := 0 ; i < numSilences ; i ++ {
387+ s := & silencepb.Silence {
388+ Matchers : []* silencepb.Matcher {{
389+ Type : silencepb .Matcher_EQUAL ,
390+ Name : "foo" ,
391+ Pattern : "bar" ,
392+ }},
393+ StartsAt : now ,
394+ EndsAt : now .Add (time .Minute ),
395+ }
396+ require .NoError (b , silences .Set (s ))
397+ }
398+
399+ m := types .NewMarker (prometheus .NewRegistry ())
400+ silencer := NewSilencer (silences , m , promslog .NewNopLogger ())
401+
402+ b .ResetTimer ()
403+
404+ // Run Mutes in parallel
405+ b .RunParallel (func (pb * testing.PB ) {
406+ for pb .Next () {
407+ silencer .Mutes (model.LabelSet {"foo" : "bar" })
408+ }
409+ })
410+ }
0 commit comments