11// Microbenchmarks for write-path contention in QueryWatchSession.
22// Compares production "lock + copy-on-stop" vs an alternative "ConcurrentQueue + snapshot".
3- #nullable enable
43using System . Collections . Concurrent ;
54using BenchmarkDotNet . Attributes ;
65
@@ -19,31 +18,31 @@ public class RecordThroughputBench {
1918 [ Params ( 2_000 ) ]
2019 public int EventsPerThread { get ; set ; } = 2_000 ;
2120
22- private KeelMatrix . QueryWatch . QueryWatchOptions _optsNoText = null ! ;
21+ private QueryWatchOptions _optsNoText = null ! ;
2322
2423 [ GlobalSetup ]
2524 public void Setup ( ) {
26- _optsNoText = new KeelMatrix . QueryWatch . QueryWatchOptions {
25+ _optsNoText = new QueryWatchOptions {
2726 CaptureSqlText = false // exercise the hot path without redaction overhead
2827 } ;
2928 }
3029
3130 [ Benchmark ( Baseline = true , Description = "lock[List] + copy-on-stop (production)" ) ]
32- public KeelMatrix . QueryWatch . QueryWatchReport LockList ( ) {
33- var session = new KeelMatrix . QueryWatch . QueryWatchSession ( _optsNoText ) ;
31+ public QueryWatchReport LockList ( ) {
32+ QueryWatchSession session = new ( _optsNoText ) ;
3433 RunWorkers ( Threads , EventsPerThread , ( ) => session . Record ( "SELECT 1" , TimeSpan . FromMilliseconds ( 1 ) ) ) ;
3534 return session . Stop ( ) ;
3635 }
3736
3837 [ Benchmark ( Description = "ConcurrentQueue + snapshot (alternative)" ) ]
39- public KeelMatrix . QueryWatch . QueryWatchReport ConcurrentQueueSnapshot ( ) {
40- var session = new ConcurrentQueueSession ( _optsNoText ) ;
38+ public QueryWatchReport ConcurrentQueueSnapshot ( ) {
39+ ConcurrentQueueSession session = new ( _optsNoText ) ;
4140 RunWorkers ( Threads , EventsPerThread , ( ) => session . Record ( "SELECT 1" , TimeSpan . FromMilliseconds ( 1 ) ) ) ;
4241 return session . Stop ( ) ;
4342 }
4443
4544 private static void RunWorkers ( int threads , int eventsPerThread , Action action ) {
46- var tasks = new Task [ threads ] ;
45+ Task [ ] tasks = new Task [ threads ] ;
4746 for ( int t = 0 ; t < threads ; t ++ ) {
4847 tasks [ t ] = Task . Run ( ( ) => {
4948 for ( int i = 0 ; i < eventsPerThread ; i ++ ) action ( ) ;
@@ -54,16 +53,11 @@ private static void RunWorkers(int threads, int eventsPerThread, Action action)
5453 }
5554
5655 // Minimal alternative session used only for benchmarking.
57- internal sealed class ConcurrentQueueSession {
58- private readonly ConcurrentQueue < KeelMatrix . QueryWatch . QueryEvent > _q = new ( ) ;
59- private readonly KeelMatrix . QueryWatch . QueryWatchOptions _options ;
56+ internal sealed class ConcurrentQueueSession ( QueryWatchOptions options ) {
57+ private readonly ConcurrentQueue < QueryEvent > _q = new ( ) ;
58+ private readonly QueryWatchOptions _options = options ?? throw new ArgumentNullException ( nameof ( options ) ) ;
6059 private int _stopped ; // 0=running,1=stopped
6160 private readonly DateTimeOffset _startedAt = DateTimeOffset . UtcNow ;
62- private DateTimeOffset ? _stoppedAt ;
63-
64- public ConcurrentQueueSession ( KeelMatrix . QueryWatch . QueryWatchOptions options ) {
65- _options = options ?? throw new ArgumentNullException ( nameof ( options ) ) ;
66- }
6761
6862 public void Record ( string commandText , TimeSpan duration ) {
6963 if ( Volatile . Read ( ref _stopped ) != 0 )
@@ -73,30 +67,27 @@ public void Record(string commandText, TimeSpan duration) {
7367 string text = string . Empty ;
7468 if ( _options . CaptureSqlText ) {
7569 text = commandText ?? string . Empty ;
76- foreach ( var r in _options . Redactors ) text = r . Redact ( text ) ;
70+ foreach ( IQueryTextRedactor r in _options . Redactors ) text = r . Redact ( text ) ;
7771 }
7872
7973 // second check to minimize recording after stop (not strictly needed for the benchmark)
8074 if ( Volatile . Read ( ref _stopped ) != 0 )
8175 throw new InvalidOperationException ( "Session has been stopped; cannot record new events." ) ;
8276
8377 // FIX: use the public 3-arg ctor; the 4-arg (with meta) is internal
84- var ev = new KeelMatrix . QueryWatch . QueryEvent ( text , duration , DateTimeOffset . UtcNow ) ;
78+ QueryEvent ev = new ( text , duration , DateTimeOffset . UtcNow ) ;
8579 _q . Enqueue ( ev ) ;
8680 }
8781
88- public KeelMatrix . QueryWatch . QueryWatchReport Stop ( ) {
89- var now = DateTimeOffset . UtcNow ;
90- if ( Interlocked . CompareExchange ( ref _stopped , 1 , 0 ) == 0 ) {
91- _stoppedAt = now ;
92- }
93- else {
94- throw new InvalidOperationException ( "Session has already been stopped." ) ;
95- }
82+ public QueryWatchReport Stop ( ) {
83+ DateTimeOffset now = DateTimeOffset . UtcNow ;
84+ DateTimeOffset ? _stoppedAt = Interlocked . CompareExchange ( ref _stopped , 1 , 0 ) == 0
85+ ? ( DateTimeOffset ? ) now
86+ : throw new InvalidOperationException ( "Session has already been stopped." ) ;
9687
97- var arr = _q . ToArray ( ) ;
98- var list = new List < KeelMatrix . QueryWatch . QueryEvent > ( arr ) ;
99- return KeelMatrix . QueryWatch . QueryWatchReport . CreateSnapshot ( list , _options , _startedAt , _stoppedAt ?? now ) ;
88+ QueryEvent [ ] arr = [ .. _q ] ;
89+ List < QueryEvent > list = [ .. arr ] ;
90+ return QueryWatchReport . CreateSnapshot ( list , _options , _startedAt , _stoppedAt ?? now ) ;
10091 }
10192 }
10293}
0 commit comments