1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+
4
+ namespace VirtualClient . Actions
5
+ {
6
+ using System ;
7
+ using System . Collections . Generic ;
8
+ using System . IO . Abstractions ;
9
+ using System . Runtime . InteropServices ;
10
+ using System . Threading ;
11
+ using System . Threading . Tasks ;
12
+ using Microsoft . CodeAnalysis ;
13
+ using Microsoft . Extensions . DependencyInjection ;
14
+ using VirtualClient . Common ;
15
+ using VirtualClient . Common . Extensions ;
16
+ using VirtualClient . Common . Telemetry ;
17
+ using VirtualClient . Contracts ;
18
+ using VirtualClient . Contracts . Metadata ;
19
+ using static System . Net . Mime . MediaTypeNames ;
20
+
21
+ /// <summary>
22
+ /// The AspNetBench workload executor.
23
+ /// </summary>
24
+ public abstract class AspNetBenchBaseExecutor : VirtualClientMultiRoleComponent
25
+ {
26
+ private IFileSystem fileSystem ;
27
+ private IPackageManager packageManager ;
28
+ private IStateManager stateManager ;
29
+ private ISystemManagement systemManagement ;
30
+
31
+ private string dotnetExePath ;
32
+ private string aspnetBenchDirectory ;
33
+ private string aspnetBenchDllPath ;
34
+ private string bombardierFilePath ;
35
+ private string wrkFilePath ;
36
+ private string serverArgument ;
37
+ private string clientArgument ;
38
+
39
+ /// <summary>
40
+ /// Constructor for <see cref="AspNetBenchExecutor"/>
41
+ /// </summary>
42
+ /// <param name="dependencies">Provides required dependencies to the component.</param>
43
+ /// <param name="parameters">Parameters defined in the profile or supplied on the command line.</param>
44
+ public AspNetBenchBaseExecutor ( IServiceCollection dependencies , IDictionary < string , IConvertible > parameters )
45
+ : base ( dependencies , parameters )
46
+ {
47
+ this . systemManagement = this . Dependencies . GetService < ISystemManagement > ( ) ;
48
+ this . packageManager = this . systemManagement . PackageManager ;
49
+ this . stateManager = this . systemManagement . StateManager ;
50
+ this . fileSystem = this . systemManagement . FileSystem ;
51
+ }
52
+
53
+ /// <summary>
54
+ /// The name of the package where the AspNetBench package is downloaded.
55
+ /// </summary>
56
+ public string TargetFramework
57
+ {
58
+ get
59
+ {
60
+ // Lower case to prevent build path issue.
61
+ return this . Parameters . GetValue < string > ( nameof ( AspNetBenchBaseExecutor . TargetFramework ) ) . ToLower ( ) ;
62
+ }
63
+ }
64
+
65
+ /// <summary>
66
+ /// The port for ASPNET to run.
67
+ /// </summary>
68
+ public string Port
69
+ {
70
+ get
71
+ {
72
+ // Lower case to prevent build path issue.
73
+ return this . Parameters . GetValue < string > ( nameof ( AspNetBenchBaseExecutor . Port ) , "9876" ) ;
74
+ }
75
+ }
76
+
77
+ /// <summary>
78
+ /// The name of the package where the bombardier package is downloaded.
79
+ /// </summary>
80
+ public string BombardierPackageName
81
+ {
82
+ get
83
+ {
84
+ return this . Parameters . GetValue < string > ( nameof ( AspNetBenchBaseExecutor . BombardierPackageName ) , "bombardier" ) ;
85
+ }
86
+ }
87
+
88
+ /// <summary>
89
+ /// The name of the package where the DotNetSDK package is downloaded.
90
+ /// </summary>
91
+ public string DotNetSdkPackageName
92
+ {
93
+ get
94
+ {
95
+ return this . Parameters . GetValue < string > ( nameof ( AspNetBenchBaseExecutor . DotNetSdkPackageName ) , "dotnetsdk" ) ;
96
+ }
97
+ }
98
+
99
+ /// <summary>
100
+ /// ASPNETCORE_threadCount
101
+ /// </summary>
102
+ public string AspNetCoreThreadCount
103
+ {
104
+ get
105
+ {
106
+ // Lower case to prevent build path issue.
107
+ return this . Parameters . GetValue < string > ( nameof ( AspNetBenchBaseExecutor . AspNetCoreThreadCount ) , 1 ) ;
108
+ }
109
+ }
110
+
111
+ /// <summary>
112
+ /// DOTNET_SYSTEM_NET_SOCKETS_THREAD_COUNT
113
+ /// </summary>
114
+ public string DotNetSystemNetSocketsThreadCount
115
+ {
116
+ get
117
+ {
118
+ // Lower case to prevent build path issue.
119
+ return this . Parameters . GetValue < string > ( nameof ( AspNetBenchBaseExecutor . DotNetSystemNetSocketsThreadCount ) , 1 ) ;
120
+ }
121
+ }
122
+
123
+ /// <summary>
124
+ /// wrk commandline
125
+ /// </summary>
126
+ public string WrkCommandLine
127
+ {
128
+ get
129
+ {
130
+ // Lower case to prevent build path issue.
131
+ return this . Parameters . GetValue < string > ( nameof ( AspNetBenchBaseExecutor . WrkCommandLine ) , string . Empty ) ;
132
+ }
133
+ }
134
+
135
+ /// <summary>
136
+ /// Initializes the environment for execution of the AspNetBench workload.
137
+ /// </summary>
138
+ protected override async Task InitializeAsync ( EventContext telemetryContext , CancellationToken cancellationToken )
139
+ {
140
+ // This workload needs three packages: aspnetbenchmarks, dotnetsdk, bombardier
141
+ DependencyPath workloadPackage = await this . packageManager . GetPackageAsync ( this . PackageName , CancellationToken . None )
142
+ . ConfigureAwait ( false ) ;
143
+
144
+ if ( workloadPackage != null )
145
+ {
146
+ // the directory we are looking for is at the src/Benchmarks
147
+ this . aspnetBenchDirectory = this . Combine ( workloadPackage . Path , "src" , "Benchmarks" ) ;
148
+ }
149
+
150
+ DependencyPath bombardierPackage = await this . packageManager . GetPlatformSpecificPackageAsync ( this . BombardierPackageName , this . Platform , this . CpuArchitecture , cancellationToken )
151
+ . ConfigureAwait ( false ) ;
152
+
153
+ if ( bombardierPackage != null )
154
+ {
155
+ this . bombardierFilePath = this . Combine ( bombardierPackage . Path , this . Platform == PlatformID . Unix ? "bombardier" : "bombardier.exe" ) ;
156
+ await this . systemManagement . MakeFileExecutableAsync ( this . bombardierFilePath , this . Platform , cancellationToken )
157
+ . ConfigureAwait ( false ) ;
158
+ }
159
+
160
+ DependencyPath wrkPackage = await this . packageManager . GetPackageAsync ( "wrk" , cancellationToken )
161
+ . ConfigureAwait ( false ) ;
162
+
163
+ if ( wrkPackage != null )
164
+ {
165
+ this . wrkFilePath = this . Combine ( wrkPackage . Path , "wrk" ) ;
166
+ await this . systemManagement . MakeFileExecutableAsync ( this . wrkFilePath , this . Platform , cancellationToken )
167
+ . ConfigureAwait ( false ) ;
168
+ }
169
+ }
170
+
171
+ /// <summary>
172
+ ///
173
+ /// </summary>
174
+ /// <param name="telemetryContext">Provides context information that will be captured with telemetry events.</param>
175
+ /// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
176
+ /// <returns></returns>
177
+ /// <exception cref="DependencyException"></exception>
178
+ protected async Task BuildAspNetBenchAsync ( EventContext telemetryContext , CancellationToken cancellationToken )
179
+ {
180
+ DependencyPath dotnetSdkPackage = await this . packageManager . GetPackageAsync ( this . DotNetSdkPackageName , CancellationToken . None )
181
+ . ConfigureAwait ( false ) ;
182
+
183
+ if ( dotnetSdkPackage == null )
184
+ {
185
+ throw new DependencyException (
186
+ $ "The expected DotNet SDK package does not exist on the system or is not registered.",
187
+ ErrorReason . WorkloadDependencyMissing ) ;
188
+ }
189
+
190
+ this . dotnetExePath = this . Combine ( dotnetSdkPackage . Path , this . Platform == PlatformID . Unix ? "dotnet" : "dotnet.exe" ) ;
191
+ // ~/vc/packages/dotnet/dotnet build -c Release -p:BenchmarksTargetFramework=net8.0
192
+ // Build the aspnetbenchmark project
193
+ string buildArgument = $ "build -c Release -p:BenchmarksTargetFramework={ this . TargetFramework } ";
194
+ await this . ExecuteCommandAsync ( this . dotnetExePath , buildArgument , this . aspnetBenchDirectory , telemetryContext , cancellationToken )
195
+ . ConfigureAwait ( false ) ;
196
+
197
+ // "C:\Users\vcvmadmin\Benchmarks\src\Benchmarks\bin\Release\net8.0\Benchmarks.dll"
198
+ this . aspnetBenchDllPath = this . Combine (
199
+ this . aspnetBenchDirectory ,
200
+ "bin" ,
201
+ "Release" ,
202
+ this . TargetFramework ,
203
+ "Benchmarks.dll" ) ;
204
+ }
205
+
206
+ /// <summary>
207
+ ///
208
+ /// </summary>
209
+ /// <param name="process"></param>
210
+ /// <param name="telemetryContext">Provides context information that will be captured with telemetry events.</param>
211
+ /// <exception cref="WorkloadResultsException"></exception>
212
+ protected void CaptureMetrics ( IProcessProxy process , EventContext telemetryContext )
213
+ {
214
+ try
215
+ {
216
+ this . MetadataContract . AddForScenario (
217
+ "AspNetBench" ,
218
+ $ "{ this . clientArgument } ,{ this . serverArgument } ",
219
+ toolVersion : null ) ;
220
+
221
+ this . MetadataContract . Apply ( telemetryContext ) ;
222
+
223
+ WrkMetricParser parser = new WrkMetricParser ( process . StandardOutput . ToString ( ) ) ;
224
+
225
+ this . Logger . LogMetrics (
226
+ toolName : "AspNetBench" ,
227
+ scenarioName : $ "ASP.NET_{ this . TargetFramework } _Performance",
228
+ process . StartTime ,
229
+ process . ExitTime ,
230
+ parser . Parse ( ) ,
231
+ metricCategorization : "json" ,
232
+ scenarioArguments : $ "Client: { this . clientArgument } | Server: { this . serverArgument } ",
233
+ this . Tags ,
234
+ telemetryContext ) ;
235
+ }
236
+ catch ( Exception exc )
237
+ {
238
+ throw new WorkloadResultsException ( $ "Failed to parse bombardier output.", exc , ErrorReason . InvalidResults ) ;
239
+ }
240
+ }
241
+
242
+ /// <summary>
243
+ ///
244
+ /// </summary>
245
+ /// <param name="telemetryContext">Provides context information that will be captured with telemetry events.</param>
246
+ /// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
247
+ /// <returns></returns>
248
+ protected Task StartAspNetServerAsync ( EventContext telemetryContext , CancellationToken cancellationToken )
249
+ {
250
+ // Example:
251
+ // dotnet <path_to_binary>\Benchmarks.dll --nonInteractive true --scenarios json --urls http://localhost:5000 --server Kestrel --kestrelTransport Sockets --protocol http
252
+ // --header "Accept: application/json,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7" --header "Connection: keep-alive"
253
+
254
+ string options = $ "--nonInteractive true --scenarios json --urls http://*:{ this . Port } --server Kestrel --kestrelTransport Sockets --protocol http";
255
+ string headers = @"--header ""Accept: application/json,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7"" --header ""Connection: keep-alive""" ;
256
+ this . serverArgument = $ "{ this . aspnetBenchDllPath } { options } { headers } ";
257
+
258
+ return this . ExecuteCommandAsync ( this . dotnetExePath , this . serverArgument , this . aspnetBenchDirectory , telemetryContext , cancellationToken ) ;
259
+ }
260
+
261
+ /// <summary>
262
+ ///
263
+ /// </summary>
264
+ /// <param name="ipAddress"></param>
265
+ /// <param name="telemetryContext">Provides context information that will be captured with telemetry events.</param>
266
+ /// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
267
+ /// <returns></returns>
268
+ protected async Task RunBombardierAsync ( string ipAddress , EventContext telemetryContext , CancellationToken cancellationToken )
269
+ {
270
+ using ( BackgroundOperations profiling = BackgroundOperations . BeginProfiling ( this , cancellationToken ) )
271
+ {
272
+ // https://pkg.go.dev/github.com/codesenberg/bombardier
273
+ // ./bombardier --duration 15s --connections 256 --timeout 10s --fasthttp --insecure -l http://localhost:5000/json --print r --format json
274
+ this . clientArgument = $ "--duration 15s --connections 256 --timeout 10s --fasthttp --insecure -l http://{ ipAddress } :{ this . Port } /json --print r --format json";
275
+
276
+ using ( IProcessProxy process = await this . ExecuteCommandAsync ( this . bombardierFilePath , this . clientArgument , this . aspnetBenchDirectory , telemetryContext , cancellationToken , runElevated : true )
277
+ . ConfigureAwait ( false ) )
278
+ {
279
+ if ( ! cancellationToken . IsCancellationRequested )
280
+ {
281
+ await this . LogProcessDetailsAsync ( process , telemetryContext , "AspNetBench" , logToFile : true ) ;
282
+
283
+ process . ThrowIfWorkloadFailed ( ) ;
284
+ this . CaptureMetrics ( process , telemetryContext ) ;
285
+ }
286
+ }
287
+ }
288
+ }
289
+
290
+ /// <summary>
291
+ ///
292
+ /// </summary>
293
+ /// <param name="ipAddress"></param>
294
+ /// <param name="telemetryContext">Provides context information that will be captured with telemetry events.</param>
295
+ /// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
296
+ /// <returns></returns>
297
+ protected async Task RunWrkAsync ( string ipAddress , EventContext telemetryContext , CancellationToken cancellationToken )
298
+ {
299
+ using ( BackgroundOperations profiling = BackgroundOperations . BeginProfiling ( this , cancellationToken ) )
300
+ {
301
+ // https://pkg.go.dev/github.com/codesenberg/bombardier
302
+ // ./wrk -t 256 -c 256 -d 15s --timeout 10s http://10.1.0.23:9876/json --header "Accept: application/json,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7"
303
+ this . clientArgument = this . WrkCommandLine ;
304
+ this . clientArgument = this . clientArgument . Replace ( "{ipAddress}" , ipAddress ) ;
305
+ this . clientArgument = this . clientArgument . Replace ( "{port}" , this . Port ) ;
306
+
307
+ using ( IProcessProxy process = await this . ExecuteCommandAsync ( this . wrkFilePath , this . clientArgument , this . aspnetBenchDirectory , telemetryContext , cancellationToken , runElevated : true )
308
+ . ConfigureAwait ( false ) )
309
+ {
310
+ if ( ! cancellationToken . IsCancellationRequested )
311
+ {
312
+ await this . LogProcessDetailsAsync ( process , telemetryContext , "wrk" , logToFile : true ) ;
313
+
314
+ process . ThrowIfWorkloadFailed ( ) ;
315
+ this . CaptureMetrics ( process , telemetryContext ) ;
316
+ }
317
+ }
318
+ }
319
+ }
320
+ }
321
+ }
0 commit comments