From bcb81fbfedd23f04cdf67228954da68bfb588f78 Mon Sep 17 00:00:00 2001 From: Joe Batt Date: Mon, 30 Jan 2023 20:39:14 +0000 Subject: [PATCH 1/3] Added stow endpoint Signed-off-by: Joe Batt --- .../Controllers/StowController.cs | 53 +++++++++++++++++++ .../dotnet-performance-app/Program.cs | 2 + .../dotnet-performance-app/Support/Stow.cs | 50 +++++++++++++++++ .../dotnet-performance-app.csproj | 15 ------ 4 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 performance-testing/dotnet-performance-app/Controllers/StowController.cs create mode 100644 performance-testing/dotnet-performance-app/Support/Stow.cs diff --git a/performance-testing/dotnet-performance-app/Controllers/StowController.cs b/performance-testing/dotnet-performance-app/Controllers/StowController.cs new file mode 100644 index 0000000..1dc146a --- /dev/null +++ b/performance-testing/dotnet-performance-app/Controllers/StowController.cs @@ -0,0 +1,53 @@ +using dotnet_performance_app.Support; +using Microsoft.AspNetCore.Mvc; +using System.Net; +using System.Reflection; + +namespace dotnet_performance_app.Controllers +{ + [ApiController] + [Route("stow")] + public class StowController : ControllerBase + { + public StowController(IConfiguration configuration, IHttpClientFactory httpClientFactory) + { + Host = configuration.GetValue("InformaticsGateway:Host"); + Port = configuration.GetValue("InformaticsGateway:Port"); + Stow = new Stow(httpClientFactory); + } + + private string? Host { get; set; } + private int Port { get; set; } + private Stow Stow { get; set; } + + [HttpGet] + public async Task DicomAssociation( + [FromQuery(Name = "modality")] string modality) + { + var result = Stow.SendStowRequest(GetFolder(modality.ToUpper()).ToString(), $"https://{Host}:{Port}").Result; + + if (result.StatusCode.Equals(HttpStatusCode.OK)) + { + return Ok(); + } + else + { + return BadRequest(result.Content); + } + } + + private DirectoryInfo GetFolder(string subfolder) + { + var rand = new Random(); + var pathname = Path.Combine(GetDirectory(), "Data", "DICOM", subfolder); + var directory = new DirectoryInfo(pathname); + var directories = directory.GetDirectories(); + return directories[rand.Next(directories.Length)]; + } + + private string GetDirectory() + { + return Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + } + } +} diff --git a/performance-testing/dotnet-performance-app/Program.cs b/performance-testing/dotnet-performance-app/Program.cs index 22ac271..b109337 100644 --- a/performance-testing/dotnet-performance-app/Program.cs +++ b/performance-testing/dotnet-performance-app/Program.cs @@ -8,6 +8,8 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +builder.Services.AddHttpClient(); + var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/performance-testing/dotnet-performance-app/Support/Stow.cs b/performance-testing/dotnet-performance-app/Support/Stow.cs new file mode 100644 index 0000000..6c43c22 --- /dev/null +++ b/performance-testing/dotnet-performance-app/Support/Stow.cs @@ -0,0 +1,50 @@ +namespace dotnet_performance_app.Support +{ + public class Stow + { + private readonly IHttpClientFactory _httpClientFactory; + + public Stow(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + public async Task SendStowRequest(string directory, string url) + { + var client = _httpClientFactory.CreateClient(); + var mimeType = "application/dicom"; + var multiContent = GetMultipartContent(mimeType); + + foreach (var path in Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories)) + { + var sContent = new StreamContent(File.OpenRead(path)); + + sContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(mimeType); + + multiContent.Add(sContent); + } + + if (multiContent.Count() > 0) + { + var request = new HttpRequestMessage(HttpMethod.Post, url); + + request.Content = multiContent; + + var response = await client.SendAsync(request); + + return response; + } + + return null; + } + + private static MultipartContent GetMultipartContent(string mimeType) + { + var multiContent = new MultipartContent("related", "DICOM DATA BOUNDARY"); + + multiContent.Headers.ContentType.Parameters.Add(new System.Net.Http.Headers.NameValueHeaderValue("type", "\"" + mimeType + "\"")); + + return multiContent; + } + } +} diff --git a/performance-testing/dotnet-performance-app/dotnet-performance-app.csproj b/performance-testing/dotnet-performance-app/dotnet-performance-app.csproj index 4a22d23..e3c8693 100644 --- a/performance-testing/dotnet-performance-app/dotnet-performance-app.csproj +++ b/performance-testing/dotnet-performance-app/dotnet-performance-app.csproj @@ -10,21 +10,6 @@ . - - - - - - - - - - - - - - - From e70055a08c09d485c9701f4ead7efbc8c9863248 Mon Sep 17 00:00:00 2001 From: Joe Batt Date: Thu, 2 Feb 2023 15:34:07 +0000 Subject: [PATCH 2/3] Added stow functionality to test harness Signed-off-by: Joe Batt --- .../Controllers/StowController.cs | 10 ++++- .../dotnet-performance-app/Program.cs | 9 +---- .../dotnet-performance-app/README.md | 37 +++++++++++++++++++ .../Support/DicomScu.cs | 14 +++++++ .../dotnet-performance-app/Support/Stow.cs | 12 ++++-- .../dotnet-performance-app/appsettings.json | 6 ++- 6 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 performance-testing/dotnet-performance-app/README.md diff --git a/performance-testing/dotnet-performance-app/Controllers/StowController.cs b/performance-testing/dotnet-performance-app/Controllers/StowController.cs index 1dc146a..ede8078 100644 --- a/performance-testing/dotnet-performance-app/Controllers/StowController.cs +++ b/performance-testing/dotnet-performance-app/Controllers/StowController.cs @@ -12,19 +12,25 @@ public class StowController : ControllerBase public StowController(IConfiguration configuration, IHttpClientFactory httpClientFactory) { Host = configuration.GetValue("InformaticsGateway:Host"); - Port = configuration.GetValue("InformaticsGateway:Port"); + Port = configuration.GetValue("InformaticsGateway:StowPort"); + Endpoint = $"/dicomweb/{configuration.GetValue("InformaticsGateway:StowWorkflowId")}/studies"; + User = configuration.GetValue("InformaticsGateway:StowUser"); + Password = configuration.GetValue("InformaticsGateway:StowPassword"); Stow = new Stow(httpClientFactory); } private string? Host { get; set; } + private string? Endpoint { get; set; } private int Port { get; set; } + private string User { get; set; } + private string Password { get; set; } private Stow Stow { get; set; } [HttpGet] public async Task DicomAssociation( [FromQuery(Name = "modality")] string modality) { - var result = Stow.SendStowRequest(GetFolder(modality.ToUpper()).ToString(), $"https://{Host}:{Port}").Result; + var result = Stow.SendStowRequest(GetFolder(modality.ToUpper()).ToString(), $"http://{Host}:{Port}{Endpoint}", $"{User}:{Password}").Result; if (result.StatusCode.Equals(HttpStatusCode.OK)) { diff --git a/performance-testing/dotnet-performance-app/Program.cs b/performance-testing/dotnet-performance-app/Program.cs index b109337..6f85668 100644 --- a/performance-testing/dotnet-performance-app/Program.cs +++ b/performance-testing/dotnet-performance-app/Program.cs @@ -13,13 +13,8 @@ var app = builder.Build(); // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - -app.UseHttpsRedirection(); +app.UseSwagger(); +app.UseSwaggerUI(); app.MapControllers(); diff --git a/performance-testing/dotnet-performance-app/README.md b/performance-testing/dotnet-performance-app/README.md new file mode 100644 index 0000000..9d6b897 --- /dev/null +++ b/performance-testing/dotnet-performance-app/README.md @@ -0,0 +1,37 @@ +# Description +The dotnet-performance-app can be used for sending c-store and stow-rs requests for CT, US, MR and RF data. + +## Running dotnet-performance-app +### Building the app locally + +```bash +cd performance-testing/dotnet-performance-app +``` + +```bash +dotnet build +``` + +### Running as docker image +```bash +cd performance-testing/dotnet-performance-app +``` + +```bash +docker build -t dotnet-performance-app . +``` + +```bash +docker run -it --rm -p 5000:80 -p 5001:443 -e InformaticsGateway__Host={host} -e InformaticsGateway__Port={port} -e InformaticsGateway__StowPort={stowport} -e InformaticsGateway__StowUser={stowuser} -e InformaticsGateway__StowPassword={stowpassword} -e InformaticsGateway__StowWorkflowId={stowworkflowid} dotnet-performance-app +``` +> **host** is to be replaced with the host that MIG is running on +> **port** is to be replaced with the port that MIG is set up to receive C-STORE request on +> **stowport** is to be replaced with the port that MIG is set up to receive STOW requests on +> **stowuser** is to be replaced with the user that is required for MIG auth. **ONLY** required if MIG auth is enabled +> **stowpassword** is to be replaced with the password that is required for MIG auth. **ONLY** required if MIG auth is enabled +> **stowworkflowid** is to be replaced with the workflow id that you want the STOW request to trigger. This is the unique ID for a workflow within Workflow Manager + +Navigate to http://localhost:5000/swagger to see endpoint documentation + + + diff --git a/performance-testing/dotnet-performance-app/Support/DicomScu.cs b/performance-testing/dotnet-performance-app/Support/DicomScu.cs index aa36ac7..5fb911a 100644 --- a/performance-testing/dotnet-performance-app/Support/DicomScu.cs +++ b/performance-testing/dotnet-performance-app/Support/DicomScu.cs @@ -17,8 +17,13 @@ public async Task CStore(string host, int port, string callingAeTit var dicomClient = CreateClient(host, port, callingAeTitle, calledAeTitle); var countdownEvent = new CountdownEvent(dicomFiles.Count); var failureStatus = new List(); + var seriesUID = await Anonymize(dicomFiles[0].Dataset.GetString(DicomTag.SeriesInstanceUID).Trim()); + var studyUID = await Anonymize(dicomFiles[0].Dataset.GetString(DicomTag.StudyInstanceUID).Trim()); + foreach (var file in dicomFiles) { + file.Dataset.AddOrUpdate(DicomTag.SeriesInstanceUID, seriesUID); + file.Dataset.AddOrUpdate(DicomTag.StudyInstanceUID, studyUID); var cStoreRequest = new DicomCStoreRequest(file); cStoreRequest.OnResponseReceived += (DicomCStoreRequest request, DicomCStoreResponse response) => { @@ -48,6 +53,15 @@ public async Task CStore(string host, int port, string callingAeTit return failureStatus.First(); } + private async Task Anonymize(string uid) + { + uid = uid.Substring(0, uid.Length - 10); + var r = new Random(); + var x = r.Next(0, 99999); + var s = x.ToString("000000"); + return uid + s; + } + private IDicomClient CreateClient(string host, int port, string callingAeTitle, string calledAeTitle) { return DicomClientFactory.Create(host, port, false, callingAeTitle, calledAeTitle); diff --git a/performance-testing/dotnet-performance-app/Support/Stow.cs b/performance-testing/dotnet-performance-app/Support/Stow.cs index 6c43c22..4430d2f 100644 --- a/performance-testing/dotnet-performance-app/Support/Stow.cs +++ b/performance-testing/dotnet-performance-app/Support/Stow.cs @@ -1,4 +1,6 @@ -namespace dotnet_performance_app.Support +using System.Net.Http.Headers; + +namespace dotnet_performance_app.Support { public class Stow { @@ -9,7 +11,7 @@ public Stow(IHttpClientFactory httpClientFactory) _httpClientFactory = httpClientFactory; } - public async Task SendStowRequest(string directory, string url) + public async Task SendStowRequest(string directory, string url, string authenticationString) { var client = _httpClientFactory.CreateClient(); var mimeType = "application/dicom"; @@ -19,7 +21,7 @@ public async Task SendStowRequest(string directory, string { var sContent = new StreamContent(File.OpenRead(path)); - sContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(mimeType); + sContent.Headers.ContentType = new MediaTypeHeaderValue(mimeType); multiContent.Add(sContent); } @@ -28,6 +30,10 @@ public async Task SendStowRequest(string directory, string { var request = new HttpRequestMessage(HttpMethod.Post, url); + var base64EncodedAuthenticationString = Convert.ToBase64String(System.Text.ASCIIEncoding.UTF8.GetBytes(authenticationString)); + + request.Headers.Add("Authorization", "Basic " + base64EncodedAuthenticationString); + request.Content = multiContent; var response = await client.SendAsync(request); diff --git a/performance-testing/dotnet-performance-app/appsettings.json b/performance-testing/dotnet-performance-app/appsettings.json index f39c873..97f7795 100644 --- a/performance-testing/dotnet-performance-app/appsettings.json +++ b/performance-testing/dotnet-performance-app/appsettings.json @@ -7,7 +7,11 @@ }, "InformaticsGateway": { "Host": "", - "Port": 11511 + "Port": 104, + "StowPort": 5000, + "StowUser": "", + "StowPassword": "", + "StowWorkflowId": "" }, "AllowedHosts": "*" } From 27e825c2ec9ec88d8d05071b6ef6a392ba7b4978 Mon Sep 17 00:00:00 2001 From: Joe Batt Date: Mon, 6 Feb 2023 14:40:29 +0000 Subject: [PATCH 3/3] Added stress test config Signed-off-by: Joe Batt --- .../k6/dicom/config/stressConfig.json | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 performance-testing/k6/dicom/config/stressConfig.json diff --git a/performance-testing/k6/dicom/config/stressConfig.json b/performance-testing/k6/dicom/config/stressConfig.json new file mode 100644 index 0000000..fa21183 --- /dev/null +++ b/performance-testing/k6/dicom/config/stressConfig.json @@ -0,0 +1,46 @@ +{ + "lowerThinkTime": 60, + "upperThinkTime": 120, + "ct": { + "vus": 3, + "iterations": 7, + "maxDuration": "60m", + "pacing": 364 + }, + "ct_no": { + "vus": 3, + "iterations": 3, + "maxDuration": "60m", + "pacing": 1050 + }, + "mr": { + "vus": 3, + "iterations": 9, + "maxDuration": "60m", + "pacing": 250 + }, + "mr_no": { + "vus": 3, + "iterations": 4, + "maxDuration": "60m", + "pacing": 750 + }, + "us": { + "vus": 3, + "iterations": 3, + "maxDuration": "60m", + "pacing": 1050 + }, + "us_no": { + "vus": 3, + "iterations": 7, + "maxDuration": "60m", + "pacing": 364 + }, + "rf": { + "vus": 6, + "iterations": 30, + "maxDuration": "60m", + "pacing": 90 + } +} \ No newline at end of file