/// <summary> /// Runs the specified uris concurrently and reports on the results for them. /// </summary> /// <param name="client">The HTTP client.</param> /// <param name="uris">The uris to run.</param> /// <param name="uriRunnerOptions">The options for the runner.</param> /// <returns>The results for the uri.</returns> public static Task<UriRunnerResult[]> RunUrisConcurrentlyAsync(HttpClient client, IReadOnlyList<string> uris, UriRunnerOptions uriRunnerOptions) { var tasks = new Task<UriRunnerResult>[uris.Count]; Parallel.For( fromInclusive: 0, toExclusive: uris.Count, body: (i, state) => { var uri = uris[i]; tasks[i] = RunUriAsync( client: client, uri: uri, uriRunnerOptions: uriRunnerOptions); }); return Task.WhenAll(tasks); }
/// <summary> /// Runs the specified URI asynchronously and reports timing results. /// </summary> /// <param name="client">The HTTP client.</param> /// <param name="uri">The URI to run.</param> /// <param name="uriRunnerOptions">The options.</param> /// <returns>The timing result.</returns> private static async Task<UriRunnerResult> RunUriAsync(HttpClient client, string uri, UriRunnerOptions uriRunnerOptions) { var sw = new Stopwatch(); sw.Start(); using (var response = await client.GetAsync(uri, completionOption: HttpCompletionOption.ResponseHeadersRead)) { var responseTime = sw.Elapsed; var statusCode = response.StatusCode; // Do not need to get the body if we don't need to calculate body size or checksum. if (!uriRunnerOptions.CalculateBodyChecksum && !uriRunnerOptions.CalculateBodySize) { // Optimistically get the body size from the headers if it's there. var contentLength = response.Content.Headers.ContentLength; return new UriRunnerResult( uri: uri, responseTime: responseTime, statusCode: statusCode, bodySize: contentLength.GetValueOrDefault(), bodyChecksum: "0"); } using (var httpContent = response.Content) { var body = await httpContent.ReadAsByteArrayAsync(); var bodySize = body.Length; var md5 = MD5.Create(); var hash = md5.ComputeHash(body); var bodyChecksum = ByteArrayToHexString(hash); return new UriRunnerResult( uri: uri, responseTime: responseTime, statusCode: statusCode, bodySize: bodySize, bodyChecksum: bodyChecksum); } } }
/// <summary> /// Mains entry-point. /// </summary> /// <param name="args">The command-line arguments. We expect none at the moment.</param> public static void Main(string[] args) { // Just to make sure there isn't anything fuffing around with the number of concurrent requests. System.Net.ServicePointManager.DefaultConnectionLimit = int.MaxValue; var consoleWriteLine = StringWriterFuns.ConsoleWriter(); var options = new CommandLineOptions(); if (!CommandLine.Parser.Default.ParseArgumentsStrict(args, options)) { consoleWriteLine("Something is wrong with command-line arguments."); Environment.Exit(-1); } var replayFileNames = options.ExpandedReplayFiles(); var replayFiles = new ReplayFile[replayFileNames.Length]; var replayFileErrors = 0; for (var i = 0; i < replayFileNames.Length; i++) { var replayFileName = replayFileNames[i]; try { var content = File.ReadAllText(replayFileName); content = Environment.ExpandEnvironmentVariables(content); var replay = Newtonsoft.Json.JsonConvert.DeserializeObject <ReplayFile>(content); replayFiles[i] = replay; consoleWriteLine("Yay!: {0}", replayFileName); consoleWriteLine(" - {0}", replay.Name); consoleWriteLine(" - {0}", replay.Description); consoleWriteLine(" - {0}", replay.BaseUri); consoleWriteLine(" - {0}", replay.Headers); consoleWriteLine(" - {0} uris", replay.Uris.Length.ToString("G")); } catch (Exception e) { consoleWriteLine("**** ERROR reading replay file: {0}", replayFileName); consoleWriteLine(e.ToString()); replayFileErrors++; } } if (replayFileErrors > 0) { consoleWriteLine("+++++ Could not read all replay files. Exiting."); Environment.Exit(-1); } if (replayFiles.Length == 0) { consoleWriteLine("+++++ No suitable replay files."); Environment.Exit(-1); } if (options.OutFile != null && File.Exists(options.OutFile)) { File.Delete(options.OutFile); } StringWriterFuns.WriteLineFun writeReportLine; if (options.OutFile != null) { writeReportLine = StringWriterFuns.AggregateWriter( new[] { StringWriterFuns.ConsoleWriter(), StringWriterFuns.FileWriter(options.OutFile) }); } else { writeReportLine = StringWriterFuns.ConsoleWriter(); } // The CSV header with field names. const string Header = "Sequence, Iterations, Concurrent Requests, TestStartTimeUtc, TestEndTimeUtc, TestDurationMs, Status Code, Response Size, Response Checksum, Req/Sec, min, perc50, perc75, perc90, perc95, max, description, baseUri, uri"; writeReportLine(Header); // The callback for completed HTTP request. We use it here to // write the CSV result into the file. Keep the sequence number // so that we can print that out too. var sequenceNumber = 0; Action <ReplayFilerRunnerResult> callback = (r) => { sequenceNumber++; var line = string.Format( "{0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, {15}, {16}, {17}, {18}", sequenceNumber.ToString("G"), r.Iterations.ToString("G"), r.ConcurrentRequests.ToString("G"), r.TestStartTime.ToUniversalTime().ToString("yyy-mm-dd HH:MM:ss.fff"), r.TestEndTime.ToUniversalTime().ToString("yyy-mm-dd HH:MM:ss.fff"), r.TestDuration.TotalMilliseconds.ToString("F"), r.LastStatusCode.ToString("G"), r.LastResponseBodySize.ToString("G"), r.LastResponseBodyChecksum, r.RequestsPerSec.ToString("F"), r.ResponseTimeMin.TotalMilliseconds.ToString("G"), r.ResponseTimePerc50.TotalMilliseconds.ToString("G"), r.ResponseTimePerc75.TotalMilliseconds.ToString("G"), r.ResponseTimePerc90.TotalMilliseconds.ToString("G"), r.ResponseTimePerc95.TotalMilliseconds.ToString("G"), r.ResponseTimeMax.TotalMilliseconds.ToString("G"), r.Description, r.BaseUri, r.Uri); writeReportLine(line); }; foreach (var replayFile in replayFiles) { var uriRunnerOptions = new UriRunnerOptions( calculateBodySize: options.CalculateBodySize, calculateBodyChecksum: options.CalculateBodyChecksum); ReplayFileRunner.RunReplayFileAsync( replayFile: replayFile, callback: callback, uriRunnerOptions: uriRunnerOptions, iterations: options.Iterations, concurrentRequests: options.Concurrency).Wait(); } }
/// <summary> /// Asynchronously runs the specified replay file. /// </summary> /// <param name="replayFile">The replay file.</param> /// <param name="callback">The callback to call at the completion of each of the request in the replay file..</param> /// <param name="uriRunnerOptions">The options for the uri runner.</param> /// <param name="iterations">The iterations.</param> /// <param name="concurrentRequests">The number of concurrent requests to execute.</param> /// <returns>The list of the results.</returns> public static async Task RunReplayFileAsync( ReplayFile replayFile, Action<ReplayFilerRunnerResult> callback, UriRunnerOptions uriRunnerOptions, int iterations = 1, int concurrentRequests = 1) { var consoleWriteLine = StringWriterFuns.ConsoleWriter(); using (var handler = new HttpClientHandler()) using (var client = new HttpClient(handler)) { handler.AllowAutoRedirect = true; handler.AutomaticDecompression = DecompressionMethods.None; handler.UseProxy = true; // Need to set this to false to make sure the http client does not // mess around with our own cookies. handler.UseCookies = false; client.BaseAddress = new Uri(replayFile.BaseUri); foreach (var header in replayFile.Headers) { if (!client.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value)) { consoleWriteLine("Failed to add header: {0} = {1}", header.Key, header.Value); } } foreach (var uri in replayFile.Uris) { var sw = new Stopwatch(); var numberOfRequests = iterations * concurrentRequests; var allResults = new List<UriRunnerResult>(capacity: numberOfRequests); var startTimeUtc = DateTimeOffset.UtcNow; // Exec same URI with the specified concurrency level var uris = Enumerable.Repeat(uri, concurrentRequests).ToArray(); for (var i = 0; i < iterations; i++) { sw.Start(); var runUrisResults = await UriRunner.RunUrisConcurrentlyAsync( client: client, uris: uris, uriRunnerOptions: uriRunnerOptions); sw.Stop(); allResults.AddRange(runUrisResults); } var endTimeUtc = DateTimeOffset.UtcNow; var requestsPerSec = (double)numberOfRequests / sw.Elapsed.TotalSeconds; var responseTimes = allResults.Select(r => r.ResponseTime); var percentiles = new PercentileStats<TimeSpan>(responseTimes); var lastResult = allResults.Last(); var reportLine = new ReplayFilerRunnerResult( iterations: iterations, concurrentRequests: concurrentRequests, lastStatusCode: (int)lastResult.StatusCode, lastResponseBodySize: lastResult.BodySize, lastResponseBodyChecksum: lastResult.BodyChecksum, responseTimeStats: percentiles, description: replayFile.Description, baseUri: replayFile.BaseUri, uri: uri, requestsPerSec: requestsPerSec, testStartTime: startTimeUtc, testEndTime: endTimeUtc); if (callback != null) { callback(reportLine); } } } }
/// <summary> /// Runs the specified URI asynchronously and reports timing results. /// </summary> /// <param name="client">The HTTP client.</param> /// <param name="uri">The URI to run.</param> /// <param name="uriRunnerOptions">The options.</param> /// <returns>The timing result.</returns> private static async Task <UriRunnerResult> RunUriAsync(HttpClient client, string uri, UriRunnerOptions uriRunnerOptions) { var sw = new Stopwatch(); sw.Start(); using (var response = await client.GetAsync(uri, completionOption: HttpCompletionOption.ResponseHeadersRead)) { var responseTime = sw.Elapsed; var statusCode = response.StatusCode; // Do not need to get the body if we don't need to calculate body size or checksum. if (!uriRunnerOptions.CalculateBodyChecksum && !uriRunnerOptions.CalculateBodySize) { // Optimistically get the body size from the headers if it's there. var contentLength = response.Content.Headers.ContentLength; return(new UriRunnerResult( uri: uri, responseTime: responseTime, statusCode: statusCode, bodySize: contentLength.GetValueOrDefault(), bodyChecksum: "0")); } using (var httpContent = response.Content) { var body = await httpContent.ReadAsByteArrayAsync(); var bodySize = body.Length; var md5 = MD5.Create(); var hash = md5.ComputeHash(body); var bodyChecksum = ByteArrayToHexString(hash); return(new UriRunnerResult( uri: uri, responseTime: responseTime, statusCode: statusCode, bodySize: bodySize, bodyChecksum: bodyChecksum)); } } }
/// <summary> /// Runs the specified uris concurrently and reports on the results for them. /// </summary> /// <param name="client">The HTTP client.</param> /// <param name="uris">The uris to run.</param> /// <param name="uriRunnerOptions">The options for the runner.</param> /// <returns>The results for the uri.</returns> public static Task <UriRunnerResult[]> RunUrisConcurrentlyAsync(HttpClient client, IReadOnlyList <string> uris, UriRunnerOptions uriRunnerOptions) { var tasks = new Task <UriRunnerResult> [uris.Count]; Parallel.For( fromInclusive: 0, toExclusive: uris.Count, body: (i, state) => { var uri = uris[i]; tasks[i] = RunUriAsync( client: client, uri: uri, uriRunnerOptions: uriRunnerOptions); }); return(Task.WhenAll(tasks)); }
/// <summary> /// Asynchronously runs the specified replay file. /// </summary> /// <param name="replayFile">The replay file.</param> /// <param name="callback">The callback to call at the completion of each of the request in the replay file..</param> /// <param name="uriRunnerOptions">The options for the uri runner.</param> /// <param name="iterations">The iterations.</param> /// <param name="concurrentRequests">The number of concurrent requests to execute.</param> /// <returns>The list of the results.</returns> public static async Task RunReplayFileAsync( ReplayFile replayFile, Action <ReplayFilerRunnerResult> callback, UriRunnerOptions uriRunnerOptions, int iterations = 1, int concurrentRequests = 1) { var consoleWriteLine = StringWriterFuns.ConsoleWriter(); using (var handler = new HttpClientHandler()) using (var client = new HttpClient(handler)) { handler.AllowAutoRedirect = true; handler.AutomaticDecompression = DecompressionMethods.None; handler.UseProxy = true; // Need to set this to false to make sure the http client does not // mess around with our own cookies. handler.UseCookies = false; client.BaseAddress = new Uri(replayFile.BaseUri); foreach (var header in replayFile.Headers) { if (!client.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value)) { consoleWriteLine("Failed to add header: {0} = {1}", header.Key, header.Value); } } foreach (var uri in replayFile.Uris) { var sw = new Stopwatch(); var numberOfRequests = iterations * concurrentRequests; var allResults = new List <UriRunnerResult>(capacity: numberOfRequests); var startTimeUtc = DateTimeOffset.UtcNow; // Exec same URI with the specified concurrency level var uris = Enumerable.Repeat(uri, concurrentRequests).ToArray(); for (var i = 0; i < iterations; i++) { sw.Start(); var runUrisResults = await UriRunner.RunUrisConcurrentlyAsync( client : client, uris : uris, uriRunnerOptions : uriRunnerOptions); sw.Stop(); allResults.AddRange(runUrisResults); } var endTimeUtc = DateTimeOffset.UtcNow; var requestsPerSec = (double)numberOfRequests / sw.Elapsed.TotalSeconds; var responseTimes = allResults.Select(r => r.ResponseTime); var percentiles = new PercentileStats <TimeSpan>(responseTimes); var lastResult = allResults.Last(); var reportLine = new ReplayFilerRunnerResult( iterations: iterations, concurrentRequests: concurrentRequests, lastStatusCode: (int)lastResult.StatusCode, lastResponseBodySize: lastResult.BodySize, lastResponseBodyChecksum: lastResult.BodyChecksum, responseTimeStats: percentiles, description: replayFile.Description, baseUri: replayFile.BaseUri, uri: uri, requestsPerSec: requestsPerSec, testStartTime: startTimeUtc, testEndTime: endTimeUtc); if (callback != null) { callback(reportLine); } } } }
/// <summary> /// Mains entry-point. /// </summary> /// <param name="args">The command-line arguments. We expect none at the moment.</param> public static void Main(string[] args) { // Just to make sure there isn't anything fuffing around with the number of concurrent requests. System.Net.ServicePointManager.DefaultConnectionLimit = int.MaxValue; var consoleWriteLine = StringWriterFuns.ConsoleWriter(); var options = new CommandLineOptions(); if (!CommandLine.Parser.Default.ParseArgumentsStrict(args, options)) { consoleWriteLine("Something is wrong with command-line arguments."); Environment.Exit(-1); } var replayFileNames = options.ExpandedReplayFiles(); var replayFiles = new ReplayFile[replayFileNames.Length]; var replayFileErrors = 0; for (var i = 0; i < replayFileNames.Length; i++) { var replayFileName = replayFileNames[i]; try { var content = File.ReadAllText(replayFileName); content = Environment.ExpandEnvironmentVariables(content); var replay = Newtonsoft.Json.JsonConvert.DeserializeObject<ReplayFile>(content); replayFiles[i] = replay; consoleWriteLine("Yay!: {0}", replayFileName); consoleWriteLine(" - {0}", replay.Name); consoleWriteLine(" - {0}", replay.Description); consoleWriteLine(" - {0}", replay.BaseUri); consoleWriteLine(" - {0}", replay.Headers); consoleWriteLine(" - {0} uris", replay.Uris.Length.ToString("G")); } catch (Exception e) { consoleWriteLine("**** ERROR reading replay file: {0}", replayFileName); consoleWriteLine(e.ToString()); replayFileErrors++; } } if (replayFileErrors > 0) { consoleWriteLine("+++++ Could not read all replay files. Exiting."); Environment.Exit(-1); } if (replayFiles.Length == 0) { consoleWriteLine("+++++ No suitable replay files."); Environment.Exit(-1); } if (options.OutFile != null && File.Exists(options.OutFile)) { File.Delete(options.OutFile); } StringWriterFuns.WriteLineFun writeReportLine; if (options.OutFile != null) { writeReportLine = StringWriterFuns.AggregateWriter( new[] { StringWriterFuns.ConsoleWriter(), StringWriterFuns.FileWriter(options.OutFile) }); } else { writeReportLine = StringWriterFuns.ConsoleWriter(); } // The CSV header with field names. const string Header = "Sequence, Iterations, Concurrent Requests, TestStartTimeUtc, TestEndTimeUtc, TestDurationMs, Status Code, Response Size, Response Checksum, Req/Sec, min, perc50, perc75, perc90, perc95, max, description, baseUri, uri"; writeReportLine(Header); // The callback for completed HTTP request. We use it here to // write the CSV result into the file. Keep the sequence number // so that we can print that out too. var sequenceNumber = 0; Action<ReplayFilerRunnerResult> callback = (r) => { sequenceNumber++; var line = string.Format( "{0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, {15}, {16}, {17}, {18}", sequenceNumber.ToString("G"), r.Iterations.ToString("G"), r.ConcurrentRequests.ToString("G"), r.TestStartTime.ToUniversalTime().ToString("yyy-mm-dd HH:MM:ss.fff"), r.TestEndTime.ToUniversalTime().ToString("yyy-mm-dd HH:MM:ss.fff"), r.TestDuration.TotalMilliseconds.ToString("F"), r.LastStatusCode.ToString("G"), r.LastResponseBodySize.ToString("G"), r.LastResponseBodyChecksum, r.RequestsPerSec.ToString("F"), r.ResponseTimeMin.TotalMilliseconds.ToString("G"), r.ResponseTimePerc50.TotalMilliseconds.ToString("G"), r.ResponseTimePerc75.TotalMilliseconds.ToString("G"), r.ResponseTimePerc90.TotalMilliseconds.ToString("G"), r.ResponseTimePerc95.TotalMilliseconds.ToString("G"), r.ResponseTimeMax.TotalMilliseconds.ToString("G"), r.Description, r.BaseUri, r.Uri); writeReportLine(line); }; foreach (var replayFile in replayFiles) { var uriRunnerOptions = new UriRunnerOptions( calculateBodySize: options.CalculateBodySize, calculateBodyChecksum: options.CalculateBodyChecksum); ReplayFileRunner.RunReplayFileAsync( replayFile: replayFile, callback: callback, uriRunnerOptions: uriRunnerOptions, iterations: options.Iterations, concurrentRequests: options.Concurrency).Wait(); } }