1313// limitations under the License.
1414
1515using System ;
16+ using System . Collections . Generic ;
1617using System . IO ;
1718using System . Linq ;
18- using System . Security . Cryptography ;
19- using System . Text ;
19+ using System . Threading ;
2020using System . Threading . Tasks ;
2121using Newtonsoft . Json ;
22+ using Seq . Api ;
23+ using Seq . Api . Model . Data ;
2224using Seq . Api . Model . Signals ;
2325using SeqCli . Cli . Features ;
2426using SeqCli . Connection ;
27+ using SeqCli . Sample . Loader ;
2528using SeqCli . Util ;
2629using Serilog ;
2730using Serilog . Context ;
@@ -70,6 +73,8 @@ class BenchCommand : Command
7073 string _reportingServerUrl = "" ;
7174 string _reportingServerApiKey = "" ;
7275 string _description = "" ;
76+ bool _withIngestion = false ;
77+ bool _withQueries = false ;
7378
7479 public BenchCommand ( SeqConnectionFactory connectionFactory )
7580 {
@@ -96,70 +101,102 @@ public BenchCommand(SeqConnectionFactory connectionFactory)
96101 "description=" ,
97102 "Optional description of the bench test run" ,
98103 a => _description = a ) ;
104+ Options . Add (
105+ "with-ingestion" ,
106+ "Should the benchmark include sending events to Seq" ,
107+ _ => _withIngestion = true ) ;
108+ Options . Add (
109+ "with-queries" ,
110+ "Should the benchmark include querying Seq" ,
111+ _ => _withQueries = true ) ;
99112 }
100113
101114 protected override async Task < int > Run ( )
102115 {
116+ if ( ! _withIngestion && ! _withQueries )
117+ {
118+ Log . Error ( "Use at least one of --with-ingestion and --with-queries" ) ;
119+ return 1 ;
120+ }
121+
103122 try
104123 {
124+ var ( _, apiKey ) = _connectionFactory . GetConnectionDetails ( _connection ) ;
105125 var connection = _connectionFactory . Connect ( _connection ) ;
106126 var seqVersion = ( await connection . Client . GetRootAsync ( ) ) . Version ;
127+ await using var reportingLogger = BuildReportingLogger ( ) ;
107128
108- var cases = ReadCases ( _cases ) ;
109129 var runId = Guid . NewGuid ( ) . ToString ( "N" ) [ ..16 ] ;
130+ CancellationTokenSource cancellationTokenSource = new ( ) ;
131+ var cancellationToken = cancellationTokenSource . Token ;
110132
111- await using var reportingLogger = BuildReportingLogger ( ) ;
112-
113- using ( ! string . IsNullOrWhiteSpace ( _description )
114- ? LogContext . PushProperty ( "Description" , _description )
115- : null )
116- {
117- reportingLogger . Information (
118- "Bench run {RunId} against {ServerUrl} ({SeqVersion}); {CaseCount} cases, {Runs} runs, from {Start} to {End}" ,
119- runId , connection . Client . ServerUrl , seqVersion , cases . Cases . Count , _runs , _range . Start , _range . End ) ;
120- }
121-
122133 using ( LogContext . PushProperty ( "RunId" , runId ) )
134+ using ( LogContext . PushProperty ( "SeqVersion" , seqVersion ) )
135+ using ( LogContext . PushProperty ( "WithIngestion" , _withIngestion ) )
136+ using ( LogContext . PushProperty ( "WithQueries" , _withQueries ) )
123137 using ( LogContext . PushProperty ( "Start" , _range . Start ) )
124138 using ( LogContext . PushProperty ( "End" , _range . End ) )
139+ using ( ! string . IsNullOrWhiteSpace ( _description )
140+ ? LogContext . PushProperty ( "Description" , _description )
141+ : null )
125142 {
126- foreach ( var c in cases . Cases . OrderBy ( c => c . Id ) )
143+ if ( _withIngestion )
127144 {
128- var timings = new BenchCaseTimings ( ) ;
129- object ? lastResult = null ;
145+ var t = IngestionBenchmark ( reportingLogger , runId , connection , apiKey , seqVersion ,
146+ isQueryBench : _withQueries , cancellationToken )
147+ . ContinueWith ( t =>
148+ {
149+ if ( t . Exception is not null )
150+ {
151+ return Console . Error . WriteLineAsync ( t . Exception . Message ) ;
152+ }
130153
131- foreach ( var i in Enumerable . Range ( 1 , _runs ) )
154+ return Task . CompletedTask ;
155+ } ) ;
156+
157+ if ( ! _withQueries )
132158 {
159+ int benchDurationMs = 120_000 ;
160+ await Task . Delay ( benchDurationMs ) ;
161+ cancellationTokenSource . Cancel ( ) ;
162+
133163 var response = await connection . Data . QueryAsync (
134- c . Query ,
135- _range . Start ,
136- _range . End ,
137- c . SignalExpression != null ? SignalExpressionPart . Signal ( c . SignalExpression ) : null
164+ "select count(*) from stream group by time(1s)" ,
165+ DateTime . Now . Add ( - 1 * TimeSpan . FromMilliseconds ( benchDurationMs ) )
138166 ) ;
139-
140- timings . PushElapsed ( response . Statistics . ElapsedMilliseconds ) ;
141-
142- if ( response . Rows != null )
167+
168+ if ( response . Slices == null )
143169 {
144- var isScalarResult = response . Rows . Length == 1 && response . Rows [ 0 ] . Length == 1 ;
145- if ( isScalarResult && i == _runs )
146- {
147- lastResult = response . Rows [ 0 ] [ 0 ] ;
148- }
170+ throw new Exception ( "Failed to query ingestion benchmark results" ) ;
171+ }
172+
173+ var counts = response . Slices . Skip ( 30 ) // ignore the warmup
174+ . Select ( s => Convert . ToDouble ( s . Rows [ 0 ] [ 0 ] ) ) // extract per-second counts
175+ . Where ( c => c > 10000 ) // ignore any very small values
176+ . ToArray ( ) ;
177+ counts = counts . SkipLast ( 5 ) . ToArray ( ) ; // ignore warmdown
178+ var countsMean = counts . Sum ( ) / counts . Length ;
179+ var countsRSD = QueryBenchCaseTimings . StandardDeviation ( counts ) / countsMean ;
180+
181+ using ( LogContext . PushProperty ( "EventsPerSecond" , counts ) )
182+ {
183+ reportingLogger . Information (
184+ "Ingestion benchmark {Description} ran for {RunDuration:N0}ms; ingested {TotalIngested:N0} "
185+ + "at {EventsPerMinute:N0}events/min; with RSD {RelativeStandardDeviationPercentage,4:N1}%" ,
186+ _description ,
187+ benchDurationMs ,
188+ counts . Sum ( ) ,
189+ countsMean * 60 ,
190+ countsRSD * 100 ) ;
149191 }
150192 }
193+ }
151194
152- using ( lastResult != null ? LogContext . PushProperty ( "LastResult" , lastResult ) : null )
153- using ( ! string . IsNullOrWhiteSpace ( c . SignalExpression )
154- ? LogContext . PushProperty ( "SignalExpression" , c . SignalExpression )
155- : null )
156- using ( LogContext . PushProperty ( "StandardDeviationElapsed" , timings . StandardDeviationElapsed ) )
157- using ( LogContext . PushProperty ( "Query" , c . Query ) )
158- {
159- reportingLogger . Information (
160- "Case {Id,-40} mean {MeanElapsed,5:N0} ms (first {FirstElapsed,5:N0} ms, min {MinElapsed,5:N0} ms, max {MaxElapsed,5:N0} ms, RSD {RelativeStandardDeviationElapsed,4:N2})" ,
161- c . Id , timings . MeanElapsed , timings . FirstElapsed , timings . MinElapsed , timings . MaxElapsed , timings . RelativeStandardDeviationElapsed ) ;
162- }
195+ if ( _withQueries )
196+ {
197+ var collectedTimings = await QueryBenchmark ( reportingLogger , runId , connection , seqVersion ) ;
198+ collectedTimings . LogSummary ( _description ) ;
199+ cancellationTokenSource . Cancel ( ) ;
163200 }
164201 }
165202
@@ -172,6 +209,82 @@ protected override async Task<int> Run()
172209 }
173210 }
174211
212+ async Task IngestionBenchmark ( Logger reportingLogger , string runId , SeqConnection connection , string ? apiKey ,
213+ string seqVersion , bool isQueryBench , CancellationToken cancellationToken = default )
214+ {
215+ reportingLogger . Information (
216+ "Ingestion bench run {RunId} against {ServerUrl} ({SeqVersion})" ,
217+ runId , connection . Client . ServerUrl , seqVersion ) ;
218+
219+ if ( isQueryBench )
220+ {
221+ var simulationTasks = Enumerable . Range ( 1 , 500 )
222+ . Select ( i => Simulation . RunAsync ( connection , apiKey , 10000 , echoToStdout : false , cancellationToken ) )
223+ . ToArray ( ) ;
224+ await Task . Delay ( 20_000 ) ; // how long to ingest before beginning queries
225+ }
226+ else
227+ {
228+ var simulationTasks = Enumerable . Range ( 1 , 2000 )
229+ . Select ( i => Simulation . RunAsync ( connection , apiKey , 10000 , echoToStdout : false , cancellationToken ) )
230+ . ToArray ( ) ;
231+ }
232+ }
233+
234+ async Task < QueryBenchRunResults > QueryBenchmark ( Logger reportingLogger , string runId , SeqConnection connection , string seqVersion )
235+ {
236+ var cases = ReadCases ( _cases ) ;
237+ QueryBenchRunResults queryBenchRunResults = new ( reportingLogger ) ;
238+ reportingLogger . Information (
239+ "Query benchmark run {RunId} against {ServerUrl} ({SeqVersion}); {CaseCount} cases, {Runs} runs, from {Start} to {End}" ,
240+ runId , connection . Client . ServerUrl , seqVersion , cases . Cases . Count , _runs , _range . Start , _range . End ) ;
241+
242+
243+ foreach ( var c in cases . Cases . OrderBy ( c => c . Id )
244+ . Concat ( new [ ] { QueryBenchRunResults . FINAL_COUNT_CASE } ) )
245+ {
246+ var timings = new QueryBenchCaseTimings ( c ) ;
247+ queryBenchRunResults . Add ( timings ) ;
248+
249+ foreach ( var i in Enumerable . Range ( 1 , _runs ) )
250+ {
251+ var response = await connection . Data . QueryAsync (
252+ c . Query ,
253+ _range . Start ,
254+ _range . End ,
255+ c . SignalExpression != null ? SignalExpressionPart . Signal ( c . SignalExpression ) : null ,
256+ null ,
257+ TimeSpan . FromMinutes ( 4 )
258+ ) ;
259+
260+ timings . PushElapsed ( response . Statistics . ElapsedMilliseconds ) ;
261+
262+ if ( response . Rows != null )
263+ {
264+ var isScalarResult = response . Rows . Length == 1 && response . Rows [ 0 ] . Length == 1 ;
265+ if ( isScalarResult && i == _runs )
266+ {
267+ timings . LastResult = response . Rows [ 0 ] [ 0 ] ;
268+ }
269+ }
270+ }
271+
272+ using ( timings . LastResult != null ? LogContext . PushProperty ( "LastResult" , timings . LastResult ) : null )
273+ using ( ! string . IsNullOrWhiteSpace ( c . SignalExpression )
274+ ? LogContext . PushProperty ( "SignalExpression" , c . SignalExpression )
275+ : null )
276+ using ( LogContext . PushProperty ( "StandardDeviationElapsed" , timings . StandardDeviationElapsed ) )
277+ using ( LogContext . PushProperty ( "Query" , c . Query ) )
278+ {
279+ reportingLogger . Information (
280+ "Case {Id,-40} ({LastResult}) mean {MeanElapsed,5:N0} ms (first {FirstElapsed,5:N0} ms, min {MinElapsed,5:N0} ms, max {MaxElapsed,5:N0} ms, RSD {RelativeStandardDeviationElapsed,4:N2})" ,
281+ c . Id , timings . LastResult , timings . MeanElapsed , timings . FirstElapsed , timings . MinElapsed , timings . MaxElapsed , timings . RelativeStandardDeviationElapsed ) ;
282+ }
283+ }
284+
285+ return queryBenchRunResults ;
286+ }
287+
175288 /// <summary>
176289 /// Build a second Serilog logger for logging benchmark results.
177290 /// </summary>
0 commit comments