diff --git a/src/libraries/System.Net.Http/tests/StressTests/HttpStress/Program.cs b/src/libraries/System.Net.Http/tests/StressTests/HttpStress/Program.cs index 407c0acd0db25b..b411d637a54f14 100644 --- a/src/libraries/System.Net.Http/tests/StressTests/HttpStress/Program.cs +++ b/src/libraries/System.Net.Http/tests/StressTests/HttpStress/Program.cs @@ -17,209 +17,212 @@ [assembly:SupportedOSPlatform("windows")] [assembly:SupportedOSPlatform("linux")] -/// -/// Simple HttpClient stress app that launches Kestrel in-proc and runs many concurrent requests of varying types against it. -/// -public static class Program +namespace HttpStress { - public enum ExitCode { Success = 0, StressError = 1, CliError = 2 }; - - public static async Task Main(string[] args) + /// + /// Simple HttpClient stress app that launches Kestrel in-proc and runs many concurrent requests of varying types against it. + /// + public static class Program { - if (!TryParseCli(args, out Configuration? config)) - { - return (int) ExitCode.CliError; - } + public enum ExitCode { Success = 0, StressError = 1, CliError = 2 }; - return (int) await Run(config); - } - - private static bool TryParseCli(string[] args, [NotNullWhen(true)] out Configuration? config) - { - var cmd = new RootCommand(); - cmd.AddOption(new Option("-n", "Max number of requests to make concurrently.") { Argument = new Argument("numWorkers", Environment.ProcessorCount) }); - cmd.AddOption(new Option("-serverUri", "Stress suite server uri.") { Argument = new Argument("serverUri", "https://localhost:5001") }); - cmd.AddOption(new Option("-runMode", "Stress suite execution mode. Defaults to Both.") { Argument = new Argument("runMode", RunMode.both) }); - cmd.AddOption(new Option("-maxExecutionTime", "Maximum stress execution time, in minutes. Defaults to infinity.") { Argument = new Argument("minutes", null) }); - cmd.AddOption(new Option("-maxContentLength", "Max content length for request and response bodies.") { Argument = new Argument("numBytes", 1000) }); - cmd.AddOption(new Option("-maxRequestUriSize", "Max query string length support by the server.") { Argument = new Argument("numChars", 5000) }); - cmd.AddOption(new Option("-maxRequestHeaderCount", "Maximum number of headers to place in request") { Argument = new Argument("numHeaders", 90) }); - cmd.AddOption(new Option("-maxRequestHeaderTotalSize", "Max request header total size.") { Argument = new Argument("numBytes", 1000) }); - cmd.AddOption(new Option("-http", "HTTP version (1.1 or 2.0)") { Argument = new Argument("version", HttpVersion.Version20) }); - cmd.AddOption(new Option("-connectionLifetime", "Max connection lifetime length (milliseconds).") { Argument = new Argument("connectionLifetime", null) }); - cmd.AddOption(new Option("-ops", "Indices of the operations to use") { Argument = new Argument("space-delimited indices", null) }); - cmd.AddOption(new Option("-xops", "Indices of the operations to exclude") { Argument = new Argument("space-delimited indices", null) }); - cmd.AddOption(new Option("-trace", "Enable System.Net.Http.InternalDiagnostics (client) and/or ASP.NET dignostics (server) tracing.") { Argument = new Argument("enable", false) }); - cmd.AddOption(new Option("-aspnetlog", "Enable ASP.NET warning and error logging.") { Argument = new Argument("enable", false) }); - cmd.AddOption(new Option("-listOps", "List available options.") { Argument = new Argument("enable", false) }); - cmd.AddOption(new Option("-seed", "Seed for generating pseudo-random parameters for a given -n argument.") { Argument = new Argument("seed", null) }); - cmd.AddOption(new Option("-numParameters", "Max number of query parameters or form fields for a request.") { Argument = new Argument("queryParameters", 1) }); - cmd.AddOption(new Option("-cancelRate", "Number between 0 and 1 indicating rate of client-side request cancellation attempts. Defaults to 0.1.") { Argument = new Argument("probability", 0.1) }); - cmd.AddOption(new Option("-httpSys", "Use http.sys instead of Kestrel.") { Argument = new Argument("enable", false) }); - cmd.AddOption(new Option("-winHttp", "Use WinHttpHandler for the stress client.") { Argument = new Argument("enable", false) }); - cmd.AddOption(new Option("-displayInterval", "Client stats display interval in seconds. Defaults to 5 seconds.") { Argument = new Argument("seconds", 5) }); - cmd.AddOption(new Option("-clientTimeout", "Default HttpClient timeout in seconds. Defaults to 60 seconds.") { Argument = new Argument("seconds", 60) }); - cmd.AddOption(new Option("-serverMaxConcurrentStreams", "Overrides kestrel max concurrent streams per connection.") { Argument = new Argument("streams", null) }); - cmd.AddOption(new Option("-serverMaxFrameSize", "Overrides kestrel max frame size setting.") { Argument = new Argument("bytes", null) }); - cmd.AddOption(new Option("-serverInitialConnectionWindowSize", "Overrides kestrel initial connection window size setting.") { Argument = new Argument("bytes", null) }); - cmd.AddOption(new Option("-serverMaxRequestHeaderFieldSize", "Overrides kestrel max request header field size.") { Argument = new Argument("bytes", null) }); - - ParseResult cmdline = cmd.Parse(args); - if (cmdline.Errors.Count > 0) + public static async Task Main(string[] args) { - foreach (ParseError error in cmdline.Errors) + if (!TryParseCli(args, out Configuration? config)) { - Console.WriteLine(error); + return (int)ExitCode.CliError; } - Console.WriteLine(); - new HelpBuilder(new SystemConsole()).Write(cmd); - config = null; - return false; + + return (int)await Run(config); } - config = new Configuration() + private static bool TryParseCli(string[] args, [NotNullWhen(true)] out Configuration? config) { - RunMode = cmdline.ValueForOption("-runMode"), - ServerUri = cmdline.ValueForOption("-serverUri"), - ListOperations = cmdline.ValueForOption("-listOps"), - - HttpVersion = cmdline.ValueForOption("-http"), - UseWinHttpHandler = cmdline.ValueForOption("-winHttp"), - ConcurrentRequests = cmdline.ValueForOption("-n"), - RandomSeed = cmdline.ValueForOption("-seed") ?? new Random().Next(), - MaxContentLength = cmdline.ValueForOption("-maxContentLength"), - MaxRequestUriSize = cmdline.ValueForOption("-maxRequestUriSize"), - MaxRequestHeaderCount = cmdline.ValueForOption("-maxRequestHeaderCount"), - MaxRequestHeaderTotalSize = cmdline.ValueForOption("-maxRequestHeaderTotalSize"), - OpIndices = cmdline.ValueForOption("-ops"), - ExcludedOpIndices = cmdline.ValueForOption("-xops"), - MaxParameters = cmdline.ValueForOption("-numParameters"), - DisplayInterval = TimeSpan.FromSeconds(cmdline.ValueForOption("-displayInterval")), - DefaultTimeout = TimeSpan.FromSeconds(cmdline.ValueForOption("-clientTimeout")), - ConnectionLifetime = cmdline.ValueForOption("-connectionLifetime").Select(TimeSpan.FromMilliseconds), - CancellationProbability = Math.Max(0, Math.Min(1, cmdline.ValueForOption("-cancelRate"))), - MaximumExecutionTime = cmdline.ValueForOption("-maxExecutionTime").Select(TimeSpan.FromMinutes), - - UseHttpSys = cmdline.ValueForOption("-httpSys"), - LogAspNet = cmdline.ValueForOption("-aspnetlog"), - Trace = cmdline.ValueForOption("-trace"), - ServerMaxConcurrentStreams = cmdline.ValueForOption("-serverMaxConcurrentStreams"), - ServerMaxFrameSize = cmdline.ValueForOption("-serverMaxFrameSize"), - ServerInitialConnectionWindowSize = cmdline.ValueForOption("-serverInitialConnectionWindowSize"), - ServerMaxRequestHeaderFieldSize = cmdline.ValueForOption("-serverMaxRequestHeaderFieldSize"), - }; - - return true; - } - - private static async Task Run(Configuration config) - { - (string name, Func op)[] clientOperations = - ClientOperations.Operations - // annotate the operation name with its index - .Select((op, i) => ($"{i.ToString().PadLeft(2)}: {op.name}", op.operation)) - .ToArray(); + var cmd = new RootCommand(); + cmd.AddOption(new Option("-n", "Max number of requests to make concurrently.") { Argument = new Argument("numWorkers", Environment.ProcessorCount) }); + cmd.AddOption(new Option("-serverUri", "Stress suite server uri.") { Argument = new Argument("serverUri", "https://localhost:5001") }); + cmd.AddOption(new Option("-runMode", "Stress suite execution mode. Defaults to Both.") { Argument = new Argument("runMode", RunMode.both) }); + cmd.AddOption(new Option("-maxExecutionTime", "Maximum stress execution time, in minutes. Defaults to infinity.") { Argument = new Argument("minutes", null) }); + cmd.AddOption(new Option("-maxContentLength", "Max content length for request and response bodies.") { Argument = new Argument("numBytes", 1000) }); + cmd.AddOption(new Option("-maxRequestUriSize", "Max query string length support by the server.") { Argument = new Argument("numChars", 5000) }); + cmd.AddOption(new Option("-maxRequestHeaderCount", "Maximum number of headers to place in request") { Argument = new Argument("numHeaders", 90) }); + cmd.AddOption(new Option("-maxRequestHeaderTotalSize", "Max request header total size.") { Argument = new Argument("numBytes", 1000) }); + cmd.AddOption(new Option("-http", "HTTP version (1.1 or 2.0)") { Argument = new Argument("version", HttpVersion.Version20) }); + cmd.AddOption(new Option("-connectionLifetime", "Max connection lifetime length (milliseconds).") { Argument = new Argument("connectionLifetime", null) }); + cmd.AddOption(new Option("-ops", "Indices of the operations to use") { Argument = new Argument("space-delimited indices", null) }); + cmd.AddOption(new Option("-xops", "Indices of the operations to exclude") { Argument = new Argument("space-delimited indices", null) }); + cmd.AddOption(new Option("-trace", "Enable System.Net.Http.InternalDiagnostics (client) and/or ASP.NET dignostics (server) tracing.") { Argument = new Argument("enable", false) }); + cmd.AddOption(new Option("-aspnetlog", "Enable ASP.NET warning and error logging.") { Argument = new Argument("enable", false) }); + cmd.AddOption(new Option("-listOps", "List available options.") { Argument = new Argument("enable", false) }); + cmd.AddOption(new Option("-seed", "Seed for generating pseudo-random parameters for a given -n argument.") { Argument = new Argument("seed", null) }); + cmd.AddOption(new Option("-numParameters", "Max number of query parameters or form fields for a request.") { Argument = new Argument("queryParameters", 1) }); + cmd.AddOption(new Option("-cancelRate", "Number between 0 and 1 indicating rate of client-side request cancellation attempts. Defaults to 0.1.") { Argument = new Argument("probability", 0.1) }); + cmd.AddOption(new Option("-httpSys", "Use http.sys instead of Kestrel.") { Argument = new Argument("enable", false) }); + cmd.AddOption(new Option("-winHttp", "Use WinHttpHandler for the stress client.") { Argument = new Argument("enable", false) }); + cmd.AddOption(new Option("-displayInterval", "Client stats display interval in seconds. Defaults to 5 seconds.") { Argument = new Argument("seconds", 5) }); + cmd.AddOption(new Option("-clientTimeout", "Default HttpClient timeout in seconds. Defaults to 60 seconds.") { Argument = new Argument("seconds", 60) }); + cmd.AddOption(new Option("-serverMaxConcurrentStreams", "Overrides kestrel max concurrent streams per connection.") { Argument = new Argument("streams", null) }); + cmd.AddOption(new Option("-serverMaxFrameSize", "Overrides kestrel max frame size setting.") { Argument = new Argument("bytes", null) }); + cmd.AddOption(new Option("-serverInitialConnectionWindowSize", "Overrides kestrel initial connection window size setting.") { Argument = new Argument("bytes", null) }); + cmd.AddOption(new Option("-serverMaxRequestHeaderFieldSize", "Overrides kestrel max request header field size.") { Argument = new Argument("bytes", null) }); + + ParseResult cmdline = cmd.Parse(args); + if (cmdline.Errors.Count > 0) + { + foreach (ParseError error in cmdline.Errors) + { + Console.WriteLine(error); + } + Console.WriteLine(); + new HelpBuilder(new SystemConsole()).Write(cmd); + config = null; + return false; + } - if ((config.RunMode & RunMode.both) == 0) - { - Console.Error.WriteLine("Must specify a valid run mode"); - return ExitCode.CliError; + config = new Configuration() + { + RunMode = cmdline.ValueForOption("-runMode"), + ServerUri = cmdline.ValueForOption("-serverUri"), + ListOperations = cmdline.ValueForOption("-listOps"), + + HttpVersion = cmdline.ValueForOption("-http"), + UseWinHttpHandler = cmdline.ValueForOption("-winHttp"), + ConcurrentRequests = cmdline.ValueForOption("-n"), + RandomSeed = cmdline.ValueForOption("-seed") ?? new Random().Next(), + MaxContentLength = cmdline.ValueForOption("-maxContentLength"), + MaxRequestUriSize = cmdline.ValueForOption("-maxRequestUriSize"), + MaxRequestHeaderCount = cmdline.ValueForOption("-maxRequestHeaderCount"), + MaxRequestHeaderTotalSize = cmdline.ValueForOption("-maxRequestHeaderTotalSize"), + OpIndices = cmdline.ValueForOption("-ops"), + ExcludedOpIndices = cmdline.ValueForOption("-xops"), + MaxParameters = cmdline.ValueForOption("-numParameters"), + DisplayInterval = TimeSpan.FromSeconds(cmdline.ValueForOption("-displayInterval")), + DefaultTimeout = TimeSpan.FromSeconds(cmdline.ValueForOption("-clientTimeout")), + ConnectionLifetime = cmdline.ValueForOption("-connectionLifetime").Select(TimeSpan.FromMilliseconds), + CancellationProbability = Math.Max(0, Math.Min(1, cmdline.ValueForOption("-cancelRate"))), + MaximumExecutionTime = cmdline.ValueForOption("-maxExecutionTime").Select(TimeSpan.FromMinutes), + + UseHttpSys = cmdline.ValueForOption("-httpSys"), + LogAspNet = cmdline.ValueForOption("-aspnetlog"), + Trace = cmdline.ValueForOption("-trace"), + ServerMaxConcurrentStreams = cmdline.ValueForOption("-serverMaxConcurrentStreams"), + ServerMaxFrameSize = cmdline.ValueForOption("-serverMaxFrameSize"), + ServerInitialConnectionWindowSize = cmdline.ValueForOption("-serverInitialConnectionWindowSize"), + ServerMaxRequestHeaderFieldSize = cmdline.ValueForOption("-serverMaxRequestHeaderFieldSize"), + }; + + return true; } - if (!config.ServerUri.StartsWith("http")) + private static async Task Run(Configuration config) { - Console.Error.WriteLine("Invalid server uri"); - return ExitCode.CliError; - } + (string name, Func op)[] clientOperations = + ClientOperations.Operations + // annotate the operation name with its index + .Select((op, i) => ($"{i.ToString().PadLeft(2)}: {op.name}", op.operation)) + .ToArray(); - if (config.ListOperations) - { - for (int i = 0; i < clientOperations.Length; i++) + if ((config.RunMode & RunMode.both) == 0) { - Console.WriteLine(clientOperations[i].name); + Console.Error.WriteLine("Must specify a valid run mode"); + return ExitCode.CliError; } - return ExitCode.Success; - } - // derive client operations based on arguments - (string name, Func op)[] usedClientOperations = (config.OpIndices, config.ExcludedOpIndices) switch - { - (null, null) => clientOperations, - (int[] incl, null) => incl.Select(i => clientOperations[i]).ToArray(), - (_, int[] excl) => - Enumerable - .Range(0, clientOperations.Length) - .Except(excl) - .Select(i => clientOperations[i]) - .ToArray(), - }; - - string GetAssemblyInfo(Assembly assembly) => $"{assembly.Location}, modified {new FileInfo(assembly.Location).LastWriteTime}"; - - Console.WriteLine(" .NET Core: " + GetAssemblyInfo(typeof(object).Assembly)); - Console.WriteLine(" ASP.NET Core: " + GetAssemblyInfo(typeof(WebHost).Assembly)); - Console.WriteLine(" System.Net.Http: " + GetAssemblyInfo(typeof(System.Net.Http.HttpClient).Assembly)); - Console.WriteLine(" Server: " + (config.UseHttpSys ? "http.sys" : "Kestrel")); - Console.WriteLine(" Server URL: " + config.ServerUri); - Console.WriteLine(" Client Tracing: " + (config.Trace && config.RunMode.HasFlag(RunMode.client) ? "ON (client.log)" : "OFF")); - Console.WriteLine(" Server Tracing: " + (config.Trace && config.RunMode.HasFlag(RunMode.server) ? "ON (server.log)" : "OFF")); - Console.WriteLine(" ASP.NET Log: " + config.LogAspNet); - Console.WriteLine(" Concurrency: " + config.ConcurrentRequests); - Console.WriteLine(" Content Length: " + config.MaxContentLength); - Console.WriteLine(" HTTP Version: " + config.HttpVersion); - Console.WriteLine(" Lifetime: " + (config.ConnectionLifetime.HasValue ? $"{config.ConnectionLifetime.Value.TotalMilliseconds}ms" : "(infinite)")); - Console.WriteLine(" Operations: " + string.Join(", ", usedClientOperations.Select(o => o.name))); - Console.WriteLine(" Random Seed: " + config.RandomSeed); - Console.WriteLine(" Cancellation: " + 100 * config.CancellationProbability + "%"); - Console.WriteLine("Max Content Size: " + config.MaxContentLength); - Console.WriteLine("Query Parameters: " + config.MaxParameters); - Console.WriteLine(); - - - StressServer? server = null; - if (config.RunMode.HasFlag(RunMode.server)) - { - // Start the Kestrel web server in-proc. - Console.WriteLine($"Starting {(config.UseHttpSys ? "http.sys" : "Kestrel")} server."); - server = new StressServer(config); - Console.WriteLine($"Server started at {server.ServerUri}"); - } + if (!config.ServerUri.StartsWith("http")) + { + Console.Error.WriteLine("Invalid server uri"); + return ExitCode.CliError; + } - StressClient? client = null; - if (config.RunMode.HasFlag(RunMode.client)) - { - // Start the client. - Console.WriteLine($"Starting {config.ConcurrentRequests} client workers."); + if (config.ListOperations) + { + for (int i = 0; i < clientOperations.Length; i++) + { + Console.WriteLine(clientOperations[i].name); + } + return ExitCode.Success; + } - client = new StressClient(usedClientOperations, config); - client.Start(); - } + // derive client operations based on arguments + (string name, Func op)[] usedClientOperations = (config.OpIndices, config.ExcludedOpIndices) switch + { + (null, null) => clientOperations, + (int[] incl, null) => incl.Select(i => clientOperations[i]).ToArray(), + (_, int[] excl) => + Enumerable + .Range(0, clientOperations.Length) + .Except(excl) + .Select(i => clientOperations[i]) + .ToArray(), + }; + + string GetAssemblyInfo(Assembly assembly) => $"{assembly.Location}, modified {new FileInfo(assembly.Location).LastWriteTime}"; + + Console.WriteLine(" .NET Core: " + GetAssemblyInfo(typeof(object).Assembly)); + Console.WriteLine(" ASP.NET Core: " + GetAssemblyInfo(typeof(WebHost).Assembly)); + Console.WriteLine(" System.Net.Http: " + GetAssemblyInfo(typeof(System.Net.Http.HttpClient).Assembly)); + Console.WriteLine(" Server: " + (config.UseHttpSys ? "http.sys" : "Kestrel")); + Console.WriteLine(" Server URL: " + config.ServerUri); + Console.WriteLine(" Client Tracing: " + (config.Trace && config.RunMode.HasFlag(RunMode.client) ? "ON (client.log)" : "OFF")); + Console.WriteLine(" Server Tracing: " + (config.Trace && config.RunMode.HasFlag(RunMode.server) ? "ON (server.log)" : "OFF")); + Console.WriteLine(" ASP.NET Log: " + config.LogAspNet); + Console.WriteLine(" Concurrency: " + config.ConcurrentRequests); + Console.WriteLine(" Content Length: " + config.MaxContentLength); + Console.WriteLine(" HTTP Version: " + config.HttpVersion); + Console.WriteLine(" Lifetime: " + (config.ConnectionLifetime.HasValue ? $"{config.ConnectionLifetime.Value.TotalMilliseconds}ms" : "(infinite)")); + Console.WriteLine(" Operations: " + string.Join(", ", usedClientOperations.Select(o => o.name))); + Console.WriteLine(" Random Seed: " + config.RandomSeed); + Console.WriteLine(" Cancellation: " + 100 * config.CancellationProbability + "%"); + Console.WriteLine("Max Content Size: " + config.MaxContentLength); + Console.WriteLine("Query Parameters: " + config.MaxParameters); + Console.WriteLine(); - await WaitUntilMaxExecutionTimeElapsedOrKeyboardInterrupt(config.MaximumExecutionTime); - client?.Stop(); - client?.PrintFinalReport(); + StressServer? server = null; + if (config.RunMode.HasFlag(RunMode.server)) + { + // Start the Kestrel web server in-proc. + Console.WriteLine($"Starting {(config.UseHttpSys ? "http.sys" : "Kestrel")} server."); + server = new StressServer(config); + Console.WriteLine($"Server started at {server.ServerUri}"); + } - // return nonzero status code if there are stress errors - return client?.TotalErrorCount == 0 ? ExitCode.Success : ExitCode.StressError; - } + StressClient? client = null; + if (config.RunMode.HasFlag(RunMode.client)) + { + // Start the client. + Console.WriteLine($"Starting {config.ConcurrentRequests} client workers."); - private static async Task WaitUntilMaxExecutionTimeElapsedOrKeyboardInterrupt(TimeSpan? maxExecutionTime = null) - { - var tcs = new TaskCompletionSource(); - Console.CancelKeyPress += (sender,args) => { Console.Error.WriteLine("Keyboard interrupt"); args.Cancel = true; tcs.TrySetResult(false); }; - if (maxExecutionTime.HasValue) - { - Console.WriteLine($"Running for a total of {maxExecutionTime.Value.TotalMinutes:0.##} minutes"); - var cts = new System.Threading.CancellationTokenSource(delay: maxExecutionTime.Value); - cts.Token.Register(() => { Console.WriteLine("Max execution time elapsed"); tcs.TrySetResult(false); }); + client = new StressClient(usedClientOperations, config); + client.Start(); + } + + await WaitUntilMaxExecutionTimeElapsedOrKeyboardInterrupt(config.MaximumExecutionTime); + + client?.Stop(); + client?.PrintFinalReport(); + + // return nonzero status code if there are stress errors + return client?.TotalErrorCount == 0 ? ExitCode.Success : ExitCode.StressError; } - await tcs.Task; - } + private static async Task WaitUntilMaxExecutionTimeElapsedOrKeyboardInterrupt(TimeSpan? maxExecutionTime = null) + { + var tcs = new TaskCompletionSource(); + Console.CancelKeyPress += (sender, args) => { Console.Error.WriteLine("Keyboard interrupt"); args.Cancel = true; tcs.TrySetResult(false); }; + if (maxExecutionTime.HasValue) + { + Console.WriteLine($"Running for a total of {maxExecutionTime.Value.TotalMinutes:0.##} minutes"); + var cts = new System.Threading.CancellationTokenSource(delay: maxExecutionTime.Value); + cts.Token.Register(() => { Console.WriteLine("Max execution time elapsed"); tcs.TrySetResult(false); }); + } - private static S? Select(this T? value, Func mapper) where T : struct where S : struct - { - return value is null ? null : new S?(mapper(value.Value)); + await tcs.Task; + } + + private static S? Select(this T? value, Func mapper) where T : struct where S : struct + { + return value is null ? null : new S?(mapper(value.Value)); + } } }