Skip to content

Commit 9b4a8b8

Browse files
authored
Add server/client for ASP.NET (WIP) (#318)
* save * save * save * save * Save * save * save * Change * save * Add server/client for ASP.NET (WIP) * save * save
1 parent 543f172 commit 9b4a8b8

File tree

15 files changed

+1400
-17
lines changed

15 files changed

+1400
-17
lines changed

src/VirtualClient/VirtualClient.Actions.FunctionalTests/AspNetBenchProfileTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ private IEnumerable<string> GetProfileExpectedCommands(PlatformID platform)
106106
case PlatformID.Win32NT:
107107
commands = new List<string>
108108
{
109-
@"dotnet\.exe build -c Release -p:BenchmarksTargetFramework=net7.0",
109+
@"dotnet\.exe build -c Release -p:BenchmarksTargetFramework=net8.0",
110110
@"dotnet\.exe .+Benchmarks.dll --nonInteractive true --scenarios json --urls http://localhost:9876 --server Kestrel --kestrelTransport Sockets --protocol http --header ""Accept:.+ keep-alive",
111111
@"bombardier\.exe --duration 15s --connections 256 --timeout 10s --fasthttp --insecure -l http://localhost:9876/json --print r --format json"
112112
};
@@ -116,7 +116,7 @@ private IEnumerable<string> GetProfileExpectedCommands(PlatformID platform)
116116
commands = new List<string>
117117
{
118118
@"chmod \+x .+bombardier",
119-
@"dotnet build -c Release -p:BenchmarksTargetFramework=net7.0",
119+
@"dotnet build -c Release -p:BenchmarksTargetFramework=net8.0",
120120
@"dotnet .+Benchmarks.dll --nonInteractive true --scenarios json --urls http://localhost:9876 --server Kestrel --kestrelTransport Sockets --protocol http --header ""Accept:.+ keep-alive",
121121
@"bombardier --duration 15s --connections 256 --timeout 10s --fasthttp --insecure -l http://localhost:9876/json --print r --format json"
122122
};
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
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

Comments
 (0)