9
9
using CliWrap ;
10
10
using CliWrap . Buffered ;
11
11
using DatadogTestLogger . Vendors . Datadog . Trace ;
12
+ using MathNet . Numerics . Distributions ;
12
13
using MathNet . Numerics . Statistics ;
13
14
using Spectre . Console ;
14
15
using TimeItSharp . Common . Assertors ;
@@ -29,6 +30,8 @@ internal sealed class ScenarioProcessor
29
30
30
31
private static readonly IDictionary EnvironmentVariables = Environment . GetEnvironmentVariables ( ) ;
31
32
33
+ private double _remainingTimeInMinutes ;
34
+
32
35
public ScenarioProcessor (
33
36
Config configuration ,
34
37
TemplateVariables templateVariables ,
@@ -41,6 +44,7 @@ public ScenarioProcessor(
41
44
_assertors = assertors ;
42
45
_services = services ;
43
46
_callbacksTriggers = callbacksTriggers ;
47
+ _remainingTimeInMinutes = configuration . MaximumDurationInMinutes ;
44
48
}
45
49
46
50
[ UnconditionalSuppressMessage ( "SingleFile" , "IL3000:Avoid accessing Assembly file path when publishing as a single file" , Justification = "Case is being handled" ) ]
@@ -212,6 +216,7 @@ public void CleanScenario(Scenario scenario)
212
216
AnsiConsole . Markup ( " [gold3_1]Warming up[/]" ) ;
213
217
watch . Restart ( ) ;
214
218
await RunScenarioAsync ( _configuration . WarmUpCount , index , scenario , TimeItPhase . WarmUp , false ,
219
+ stopwatch : watch ,
215
220
cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
216
221
watch . Stop ( ) ;
217
222
if ( cancellationToken . IsCancellationRequested )
@@ -225,7 +230,9 @@ await RunScenarioAsync(_configuration.WarmUpCount, index, scenario, TimeItPhase.
225
230
AnsiConsole . Markup ( " [green3]Run[/]" ) ;
226
231
var start = DateTime . UtcNow ;
227
232
watch . Restart ( ) ;
228
- var dataPoints = await RunScenarioAsync ( _configuration . Count , index , scenario , TimeItPhase . Run , true , cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
233
+ var dataPoints = await RunScenarioAsync ( _configuration . Count , index , scenario , TimeItPhase . Run , true ,
234
+ stopwatch : watch ,
235
+ cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
229
236
watch . Stop ( ) ;
230
237
if ( cancellationToken . IsCancellationRequested )
231
238
{
@@ -241,6 +248,7 @@ await RunScenarioAsync(_configuration.WarmUpCount, index, scenario, TimeItPhase.
241
248
scenario . ParentService = repeat . ServiceAskingForRepeat ;
242
249
watch . Restart ( ) ;
243
250
await RunScenarioAsync ( repeat . Count , index , scenario , TimeItPhase . ExtraRun , false ,
251
+ stopwatch : watch ,
244
252
cancellationToken : cancellationToken ) . ConfigureAwait ( false ) ;
245
253
watch . Stop ( ) ;
246
254
if ( cancellationToken . IsCancellationRequested )
@@ -416,8 +424,17 @@ await RunScenarioAsync(repeat.Count, index, scenario, TimeItPhase.ExtraRun, fals
416
424
return scenarioResult ;
417
425
}
418
426
419
- private async Task < List < DataPoint > > RunScenarioAsync ( int count , int index , Scenario scenario , TimeItPhase phase , bool checkShouldContinue , CancellationToken cancellationToken )
427
+ private async Task < List < DataPoint > > RunScenarioAsync ( int count , int index , Scenario scenario , TimeItPhase phase , bool checkShouldContinue , Stopwatch stopwatch , CancellationToken cancellationToken )
420
428
{
429
+ var minIterations = count / 3 ;
430
+ minIterations = minIterations < 10 ? 10 : minIterations ;
431
+ var confidenceLevel = _configuration . ConfidenceLevel ;
432
+ if ( confidenceLevel is <= 0 or >= 1 )
433
+ {
434
+ confidenceLevel = 0.95 ;
435
+ }
436
+ var previousRelativeWidth = double . MaxValue ;
437
+
421
438
var dataPoints = new List < DataPoint > ( ) ;
422
439
AnsiConsole . Markup ( " " ) ;
423
440
for ( var i = 0 ; i < count ; i ++ )
@@ -440,10 +457,119 @@ private async Task<List<DataPoint>> RunScenarioAsync(int count, int index, Scena
440
457
{
441
458
break ;
442
459
}
460
+
461
+ try
462
+ {
463
+ // If we are in a run phase, let's do the automatic checks
464
+ if ( phase == TimeItPhase . Run )
465
+ {
466
+ static double GetDuration ( DataPoint point )
467
+ {
468
+ #if NET7_0_OR_GREATER
469
+ return point . Duration . TotalNanoseconds ;
470
+ #else
471
+ return Utils . FromTimeSpanToNanoseconds ( point . Duration ) ;
472
+ #endif
473
+ }
474
+
475
+ var durations = Utils . RemoveOutliers ( dataPoints . Select ( GetDuration ) , threshold : 1.5 ) . ToList ( ) ;
476
+ if ( durations . Count >= minIterations || stopwatch . Elapsed . TotalMinutes >= _remainingTimeInMinutes )
477
+ {
478
+ var mean = durations . Average ( ) ;
479
+ var stdev = durations . StandardDeviation ( ) ;
480
+ var stderr = stdev / Math . Sqrt ( durations . Count ) ;
481
+
482
+ // Critical t value
483
+ var tCritical = StudentT . InvCDF ( 0 , 1 , durations . Count - 1 , 1 - ( 1 - confidenceLevel ) / 2 ) ;
484
+
485
+ // Confidence intervals
486
+ var marginOfError = tCritical * stderr ;
487
+ var confidenceIntervalLower = mean - marginOfError ;
488
+ var confidenceIntervalUpper = mean + marginOfError ;
489
+ var relativeWidth = ( confidenceIntervalUpper - confidenceIntervalLower ) / mean ;
490
+
491
+ // Check if the maximum duration is reached
492
+ if ( stopwatch . Elapsed . TotalMinutes >= _remainingTimeInMinutes )
493
+ {
494
+ AnsiConsole . WriteLine ( ) ;
495
+ AnsiConsole . MarkupLine (
496
+ " [blueviolet]Maximum duration has been reached. Stopping iterations for this scenario.[/]" ) ;
497
+ AnsiConsole . MarkupLine ( " [blueviolet]N: {0}[/]" , durations . Count ) ;
498
+ AnsiConsole . MarkupLine ( " [blueviolet]Mean: {0}ms[/]" ,
499
+ Math . Round ( Utils . FromNanosecondsToMilliseconds ( mean ) , 3 ) ) ;
500
+ AnsiConsole . Markup (
501
+ " [blueviolet]Confidence Interval at {0}: [[{1}ms, {2}ms]]. Relative width: {3}%[/]" ,
502
+ confidenceLevel * 100 ,
503
+ Math . Round ( Utils . FromNanosecondsToMilliseconds ( confidenceIntervalLower ) , 3 ) ,
504
+ Math . Round ( Utils . FromNanosecondsToMilliseconds ( confidenceIntervalUpper ) , 3 ) ,
505
+ Math . Round ( relativeWidth * 100 , 4 ) ) ;
506
+
507
+ break ;
508
+ }
509
+
510
+ // Check if the statistical criterion is met
511
+ if ( relativeWidth < _configuration . AcceptableRelativeWidth )
512
+ {
513
+ AnsiConsole . WriteLine ( ) ;
514
+ AnsiConsole . MarkupLine (
515
+ " [blueviolet]Acceptable relative width criteria met. Stopping iterations for this scenario.[/]" ) ;
516
+ AnsiConsole . MarkupLine ( " [blueviolet]N: {0}[/]" , durations . Count ) ;
517
+ AnsiConsole . MarkupLine ( " [blueviolet]Mean: {0}ms[/]" ,
518
+ Math . Round ( Utils . FromNanosecondsToMilliseconds ( mean ) , 3 ) ) ;
519
+ AnsiConsole . Markup (
520
+ " [blueviolet]Confidence Interval at {0}: [[{1}ms, {2}ms]]. Relative width: {3}%[/]" ,
521
+ confidenceLevel * 100 ,
522
+ Math . Round ( Utils . FromNanosecondsToMilliseconds ( confidenceIntervalLower ) , 3 ) ,
523
+ Math . Round ( Utils . FromNanosecondsToMilliseconds ( confidenceIntervalUpper ) , 3 ) ,
524
+ Math . Round ( relativeWidth * 100 , 4 ) ) ;
525
+ break ;
526
+ }
527
+
528
+ // Check for each `evaluationInterval` iteration
529
+ if ( ( durations . Count - minIterations ) % _configuration . EvaluationInterval == 0 )
530
+ {
531
+ var errorReduction = ( previousRelativeWidth - relativeWidth ) / previousRelativeWidth ;
532
+ if ( errorReduction < _configuration . MinimumErrorReduction )
533
+ {
534
+ AnsiConsole . WriteLine ( ) ;
535
+ AnsiConsole . MarkupLine (
536
+ " [blueviolet]The error is not decreasing significantly. Stopping iterations for this scenario.[/]" ) ;
537
+ AnsiConsole . MarkupLine ( " [blueviolet]N: {0}[/]" , durations . Count ) ;
538
+ AnsiConsole . MarkupLine ( " [blueviolet]Mean: {0}ms[/]" ,
539
+ Math . Round ( Utils . FromNanosecondsToMilliseconds ( mean ) , 3 ) ) ;
540
+ AnsiConsole . MarkupLine (
541
+ " [blueviolet]Confidence Interval at {0}: [[{1}ms, {2}ms]]. Relative width: {3}%[/]" ,
542
+ confidenceLevel * 100 ,
543
+ Math . Round ( Utils . FromNanosecondsToMilliseconds ( confidenceIntervalLower ) , 3 ) ,
544
+ Math . Round ( Utils . FromNanosecondsToMilliseconds ( confidenceIntervalUpper ) , 3 ) ,
545
+ Math . Round ( relativeWidth * 100 , 4 ) ) ;
546
+ AnsiConsole . Markup ( " [blueviolet]Error reduction: {0}%. Minimal expected: {1}%[/]" ,
547
+ Math . Round ( errorReduction * 100 , 4 ) ,
548
+ Math . Round ( _configuration . MinimumErrorReduction * 100 , 4 ) ) ;
549
+
550
+ break ;
551
+ }
552
+
553
+ previousRelativeWidth = relativeWidth ;
554
+ }
555
+ }
556
+ }
557
+ }
558
+ catch ( Exception ex )
559
+ {
560
+ AnsiConsole . WriteLine ( ) ;
561
+ AnsiConsole . MarkupLine ( " [red]Error: {0}[/]" , ex . Message ) ;
562
+ break ;
563
+ }
443
564
}
444
565
445
566
AnsiConsole . WriteLine ( ) ;
446
567
568
+ if ( phase == TimeItPhase . Run )
569
+ {
570
+ _remainingTimeInMinutes -= ( int ) stopwatch . Elapsed . TotalMinutes ;
571
+ }
572
+
447
573
return dataPoints ;
448
574
}
449
575
0 commit comments