├── ThroughputTest ├── app.config ├── Properties │ ├── launchSettings.json │ └── PublishProfiles │ │ └── FolderProfile.pubxml ├── EntityType.cs ├── ThroughputTest.csproj ├── Experiment.cs ├── ThroughputTest.sln ├── MetricsData.cs ├── PerformanceTask.cs ├── DynamicSemaphoreSlim.cs ├── IncreaseInflightSendsExperiment.cs ├── IncreaseInflightReceivesExperiment.cs ├── Program.cs ├── Observable.cs ├── Metrics.cs ├── TaskEx.cs ├── Settings.cs ├── PerformanceApp.cs ├── SenderTask.cs ├── README.md └── ReceiverTask.cs ├── ThroughputTest_v2 ├── ServiceBusThroughputTest │ ├── App.config │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── MetricsData.cs │ ├── ServiceBusThroughputTest.sln │ ├── ServiceBusThroughputTest.csproj │ ├── Metrics.cs │ └── Program.cs ├── ServiceBusThroughputTest.Common │ ├── ILogger.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ └── ServiceBusThroughputTest.Common.csproj ├── ServiceBusThroughputTestLib │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Extensions.cs │ ├── ServiceBusThroughputTestLib.csproj │ ├── Sender.cs │ └── Receiver.cs └── README.md ├── service-bus-dotnet-msg.yml ├── README.md ├── CONTRIBUTING.md ├── LICENSE ├── .gitattributes ├── .gitignore └── Results.txt /ThroughputTest/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ThroughputTest/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "ServiceBusPerfSample": { 4 | "commandName": "Project", 5 | "commandLineArgs": "" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /ThroughputTest_v2/ServiceBusThroughputTest/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /service-bus-dotnet-msg.yml: -------------------------------------------------------------------------------- 1 | ### YamlMime:Sample 2 | sample: 3 | - name: Service Bus Premium Messaging .NET Performance Test 4 | description: The sample in this repo can be used to help benchmark Service Bus Premium Messaging throughput, and can be used to study performance best practices. 5 | generateZip: true 6 | author: celemensv 7 | readme: readme.md 8 | languages: 9 | - csharp 10 | technologies: 11 | - service-bus 12 | - service-bus-messaging 13 | - azure 14 | -------------------------------------------------------------------------------- /ThroughputTest/EntityType.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ThroughputTest 10 | { 11 | enum EntityType 12 | { 13 | Queue, 14 | Topic 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ThroughputTest/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | FileSystem 9 | Release 10 | netcoreapp2.0 11 | bin\Release\PublishOutput 12 | win-x64 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Service Bus Premium Messaging .NET Performance Test 2 | 3 | [The sample](./ThroughputTest) in this repo can be used to help benchmark Service Bus Premium Messaging throughput, 4 | and can be used to study performance best practices. 5 | 6 | A latency-focused sample will be published in the near future, as measuring latency and throughput limits at the same time is not possible. Send operations are generally somewhat less expensive than receives, and therefore 10000 sends in a fast-as-possible burst create a prompt traffic jam in the queue that a receiver simply can’t keep up with. That means the end-to-end passthrough latency for each message goes up when the throughput limits are pushed. Optimal latency, meaning a minimal passthrough time of messages through the system, is not achievable under maximum throughput pressure. 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Azure samples 2 | 3 | Thank you for your interest in contributing to Azure samples! 4 | 5 | ## Ways to contribute 6 | 7 | You can contribute to [Azure samples](https://azure.microsoft.com/documentation/samples/) in a few different ways: 8 | 9 | - Submit feedback on [this sample page](https://azure.microsoft.com/documentation/samples/service-bus-dotnet-messaging-performance/) whether it was helpful or not. 10 | - Submit issues through [issue tracker](https://github.com/Azure-Samples/service-bus-dotnet-messaging-performance/issues) on GitHub. We are actively monitoring the issues and improving our samples. 11 | - If you wish to make code changes to samples, or contribute something new, please follow the [GitHub Forks / Pull requests model](https://help.github.com/articles/fork-a-repo/): Fork the sample repo, make the change and propose it back by submitting a pull request. -------------------------------------------------------------------------------- /ThroughputTest/ThroughputTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.0;netstandard2.0;net472 6 | win-x64;linux-x64 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /ThroughputTest/Experiment.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ThroughputTest 10 | { 11 | using System.Threading.Tasks; 12 | 13 | abstract class Experiment 14 | { 15 | public Experiment(Metrics metrics, Settings settings) 16 | { 17 | Metrics = metrics; 18 | Settings = settings; 19 | } 20 | 21 | public Metrics Metrics { get; } 22 | public Settings Settings { get; } 23 | 24 | public abstract Task Run(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Microsoft Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /ThroughputTest/ThroughputTest.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2027 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThroughputTest", "ThroughputTest.csproj", "{DEF227A5-2189-4781-B2A2-DDA2A4DDB950}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {DEF227A5-2189-4781-B2A2-DDA2A4DDB950}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {DEF227A5-2189-4781-B2A2-DDA2A4DDB950}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {DEF227A5-2189-4781-B2A2-DDA2A4DDB950}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {DEF227A5-2189-4781-B2A2-DDA2A4DDB950}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {6F83BE47-2957-4C46-9340-54180160B5A7} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /ThroughputTest_v2/ServiceBusThroughputTest.Common/ILogger.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ServiceBusThroughputTest.Common 10 | { 11 | using System; 12 | 13 | public interface ILogger : IDisposable 14 | { 15 | Guid StartRecordTime(); 16 | void StartCompletionRecordTime(Guid sessionId); 17 | double StopCompletionRecordTimeAndGetElapsedMs(Guid sessionId); 18 | double StopRecordTimeAndGetElapsedMs(Guid sessionId); 19 | void IncrementActionCount(Guid sessionId); 20 | void IncrementMetricValueBy(Guid sessionId, int count); 21 | void IncrementErrorCount(Guid sessionId); 22 | void IncrementBusyErrorCount(Guid sessionId); 23 | void DisposeSession(Guid sessionId); 24 | void AddTrace(string trace); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ThroughputTest_v2/ServiceBusThroughputTest/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ServiceBusThroughputTest")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ServiceBusThroughputTest")] 13 | [assembly: AssemblyCopyright("Copyright © 2023")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("72a4a64b-eadb-4a0d-8541-6893895adfad")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /ThroughputTest_v2/ServiceBusThroughputTestLib/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ServiceBusThroughputTestLib")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ServiceBusThroughputTestLib")] 13 | [assembly: AssemblyCopyright("Copyright © 2023")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("da73050d-b46d-4760-aef5-cd86e25dfab5")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /ThroughputTest_v2/ServiceBusThroughputTest.Common/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ServiceBusThroughputTest.Common")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ServiceBusThroughputTest.Common")] 13 | [assembly: AssemblyCopyright("Copyright © 2023")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("5d8a1d8a-c5b5-4fbc-b10e-1265a4a4ecbf")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /ThroughputTest_v2/ServiceBusThroughputTest/MetricsData.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ServiceBusThroughputTest 10 | { 11 | public sealed class SendMetrics 12 | { 13 | public long Tick { get; set; } 14 | public int Messages { get; set; } 15 | public int Sends { get; set; } 16 | public int Size { get; set; } 17 | public long SendDuration100ns { get; set; } 18 | public int Errors { get; set; } 19 | public int BusyErrors { get; set; } 20 | public int InflightSends { get; internal set; } 21 | } 22 | 23 | public sealed class ReceiveMetrics 24 | { 25 | public long Tick { get; set; } 26 | public int Messages { get; set; } 27 | public int Receives { get; set; } 28 | public int Size { get; set; } 29 | public long ReceiveDuration100ns { get; set; } 30 | public long CompletionStartTick { get; set; } 31 | public long CompleteDuration100ns { get; set; } 32 | public int Errors { get; set; } 33 | public int BusyErrors { get; set; } 34 | public int Completions { get; internal set; } 35 | public int CompleteCalls { get; internal set; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ThroughputTest/MetricsData.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ThroughputTest 10 | { 11 | sealed class SendMetrics 12 | { 13 | public long Tick { get; set; } 14 | public int Messages { get; set; } 15 | public int Sends { get; set; } 16 | public int Size { get; set; } 17 | public long SendDuration100ns { get; set; } 18 | public long GateLockDuration100ns { get; set; } 19 | public int Errors { get; set; } 20 | public int BusyErrors { get; set; } 21 | public int InflightSends { get; internal set; } 22 | } 23 | 24 | sealed class ReceiveMetrics 25 | { 26 | public long Tick { get; set; } 27 | public int Messages { get; set; } 28 | public int Receives { get; set; } 29 | public int Size { get; set; } 30 | public long ReceiveDuration100ns { get; set; } 31 | public long CompleteDuration100ns { get; set; } 32 | public long GateLockDuration100ns { get; set; } 33 | public int Errors { get; set; } 34 | public int BusyErrors { get; set; } 35 | public int Completions { get; internal set; } 36 | public int CompleteCalls { get; internal set; } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ThroughputTest/PerformanceTask.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ThroughputTest 10 | { 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | 14 | abstract class PerformanceTask 15 | { 16 | protected PerformanceTask(Settings settings, Metrics metrics, CancellationToken cancellationToken) 17 | { 18 | this.Settings = settings; 19 | this.Metrics = metrics; 20 | this.CancellationToken = cancellationToken; 21 | } 22 | 23 | protected Settings Settings { get; private set; } 24 | 25 | protected string ConnectionString { get; private set; } 26 | 27 | protected Metrics Metrics { get; private set; } 28 | 29 | protected CancellationToken CancellationToken { get; private set; } 30 | 31 | public void Close() 32 | { 33 | this.CloseAsync().Fork(); 34 | } 35 | 36 | public async Task OpenAsync() 37 | { 38 | await OnOpenAsync().ConfigureAwait(false); 39 | } 40 | 41 | public Task StartAsync() 42 | { 43 | return OnStartAsync(); 44 | } 45 | 46 | public Task CloseAsync() 47 | { 48 | return Task.CompletedTask; 49 | } 50 | 51 | protected abstract Task OnOpenAsync(); 52 | 53 | protected abstract Task OnStartAsync(); 54 | 55 | 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ThroughputTest_v2/ServiceBusThroughputTestLib/Extensions.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ServiceBusThroughputTestLib.Extensions 10 | { 11 | using Azure.Messaging.ServiceBus; 12 | using ServiceBusThroughputTest.Common; 13 | using System; 14 | using System.Threading; 15 | 16 | static class Extensions 17 | { 18 | public static void HandleExceptions(this AggregateException ex, ILogger logger, Guid? sessionId, CancellationToken cancellationToken) 19 | { 20 | // bool wait = false; 21 | 22 | logger?.AddTrace(ex.InnerException.ToString()); 23 | 24 | ex.Handle((x) => 25 | { 26 | if (sessionId != null) 27 | { 28 | if ((x as ServiceBusException)?.Reason == ServiceBusFailureReason.ServiceBusy) 29 | { 30 | logger?.IncrementBusyErrorCount(sessionId.Value); 31 | if (!cancellationToken.IsCancellationRequested) 32 | { 33 | // wait = true; 34 | } 35 | } 36 | else 37 | { 38 | logger?.IncrementErrorCount(sessionId.Value); 39 | } 40 | } 41 | 42 | return true; 43 | }); 44 | 45 | //if (wait) 46 | //{ 47 | // await Task.Delay(3000, cancellationToken).ConfigureAwait(false); 48 | //} 49 | 50 | if (sessionId != null) 51 | logger?.DisposeSession(sessionId.Value); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ThroughputTest_v2/ServiceBusThroughputTest/ServiceBusThroughputTest.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.33801.447 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceBusThroughputTest", "ServiceBusThroughputTest.csproj", "{72A4A64B-EADB-4A0D-8541-6893895ADFAD}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceBusThroughputTest.Common", "..\ServiceBusThroughputTest.Common\ServiceBusThroughputTest.Common.csproj", "{5D8A1D8A-C5B5-4FBC-B10E-1265A4A4ECBF}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceBusThroughputTestLib", "..\ServiceBusThroughputTestLib\ServiceBusThroughputTestLib.csproj", "{DA73050D-B46D-4760-AEF5-CD86E25DFAB5}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {72A4A64B-EADB-4A0D-8541-6893895ADFAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {72A4A64B-EADB-4A0D-8541-6893895ADFAD}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {72A4A64B-EADB-4A0D-8541-6893895ADFAD}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {72A4A64B-EADB-4A0D-8541-6893895ADFAD}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {5D8A1D8A-C5B5-4FBC-B10E-1265A4A4ECBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {5D8A1D8A-C5B5-4FBC-B10E-1265A4A4ECBF}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {5D8A1D8A-C5B5-4FBC-B10E-1265A4A4ECBF}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {5D8A1D8A-C5B5-4FBC-B10E-1265A4A4ECBF}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {DA73050D-B46D-4760-AEF5-CD86E25DFAB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {DA73050D-B46D-4760-AEF5-CD86E25DFAB5}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {DA73050D-B46D-4760-AEF5-CD86E25DFAB5}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {DA73050D-B46D-4760-AEF5-CD86E25DFAB5}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {860DD522-AB3C-42BC-97DA-33678D48995D} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /ThroughputTest_v2/ServiceBusThroughputTest.Common/ServiceBusThroughputTest.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {5D8A1D8A-C5B5-4FBC-B10E-1265A4A4ECBF} 8 | Library 9 | Properties 10 | ServiceBusThroughputTest.Common 11 | ServiceBusThroughputTest.Common 12 | v4.7.2 13 | 512 14 | true 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /ThroughputTest/DynamicSemaphoreSlim.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ThroughputTest 10 | { 11 | using System; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | public class DynamicSemaphoreSlim : SemaphoreSlim 15 | { 16 | const int overallocationCount = 10000; 17 | long remainingOverallocation; 18 | public DynamicSemaphoreSlim(int initialCount) : base(initialCount + overallocationCount) 19 | { 20 | remainingOverallocation = overallocationCount; 21 | // grab the overallocatedCounts 22 | for (int i = 0; i < overallocationCount; i++) 23 | { 24 | this.Wait(); 25 | } 26 | } 27 | 28 | /// 29 | /// Grant one more count 30 | /// 31 | public void Grant() 32 | { 33 | if (Interlocked.Decrement(ref remainingOverallocation) < 0) 34 | { 35 | throw new InvalidOperationException(); 36 | } 37 | this.Release(); 38 | } 39 | 40 | /// 41 | /// Revoke a count once possible 42 | /// 43 | /// Task that completes when revocation is complete 44 | public Task RevokeAsync() 45 | { 46 | if (Interlocked.Increment(ref remainingOverallocation) > overallocationCount) 47 | { 48 | throw new InvalidOperationException(); 49 | } 50 | return this.WaitAsync(); 51 | } 52 | 53 | /// 54 | /// Revoke a count once possible (does not block) 55 | /// 56 | public void Revoke() 57 | { 58 | if (Interlocked.Increment(ref remainingOverallocation) > overallocationCount) 59 | { 60 | throw new InvalidOperationException(); 61 | } 62 | this.WaitAsync().ContinueWith((t)=> { }); // don't await 63 | } 64 | 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ThroughputTest/IncreaseInflightSendsExperiment.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ThroughputTest 10 | { 11 | using System; 12 | using System.Linq; 13 | using System.Reactive.Linq; 14 | using System.Threading.Tasks; 15 | 16 | class IncreaseInflightSendsExperiment : Experiment 17 | { 18 | private int count; 19 | 20 | public IncreaseInflightSendsExperiment(int count, Metrics metrics, Settings settings) : base(metrics, settings) 21 | { 22 | this.count = count; 23 | } 24 | 25 | public override async Task Run() 26 | { 27 | if (Settings.SenderCount == 0) 28 | { 29 | return null; 30 | } 31 | 32 | await Task.Delay(10).ConfigureAwait(false); // 10 seconds to meter 33 | var firstObservation = await ((IObservable)Metrics).Buffer(TimeSpan.FromSeconds(5)).FirstAsync(); 34 | var firstMessageCount = firstObservation.Sum(i => i.Messages); 35 | if ((Settings.MaxInflightSends.Value + count) < 1) 36 | { 37 | Settings.MaxInflightSends.Value = 1; 38 | count = 1; 39 | } 40 | { 41 | Settings.MaxInflightSends.Value += count; 42 | } 43 | await Task.Delay(20).ConfigureAwait(false); // 20 seconds to settle 44 | 45 | var secondObservation = await ((IObservable)Metrics).Buffer(TimeSpan.FromSeconds(5)).FirstAsync(); 46 | var secondMessageCount = secondObservation.Sum(i => i.Messages); 47 | 48 | if (secondMessageCount > firstMessageCount) 49 | { 50 | return new IncreaseInflightSendsExperiment(this.count, this.Metrics, this.Settings); 51 | } 52 | else 53 | { 54 | Settings.MaxInflightSends.Value -= count; 55 | return new IncreaseInflightSendsExperiment(this.count, this.Metrics, this.Settings); 56 | } 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /ThroughputTest/IncreaseInflightReceivesExperiment.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ThroughputTest 10 | { 11 | using System; 12 | using System.Linq; 13 | using System.Reactive.Linq; 14 | using System.Threading.Tasks; 15 | 16 | class IncreaseInflightReceivesExperiment : Experiment 17 | { 18 | private int count; 19 | 20 | public IncreaseInflightReceivesExperiment(int count, Metrics metrics, Settings settings) : base(metrics, settings) 21 | { 22 | this.count = count; 23 | } 24 | 25 | public override async Task Run() 26 | { 27 | if (Settings.ReceiverCount == 0) 28 | { 29 | return null; 30 | } 31 | 32 | await Task.Delay(10).ConfigureAwait(false); // 10 seconds to meter 33 | var firstObservation = await ((IObservable)Metrics).Buffer(TimeSpan.FromSeconds(5)).FirstAsync(); 34 | var firstMessageCount = firstObservation.Sum(i => i.Messages); 35 | if ((Settings.MaxInflightReceives.Value + count) < 1) 36 | { 37 | Settings.MaxInflightReceives.Value = 1; 38 | count = 1; 39 | } 40 | { 41 | Settings.MaxInflightReceives.Value += count; 42 | } 43 | await Task.Delay(20).ConfigureAwait(false); // 20 seconds to settle and meter 44 | 45 | var secondObservation = await ((IObservable)Metrics).Buffer(TimeSpan.FromSeconds(5)).FirstAsync(); 46 | var secondMessageCount = secondObservation.Sum(i => i.Messages); 47 | 48 | if (secondMessageCount > firstMessageCount) 49 | { 50 | return new IncreaseInflightReceivesExperiment(this.count, this.Metrics, this.Settings); 51 | } 52 | else 53 | { 54 | Settings.MaxInflightReceives.Value -= count; 55 | return new IncreaseInflightReceivesExperiment(this.count, this.Metrics, this.Settings); 56 | } 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /ThroughputTest/Program.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ThroughputTest 10 | { 11 | using CommandLine; 12 | using System; 13 | using System.Linq; 14 | using Azure.Messaging.ServiceBus; 15 | 16 | class Program 17 | { 18 | static int result = 0; 19 | static int Main(params string[] args) 20 | { 21 | CommandLine.Parser.Default.ParseArguments(args) 22 | .WithParsed(opts => RunOptionsAndReturnExitCode(opts)); 23 | return result; 24 | } 25 | 26 | static void RunOptionsAndReturnExitCode(Settings settings) 27 | { 28 | ServiceBusConnectionStringProperties cb = ServiceBusConnectionStringProperties.Parse(settings.ConnectionString); 29 | if (string.IsNullOrWhiteSpace(cb.EntityPath)) 30 | { 31 | if (string.IsNullOrWhiteSpace(settings.SendPath)) 32 | { 33 | Console.WriteLine("--send-path option must be specified if there's no EntityPath in the connection string."); 34 | result = 1; 35 | return; 36 | } 37 | } 38 | else 39 | { 40 | if (string.IsNullOrWhiteSpace(settings.SendPath)) 41 | { 42 | settings.SendPath = cb.EntityPath; 43 | } 44 | 45 | settings.ConnectionString = cb.ToString(); 46 | } 47 | if (settings.ReceivePaths == null || settings.ReceivePaths.Count() == 0) 48 | { 49 | settings.ReceivePaths = new string[] { settings.SendPath }; 50 | } 51 | Console.WriteLine("\n\nPress to STOP at anytime\n"); 52 | Metrics metrics = new Metrics(settings); 53 | ServiceBusPerformanceApp app = new ServiceBusPerformanceApp(settings, metrics); 54 | var experiments = new Experiment[] 55 | { 56 | // new IncreaseInflightSendsExperiment(50, metrics, settings), 57 | // new IncreaseInflightReceivesExperiment(10, metrics, settings) 58 | }; 59 | app.Run(experiments).Wait(); 60 | Console.WriteLine("Complete"); 61 | 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ThroughputTest/Observable.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ThroughputTest 10 | { 11 | using System; 12 | 13 | public class Observable 14 | { 15 | public class ChangingEventArgs : EventArgs 16 | { 17 | public T OldValue { get; private set; } 18 | public T NewValue { get; private set; } 19 | public bool Cancel { get; set; } 20 | 21 | public ChangingEventArgs(T oldValue, T newValue) 22 | { 23 | OldValue = oldValue; 24 | NewValue = newValue; 25 | Cancel = false; 26 | } 27 | } 28 | 29 | public class ChangedEventArgs : EventArgs 30 | { 31 | public T Value { get; private set; } 32 | 33 | public ChangedEventArgs(T value) 34 | { 35 | Value = value; 36 | } 37 | } 38 | 39 | public event EventHandler Changing; 40 | public event EventHandler Changed; 41 | 42 | protected T value; 43 | 44 | public T Value 45 | { 46 | get 47 | { 48 | return value; 49 | } 50 | set 51 | { 52 | if (this.value.Equals(value)) 53 | { 54 | return; 55 | } 56 | ChangingEventArgs e = new ChangingEventArgs(this.value, value); 57 | OnChanging(e); 58 | if (e.Cancel) 59 | { 60 | return; 61 | } 62 | this.value = value; 63 | OnChanged(new ChangedEventArgs(this.value)); 64 | } 65 | } 66 | 67 | public Observable() { } 68 | 69 | public Observable(T value) 70 | { 71 | this.value = value; 72 | } 73 | 74 | protected virtual void OnChanging(ChangingEventArgs e) 75 | { 76 | EventHandler handler = Changing; 77 | if (handler == null) 78 | { 79 | return; 80 | } 81 | handler(this, e); 82 | } 83 | 84 | protected virtual void OnChanged(ChangedEventArgs e) 85 | { 86 | EventHandler handler = Changed; 87 | if (handler == null) 88 | { 89 | return; 90 | } 91 | handler(this, e); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /ThroughputTest_v2/ServiceBusThroughputTestLib/ServiceBusThroughputTestLib.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {DA73050D-B46D-4760-AEF5-CD86E25DFAB5} 8 | Library 9 | Properties 10 | ServiceBusThroughputTestLib 11 | ServiceBusThroughputTestLib 12 | v4.7.2 13 | 512 14 | true 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 7.15.0 52 | 53 | 54 | 55 | 56 | {5d8a1d8a-c5b5-4fbc-b10e-1265a4a4ecbf} 57 | ServiceBusThroughputTest.Common 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /ThroughputTest/Metrics.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ThroughputTest 10 | { 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Threading.Tasks; 14 | 15 | sealed class Metrics : IObservable, IObservable 16 | { 17 | object sendMetricsObserverLock = new object(); 18 | List> sendMetricsObservers = new List>(); 19 | object receiveMetricsObserverLock = new object(); 20 | List> receiveMetricsObservers = new List>(); 21 | 22 | public Metrics(Settings settings) 23 | { 24 | } 25 | 26 | public void PushSendMetrics(SendMetrics sendMetrics) 27 | { 28 | Task.Run(() => 29 | { 30 | IObserver[] observers; 31 | lock (sendMetricsObserverLock) 32 | { 33 | observers = sendMetricsObservers.ToArray(); 34 | } 35 | 36 | foreach (var observer in observers) 37 | { 38 | observer.OnNext(sendMetrics); 39 | } 40 | }).Fork(); 41 | } 42 | 43 | public void PushReceiveMetrics(ReceiveMetrics receiveMetrics) 44 | { 45 | Task.Run(() => 46 | { 47 | IObserver[] observers; 48 | lock (receiveMetricsObserverLock) 49 | { 50 | observers = receiveMetricsObservers.ToArray(); 51 | } 52 | 53 | foreach (var observer in observers) 54 | { 55 | observer.OnNext(receiveMetrics); 56 | } 57 | }).Fork(); 58 | } 59 | 60 | public IDisposable Subscribe(IObserver observer) 61 | { 62 | lock (receiveMetricsObserverLock) 63 | { 64 | receiveMetricsObservers.Add(observer); 65 | } 66 | return System.Reactive.Disposables.Disposable.Create(() => 67 | { 68 | lock (receiveMetricsObserverLock) 69 | { 70 | receiveMetricsObservers.Remove(observer); 71 | } 72 | }); 73 | } 74 | 75 | public IDisposable Subscribe(IObserver observer) 76 | { 77 | lock (sendMetricsObserverLock) 78 | { 79 | sendMetricsObservers.Add(observer); 80 | } 81 | return System.Reactive.Disposables.Disposable.Create(() => 82 | { 83 | lock (sendMetricsObserverLock) 84 | { 85 | sendMetricsObservers.Remove(observer); 86 | } 87 | }); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /ThroughputTest/TaskEx.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ThroughputTest 10 | { 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | 16 | static class TaskEx 17 | { 18 | public static Task ParallelForEachAsync(this IEnumerable source, Func action) 19 | { 20 | List tasks = new List(); 21 | foreach (TSource i in source) 22 | { 23 | tasks.Add(action(i)); 24 | } 25 | 26 | return Task.WhenAll(tasks.ToArray()); 27 | } 28 | 29 | public static Task ParallelForEachAsync(this IEnumerable source, Func action) 30 | { 31 | List tasks = new List(); 32 | 33 | long index = 0; 34 | foreach (TSource i in source) 35 | { 36 | tasks.Add(action(i, index)); 37 | index++; 38 | } 39 | 40 | return Task.WhenAll(tasks.ToArray()); 41 | } 42 | 43 | public static void ForEach(this IEnumerable source, Action action) 44 | { 45 | long index = 0; 46 | foreach (TSource i in source) 47 | { 48 | action(i, index); 49 | index++; 50 | } 51 | } 52 | 53 | public static void Fork(this Task thisTask) 54 | { 55 | thisTask.ContinueWith(t => { }); 56 | } 57 | 58 | public static void For(long start, long end, Action action) 59 | { 60 | for (long i = start; i < end; i++) 61 | { 62 | action(i); 63 | } 64 | } 65 | 66 | public static async Task ParallelForAsync(long start, long end, Func action) 67 | { 68 | List tasks = new List(); 69 | 70 | for (long i = start; i < end; i++) 71 | { 72 | tasks.Add(action(i)); 73 | } 74 | 75 | await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); 76 | } 77 | 78 | public static async Task Delay(TimeSpan delay, CancellationToken cancellationToken) 79 | { 80 | try 81 | { 82 | await Task.Delay(delay, cancellationToken).ConfigureAwait(false); 83 | } 84 | catch (TaskCanceledException) 85 | { 86 | 87 | } 88 | } 89 | 90 | public static async Task IgnoreExceptionAsync(Func task) 91 | { 92 | try 93 | { 94 | await task().ConfigureAwait(false); 95 | } 96 | catch 97 | { 98 | 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /ThroughputTest_v2/ServiceBusThroughputTest/ServiceBusThroughputTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {72A4A64B-EADB-4A0D-8541-6893895ADFAD} 8 | Exe 9 | ServiceBusThroughputTest 10 | ServiceBusThroughputTest 11 | v4.7.2 12 | 512 13 | true 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 2.9.1 58 | 59 | 60 | 13.0.3 61 | 62 | 63 | 4.5.1 64 | 65 | 66 | 67 | 68 | {5d8a1d8a-c5b5-4fbc-b10e-1265a4a4ecbf} 69 | ServiceBusThroughputTest.Common 70 | 71 | 72 | {da73050d-b46d-4760-aef5-cd86e25dfab5} 73 | ServiceBusThroughputTestLib 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | [Xx]64/ 19 | [Xx]86/ 20 | [Bb]uild/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | 85 | # Visual Studio profiler 86 | *.psess 87 | *.vsp 88 | *.vspx 89 | *.sap 90 | 91 | # TFS 2012 Local Workspace 92 | $tf/ 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | *.DotSettings.user 101 | 102 | # JustCode is a .NET coding add-in 103 | .JustCode 104 | 105 | # TeamCity is a build add-in 106 | _TeamCity* 107 | 108 | # DotCover is a Code Coverage Tool 109 | *.dotCover 110 | 111 | # NCrunch 112 | _NCrunch_* 113 | .*crunch*.local.xml 114 | nCrunchTemp_* 115 | 116 | # MightyMoose 117 | *.mm.* 118 | AutoTest.Net/ 119 | 120 | # Web workbench (sass) 121 | .sass-cache/ 122 | 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | 126 | # DocProject is a documentation generator add-in 127 | DocProject/buildhelp/ 128 | DocProject/Help/*.HxT 129 | DocProject/Help/*.HxC 130 | DocProject/Help/*.hhc 131 | DocProject/Help/*.hhk 132 | DocProject/Help/*.hhp 133 | DocProject/Help/Html2 134 | DocProject/Help/html 135 | 136 | # Click-Once directory 137 | publish/ 138 | 139 | # Publish Web Output 140 | *.[Pp]ublish.xml 141 | *.azurePubxml 142 | 143 | # TODO: Un-comment the next line if you do not want to checkin 144 | # your web deploy settings because they may include unencrypted 145 | # passwords 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # NuGet Packages 150 | *.nupkg 151 | # The packages folder can be ignored because of Package Restore 152 | **/packages/* 153 | # except build/, which is used as an MSBuild target. 154 | !**/packages/build/ 155 | # Uncomment if necessary however generally it will be regenerated when needed 156 | #!**/packages/repositories.config 157 | # NuGet v3's project.json files produces more ignoreable files 158 | *.nuget.props 159 | *.nuget.targets 160 | 161 | # Microsoft Azure Build Output 162 | csx/ 163 | *.build.csdef 164 | 165 | # Microsoft Azure Emulator 166 | ecf/ 167 | rcf/ 168 | 169 | # Microsoft Azure ApplicationInsights config file 170 | ApplicationInsights.config 171 | 172 | # Windows Store app package directory 173 | AppPackages/ 174 | BundleArtifacts/ 175 | 176 | # Visual Studio cache files 177 | # files ending in .cache can be ignored 178 | *.[Cc]ache 179 | # but keep track of directories ending in .cache 180 | !*.[Cc]ache/ 181 | 182 | # Others 183 | ClientBin/ 184 | [Ss]tyle[Cc]op.* 185 | ~$* 186 | *~ 187 | *.dbmdl 188 | *.dbproj.schemaview 189 | *.pfx 190 | *.publishsettings 191 | node_modules/ 192 | orleans.codegen.cs 193 | 194 | # RIA/Silverlight projects 195 | Generated_Code/ 196 | 197 | # Backup & report files from converting an old project file 198 | # to a newer Visual Studio version. Backup files are not needed, 199 | # because we have git ;-) 200 | _UpgradeReport_Files/ 201 | Backup*/ 202 | UpgradeLog*.XML 203 | UpgradeLog*.htm 204 | 205 | # SQL Server files 206 | *.mdf 207 | *.ldf 208 | 209 | # Business Intelligence projects 210 | *.rdl.data 211 | *.bim.layout 212 | *.bim_*.settings 213 | 214 | # Microsoft Fakes 215 | FakesAssemblies/ 216 | 217 | # GhostDoc plugin setting file 218 | *.GhostDoc.xml 219 | 220 | # Node.js Tools for Visual Studio 221 | .ntvs_analysis.dat 222 | 223 | # Visual Studio 6 build log 224 | *.plg 225 | 226 | # Visual Studio 6 workspace options file 227 | *.opt 228 | 229 | # Visual Studio LightSwitch build output 230 | **/*.HTMLClient/GeneratedArtifacts 231 | **/*.DesktopClient/GeneratedArtifacts 232 | **/*.DesktopClient/ModelManifest.xml 233 | **/*.Server/GeneratedArtifacts 234 | **/*.Server/ModelManifest.xml 235 | _Pvt_Extensions 236 | 237 | # LightSwitch generated files 238 | GeneratedArtifacts/ 239 | ModelManifest.xml 240 | 241 | # Paket dependency manager 242 | .paket/paket.exe 243 | 244 | # FAKE - F# Make 245 | .fake/ 246 | /ServiceBusPerfSample/Properties/PublishProfiles/cvtest6 - Web Deploy.pubxml 247 | /ServiceBusPerfSample/Properties/Settings.settings 248 | /ServiceBusPerfSample/Properties/webjob-publish-settings.json 249 | -------------------------------------------------------------------------------- /ThroughputTest/Settings.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ThroughputTest 10 | { 11 | using System; 12 | using System.Collections.Generic; 13 | using CommandLine; 14 | using CommandLine.Text; 15 | using Azure.Messaging.ServiceBus; 16 | 17 | class Settings 18 | { 19 | [Option('C', "connection-string", Required = true, HelpText = "Connection string")] 20 | public string ConnectionString { get; set; } 21 | 22 | [Option('S', "send-path", Required = false, HelpText = "Send path. Queue or topic name, unless set in connection string EntityPath.")] 23 | public string SendPath { get; set; } 24 | 25 | [Option('R', "receive-paths", Required = false, HelpText = "Receive paths. Mandatory for receiving from topic subscriptions. Must be {topic}/subscriptions/{subscription-name} or {queue-name}")] 26 | public IEnumerable ReceivePaths { get; set; } 27 | 28 | [Option('n', "number-of-messages", Required = false, HelpText = "Number of messages to send (default 1000000)")] 29 | public long MessageCount { get; set; } = 1000000; 30 | 31 | [Option('b', "message-size-bytes", Required = false, HelpText = "Bytes per message (default 1024)")] 32 | public int MessageSizeInBytes { get; set; } = 1024; 33 | 34 | [Option('f', "frequency-metrics", Required = false, HelpText = "Frequency of metrics display (seconds, default 10s)")] 35 | public int MetricsDisplayFrequency { get; set; } = 10; 36 | 37 | [Option('m', "receive-mode", Required = false, HelpText = "Receive mode.'PeekLock' (default) or 'ReceiveAndDelete'")] 38 | public ServiceBusReceiveMode ReceiveMode { get; set; } = ServiceBusReceiveMode.PeekLock; 39 | 40 | [Option('r', "receiver-count", Required = false, HelpText = "Number of concurrent receivers (default 1)")] 41 | public int ReceiverCount { get; set; } = 5; 42 | 43 | [Option('e', "prefetch-count", Required = false, HelpText = "Prefetch count (default 0)")] 44 | public int PrefetchCount { get; set; } = 100; 45 | 46 | [Option('t', "send-batch-count", Required = false, HelpText = "Number of messages per batch (default 0, no batching)")] 47 | public int SendBatchCount { get; set; } = 0; 48 | 49 | [Option('s', "sender-count", Required = false, HelpText = "Number of concurrent senders (default 1)")] 50 | public int SenderCount { get; set; } = 1; 51 | 52 | [Option('d', "send-delay", Required = false, HelpText = "Delay between sends of any sender (milliseconds, default 0)")] 53 | public int SendDelay { get; private set; } = 0; 54 | 55 | [Option('i', "inflight-sends", Required = false, HelpText = "Maximum numbers of concurrent in-flight send operations (default 1)")] 56 | public int CfgMaxInflightSends { get { return MaxInflightSends.Value; } set { MaxInflightSends = new Observable(value); } } 57 | 58 | public Observable MaxInflightSends { get; internal set; } = new Observable(1); 59 | 60 | [Option('j', "inflight-receives", Required = false, HelpText = "Maximum number of concurrent in-flight receive operations per receiver (default 1)")] 61 | public int CfgMaxInflightReceives { get { return MaxInflightReceives.Value; } set { MaxInflightReceives = new Observable(value); } } 62 | public Observable MaxInflightReceives { get; internal set; } = new Observable(1); 63 | 64 | [Option('v', "receive-batch-count", Required = false, HelpText = "Max number of messages per batch (default 0, no batching)")] 65 | public int ReceiveBatchCount { get; private set; } = 0; 66 | 67 | [Option('w', "receive-work-duration", Required = false, HelpText = "Work simulation delay between receive and completion (milliseconds, default 0, no work)")] 68 | public int WorkDuration { get; private set; } = 0; 69 | 70 | public void PrintSettings() 71 | { 72 | Console.WriteLine("Settings:"); 73 | Console.WriteLine("{0}: {1}", "ReceivePaths", string.Join(",", this.ReceivePaths)); 74 | Console.WriteLine("{0}: {1}", "SendPaths", this.SendPath); 75 | Console.WriteLine("{0}: {1}", "MessageCount", this.MessageCount); 76 | Console.WriteLine("{0}: {1}", "MessageSizeInBytes", this.MessageSizeInBytes); 77 | Console.WriteLine("{0}: {1}", "SenderCount", this.SenderCount); 78 | Console.WriteLine("{0}: {1}", "SendBatchCount", this.SendBatchCount); 79 | Console.WriteLine("{0}: {1}", "MaxInflightSends", this.CfgMaxInflightSends); 80 | Console.WriteLine("{0}: {1}", "ReceiveMode", this.ReceiveMode); 81 | Console.WriteLine("{0}: {1}", "ReceiverCount", this.ReceiverCount); 82 | Console.WriteLine("{0}: {1}", "ReceiveBatchCount", this.ReceiveBatchCount); 83 | Console.WriteLine("{0}: {1}", "ReceiveMode", this.ReceiveMode); 84 | Console.WriteLine("{0}: {1}", "MaxInflightReceives", this.CfgMaxInflightReceives); 85 | Console.WriteLine("{0}: {1}", "MetricsDisplayFrequency", this.MetricsDisplayFrequency); 86 | Console.WriteLine("{0}: {1}", "WorkDuration", this.WorkDuration); 87 | 88 | Console.WriteLine(); 89 | } 90 | 91 | [Usage()] 92 | public static IEnumerable Examples 93 | { 94 | get 95 | { 96 | yield return new Example("queue scenario", new Settings { ConnectionString = "{Connection-String-with-EntityPath}" }); 97 | yield return new Example("topic scenario", new Settings { ConnectionString = "{Connection-String}", SendPath = "{Topic-Name}", ReceivePaths = new string[] { "{Topic-Name}/subscriptions/{Subscription-Name-1}", "{Topic-Name}/subscriptions/{Subscription-Name-2}" } }); 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /ThroughputTest_v2/ServiceBusThroughputTestLib/Sender.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ServiceBusThroughputTestLib 10 | { 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Diagnostics; 14 | using System.Text; 15 | using System.Threading; 16 | using System.Threading.Tasks; 17 | using Azure.Messaging.ServiceBus; 18 | using Extensions; 19 | using ServiceBusThroughputTest.Common; 20 | 21 | public class Sender 22 | { 23 | string connectionString; 24 | string queueName; 25 | 26 | int payloadSize = 600; 27 | int callIntervalMS = 0; 28 | int batchSize = 1; 29 | int sendersCount = 1; 30 | int concurrentCalls = 10; 31 | ILogger logger = null; 32 | 33 | public Sender(string connectionString, string queueName, int payloadSize, int batchSize, int callIntervalMS, int concurrentCalls, int sendersCount, ILogger logger = null) 34 | { 35 | this.connectionString = connectionString; 36 | this.queueName = queueName; 37 | this.payloadSize = payloadSize; 38 | this.batchSize = batchSize; 39 | this.callIntervalMS = callIntervalMS; 40 | this.concurrentCalls = concurrentCalls; 41 | this.sendersCount = sendersCount; 42 | this.logger = logger; 43 | } 44 | 45 | public async Task Run(CancellationToken cancellationToken) 46 | { 47 | ServiceBusClient[] clients = new ServiceBusClient[this.sendersCount]; 48 | try 49 | { 50 | Task[] sendTasks = new Task[this.sendersCount]; 51 | var senders = new ServiceBusSender[this.sendersCount]; 52 | 53 | for (var i = 0; i < this.sendersCount; i++) 54 | { 55 | clients[i] = new ServiceBusClient(connectionString); 56 | 57 | senders[i] = clients[i].CreateSender(queueName); 58 | sendTasks[i] = Start(senders[i], cancellationToken); 59 | } 60 | 61 | await Task.WhenAll(sendTasks); 62 | } 63 | catch (Exception e) 64 | { 65 | logger?.AddTrace(e.ToString()); 66 | } 67 | finally 68 | { 69 | Parallel.ForEach(clients, async client => 70 | { 71 | await client.DisposeAsync(); 72 | }); 73 | } 74 | 75 | if (logger != null) 76 | { 77 | logger.Dispose(); 78 | logger = null; 79 | } 80 | } 81 | 82 | async Task Start(ServiceBusSender sender, CancellationToken cancellationToken) 83 | { 84 | // Create payload 85 | byte[] payload = UTF8Encoding.ASCII.GetBytes(new string('a', payloadSize)); 86 | ServiceBusMessage msg = new ServiceBusMessage(payload); 87 | var batch = new List(); 88 | for (var i = 0; i < this.batchSize; i++) 89 | { 90 | batch.Add(msg); 91 | } 92 | 93 | var semaphore = new SemaphoreSlim(this.concurrentCalls); 94 | var sw = Stopwatch.StartNew(); 95 | 96 | // Loop until cancelled 97 | while (!cancellationToken.IsCancellationRequested) 98 | { 99 | try 100 | { 101 | await semaphore.WaitAsync().ConfigureAwait(false); 102 | 103 | var sessionId = logger?.StartRecordTime(); 104 | 105 | Fork(sender.SendMessagesAsync(batch, cancellationToken).ContinueWith( 106 | async (t) => 107 | { 108 | double timeSpentMS_d = 0.0; 109 | 110 | if (sessionId != null) 111 | { 112 | timeSpentMS_d = logger.StopRecordTimeAndGetElapsedMs(sessionId.Value); 113 | } 114 | 115 | if (t.IsFaulted) 116 | { 117 | semaphore.Release(); 118 | 119 | t.Exception.HandleExceptions(logger, sessionId, cancellationToken); 120 | } 121 | else 122 | { 123 | // Delay upto time interval if needed 124 | if (callIntervalMS > 0) 125 | { 126 | if (timeSpentMS_d < callIntervalMS - 1) 127 | { 128 | await Task.Delay(callIntervalMS - (int)timeSpentMS_d - 1); 129 | } 130 | } 131 | 132 | semaphore.Release(); 133 | 134 | if (sessionId != null) 135 | { 136 | logger.IncrementActionCount(sessionId.Value); 137 | logger.IncrementMetricValueBy(sessionId.Value, batchSize); 138 | logger.DisposeSession(sessionId.Value); 139 | } 140 | } 141 | 142 | })); 143 | } 144 | catch (Exception e) 145 | { 146 | logger?.AddTrace(e.ToString()); 147 | } 148 | finally 149 | { 150 | } 151 | } 152 | 153 | await sender.DisposeAsync(); 154 | } 155 | 156 | static void Fork(Task t) 157 | { 158 | t.ContinueWith((_) => { }); 159 | } 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /ThroughputTest/PerformanceApp.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ThroughputTest 10 | { 11 | using LinqStatistics; 12 | using System; 13 | using System.Collections.Generic; 14 | using System.Diagnostics; 15 | using System.Linq; 16 | using System.Reactive.Linq; 17 | using System.Threading; 18 | using System.Threading.Tasks; 19 | 20 | sealed class ServiceBusPerformanceApp 21 | { 22 | readonly Settings settings; 23 | readonly Metrics metrics; 24 | readonly CancellationTokenSource cancellationTokenSource; 25 | readonly List tasks; 26 | private IDisposable sendMetrics; 27 | private IDisposable receiveMetrics; 28 | 29 | public ServiceBusPerformanceApp(Settings settings, Metrics metrics) 30 | { 31 | this.settings = settings; 32 | this.metrics = metrics; 33 | this.cancellationTokenSource = new CancellationTokenSource(); 34 | this.tasks = new List(); 35 | } 36 | 37 | 38 | public async Task Run(params Experiment[] experiments) 39 | { 40 | this.settings.PrintSettings(); 41 | 42 | tasks.Add(new ReceiverTask(settings, this.metrics, this.cancellationTokenSource.Token)); 43 | tasks.Add(new SenderTask(settings, this.metrics, this.cancellationTokenSource.Token)); 44 | 45 | Console.WriteLine("Starting..."); 46 | Console.WriteLine(); 47 | 48 | long sendTotal = 0, receiveTotal = 0; 49 | int windowLengthSecs = (int)this.settings.MetricsDisplayFrequency*2; 50 | if (this.settings.SenderCount > 0) 51 | { 52 | Console.ForegroundColor = ConsoleColor.Yellow; 53 | Console.Write("S|{0,10:0.00}|{1,10:0.00}|{2,5}|{3,5}|", "pstart", "pend", "sbc", "mifs"); 54 | Console.Write("{0,10:0.00}|{1,10:0.00}|{2,10:0.00}|{3,10:0.00}|{4,10:0.00}|", "snd.avg", "snd.med", "snd.dev", "snd.min", "snd.max"); 55 | Console.Write("{0,10:0.00}|{1,10:0.00}|{2,10:0.00}|{3,10:0.00}|{4,10:0.00}|", "gld.avg", "gld.med", "gld.dev", "gld.min", "gld.max"); 56 | Console.WriteLine("{0,10}|{1,10}|{2,10}|{3,10}|{4,10}|{5,10}|", "msg/s", "total", "sndop", "errs", "busy", "overall"); 57 | this.sendMetrics = ((IObservable)metrics) 58 | .Buffer(TimeSpan.FromSeconds(windowLengthSecs), TimeSpan.FromSeconds(windowLengthSecs/2)) 59 | .Subscribe((list) => 60 | { 61 | if (list.Count == 0) 62 | return; 63 | lock (Console.Out) 64 | { 65 | Console.ForegroundColor = ConsoleColor.Yellow; 66 | Console.Write("S|{0,10}|{1,10}|{2,5}|{3,5}|", list.First().Tick / Stopwatch.Frequency+1, list.Last().Tick / Stopwatch.Frequency+1, this.settings.SendBatchCount, this.settings.MaxInflightSends.Value); 67 | WriteStat(list, i => i.SendDuration100ns, Stopwatch.Frequency / 1000.0); 68 | WriteStat(list, i => i.GateLockDuration100ns, Stopwatch.Frequency / 10000.0); 69 | var msgs = list.Sum(i => i.Messages); 70 | sendTotal += msgs; 71 | Console.WriteLine("{0,10:0.00}|{1,10}|{2,10}|{3,10}|{4,10}|{5,10}|", list.Sum(i => i.Messages) / (double)windowLengthSecs, msgs, list.Sum(i => i.Sends), list.Sum(i => i.Errors), list.Sum(i => i.BusyErrors), sendTotal); 72 | } 73 | }); 74 | } 75 | if (this.settings.ReceiverCount > 0) 76 | { 77 | Console.ForegroundColor = ConsoleColor.Cyan; 78 | Console.Write("R|{0,10:0.00}|{1,10:0.00}|{2,5}|{3,5}|", "pstart", "pend", "rbc", "mifr"); 79 | Console.Write("{0,10:0.00}|{1,10:0.00}|{2,10:0.00}|{3,10:0.00}|{4,10:0.00}|", "rcv.avg", "rcv.med", "rcv.dev", "rcv.min", "rcv.max"); 80 | Console.Write("{0,10:0.00}|{1,10:0.00}|{2,10:0.00}|{3,10:0.00}|{4,10:0.00}|", "cpl.avg", "cpl.med", "cpl.dev", "cpl.min", "cpl.max"); 81 | Console.WriteLine("{0,10}|{1,10}|{2,10}|{3,10}|{4,10}|{5,10}|", "msg/s", "total", "rcvop", "errs", "busy", "overall"); 82 | 83 | this.receiveMetrics = ((IObservable)metrics) 84 | .Buffer(TimeSpan.FromSeconds(windowLengthSecs), TimeSpan.FromSeconds(windowLengthSecs/2)) 85 | .Subscribe((list) => 86 | { 87 | if (list.Count == 0) 88 | return; 89 | lock (Console.Out) 90 | { 91 | Console.ForegroundColor = ConsoleColor.Cyan; 92 | Console.Write("R|{0,10}|{1,10}|{2,5}|{3,5}|", list.First().Tick / Stopwatch.Frequency+1, list.Last().Tick / Stopwatch.Frequency+1, this.settings.ReceiveBatchCount, this.settings.MaxInflightReceives.Value); 93 | WriteStat(list, i => i.ReceiveDuration100ns, Stopwatch.Frequency / 1000.0); 94 | WriteStat(list, i => i.CompleteDuration100ns, Stopwatch.Frequency / 1000.0); 95 | var msgs = list.Sum(i => i.Messages); 96 | receiveTotal += msgs; 97 | Console.WriteLine("{0,10:0.00}|{1,10}|{2,10}|{3,10}|{4,10}|{5,10}|", list.Sum(i => i.Messages) / (double)windowLengthSecs, msgs, list.Sum(i => i.Receives), list.Sum(i => i.Errors), list.Sum(i => i.BusyErrors), receiveTotal); 98 | } 99 | }); 100 | } 101 | 102 | Task runTasks = tasks.ParallelForEachAsync((t) => t.StartAsync()); 103 | if (experiments != null && experiments.Length > 0) 104 | { 105 | var experimentTask = RunExperiments(experiments, this.cancellationTokenSource.Token); 106 | await Task.WhenAll(new Task[] { experimentTask, runTasks }); 107 | } 108 | else 109 | { 110 | await runTasks; 111 | } 112 | this.cancellationTokenSource.Cancel(); 113 | this.tasks.ForEach((t) => t.Close()); 114 | Console.ForegroundColor = ConsoleColor.White; 115 | } 116 | 117 | private Task RunExperiments(IEnumerable experiments, CancellationToken ct) 118 | { 119 | return Task.Run(async () => 120 | { 121 | foreach (var experiment in experiments) 122 | { 123 | await Task.Delay(TimeSpan.FromSeconds(10)).ContinueWith((t) => StartExperiment(experiment)).ConfigureAwait(false); 124 | } 125 | }); 126 | } 127 | 128 | private Task StartExperiment(Experiment experiment) 129 | { 130 | return experiment.Run().ContinueWith(t => 131 | { 132 | if (t.Result != null) 133 | { 134 | return Task.Delay(TimeSpan.FromSeconds(10)).ContinueWith(t1 => StartExperiment(t.Result)); 135 | } 136 | return Task.CompletedTask; 137 | }); 138 | } 139 | 140 | void WriteStat(IList list, Func f, double scale) 141 | { 142 | if (list.Count > 1) 143 | { 144 | Console.Write("{0,10:0.00}|{1,10:0.00}|{2,10:0.00}|{3,10:0.00}|{4,10:0.00}|", list.Average(f) / scale, list.Median(f) / scale, list.StandardDeviationP(f) / scale, list.Min(f) / scale, list.Max(f) / scale); 145 | } 146 | } 147 | 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /ThroughputTest/SenderTask.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ThroughputTest 10 | { 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Diagnostics; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | using System.Net.Sockets; 17 | using Azure.Messaging.ServiceBus; 18 | 19 | sealed class SenderTask : PerformanceTask 20 | { 21 | readonly List senders; 22 | 23 | public SenderTask(Settings settings, Metrics metrics, CancellationToken cancellationToken) 24 | : base(settings, metrics, cancellationToken) 25 | { 26 | this.senders = new List(); 27 | } 28 | 29 | protected override Task OnOpenAsync() 30 | { 31 | return Task.CompletedTask; 32 | } 33 | 34 | protected override Task OnStartAsync() 35 | { 36 | for (int i = 0; i < this.Settings.SenderCount; i++) 37 | { 38 | this.senders.Add(Task.Run(SendTask)); 39 | } 40 | return Task.WhenAll(senders); 41 | } 42 | 43 | async Task SendTask() 44 | { 45 | var client = new ServiceBusClient(this.Settings.ConnectionString); 46 | ServiceBusSender sender = client.CreateSender(this.Settings.SendPath); 47 | var payload = new byte[this.Settings.MessageSizeInBytes]; 48 | var semaphore = new DynamicSemaphoreSlim(this.Settings.MaxInflightSends.Value); 49 | var done = new SemaphoreSlim(1); 50 | done.Wait(); 51 | long totalSends = 0; 52 | 53 | this.Settings.MaxInflightSends.Changing += (a, e) => AdjustSemaphore(e, semaphore); 54 | var sw = Stopwatch.StartNew(); 55 | 56 | // first send will fail out if the cxn string is bad 57 | await sender.SendMessageAsync(new ServiceBusMessage(payload) { TimeToLive = TimeSpan.FromMinutes(5) }); 58 | 59 | for (int j = 0; j < Settings.MessageCount && !this.CancellationToken.IsCancellationRequested; j++) 60 | { 61 | var sendMetrics = new SendMetrics() { Tick = sw.ElapsedTicks }; 62 | 63 | var nsec = sw.ElapsedTicks; 64 | semaphore.Wait(); 65 | //await semaphore.WaitAsync().ConfigureAwait(false); 66 | sendMetrics.InflightSends = this.Settings.MaxInflightSends.Value - semaphore.CurrentCount; 67 | sendMetrics.GateLockDuration100ns = sw.ElapsedTicks - nsec; 68 | 69 | if (Settings.SendDelay > 0) 70 | { 71 | await Task.Delay(Settings.SendDelay); 72 | } 73 | if (Settings.SendBatchCount <= 1) 74 | { 75 | sender.SendMessageAsync(new ServiceBusMessage(payload) { TimeToLive = TimeSpan.FromMinutes(5) }) 76 | .ContinueWith(async (t) => 77 | { 78 | if (t.IsFaulted || t.IsCanceled) 79 | { 80 | await HandleExceptions(semaphore, sendMetrics, t.Exception); 81 | } 82 | else 83 | { 84 | sendMetrics.SendDuration100ns = sw.ElapsedTicks - nsec; 85 | sendMetrics.Sends = 1; 86 | sendMetrics.Messages = 1; 87 | semaphore.Release(); 88 | Metrics.PushSendMetrics(sendMetrics); 89 | } 90 | if (Interlocked.Increment(ref totalSends) >= Settings.MessageCount) 91 | { 92 | done.Release(); 93 | } 94 | }).Fork(); 95 | } 96 | else 97 | { 98 | var batch = new List(); 99 | for (int i = 0; i < Settings.SendBatchCount && j < Settings.MessageCount && !this.CancellationToken.IsCancellationRequested; i++, j++) 100 | { 101 | batch.Add(new ServiceBusMessage(payload) { TimeToLive = TimeSpan.FromMinutes(5) }); 102 | } 103 | sender.SendMessagesAsync(batch) 104 | .ContinueWith(async (t) => 105 | { 106 | if (t.IsFaulted || t.IsCanceled) 107 | { 108 | await HandleExceptions(semaphore, sendMetrics, t.Exception); 109 | } 110 | else 111 | { 112 | sendMetrics.SendDuration100ns = sw.ElapsedTicks - nsec; 113 | sendMetrics.Sends = 1; 114 | sendMetrics.Messages = Settings.SendBatchCount; 115 | semaphore.Release(); 116 | Metrics.PushSendMetrics(sendMetrics); 117 | } 118 | if (Interlocked.Increment(ref totalSends) >= Settings.MessageCount) 119 | { 120 | done.Release(); 121 | } 122 | }).Fork(); 123 | } 124 | } 125 | await done.WaitAsync(); 126 | } 127 | 128 | static void AdjustSemaphore(Observable.ChangingEventArgs e, DynamicSemaphoreSlim semaphore) 129 | { 130 | if (e.NewValue > e.OldValue) 131 | { 132 | for (int i = e.OldValue; i < e.NewValue; i++) 133 | { 134 | semaphore.Grant(); 135 | } 136 | } 137 | else 138 | { 139 | for (int i = e.NewValue; i < e.OldValue; i++) 140 | { 141 | semaphore.Revoke(); 142 | } 143 | } 144 | } 145 | 146 | private async Task HandleExceptions(DynamicSemaphoreSlim semaphore, SendMetrics sendMetrics, AggregateException ex) 147 | { 148 | bool wait = false; 149 | ex.Handle((x) => 150 | { 151 | if (x is ServiceBusException sbException) 152 | { 153 | if (sbException.Reason == ServiceBusFailureReason.ServiceCommunicationProblem) 154 | { 155 | if (sbException.InnerException is SocketException socketException && 156 | socketException.SocketErrorCode == SocketError.HostNotFound) 157 | { 158 | return false; 159 | } 160 | } 161 | if (sbException.Reason == ServiceBusFailureReason.ServiceBusy) 162 | { 163 | sendMetrics.BusyErrors = 1; 164 | if (!this.CancellationToken.IsCancellationRequested) 165 | { 166 | wait = true; 167 | } 168 | } 169 | else 170 | { 171 | sendMetrics.Errors = 1; 172 | } 173 | } 174 | return true; 175 | }); 176 | 177 | if (wait) 178 | { 179 | await Task.Delay(3000, this.CancellationToken).ConfigureAwait(false); 180 | } 181 | semaphore.Release(); 182 | Metrics.PushSendMetrics(sendMetrics); 183 | 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /ThroughputTest_v2/README.md: -------------------------------------------------------------------------------- 1 | # Azure Service Bus Throughput Performance Test 2 | 3 | In the v2 version, we have simplified and done some refactoring of the original console app. The gut of the app is still exactly same as before. 4 | We now have two common libraries that the console app depends on. This allows the core libraries to be hosted not only in console (as in the v2 version in the repo) but also 5 | in containerized environments such as Service Fabric for automated scaling and generating/simulating higher throughput/load scenarios 6 | (without the need of having to spin up vms manually). 7 | 8 | The console app now also prints out P99 metric which depending on the scenario is sometimes more important than average. 9 | 10 | This version is tailored more for windows and azure containerized environments so if you wish to use it on Linux, v1 is still a better option. 11 | 12 | ## Building the Tool 13 | 14 | The tool is a .NET framework project that can produce standalone executable for Windows. 15 | 16 | To build, use MSBUILD to run following commands: 17 | 18 | ``` 19 | MSBUILD /t:restore 20 | MSBUILD \ServiceBusThroughputTest.sln /property:Configuration=Release 21 | ``` 22 | The output application can be found in the ```ThroughpurTest_v2\ServiceBusThroughputTest\bin\Release```subdirectory. 23 | 24 | ## Running the Tool 25 | 26 | You can run the tool locally from your own machine or you can run it from within an Azure VM. 27 | 28 | The only required command line arguments are a connection string and a send path. If the connection string includes 29 | an EntityPath property for a queue, the send path can be omitted. If the receive path is not given, the tool assumes 30 | the send path and the receive path to be the same, which is sufficient for queues. For topics, the subscription(s) must 31 | be given with explicit receive path options. 32 | 33 | You can test any valid Service Bus topology with this tool, including Topics with subscriptions and chained 34 | entities with auto-forward set up between them. Therefore, the send path and the receive path do not have to be 35 | on the same entity. You can also receive from dead-letter queues. 36 | 37 | | Scenario | Arguments | 38 | |------------------------------|-------------------------------------------------------------------------------| 39 | | Send to and receive from a queue |```ThroughputTest -C {connection-string} -S myQueueName ``` | 40 | | Send to a topic and receive from a subscription | ``` ThroughputTest -C {connection-string} -S myTopicName -R myTopicName/subscriptions/mySubName ``` | 41 | | Send a queue |```ThroughputTest -C {connection-string} -S myQueueName -r 0 ``` | 42 | | Receive from a queue |```ThroughputTest -C {connection-string} -S myQueueName -s 0 ``` | 43 | 44 | ## Output 45 | 46 | The tool prints out interleaved statistics for sends and receives. Send information is prefixed with S (and in yellow), 47 | receive information is prefixed with R and printed in cyan. The columns are separated with the pipe symbol and therefore 48 | parseable. 49 | 50 | ### Send output columns 51 | 52 | | Column | Description 53 | |----------|--------------------------------------------------------------------------- 54 | | pstart | Begin of the data recording for this row (seconds from start of run) 55 | | pend | End of data recording for this row 56 | | sbc | Send batch count 57 | | mifs | Max inflight sends 58 | | snd.avg | Average send duration (to acknowledgement receipt) in milliseconds 59 | | snd.med | Median send duration 60 | | snd.dev | Standard deviation for send duration 61 | | snd.min | Minimum send duration 62 | | snd.max | Maximum send duration 63 | | gld.avg | Average gate lock duration in milliseconds. This measure tracks whether the internal async thread pool queue gets backed up. If this value shoots up, you should reduce the number of concurrent inflight sends, because the application sends more than what can be put on the wire. 64 | | gld.med | Median gate lock duration 65 | | gld.dev | Standard deviation for gate lock duration 66 | | gld.min | Minimum gate lock duration 67 | | gld.max | Maximum gate lock duration 68 | | msg/s | Throughput in messages per second 69 | | total | Total messages sent in this period 70 | | sndop | Total send operations in this period 71 | | errs | Errors 72 | | busy | Busy errors 73 | | overall | Total messages sent in this run 74 | 75 | ### Receive output columns 76 | 77 | | Column | Description 78 | |----------|--------------------------------------------------------------------------- 79 | | pstart | Begin of the data recording for this row (seconds from start of run) 80 | | pend | End of data recording for this row 81 | | rbc | Receive batch count 82 | | mifr | Max inflight receives 83 | | rcv.avg | Average receive duration (to message receipt) in milliseconds 84 | | rcv.med | Median receive duration 85 | | rcv.dev | Standard deviation for receive duration 86 | | rcv.min | Minimum receive duration 87 | | rcv.max | Maximum receive duration 88 | | cpl.avg | Average completion duration in milliseconds. 89 | | cpl.med | Median completion duration 90 | | cpl.dev | Standard deviation for completion duration 91 | | cpl.min | Minimum completion duration 92 | | cpl.max | Maximum completion duration 93 | | msg/s | Throughput in messages per second 94 | | total | Total messages received in this period 95 | | rcvop | Total receive operations in this period 96 | | errs | Errors 97 | | busy | Busy errors 98 | | overall | Total messages sent in this run 99 | 100 | 101 | ## Options 102 | 103 | When only called with a connection string and send/receive paths, the tool will concurrently send and receive messages 104 | to/from the chosen Service Bus entity. 105 | 106 | If you just want to send messages, set the receiver count to zero with **-r 0**. If you only want to receive messages, 107 | set the sender count to zero with **-s 0**. 108 | 109 | The biggest throughput boosts yield the enabling of send and receive batching, meaning the sending and receiving of 110 | several messages in one operation. This oprtion will maximize throughput and show you the practical limits, but you 111 | should always keep an eye on whether batching is a practical approach for your specific solution. 112 | 113 | The "inflight-sends" and "inflight-receives" options also have very significant throughput impact. "inflight-sends" 114 | tracks how many messages are being sent asynchronously and in a pipelined fashion while waiting for the operations 115 | to complete. The "inflight-receives" option controls how many messages are being received and processed concurrently. 116 | 117 | The further options are listed below: 118 | 119 | | Parameter | Description 120 | |-------------------------------|------------------------------------------------------------------------------- 121 | | **-C, --connection-string** | **Required**. Connection string 122 | | -S, --send-path | Send path. Queue or topic name, unless set in connection string EntityPath. 123 | | -R, --receive-paths | Receive paths. Mandatory for receiving from topic subscriptions. Must be {topic}/subscriptions/{subscription-name} or {queue-name} 124 | | -n, --number-of-messages | Number of messages to send (default 1000000) 125 | | -b, --message-size-bytes | Bytes per message (default 1024) 126 | | -f, --frequency-metrics | Frequency of metrics display (seconds, default 10s) 127 | | -m, --receive-mode | Receive mode.'PeekLock' (default) or 'ReceiveAndDelete' 128 | | -r, --receiver-count | Number of concurrent receivers (default 1) 129 | | -e, --prefetch-count | Prefetch count (default 0) 130 | | -t, --send-batch-count | Number of messages per batch (default 0, no batching) 131 | | -s, --sender-count | Number of concurrent senders (default 1) 132 | | -d, --send-delay | Delay between sends of any sender (milliseconds, default 0) 133 | | -i, --inflight-sends | Maximum numbers of concurrent in-flight send operations (default 1) 134 | | -j, --inflight-receives | Maximum number of concurrent in-flight receive operations per receiver (default 1) 135 | | -v, --receive-batch-count | Max number of messages per batch (default 0, no batching) 136 | | -w, --receive-work-duration | Work simulation delay between receive and completion (milliseconds, default 0, no work) 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /ThroughputTest_v2/ServiceBusThroughputTestLib/Receiver.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ServiceBusThroughputTestLib 10 | { 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Diagnostics; 14 | using System.Linq; 15 | using System.Threading; 16 | using System.Threading.Tasks; 17 | using Azure.Messaging.ServiceBus; 18 | using ServiceBusThroughputTest.Common; 19 | using ServiceBusThroughputTestLib.Extensions; 20 | 21 | public class Receiver 22 | { 23 | string connectionString; 24 | string queueName; 25 | int prefetchCount = 0; 26 | int batchSize = 1; 27 | int receiversCount = 1; 28 | int concurrentCalls = 1; 29 | ILogger logger = null; 30 | int callIntervalMS = 1000; 31 | bool isReceiveAndDelete = false; 32 | 33 | public Receiver(string connectionString, string queueName, int prefetchCount, int batchSize, int concurrentCalls, int receiversCount, int callIntervalMS, bool isReceiveAndDelete = false, ILogger logger = null) 34 | { 35 | this.connectionString = connectionString; 36 | this.queueName = queueName; 37 | this.prefetchCount = prefetchCount; 38 | this.concurrentCalls = concurrentCalls; 39 | this.receiversCount = receiversCount; 40 | this.batchSize = batchSize; 41 | this.callIntervalMS = callIntervalMS; 42 | this.isReceiveAndDelete = isReceiveAndDelete; 43 | this.logger = logger; 44 | } 45 | 46 | public async Task Run(CancellationToken cancellationToken) 47 | { 48 | ServiceBusClient[] clients = new ServiceBusClient[this.receiversCount]; 49 | 50 | try 51 | { 52 | ServiceBusReceiveMode receiveMode = this.isReceiveAndDelete ? ServiceBusReceiveMode.ReceiveAndDelete : ServiceBusReceiveMode.PeekLock; 53 | 54 | // create the options to use for configuring the processor 55 | var options = new ServiceBusReceiverOptions 56 | { 57 | // By default or when AutoCompleteMessages is set to true, the processor will complete the message after executing the message handler 58 | // Set AutoCompleteMessages to false to [settle messages](https://docs.microsoft.com/en-us/azure/service-bus-messaging/message-transfers-locks-settlement#peeklock) on your own. 59 | // In both cases, if the message handler throws an exception without settling the message, the processor will abandon the message. 60 | ReceiveMode = receiveMode, 61 | PrefetchCount = prefetchCount, 62 | }; 63 | 64 | Task[] receiveTasks = new Task[this.receiversCount]; 65 | 66 | for (var i = 0; i < this.receiversCount; i++) 67 | { 68 | clients[i] = new ServiceBusClient(connectionString); 69 | 70 | ServiceBusReceiver receiver; 71 | if (queueName.Contains(":")) 72 | { 73 | var topic = queueName.Split(':')[0]; 74 | var sub = queueName.Split(':')[1]; 75 | receiver = clients[i].CreateReceiver(topic, sub, options); 76 | } 77 | else 78 | { 79 | receiver = clients[i].CreateReceiver(queueName, options); 80 | } 81 | 82 | receiveTasks[i] = Start(receiver, cancellationToken); 83 | } 84 | 85 | await Task.WhenAll(receiveTasks); 86 | } 87 | catch (Exception e) 88 | { 89 | logger?.AddTrace(e.ToString()); 90 | } 91 | finally 92 | { 93 | //Parallel.ForEach(clients, async client => 94 | //{ 95 | // await client.DisposeAsync(); 96 | //}); 97 | } 98 | 99 | if (logger != null) 100 | { 101 | logger.AddTrace($"cancellationToken.IsCancellationRequested={cancellationToken.IsCancellationRequested}"); 102 | logger.Dispose(); 103 | logger = null; 104 | } 105 | } 106 | 107 | async Task Start(ServiceBusReceiver receiver, CancellationToken cancellationToken) 108 | { 109 | var semaphore = new SemaphoreSlim(this.concurrentCalls); 110 | var sw = Stopwatch.StartNew(); 111 | 112 | while (!cancellationToken.IsCancellationRequested) 113 | { 114 | try 115 | { 116 | await semaphore.WaitAsync().ConfigureAwait(false); 117 | 118 | var sessionId = logger?.StartRecordTime(); 119 | 120 | Fork(receiver.ReceiveMessagesAsync(this.batchSize, TimeSpan.FromMilliseconds(this.callIntervalMS)).ContinueWith( 121 | async (t) => 122 | { 123 | if (sessionId != null) 124 | { 125 | logger.StopRecordTimeAndGetElapsedMs(sessionId.Value); 126 | } 127 | 128 | if (!t.IsFaulted && !this.isReceiveAndDelete && t.Result?.Any() == true) 129 | { 130 | // following function will release semaphore so no need to do it here 131 | await CompleteMessagesAndReleaseSemaphore(t.Result, receiver, semaphore, cancellationToken, logger, sessionId); 132 | } 133 | else 134 | { 135 | semaphore.Release(); 136 | 137 | if (t.IsFaulted) 138 | { 139 | t.Exception.HandleExceptions(logger, sessionId, cancellationToken); 140 | } 141 | else if (sessionId != null) 142 | { 143 | logger.IncrementActionCount(sessionId.Value); 144 | logger.IncrementMetricValueBy(sessionId.Value, t.Result?.Count ?? 0); 145 | logger.DisposeSession(sessionId.Value); 146 | } 147 | } 148 | })); 149 | } 150 | catch (Exception ex) 151 | { 152 | logger?.AddTrace(ex.ToString()); 153 | } 154 | finally 155 | { 156 | } 157 | } 158 | 159 | await receiver.CloseAsync(); 160 | } 161 | 162 | static async Task CompleteMessagesAndReleaseSemaphore(IReadOnlyList messages, ServiceBusReceiver receiver, SemaphoreSlim semaphore, CancellationToken cancellationToken, ILogger logger = null, Guid? sessionId = null) 163 | { 164 | try 165 | { 166 | var completeTasks = new Task[messages.Count]; 167 | 168 | if (sessionId != null && logger != null) 169 | { 170 | logger.StartCompletionRecordTime(sessionId.Value); 171 | } 172 | 173 | for (var i = 0; i < messages.Count; i++) 174 | { 175 | completeTasks[i] = receiver.CompleteMessageAsync(messages[i]); 176 | } 177 | 178 | await Task.WhenAll(completeTasks).ContinueWith(t => 179 | { 180 | if (sessionId != null) 181 | { 182 | logger?.StopCompletionRecordTimeAndGetElapsedMs(sessionId.Value); 183 | } 184 | 185 | semaphore.Release(); 186 | 187 | if (t.IsFaulted) 188 | { 189 | t.Exception.HandleExceptions(logger, sessionId, cancellationToken); 190 | } 191 | else 192 | { 193 | if (sessionId != null) 194 | { 195 | logger.IncrementActionCount(sessionId.Value); 196 | logger.IncrementMetricValueBy(sessionId.Value, messages.Count); 197 | logger.DisposeSession(sessionId.Value); 198 | } 199 | } 200 | }); 201 | } 202 | catch (Exception e) 203 | { 204 | semaphore.Release(); 205 | 206 | logger?.AddTrace($"Error completing message/s - {e}"); 207 | } 208 | } 209 | 210 | static void Fork(Task t) 211 | { 212 | t.ContinueWith((_) => { }); 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /ThroughputTest/README.md: -------------------------------------------------------------------------------- 1 | # Azure Service Bus Throughput Performance Test 2 | 3 | This code sample illustrates the available throughput performance options for Azure Service Bus clients 4 | and also allows for running experiments that dynamically adjust some tuning parameters. 5 | 6 | > This tool is meant to be used with Azure Service Bus Premium and not with Azure Service Bus Standard. 7 | > Azure Service Bus Premium is designed to provide predictable performance, meaning that the results 8 | > you measure with this tool are representative of the performance you can expect for your applications. 9 | 10 | The sample assumes that you are generally familiar with the Service Bus .NET SDK and with how to set up 11 | namespaces and entities within those namespaces. 12 | 13 | The sample can either send and receive messages from a single instance, or just act as either sender or receiver, 14 | allowing simulation of different scenarios. 15 | 16 | The sending side supports sending messages singly or in batches, and it supports pipelining of send operations 17 | whereby up to a certain number of messages are kept in flight and their acknowledgement is handled asynchronously. 18 | You can start one or multiple concurrent senders, and each sender can be throttled by imposing a pause between 19 | individual send operations. 20 | 21 | The receive side supports the "ReceiveAndDelete" and "PeekLock" receive modes, one or multiple concurrent 22 | receive loops, single or batch receive operations, and single or batch completion. You can simulate having 23 | multiple concurrent handlers on a receiver and you can also impose a delay to simulate work. 24 | 25 | ## What to expect 26 | 27 | This sample is specifically designed to test throughput limits of queues and topics/subscriptions in Service Bus 28 | namespaces. It does not measure end-to-end latency, meaning how fast messages can pass through Service Bus under 29 | optimal conditions. The goals of achieving maximum throughput and the lowest end-to-end latency are fundamentally 30 | at odds, as you might know first-hand from driving a car. Either you can go fast on a street with light traffic, or 31 | the street can handle a maximum capacity of cars at the same time, but at the cost of individual speed. 32 | The goal of this sample is to find out where the capacity limits are. 33 | 34 | As discussed in the [product documentation](https://docs.microsoft.com/azure/service-bus-messaging/message-transfers-locks-settlement), 35 | network latency has very significant impact on the achievable throughput. If you are running this sample from your 36 | development workstation, throughput will be substantially lower than from within an Azure VM. If you want to 37 | test limits for a scenario where the Service Bus client will reside inside Azure, you should also run this test 38 | inside Azure on a Windows or Linux VM that resides within the same region as the Service Bus namespace you 39 | want to test. 40 | 41 | In an in-region setup, and with ideal parameters, you can achieve send rates exceeding 20000 msg/sec at 1024 bytes per message 42 | from a single client, meaning that a 1GB queue will fill up in under a minute. Also be aware that receive operations are 43 | generally more costly and therefore slower than send operations, which means that a test with maximum send 44 | pressure (several senders using batching) may not be sustainable for longer periods because the receivers might not be 45 | able to keep up. 46 | 47 | ## Building the Tool 48 | 49 | The tool is a .NET Core project that can produce standalone executables for Linux and Windows, whereby we'll assume x64 targets. 50 | 51 | For Linux, with the .NET Core 3.0 SDK installed, run: 52 | 53 | ``` 54 | dotnet publish -c Release -f netcoreapp3.0 -r linux-x64 55 | ``` 56 | The output application can be found in the ```bin/Release/netcoreapp3.0/linux-x64/publish```subdirectory. 57 | 58 | For Windows, run: 59 | 60 | ``` 61 | dotnet publish -c Release -f netcoreapp3.0 -r win-x64 62 | ``` 63 | The output application can be found in the ```bin\Release\netcoreapp3.0\win-x64\publish```subdirectory. 64 | 65 | ## Running the Tool 66 | 67 | You can run the tool locally from your own machine or you can run it from within an Azure VM. You should run the 68 | tool on the platform you are targeting, because performance differs between Linux and Windows due to their different 69 | I/O architectures and different implementation of the platform layer in .NET Core. 70 | 71 | The only required command line arguments are a connection string and a send path. If the connection string includes 72 | an EntityPath property for a queue, the send path can be omitted. If the receive path is not given, the tool assumes 73 | the send path and the receive path to be the same, which is sufficient for queues. For topics, the subscription(s) must 74 | be given with explicit receive path options. 75 | 76 | You can test any valid Service Bus topology with this tool, including Topics with subscriptions and chained 77 | entities with auto-forward set up between them. Therefore, the send path and the receive path do not have to be 78 | on the same entity. You can also receive from dead-letter queues. 79 | 80 | | Scenario | Arguments | 81 | |------------------------------|-------------------------------------------------------------------------------| 82 | | Send to and receive from a queue |```ThroughputTest -C {connection-string} -S myQueueName ``` | 83 | | Send to a topic and receive from a subscription | ``` ThroughputTest -C {connection-string} -S myTopicName -R myTopicName/subscriptions/mySubName ``` | 84 | | Send to a topic and receive from two subscriptions | ``` ThroughputTest -C {connection-string} -S myTopicName -R myTopicName/subscriptions/mySubNameA myTopicName/subscriptions/mySubNameB ``` | 85 | | Send a queue |```ThroughputTest -C {connection-string} -S myQueueName -r 0 ``` | 86 | | Receive from a queue |```ThroughputTest -C {connection-string} -S myQueueName -s 0 ``` | 87 | 88 | ## Output 89 | 90 | The tool prints out interleaved statistics for sends and receives. Send information is prefixed with S (and in yellow), 91 | receive information is prefixed with R and printed in cyan. The columns are separated with the pipe symbol and therefore 92 | parseable. 93 | 94 | ### Send output columns 95 | 96 | | Column | Description 97 | |----------|--------------------------------------------------------------------------- 98 | | pstart | Begin of the data recording for this row (seconds from start of run) 99 | | pend | End of data recording for this row 100 | | sbc | Send batch count 101 | | mifs | Max inflight sends 102 | | snd.avg | Average send duration (to acknowledgement receipt) in milliseconds 103 | | snd.med | Median send duration 104 | | snd.dev | Standard deviation for send duration 105 | | snd.min | Minimum send duration 106 | | snd.max | Maximum send duration 107 | | gld.avg | Average gate lock duration in milliseconds. This measure tracks whether the internal async thread pool queue gets backed up. If this value shoots up, you should reduce the number of concurrent inflight sends, because the application sends more than what can be put on the wire. 108 | | gld.med | Median gate lock duration 109 | | gld.dev | Standard deviation for gate lock duration 110 | | gld.min | Minimum gate lock duration 111 | | gld.max | Maximum gate lock duration 112 | | msg/s | Throughput in messages per second 113 | | total | Total messages sent in this period 114 | | sndop | Total send operations in this period 115 | | errs | Errors 116 | | busy | Busy errors 117 | | overall | Total messages sent in this run 118 | 119 | ### Receive output columns 120 | 121 | | Column | Description 122 | |----------|--------------------------------------------------------------------------- 123 | | pstart | Begin of the data recording for this row (seconds from start of run) 124 | | pend | End of data recording for this row 125 | | rbc | Receive batch count 126 | | mifr | Max inflight receives 127 | | rcv.avg | Average receive duration (to message receipt) in milliseconds 128 | | rcv.med | Median receive duration 129 | | rcv.dev | Standard deviation for receive duration 130 | | rcv.min | Minimum receive duration 131 | | rcv.max | Maximum receive duration 132 | | cpl.avg | Average completion duration in milliseconds. 133 | | cpl.med | Median completion duration 134 | | cpl.dev | Standard deviation for completion duration 135 | | cpl.min | Minimum completion duration 136 | | cpl.max | Maximum completion duration 137 | | msg/s | Throughput in messages per second 138 | | total | Total messages received in this period 139 | | rcvop | Total receive operations in this period 140 | | errs | Errors 141 | | busy | Busy errors 142 | | overall | Total messages sent in this run 143 | 144 | 145 | ## Options 146 | 147 | When only called with a connection string and send/receive paths, the tool will concurrently send and receive messages 148 | to/from the chosen Service Bus entity. 149 | 150 | If you just want to send messages, set the receiver count to zero with **-r 0**. If you only want to receive messages, 151 | set the sender count to zero with **-s 0**. 152 | 153 | The biggest throughput boosts yield the enabling of send and receive batching, meaning the sending and receiving of 154 | several messages in one operation. This oprtion will maximize throughput and show you the practical limits, but you 155 | should always keep an eye on whether batching is a practical approach for your specific solution. 156 | 157 | The "inflight-sends" and "inflight-receives" options also have very significant throughput impact. "inflight-sends" 158 | tracks how many messages are being sent asynchronously and in a pipelined fashion while waiting for the operations 159 | to complete. The "inflight-receives" option controls how many messages are being received and processed concurrently. 160 | 161 | The further options are listed below: 162 | 163 | | Parameter | Description 164 | |-------------------------------|------------------------------------------------------------------------------- 165 | | **-C, --connection-string** | **Required**. Connection string 166 | | -S, --send-path | Send path. Queue or topic name, unless set in connection string EntityPath. 167 | | -R, --receive-paths | Receive paths. Mandatory for receiving from topic subscriptions. Must be {topic}/subscriptions/{subscription-name} or {queue-name} 168 | | -n, --number-of-messages | Number of messages to send (default 1000000) 169 | | -b, --message-size-bytes | Bytes per message (default 1024) 170 | | -f, --frequency-metrics | Frequency of metrics display (seconds, default 10s) 171 | | -m, --receive-mode | Receive mode.'PeekLock' (default) or 'ReceiveAndDelete' 172 | | -r, --receiver-count | Number of concurrent receivers (default 1) 173 | | -e, --prefetch-count | Prefetch count (default 0) 174 | | -t, --send-batch-count | Number of messages per batch (default 0, no batching) 175 | | -s, --sender-count | Number of concurrent senders (default 1) 176 | | -d, --send-delay | Delay between sends of any sender (milliseconds, default 0) 177 | | -i, --inflight-sends | Maximum numbers of concurrent in-flight send operations (default 1) 178 | | -j, --inflight-receives | Maximum number of concurrent in-flight receive operations per receiver (default 1) 179 | | -v, --receive-batch-count | Max number of messages per batch (default 0, no batching) 180 | | -w, --receive-work-duration | Work simulation delay between receive and completion (milliseconds, default 0, no work) 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /ThroughputTest_v2/ServiceBusThroughputTest/Metrics.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ServiceBusThroughputTest 10 | { 11 | using ServiceBusThroughputTest.Common; 12 | using System; 13 | using System.Buffers; 14 | using System.Collections.Concurrent; 15 | using System.Collections.Generic; 16 | using System.Data.SqlTypes; 17 | using System.Diagnostics; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using System.Timers; 21 | using Timer = System.Timers.Timer; 22 | 23 | public sealed class Metrics : ILogger 24 | { 25 | public event EventHandler OnMetricsDisplay; 26 | readonly bool isSendMetric; 27 | readonly long maxMetricsSamplesPerDisplayInterval = 5000; 28 | readonly CancellationToken cancellationToken; 29 | 30 | ConcurrentDictionary sendSessions = new ConcurrentDictionary(); 31 | ConcurrentDictionary receiveSessions = new ConcurrentDictionary(); 32 | object timerLock = new object(); 33 | object metricsLock = new object(); 34 | ArrayPool sendMetricsPool = null; 35 | ArrayPool rxMetricsPool = null; 36 | 37 | SendMetrics[] activeSendMetricsBuffer = null; int sendMetricsBufferWriteIndex = -1; 38 | ReceiveMetrics[] activeReceiveMetricsBuffer = null; int rxMetricsBufferWriteIndex = -1; 39 | 40 | ObjectPool sendPool = null; 41 | ObjectPool rxPool = null; 42 | Timer timer; 43 | Stopwatch sw = Stopwatch.StartNew(); 44 | private bool disposedValue; 45 | 46 | public Metrics(CancellationToken cancellationToken = default, bool isSendMetric = true, double metricsDisplayFrequency = 10.0, int maxMetricsSamplePerDisplayInterval = 5000) 47 | { 48 | this.isSendMetric = isSendMetric; 49 | this.cancellationToken = cancellationToken; 50 | this.maxMetricsSamplesPerDisplayInterval = maxMetricsSamplePerDisplayInterval; 51 | 52 | sendPool = new ObjectPool( 53 | () => new SendMetrics(), 54 | (s) => 55 | { 56 | s.SendDuration100ns = s.Tick = 0; 57 | s.Sends = s.InflightSends = s.BusyErrors = s.Errors = s.Messages = s.Size = 0; 58 | }, 59 | maxMetricsSamplesPerDisplayInterval); 60 | 61 | rxPool = new ObjectPool( 62 | () => new ReceiveMetrics(), 63 | (r) => 64 | { 65 | r.ReceiveDuration100ns = r.Tick = r.CompleteDuration100ns = 0; 66 | r.Receives = r.Messages = r.CompleteCalls = r.Errors = r.BusyErrors = r.CompleteCalls = 0; 67 | }, 68 | maxMetricsSamplesPerDisplayInterval); 69 | 70 | sendMetricsPool = ArrayPool.Create((int)maxMetricsSamplesPerDisplayInterval, 4); 71 | rxMetricsPool = ArrayPool.Create((int)maxMetricsSamplesPerDisplayInterval, 4); 72 | 73 | activeSendMetricsBuffer = sendMetricsPool.Rent((int)maxMetricsSamplesPerDisplayInterval); 74 | activeReceiveMetricsBuffer = rxMetricsPool.Rent((int)maxMetricsSamplesPerDisplayInterval); 75 | this.rxMetricsBufferWriteIndex = this.sendMetricsBufferWriteIndex = -1; 76 | 77 | timer = new Timer(metricsDisplayFrequency * 1000); 78 | timer.Elapsed += Timer_Elapsed; 79 | 80 | timer.Enabled = true; 81 | timer.AutoReset = true; 82 | timer.Start(); 83 | } 84 | 85 | private void Timer_Elapsed(object sender, ElapsedEventArgs e) 86 | { 87 | lock (timerLock) 88 | { 89 | int sendMetricsCount = 0, rxMetricsCount = 0; 90 | SendMetrics[] sendBuffer = this.activeSendMetricsBuffer; 91 | ReceiveMetrics[] rxBuffer = this.activeReceiveMetricsBuffer; 92 | 93 | lock (this.metricsLock) 94 | { 95 | sendMetricsCount = this.sendMetricsBufferWriteIndex + 1; 96 | rxMetricsCount = this.rxMetricsBufferWriteIndex + 1; 97 | this.rxMetricsBufferWriteIndex = this.sendMetricsBufferWriteIndex = -1; 98 | this.activeSendMetricsBuffer = sendMetricsPool.Rent((int)maxMetricsSamplesPerDisplayInterval); 99 | this.activeReceiveMetricsBuffer = rxMetricsPool.Rent((int)maxMetricsSamplesPerDisplayInterval); 100 | } 101 | 102 | this.OnMetricsDisplay?.Invoke( 103 | this, 104 | new MetricsDisplayEventArgs 105 | { 106 | SendMetricsList = new ArraySegment(sendBuffer, 0, sendMetricsCount), 107 | ReceiveMetricsList = new ArraySegment(rxBuffer, 0, rxMetricsCount) 108 | }); 109 | 110 | if (sendMetricsCount > 0) 111 | { 112 | Parallel.For(0, sendMetricsCount, index => 113 | { 114 | sendPool.Return(sendBuffer[index]); 115 | }); 116 | } 117 | 118 | if (rxMetricsCount > 0) 119 | { 120 | Parallel.For(0, rxMetricsCount, index => 121 | { 122 | rxPool.Return(rxBuffer[index]); 123 | }); 124 | } 125 | 126 | this.sendMetricsPool.Return(sendBuffer, true); 127 | this.rxMetricsPool.Return(rxBuffer, true); 128 | } 129 | 130 | if (cancellationToken.IsCancellationRequested) 131 | { 132 | timer.Enabled = false; 133 | } 134 | } 135 | 136 | public void DisposeSession(Guid sessionId) 137 | { 138 | if (sessionId == Guid.Empty) 139 | { 140 | return; 141 | } 142 | 143 | if (isSendMetric) 144 | { 145 | sendSessions.TryRemove(sessionId, out SendMetrics sendMetrics); 146 | 147 | lock (metricsLock) 148 | { 149 | if (sendMetricsBufferWriteIndex < this.activeSendMetricsBuffer.Length - 1) 150 | { 151 | sendMetricsBufferWriteIndex++; 152 | 153 | activeSendMetricsBuffer[sendMetricsBufferWriteIndex] = sendMetrics; 154 | return; 155 | } 156 | } 157 | 158 | sendPool.Return(sendMetrics); 159 | } 160 | else 161 | { 162 | receiveSessions.TryRemove(sessionId, out ReceiveMetrics receiveMetrics); 163 | 164 | lock (metricsLock) 165 | { 166 | if (rxMetricsBufferWriteIndex < this.activeReceiveMetricsBuffer.Length - 1) 167 | { 168 | rxMetricsBufferWriteIndex++; 169 | 170 | activeReceiveMetricsBuffer[rxMetricsBufferWriteIndex] = receiveMetrics; 171 | return; 172 | } 173 | } 174 | 175 | rxPool.Return(receiveMetrics); 176 | } 177 | } 178 | 179 | Guid ILogger.StartRecordTime() 180 | { 181 | var sessionId = Guid.NewGuid(); 182 | if (isSendMetric) 183 | { 184 | var sendMetrics = sendPool.Get(); 185 | 186 | if (sendMetrics != null) 187 | { 188 | sendSessions[sessionId] = sendMetrics; 189 | sendSessions[sessionId].Tick = sw.ElapsedTicks; 190 | 191 | return sessionId; 192 | } 193 | } 194 | else 195 | { 196 | var rxMetrics = rxPool.Get(); 197 | 198 | if (rxMetrics != null) 199 | { 200 | receiveSessions[sessionId] = rxMetrics; 201 | receiveSessions[sessionId].Tick = sw.ElapsedTicks; 202 | 203 | return sessionId; 204 | } 205 | } 206 | 207 | return Guid.Empty; 208 | } 209 | 210 | double ILogger.StopRecordTimeAndGetElapsedMs(Guid sessionId) 211 | { 212 | if (sessionId == Guid.Empty) 213 | { 214 | return 0; 215 | } 216 | 217 | long elapsed; 218 | if (isSendMetric) 219 | { 220 | elapsed = sendSessions[sessionId].SendDuration100ns = sw.ElapsedTicks - sendSessions[sessionId].Tick; 221 | } 222 | else 223 | { 224 | elapsed = receiveSessions[sessionId].ReceiveDuration100ns = sw.ElapsedTicks - receiveSessions[sessionId].Tick; 225 | } 226 | 227 | return elapsed / (Stopwatch.Frequency / 1000.0); 228 | } 229 | 230 | void ILogger.StartCompletionRecordTime(Guid sessionId) 231 | { 232 | if (sessionId == Guid.Empty) 233 | { 234 | return; 235 | } 236 | 237 | if (!isSendMetric) 238 | { 239 | receiveSessions[sessionId].CompletionStartTick = sw.ElapsedTicks; 240 | } 241 | } 242 | 243 | double ILogger.StopCompletionRecordTimeAndGetElapsedMs(Guid sessionId) 244 | { 245 | if (sessionId == Guid.Empty || isSendMetric) 246 | { 247 | return 0; 248 | } 249 | 250 | receiveSessions[sessionId].CompleteDuration100ns = sw.ElapsedTicks - receiveSessions[sessionId].CompletionStartTick; 251 | 252 | return receiveSessions[sessionId].CompleteDuration100ns / (Stopwatch.Frequency / 1000.0); 253 | } 254 | 255 | void ILogger.IncrementActionCount(Guid sessionId) 256 | { 257 | if (sessionId == Guid.Empty) 258 | return; 259 | 260 | if (isSendMetric) 261 | { 262 | sendSessions[sessionId].Sends = sendSessions[sessionId].Sends + 1; 263 | } 264 | else 265 | { 266 | receiveSessions[sessionId].Receives = receiveSessions[sessionId].Receives + 1; 267 | } 268 | } 269 | 270 | void ILogger.IncrementMetricValueBy(Guid sessionId, int count) 271 | { 272 | if (sessionId == Guid.Empty) 273 | return; 274 | 275 | if (isSendMetric) 276 | { 277 | sendSessions[sessionId].Messages = sendSessions[sessionId].Messages + count; 278 | } 279 | else 280 | { 281 | receiveSessions[sessionId].Messages = receiveSessions[sessionId].Messages + count; 282 | } 283 | } 284 | 285 | void ILogger.IncrementErrorCount(Guid sessionId) 286 | { 287 | if (sessionId == Guid.Empty) 288 | return; 289 | 290 | if (isSendMetric) 291 | { 292 | sendSessions[sessionId].Errors = sendSessions[sessionId].Errors + 1; 293 | } 294 | else 295 | { 296 | receiveSessions[sessionId].Errors = receiveSessions[sessionId].Errors + 1; 297 | } 298 | } 299 | 300 | void ILogger.IncrementBusyErrorCount(Guid sessionId) 301 | { 302 | if (sessionId == Guid.Empty) 303 | return; 304 | 305 | if (isSendMetric) 306 | { 307 | sendSessions[sessionId].BusyErrors = sendSessions[sessionId].BusyErrors + 1; 308 | } 309 | else 310 | { 311 | receiveSessions[sessionId].BusyErrors = receiveSessions[sessionId].BusyErrors + 1; 312 | } 313 | } 314 | 315 | void ILogger.AddTrace(string trace) 316 | { 317 | } 318 | 319 | private void Dispose(bool disposing) 320 | { 321 | if (!disposedValue) 322 | { 323 | if (disposing) 324 | { 325 | this.timer.Enabled = false; 326 | 327 | lock (metricsLock) 328 | { 329 | this.timer.Enabled = false; 330 | 331 | this.sendSessions.Clear(); 332 | this.receiveSessions.Clear(); 333 | this.sendSessions = null; 334 | this.receiveSessions = null; 335 | } 336 | } 337 | 338 | disposedValue = true; 339 | } 340 | } 341 | 342 | public void Dispose() 343 | { 344 | Dispose(disposing: true); 345 | GC.SuppressFinalize(this); 346 | } 347 | } 348 | 349 | public class ObjectPool 350 | { 351 | private readonly ConcurrentBag _objects; 352 | private readonly Func _objectGenerator; 353 | private readonly Action _objectCleaner; 354 | private readonly long _maxCount; 355 | int _loaned = 0; 356 | 357 | public ObjectPool(Func objectGenerator, Action objectCleaner, long maxCount = 5000) 358 | { 359 | _objectGenerator = objectGenerator ?? throw new ArgumentNullException(nameof(objectGenerator)); 360 | _objectCleaner = objectCleaner; 361 | _objects = new ConcurrentBag(); 362 | _maxCount = maxCount; 363 | } 364 | 365 | public T Get() 366 | { 367 | if (this._loaned >= _maxCount) 368 | { 369 | return default; 370 | } 371 | 372 | Interlocked.Increment(ref this._loaned); 373 | 374 | if (_objects.TryTake(out T item)) 375 | { 376 | return item; 377 | } 378 | 379 | return _objectGenerator(); 380 | } 381 | 382 | public void Return(T item) 383 | { 384 | _objectCleaner?.Invoke(item); 385 | 386 | Interlocked.Decrement(ref this._loaned); 387 | _objects.Add(item); 388 | } 389 | } 390 | 391 | 392 | public class MetricsDisplayEventArgs : EventArgs 393 | { 394 | public ArraySegment SendMetricsList { get; set; } 395 | public ArraySegment ReceiveMetricsList { get; set; } 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /Results.txt: -------------------------------------------------------------------------------- 1 | //Here are the detailed results using a D4 V2 VM in North Europe. 2 | 3 | //Summary 4 | Scenario Send Rate (msg/s) Receive Rate (msg/s) 5 | 1MU_Queue 4401 4401 6 | 1MU_1Subscription 2223 2220 7 | 1MU_5Subscriptions 1348 2994 8 | 2MU_Queue 9070 9069 9 | 2MU_1Subscription 4499 4493 10 | 2MU_5Subscriptions 1676 8331 11 | 4MU_Queue 16782 16782 12 | 4MU_1Subscription 8960 8956 13 | 4MU_5Subscriptions 3844 19202 14 | 15 | //1MU_Queue 16 | Time Send Rate (msg/s) Send Latency (ms) Send Count (msg) Receive Rate (msg/s) Receive Latency (ms) Complete Latency (ms) Receive Count (msg) Server Busy Count Other Exceptions 17 | 8:22:43 PM 3,893 252.36 116,800 3,893 347.75 151.97 116,800 0 0 18 | 8:23:13 PM 4,313 231.14 129,400 4,293 339.47 126.96 128,801 0 0 19 | 8:23:43 PM 4,460 222.37 133,800 4,473 316.15 128.56 134,200 0 0 20 | 8:24:13 PM 4,726 211.17 141,800 4,720 318.57 108.21 141,600 0 0 21 | 8:24:43 PM 4,240 233.36 127,200 4,240 346.39 122.65 127,200 0 0 22 | 8:25:13 PM 4,410 226.37 132,300 4,400 341.29 113.83 132,000 0 0 23 | 8:25:43 PM 4,443 224.8 133,300 4,456 333.39 118.93 133,700 0 0 24 | 8:26:13 PM 4,420 224.7 132,600 4,426 333.55 117.49 132,800 0 0 25 | 8:26:43 PM 4,096 243.1 122,900 4,100 347.1 140.91 123,000 0 0 26 | 8:27:13 PM 4,406 224.05 132,200 4,403 336.65 114.78 132,100 0 0 27 | 8:27:43 PM 4,300 232.82 129,000 4,303 343.91 123.53 129,100 0 0 28 | 8:28:13 PM 4,606 214.48 138,200 4,590 313.45 119.42 137,700 0 0 29 | 8:28:43 PM 4,333 231.19 130,000 4,350 330.77 131.81 130,500 0 0 30 | 8:29:14 PM 4,640 212.87 139,200 4,636 322.09 106.86 139,100 0 0 31 | 8:29:44 PM 4,570 219.22 137,100 4,573 316.7 123.97 137,200 0 0 32 | 8:30:14 PM 4,673 210.11 140,200 4,670 307.06 116.53 140,100 0 0 33 | 8:30:44 PM 4,476 224.29 134,300 4,456 347.72 103.96 133,700 0 0 34 | 8:31:14 PM 4,256 231.93 127,700 4,280 338.29 126.86 128,400 0 0 35 | 8:31:44 PM 4,186 234.4 125,600 4,186 344.79 126.09 125,600 0 0 36 | 8:32:14 PM 4,573 223.28 137,200 4,563 334.46 114.18 136,900 0 0 37 | 8:32:44 PM 4,390 223.9 131,700 4,393 336.82 115.1 131,800 0 0 38 | SUMMARY 4,401 225.81 2,799,300 4,401 332.8 121.06 2,799,301 0 0 39 | 40 | //1MU_1Subscription 41 | Time Send Rate (msg/s) Send Latency (ms) Send Count (msg) Receive Rate (msg/s) Receive Latency (ms) Complete Latency (ms) Receive Count (msg) Server Busy Count Other Exceptions 42 | 8:44:46 PM 2,300 441.15 66,700 2,203 579.07 312.55 63,900 0 0 43 | 8:45:16 PM 2,123 470.55 63,700 2,086 664.07 301.71 62,601 0 0 44 | 8:45:46 PM 1,740 570.12 52,200 1,786 697.03 410.95 53,600 0 0 45 | 8:46:16 PM 1,980 508.73 59,400 1,943 722.51 307.86 58,300 0 0 46 | 8:46:46 PM 2,166 456.2 65,000 2,173 596.04 310.18 65,200 0 0 47 | 8:47:16 PM 2,150 463.54 64,500 2,116 609.43 339.34 63,500 0 0 48 | 8:47:46 PM 2,280 441.89 68,400 2,353 595.79 269.9 70,600 0 0 49 | 8:48:16 PM 2,433 406.75 73,000 2,426 551.85 264.73 72,800 0 0 50 | 8:48:46 PM 2,530 397.2 75,900 2,490 601.71 210.33 74,700 0 0 51 | 8:49:16 PM 2,253 437.13 67,600 2,310 622.57 242.2 69,300 0 0 52 | 8:49:46 PM 2,260 444.87 67,800 2,240 625.95 275.18 67,200 0 0 53 | 8:50:16 PM 2,240 440.64 67,200 2,223 608.56 283.09 66,700 0 0 54 | 8:50:46 PM 2,303 437.01 69,100 2,313 615.14 253.72 69,400 0 0 55 | 8:51:16 PM 2,190 449.45 65,700 2,226 571.78 321.29 66,800 0 0 56 | 8:51:46 PM 2,360 426.69 70,800 2,343 592.48 265.74 70,300 0 0 57 | 8:52:16 PM 2,156 457.32 64,700 2,176 654.37 258.51 65,300 0 0 58 | 8:52:46 PM 2,176 461.89 65,300 2,096 685.74 274.34 62,900 0 0 59 | 8:53:16 PM 2,213 453.22 66,400 2,213 649.8 255.68 66,400 0 0 60 | 8:53:46 PM 2,350 418.12 70,500 2,386 606.89 224.08 71,600 0 0 61 | 8:54:16 PM 2,310 433.58 69,300 2,300 603.49 261.65 69,000 0 0 62 | SUMMARY 2,223 447.73 1,349,900 2,220 620.5 278.22 1,347,801 0 0 63 | 64 | //1MU_5Subscriptions 65 | Time Send Rate (msg/s) Send Latency (ms) Send Count (msg) Receive Rate (msg/s) Receive Latency (ms) Complete Latency (ms) Receive Count (msg) Server Busy Count Other Exceptions 66 | 9:00:32 PM 1,476 661.77 44,300 3,360 2125.05 485.8 100,800 0 0 67 | 9:01:02 PM 1,113 887.87 33,400 3,573 2191.07 579.58 107,203 0 0 68 | 9:01:32 PM 1,276 775.87 38,300 3,513 2113.54 721.09 105,402 0 0 69 | 9:02:02 PM 1,413 720.04 42,400 3,986 1961.2 594 119,600 0 0 70 | 9:02:32 PM 1,290 773.71 38,700 3,900 2057.51 528.96 117,000 0 0 71 | 9:03:02 PM 966 1025.73 29,000 3,300 2299.03 740.32 99,000 0 0 72 | 9:03:32 PM 996 991.5 29,900 3,163 2317.46 763.06 94,900 0 0 73 | 9:04:02 PM 1,250 797.75 37,500 3,836 2031.46 619.22 115,100 0 0 74 | 9:04:32 PM 1,330 754.7 39,900 4,100 1880.07 598.58 123,000 0 0 75 | 9:05:02 PM 1,203 826.99 36,100 3,750 2025.44 580.14 112,500 0 0 76 | 9:05:32 PM 1,203 820.18 36,100 3,450 2191.53 634.05 103,500 0 0 77 | 9:06:02 PM 1,043 961.26 31,300 3,433 2425.85 595.13 103,000 0 0 78 | 9:06:32 PM 1,196 822.16 35,900 3,616 2135.4 664.64 108,500 0 0 79 | 9:07:02 PM 1,043 964.81 31,300 2,700 2618.39 731.77 81,000 0 0 80 | 9:07:32 PM 1,190 771.12 35,700 3,450 2394.99 547.67 103,500 0 0 81 | 9:08:02 PM 1,603 621.52 48,100 1,800 5077.68 375.12 54,000 0 0 82 | 9:08:32 PM 2,216 490.21 66,500 1,050 8881.22 561.24 31,500 0 0 83 | 9:09:02 PM 1,343 725.58 40,300 2,100 4471.54 515.07 63,000 0 0 84 | 9:09:32 PM 2,076 321.96 62,300 900 9134.97 263.62 27,000 4 0 85 | 9:10:02 PM 1,436 779.45 43,100 1,950 5500.23 470.82 58,500 0 0 86 | SUMMARY 1,348 730.68 837,300 2,994 2718.07 596.58 1,859,505 4 0 87 | 88 | //2MU_Queue 89 | Time Send Rate (msg/s) Send Latency (ms) Send Count (msg) Receive Rate (msg/s) Receive Latency (ms) Complete Latency (ms) Receive Count (msg) Server Busy Count Other Exceptions 90 | 9:28:12 PM 9,103 111.07 264,000 9,089 171.91 50.96 263,600 0 0 91 | 9:28:42 PM 9,090 109.75 272,700 9,086 166.44 55.49 272,601 0 0 92 | 9:29:12 PM 9,133 108.32 274,000 9,140 164.41 54.59 274,200 0 0 93 | 9:29:42 PM 9,163 108.08 274,900 9,160 164.98 53.43 274,800 0 0 94 | 9:30:12 PM 9,046 109.32 271,400 9,043 166.68 53.17 271,300 0 0 95 | 9:30:42 PM 8,790 112.76 263,700 8,780 172.36 56.62 263,400 0 0 96 | 9:31:12 PM 9,503 104.09 285,100 9,520 157.42 52.97 285,600 0 0 97 | 9:31:42 PM 9,186 107.74 275,600 9,183 164.08 53.61 275,500 0 0 98 | 9:32:12 PM 9,503 104.08 285,100 9,510 160.32 50.08 285,300 0 0 99 | 9:32:43 PM 9,393 104.37 281,800 9,396 160.49 50.37 281,900 0 0 100 | 9:33:13 PM 8,803 113.69 264,100 8,793 172.08 57.54 263,800 0 0 101 | 9:33:43 PM 8,723 113.1 261,700 8,730 167.69 61 261,900 0 0 102 | 9:34:13 PM 8,760 113.33 262,800 8,746 172.98 55.64 262,400 0 0 103 | 9:34:43 PM 9,396 105.22 281,900 9,406 157.33 55.13 282,200 0 0 104 | 9:35:13 PM 8,563 115.97 256,900 8,560 172.38 61.49 256,800 0 0 105 | 9:35:43 PM 9,473 104.49 284,200 9,476 158.04 53.65 284,300 0 0 106 | 9:36:13 PM 9,093 108.85 272,800 9,100 164.78 54.8 273,000 0 0 107 | 9:36:43 PM 8,956 110.64 268,700 8,950 164.21 59.29 268,500 0 0 108 | 9:37:13 PM 9,050 108.74 271,500 9,046 161.84 57.86 271,400 0 0 109 | 9:37:43 PM 9,193 108.21 275,800 9,206 164.38 54.02 276,200 0 0 110 | 9:38:13 PM 8,546 115.12 256,400 8,546 174.19 58.52 256,400 0 0 111 | SUMMARY 9,070 109.21 5,868,300 9,069 165.56 55.02 5,868,101 0 0 112 | 113 | //2MU_1Subscription 114 | Time Send Rate (msg/s) Send Latency (ms) Send Count (msg) Receive Rate (msg/s) Receive Latency (ms) Complete Latency (ms) Receive Count (msg) Server Busy Count Other Exceptions 115 | 9:41:25 PM 5,020 204.17 145,600 4,910 278.58 132.67 142,400 0 0 116 | 9:41:55 PM 4,640 213.75 139,200 4,670 300.04 131 140,101 0 0 117 | 9:42:25 PM 4,480 222.34 134,400 4,496 293.44 151.81 134,900 0 0 118 | 9:42:55 PM 4,772 214.63 138,400 4,775 291.15 140.61 138,500 0 0 119 | 9:43:25 PM 4,323 230.59 129,700 4,300 335.89 127.41 129,000 0 0 120 | 9:43:55 PM 4,263 230.69 127,900 4,240 315.01 154.78 127,200 0 0 121 | 9:44:25 PM 4,416 227.63 132,500 4,406 309.53 145.14 132,200 0 0 122 | 9:44:55 PM 4,070 241.74 122,100 4,076 336.27 153.77 122,300 0 0 123 | 9:45:25 PM 4,260 235.85 127,800 4,286 316.46 154.29 128,600 0 0 124 | 9:45:55 PM 4,183 235.28 125,500 4,180 306.46 167.77 125,400 0 0 125 | 9:46:25 PM 4,156 241.81 124,700 4,170 317.27 167.6 125,100 0 0 126 | 9:46:55 PM 4,350 225.91 130,500 4,336 325.03 130.29 130,100 0 0 127 | 9:47:25 PM 4,640 215.82 139,200 4,620 291.68 144.78 138,600 0 0 128 | 9:47:55 PM 4,890 203.97 146,700 4,893 261.97 143.72 146,800 0 0 129 | 9:48:25 PM 4,463 221.46 133,900 4,476 301.91 147.75 134,300 0 0 130 | 9:48:55 PM 4,590 217.69 137,700 4,596 302.68 135.51 137,900 0 0 131 | 9:49:25 PM 4,746 207.64 142,400 4,780 273.49 141.9 143,400 0 0 132 | 9:49:55 PM 4,876 205.37 146,300 4,803 282.36 134.93 144,100 0 0 133 | 9:50:25 PM 4,343 226.86 130,300 4,433 301.51 148.48 133,000 0 0 134 | 9:50:55 PM 4,763 210.66 142,900 4,720 281.47 144.64 141,600 0 0 135 | SUMMARY 4,499 221.06 2,780,600 4,493 300.43 144.54 2,777,201 0 0 136 | 137 | //2MU_5Subscriptions 138 | Time Send Rate (msg/s) Send Latency (ms) Send Count (msg) Receive Rate (msg/s) Receive Latency (ms) Complete Latency (ms) Receive Count (msg) Server Busy Count Other Exceptions 139 | 9:54:15 PM 1,743 562.37 52,300 8,166 728.15 436.19 245,000 0 0 140 | 9:54:45 PM 1,653 605.53 49,600 8,083 787.49 453.43 242,505 0 0 141 | 9:55:15 PM 1,556 625.7 46,700 8,060 738.04 488.65 241,800 0 0 142 | 9:55:45 PM 1,730 587.05 51,900 8,606 725.39 451.44 258,200 0 0 143 | 9:56:15 PM 1,616 608.64 48,500 7,683 800.74 479.05 230,500 0 0 144 | 9:56:45 PM 1,653 607.15 49,600 8,280 738.76 474.09 248,400 0 0 145 | 9:57:15 PM 1,836 539.63 55,100 7,670 970.61 316.49 230,100 0 0 146 | 9:57:45 PM 1,783 557.96 53,500 8,646 762.54 413.66 259,400 0 0 147 | 9:58:15 PM 1,773 562.97 53,200 8,803 729.78 403.42 264,100 0 0 148 | 9:58:45 PM 1,753 558.05 52,600 8,920 691.91 434.67 267,600 0 0 149 | 9:59:15 PM 1,846 542.69 55,400 9,346 676.24 400.94 280,400 0 0 150 | 9:59:45 PM 1,816 546.97 54,500 8,850 692.61 424.73 265,500 0 0 151 | 12:00:15 PM 1,760 567.43 52,800 8,826 778.32 362.28 264,800 0 0 152 | 12:00:45 PM 1,763 558.89 52,900 8,953 710.34 400.85 268,600 0 0 153 | 12:01:15 PM 1,773 564.84 53,200 8,720 681.64 479.79 261,600 0 0 154 | 12:01:45 PM 1,673 588.69 50,200 8,216 774.92 430.76 246,500 0 0 155 | 12:02:15 PM 1,470 659.45 44,100 7,090 823.23 558.85 212,700 0 0 156 | 12:02:45 PM 1,493 683.14 44,800 8,743 621.6 545.49 262,300 0 0 157 | 12:03:15 PM 1,582 645.18 45,900 8,793 691.52 487.47 255,000 0 0 158 | 12:03:45 PM 1,546 651.21 46,400 7,793 728.53 546.19 233,800 0 0 159 | 12:04:15 PM 1,570 633.17 47,100 7,826 815.24 469.88 234,800 0 0 160 | SUMMARY 1,676 593.04 1,104,900 8,331 749.38 449.07 5,490,705 0 0 161 | 162 | //4MU_Queue 163 | Time Send Rate (msg/s) Send Latency (ms) Send Count (msg) Receive Rate (msg/s) Receive Latency (ms) Complete Latency (ms) Receive Count (msg) Server Busy Count Other Exceptions 164 | 10:09:47 PM 16,848 119.57 488,600 16,827 177.34 66.46 488,000 0 0 165 | 10:10:17 PM 17,316 111.32 519,500 17,336 165.01 62.6 520,101 0 0 166 | 10:10:47 PM 16,313 122.31 489,400 16,293 180.32 68.5 488,800 0 0 167 | 10:11:17 PM 16,773 116.81 503,200 16,756 173.4 65.31 502,700 0 0 168 | 10:11:47 PM 16,400 119.42 492,000 16,420 176.48 66.99 492,600 0 0 169 | 10:12:17 PM 16,440 119.13 493,200 16,460 179.39 63.77 493,800 0 0 170 | 10:12:47 PM 17,330 113.13 519,900 17,316 169.21 61.54 519,500 0 0 171 | 10:13:17 PM 17,243 113.28 517,300 17,246 170.26 61.26 517,400 0 0 172 | 10:13:47 PM 16,393 119.88 491,800 16,383 176.52 68.07 491,500 0 0 173 | 10:14:17 PM 16,713 117.01 501,400 16,726 174.85 63.73 501,800 0 0 174 | 10:14:47 PM 16,293 120.93 488,800 16,246 177.72 68.55 487,400 0 0 175 | 10:15:17 PM 15,433 126.99 463,000 15,453 186.8 71.38 463,600 0 0 176 | 10:15:47 PM 17,400 112.94 522,000 17,396 166.77 64.36 521,900 0 0 177 | 10:16:17 PM 16,550 118.49 496,500 16,566 175.73 65.55 497,000 0 0 178 | 10:16:47 PM 17,913 109.39 537,400 17,920 164.84 58.46 537,600 0 0 179 | 10:17:17 PM 17,550 111.78 526,500 17,550 166.61 61.29 526,500 0 0 180 | 10:17:47 PM 16,656 117.89 499,700 16,650 174.45 65.71 499,500 0 0 181 | 10:18:17 PM 17,613 111.46 528,400 17,603 166.14 61.1 528,100 0 0 182 | 10:18:47 PM 16,840 116.31 505,200 16,840 173.54 63.71 505,200 0 0 183 | 10:19:17 PM 15,960 123.18 478,800 15,953 186.06 64.59 478,600 0 0 184 | 10:19:47 PM 16,833 116.51 505,000 16,860 173.03 64.16 505,800 0 0 185 | SUMMARY 16,782 116.89 10,572,700 16,782 173.82 64.54 10,572,701 0 0 186 | 187 | //4MU_1Subscription 188 | Time Send Rate (msg/s) Send Latency (ms) Send Count (msg) Receive Rate (msg/s) Receive Latency (ms) Complete Latency (ms) Receive Count (msg) Server Busy Count Other Exceptions 189 | 10:21:32 PM 9,553 103.07 286,600 9,480 153.12 55.41 284,400 0 0 190 | 10:22:02 PM 9,210 107.7 276,300 9,230 156.91 59.92 276,901 0 0 191 | 10:22:32 PM 8,626 114.13 258,800 8,620 169.03 62.22 258,600 0 0 192 | 10:23:02 PM 9,223 107.43 276,700 9,190 160.78 57.26 275,700 0 0 193 | 10:23:32 PM 8,880 111.5 266,400 8,920 165.09 59.42 267,600 0 0 194 | 10:24:02 PM 9,230 107.02 276,900 9,236 160.08 56.61 277,100 0 0 195 | 10:24:32 PM 9,460 104.5 283,800 9,390 155.73 55.98 281,700 0 0 196 | 10:25:02 PM 9,556 103.47 286,700 9,606 154.66 54.99 288,200 0 0 197 | 10:25:32 PM 8,753 112.27 262,600 8,760 166.4 60.55 262,800 0 0 198 | 10:26:02 PM 8,573 116.23 257,200 8,580 171.8 62.85 257,400 0 0 199 | 10:26:32 PM 8,600 115.02 258,000 8,603 169.9 61.79 258,100 0 0 200 | 10:27:02 PM 8,256 119.59 247,700 8,240 177.09 65.93 247,200 0 0 201 | 10:27:32 PM 8,950 110.87 268,500 8,973 163.45 59.87 269,200 0 0 202 | 10:28:02 PM 9,103 106.07 273,100 9,100 156.25 57.69 273,000 0 0 203 | 10:28:32 PM 8,333 121.46 250,000 8,300 176.68 70.24 249,000 0 0 204 | 10:29:02 PM 8,563 114.95 256,900 8,633 168.6 62.92 259,000 0 0 205 | 10:29:32 PM 9,043 109.85 271,300 9,020 164.45 57.86 270,600 0 0 206 | 10:30:02 PM 9,216 107.46 276,500 9,206 161.22 56.07 276,200 0 0 207 | 10:30:32 PM 9,043 109.45 271,300 9,026 162.05 59.7 270,800 0 0 208 | 10:31:02 PM 8,740 113.14 262,200 8,726 164.09 64.49 261,800 0 0 209 | SUMMARY 8,960 110.48 5,394,100 8,956 163.5 59.91 5,391,901 0 0 210 | 211 | //4MU_5Subscriptions 212 | Time Send Rate (msg/s) Send Latency (ms) Send Count (msg) Receive Rate (msg/s) Receive Latency (ms) Complete Latency (ms) Receive Count (msg) Server Busy Count Other Exceptions 213 | 10:31:49 PM 4,036 243.05 121,100 19,816 300.78 189.76 594,500 0 0 214 | 10:32:19 PM 3,873 252.31 116,200 19,140 337.19 183.74 574,205 0 0 215 | 10:32:49 PM 3,850 257.91 115,500 19,376 330.57 187.87 581,300 0 0 216 | 10:33:19 PM 3,866 252.7 116,000 19,340 335.22 178.4 580,200 0 0 217 | 10:33:49 PM 3,643 268.62 109,300 18,293 340.67 199.84 548,800 0 0 218 | 10:34:19 PM 3,750 267.45 112,500 18,656 356.47 190.49 559,700 0 0 219 | 10:34:49 PM 3,930 251.07 117,900 19,660 326.91 182.26 589,800 0 0 220 | 10:35:19 PM 4,010 246.95 120,300 20,136 329.46 167.61 604,100 0 0 221 | 10:35:50 PM 3,890 252.73 116,700 19,453 331.75 181.69 583,600 0 0 222 | 10:36:20 PM 4,013 246.36 120,400 19,990 324.79 177.29 599,700 0 0 223 | 10:36:50 PM 3,856 252.98 115,700 19,410 318.69 193.77 582,300 0 0 224 | 10:37:20 PM 3,746 265.39 112,400 18,606 323.13 215.6 558,200 0 0 225 | 10:37:50 PM 3,986 256.55 115,600 19,720 326.35 195.59 571,900 0 0 226 | 10:38:20 PM 3,840 257.13 115,200 19,520 332.81 183.87 585,600 0 0 227 | 10:38:50 PM 3,850 253.67 115,500 19,200 337.96 177.59 576,000 0 0 228 | 10:39:20 PM 3,876 255.4 116,300 19,253 337.77 181.8 577,600 0 0 229 | 10:39:50 PM 3,973 249.5 119,200 20,006 332.81 171.43 600,200 0 0 230 | 10:40:20 PM 3,623 272.15 108,700 17,940 344.98 207.73 538,200 0 0 231 | 10:40:50 PM 3,836 255.86 115,100 19,356 327.38 191.38 580,700 0 0 232 | 10:41:20 PM 3,646 270.88 109,400 18,280 340.33 202.58 548,400 0 0 233 | 10:41:50 PM 3,680 271.5 110,400 18,233 364.19 188.82 547,000 0 0 234 | 10:42:20 PM 3,823 256.43 114,700 19,243 337.29 180.31 577,300 0 0 235 | SUMMARY 3,844 256.86 2,545,100 19,202 333.43 187.4 12,711,805 0 0 236 | 237 | -------------------------------------------------------------------------------- /ThroughputTest_v2/ServiceBusThroughputTest/Program.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ServiceBusThroughputTest 10 | { 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Linq; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | using CommandLine; 17 | using CommandLine.Text; 18 | using System.Diagnostics; 19 | using ServiceBusThroughputTestLib; 20 | using System.Configuration; 21 | 22 | class Program 23 | { 24 | const string connString = ""; 25 | const string queue = "queue1"; 26 | const string MetricsSamplesPerDisplayIntervalSenderKeyName = "MetricsSamplesPerDisplayIntervalSender"; 27 | const string MetricsSamplesPerDisplayIntervalReceiverKeyName = "MetricsSamplesPerDisplayIntervalReceiver"; 28 | const int MetricsSamplesPerDisplayIntervalSenderDefault = 2000; 29 | const int MetricsSamplesPerDisplayIntervalReceiverDefault = 10000; 30 | 31 | static void Main(string[] args) 32 | { 33 | CommandLine.Parser.Default.ParseArguments(args) 34 | .WithParsed(opts => StartSendReceives(opts)); 35 | } 36 | 37 | static void StartSendReceives(Settings settings) 38 | { 39 | CancellationTokenSource cts = new CancellationTokenSource(); 40 | 41 | var cancelTask = Task.Run(() => 42 | { 43 | Console.ReadLine(); 44 | cts.Cancel(); 45 | }); 46 | 47 | settings.PrintSettings(); 48 | 49 | int metricsSamplesPerDisplayIntervalSender, metricsSamplesPerDisplayIntervalReceiver; 50 | GetConfigValue(MetricsSamplesPerDisplayIntervalSenderKeyName, int.Parse, out metricsSamplesPerDisplayIntervalSender, MetricsSamplesPerDisplayIntervalSenderDefault); 51 | GetConfigValue(MetricsSamplesPerDisplayIntervalReceiverKeyName, int.Parse, out metricsSamplesPerDisplayIntervalReceiver, MetricsSamplesPerDisplayIntervalReceiverDefault); 52 | 53 | Console.WriteLine("Starting senders and receivers. Press Enter key to stop at anytime..."); 54 | 55 | if (settings.SenderCount > 0) 56 | { 57 | Console.ForegroundColor = ConsoleColor.Yellow; 58 | 59 | Console.Write("S|{0,10:0.00}|{1,10:0.00}|{2,5}|{3,5}|", "pstart", "pend", "sbc", "mifs"); 60 | Console.Write("{0,10:0.00}|{1,10:0.00}|{2,10:0.00}|{3,10:0.00}|", "snd.avg", "snd.min", "snd.max", "snd.p99"); 61 | Console.WriteLine("{0,10}|{1,10}|{2,10}|{3,10}|{4,10}|{5,10}|", "msg/s", "total", "sndop", "errs", "busy", "overall"); 62 | } 63 | 64 | if (settings.ReceiverCount > 0) 65 | { 66 | Console.ForegroundColor = ConsoleColor.Cyan; 67 | 68 | Console.Write("R|{0,10:0.00}|{1,10:0.00}|{2,5}|{3,5}|", "pstart", "pend", "rbc", "mifr"); 69 | Console.Write("{0,10:0.00}|{1,10:0.00}|{2,10:0.00}|{3,10:0.00}|", "rcv.avg", "rcv.min", "rcv.max", "rcv.p99"); 70 | 71 | Console.Write("{0,10}|{1,10}|{2,10}|{3,10}|{4,10}|{5,10}|", "msg/s", "total", "rcvop", "errs", "busy", "overall"); 72 | Console.WriteLine("{0,10:0.00}|{1,10:0.00}|{2,10:0.00}|{3,10:0.00}|", "cpl.avg", "cpl.min", "cpl.max", "cpl.p99"); 73 | } 74 | 75 | Console.ForegroundColor = ConsoleColor.White; 76 | 77 | var sndRxTasks = new List { cancelTask }; 78 | long sendTotal = 0, receiveTotal = 0; 79 | 80 | if (settings.ReceiverCount > 0) 81 | { 82 | Metrics receiveMetrics = new Metrics(cts.Token, false, settings.MetricsDisplayFrequency, metricsSamplesPerDisplayIntervalReceiver); 83 | 84 | Receiver receiver = new Receiver(settings.ConnectionString, settings.QueueName, settings.PrefetchCount, settings.ReceiveBatchSize, settings.MaxInflightReceives, settings.ReceiverCount, settings.ReceiveCallIntervalMs, !settings.IsPeekLock, receiveMetrics); 85 | receiveMetrics.OnMetricsDisplay += (s, e) => Metrics_OnMetricsDisplay(s, e, settings, ref sendTotal, ref receiveTotal); 86 | 87 | sndRxTasks.Add(receiver.Run(cts.Token)); 88 | } 89 | 90 | if (settings.SenderCount > 0) 91 | { 92 | Metrics sendMetrics = new Metrics(cts.Token, true, settings.MetricsDisplayFrequency, metricsSamplesPerDisplayIntervalSender); 93 | Sender sender = new Sender(settings.ConnectionString, settings.QueueName, settings.PayloadSizeInBytes, settings.SendBatchSize, settings.SendCallIntervalMs, settings.MaxInflightSends, settings.SenderCount, sendMetrics); 94 | 95 | sendMetrics.OnMetricsDisplay += (s, e) => Metrics_OnMetricsDisplay(s, e, settings, ref sendTotal, ref receiveTotal); 96 | 97 | sndRxTasks.Add(sender.Run(cts.Token)); 98 | } 99 | 100 | Task.WhenAny(sndRxTasks).Wait(); 101 | } 102 | 103 | private static void Metrics_OnMetricsDisplay(object _, MetricsDisplayEventArgs e, Settings settings, ref long sendTotal, ref long receiveTotal) 104 | { 105 | var list = e.SendMetricsList; 106 | if (list.Count > 0) 107 | { 108 | lock (Console.Out) 109 | { 110 | Console.ForegroundColor = ConsoleColor.Yellow; 111 | Console.Write("S|{0,10}|{1,10}|{2,5}|{3,5}|", list.First().Tick / Stopwatch.Frequency + 1, list.Last().Tick / Stopwatch.Frequency + 1, settings.SendBatchSize, settings.MaxInflightSends); 112 | 113 | WriteStat(list, i => i.SendDuration100ns, Stopwatch.Frequency / 1000.0); 114 | var msgs = list.Sum(i => i.Messages); 115 | sendTotal += msgs; 116 | 117 | Console.WriteLine("{0,10:0.00}|{1,10}|{2,10}|{3,10}|{4,10}|{5,10}|", list.Sum(i => i.Messages) / (double)settings.MetricsDisplayFrequency, msgs, list.Sum(i => i.Sends), list.Sum(i => i.Errors), list.Sum(i => i.BusyErrors), sendTotal); 118 | } 119 | } 120 | 121 | var rList = e.ReceiveMetricsList; 122 | if (rList.Count > 0) 123 | { 124 | lock (Console.Out) 125 | { 126 | Console.ForegroundColor = ConsoleColor.Cyan; 127 | 128 | Console.Write("R|{0,10}|{1,10}|{2,5}|{3,5}|", rList.First().Tick / Stopwatch.Frequency + 1, rList.Last().Tick / Stopwatch.Frequency + 1, settings.ReceiveBatchSize, settings.MaxInflightReceives); 129 | WriteStat(rList, i => i.ReceiveDuration100ns, Stopwatch.Frequency / 1000.0); 130 | 131 | var msgs = rList.Sum(i => i.Messages); 132 | receiveTotal += msgs; 133 | Console.Write("{0,10:0.00}|{1,10}|{2,10}|{3,10}|{4,10}|{5,10}|", rList.Sum(i => i.Messages) / (double)settings.MetricsDisplayFrequency, msgs, rList.Sum(i => i.Receives), rList.Sum(i => i.Errors), rList.Sum(i => i.BusyErrors), receiveTotal); 134 | 135 | WriteStat(rList, i => i.CompleteDuration100ns, Stopwatch.Frequency / 1000.0); 136 | Console.WriteLine(); 137 | } 138 | } 139 | 140 | Console.ForegroundColor = ConsoleColor.White; 141 | } 142 | 143 | public static double Percentile(long[] elements, double percentile) 144 | { 145 | Array.Sort(elements); 146 | double realIndex = percentile * (elements.Length - 1); 147 | int index = (int)realIndex; 148 | double frac = realIndex - index; 149 | if (index + 1 < elements.Length) 150 | return elements[index] * (1 - frac) + elements[index + 1] * frac; 151 | else 152 | return elements[index]; 153 | } 154 | 155 | static void WriteStat(IList list, Func f, double scale) 156 | { 157 | Console.Write("{0,10:0.00}|{1,10:0.00}|{2,10:0.00}|{3,10:0.00}|", list.Average(f) / scale, list.Min(f) / scale, list.Max(f) / scale, Percentile(list.Select(s => f(s)).ToArray(), 0.99) / scale); 158 | } 159 | 160 | static bool GetConfigValue(string keyName, Func parseFunc, out T val, T defaultVal = default(T)) 161 | { 162 | if (ConfigurationManager.AppSettings.AllKeys.Contains(keyName)) 163 | { 164 | try 165 | { 166 | val = parseFunc(ConfigurationManager.AppSettings[keyName]); 167 | 168 | return true; 169 | } 170 | catch { } 171 | } 172 | 173 | val = defaultVal; 174 | 175 | return false; 176 | } 177 | 178 | static void TestSBReceiver() 179 | { 180 | ServiceBusThroughputTestLib.Receiver receiver = new ServiceBusThroughputTestLib.Receiver(connString, "queue1", 1024*10, 10, 1024, 1, 60000, false); 181 | CancellationTokenSource cts = new CancellationTokenSource(); 182 | Task rt = receiver.Run(cts.Token); 183 | Thread.Sleep(TimeSpan.FromMinutes(2)); 184 | cts.Cancel(); 185 | rt.Wait(); 186 | } 187 | 188 | static void TestSBSender() 189 | { 190 | int batchSize = 8; 191 | for (int i = 1; i < 2; i++) 192 | { 193 | Console.WriteLine("**** batchSize: " + batchSize); 194 | ServiceBusThroughputTestLib.Sender sender = new ServiceBusThroughputTestLib.Sender(connString, "queue1", 600, 25, batchSize, 1, 1); 195 | CancellationTokenSource cts = new CancellationTokenSource(); 196 | Task st = sender.Run(cts.Token); 197 | Thread.Sleep(TimeSpan.FromMinutes(5)); 198 | //cts.Cancel(); 199 | st.Wait(); 200 | batchSize *= 2; 201 | } 202 | } 203 | 204 | static void TestSBSenderLargeMsg() 205 | { 206 | int batchSize = 256; 207 | for (int i = 9; i > 0; i--) 208 | { 209 | Console.WriteLine("**** batchSize: " + batchSize); 210 | ServiceBusThroughputTestLib.Sender ehSender = new ServiceBusThroughputTestLib.Sender(connString, "queue1", 25 * 1024, 25, batchSize, 1, 1); 211 | CancellationTokenSource cts = new CancellationTokenSource(); 212 | Task st = ehSender.Run(cts.Token); 213 | Thread.Sleep(TimeSpan.FromMinutes(5)); 214 | cts.Cancel(); 215 | st.Wait(); 216 | batchSize /= 2; 217 | } 218 | } 219 | } 220 | 221 | class Settings 222 | { 223 | [Option('C', "connection-string", Required = true, HelpText = "Connection string")] 224 | public string ConnectionString { get; set; } 225 | 226 | [Option('S', "entity-path", Required = false, HelpText = "Entity path. Queue or topic. For Topic/Subscription, provide in this format - {topic}:{subscription}")] 227 | public string QueueName { get; set; } 228 | 229 | [Option('b', "payload-size-bytes", Required = false, HelpText = "Bytes per message (default 1024)")] 230 | public int PayloadSizeInBytes { get; set; } = 1024; 231 | 232 | [Option('f', "frequency-metrics", Required = false, HelpText = "Frequency of metrics display (seconds, default 10s)")] 233 | public int MetricsDisplayFrequency { get; set; } = 10; 234 | 235 | [Option('m', "receive-mode", Required = false, HelpText = "Receive mode. 'true' for PeekLock (default) or 'false' for ReceiveAndDelete")] 236 | public bool IsPeekLock { get; set; } = true; 237 | 238 | [Option('r', "receiver-count", Required = false, HelpText = "Number of concurrent receivers (default 1)")] 239 | public int ReceiverCount { get; set; } = 5; 240 | 241 | [Option('p', "prefetch-count", Required = false, HelpText = "Prefetch count (default 0)")] 242 | public int PrefetchCount { get; set; } = 100; 243 | 244 | [Option('t', "send-batch-size", Required = false, HelpText = "Number of messages per batch (default 0, no batching)")] 245 | public int SendBatchSize { get; set; } = 1; 246 | 247 | [Option('s', "sender-count", Required = false, HelpText = "Number of concurrent senders (default 1)")] 248 | public int SenderCount { get; set; } = 1; 249 | 250 | [Option('d', "send-callIntervalMs", Required = false, HelpText = "Delay between sends of any sender (milliseconds, default 0)")] 251 | public int SendCallIntervalMs { get; private set; } = 1000; 252 | 253 | [Option('e', "receive-callIntervalMs", Required = false, HelpText = "Receive timeout for receiver (milliseconds, default 0)")] 254 | public int ReceiveCallIntervalMs { get; private set; } = 5000; 255 | 256 | [Option('i', "inflight-sends", Required = false, HelpText = "Maximum numbers of concurrent in-flight send operations (default 1)")] 257 | public int MaxInflightSends { get; internal set; } = 1; 258 | 259 | [Option('j', "inflight-receives", Required = false, HelpText = "Maximum number of concurrent in-flight receive operations per receiver (default 1)")] 260 | public int MaxInflightReceives { get; internal set; } = 1; 261 | 262 | [Option('v', "receive-batch-size", Required = false, HelpText = "Max number of messages per batch (default 0, no batching)")] 263 | public int ReceiveBatchSize { get; private set; } = 1; 264 | 265 | public void PrintSettings() 266 | { 267 | Console.WriteLine("Settings:"); 268 | Console.WriteLine("{0}: {1}", "SendPaths", this.QueueName); 269 | Console.WriteLine("{0}: {1}", "PayloadSizeInBytes", this.PayloadSizeInBytes); 270 | Console.WriteLine("{0}: {1}", "SenderCount", this.SenderCount); 271 | Console.WriteLine("{0}: {1}", "SendBatchCount", this.SendBatchSize); 272 | Console.WriteLine("{0}: {1}", "SendDelay", this.SendCallIntervalMs); 273 | Console.WriteLine("{0}: {1}", "MaxInflightSends", this.MaxInflightSends); 274 | Console.WriteLine("{0}: {1}", "IsPeekLock", this.IsPeekLock); 275 | Console.WriteLine("{0}: {1}", "ReceiverCount", this.ReceiverCount); 276 | Console.WriteLine("{0}: {1}", "ReceiveBatchCount", this.ReceiveBatchSize); 277 | Console.WriteLine("{0}: {1}", "PrefetchCount", this.PrefetchCount); 278 | Console.WriteLine("{0}: {1}", "ReceiveTimeout", this.ReceiveCallIntervalMs); 279 | Console.WriteLine("{0}: {1}", "MaxInflightReceives", this.MaxInflightReceives); 280 | Console.WriteLine("{0}: {1}", "MetricsDisplayFrequency", this.MetricsDisplayFrequency); 281 | 282 | Console.WriteLine(); 283 | } 284 | 285 | [Usage()] 286 | public static IEnumerable Examples 287 | { 288 | get 289 | { 290 | yield return new Example("queue scenario", new Settings { ConnectionString = "{Connection-String}", QueueName = "{Queue-Name}" }); 291 | yield return new Example("topic scenario", new Settings { ConnectionString = "{Connection-String}", QueueName = "{Topic-Name}:{Subsription-Name}"}); 292 | } 293 | } 294 | } 295 | } -------------------------------------------------------------------------------- /ThroughputTest/ReceiverTask.cs: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------------------- 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | // THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 5 | // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES 6 | // OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. 7 | //--------------------------------------------------------------------------------- 8 | 9 | namespace ThroughputTest 10 | { 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Diagnostics; 14 | using System.Linq; 15 | using System.Threading; 16 | using System.Threading.Tasks; 17 | using Azure.Messaging.ServiceBus; 18 | 19 | sealed class ReceiverTask : PerformanceTask 20 | { 21 | readonly List receivers; 22 | 23 | public ReceiverTask(Settings settings, Metrics metrics, CancellationToken cancellationToken) 24 | : base(settings, metrics, cancellationToken) 25 | { 26 | this.receivers = new List(); 27 | } 28 | 29 | protected override Task OnOpenAsync() 30 | { 31 | return Task.CompletedTask; 32 | } 33 | 34 | protected override Task OnStartAsync() 35 | { 36 | var receiverPaths = this.Settings.ReceivePaths; 37 | foreach (var receiverPath in receiverPaths) 38 | { 39 | for (int i = 0; i < this.Settings.ReceiverCount; i++) 40 | { 41 | this.receivers.Add(Task.Run(() => ReceiveTask(receiverPath))); 42 | } 43 | } 44 | return Task.WhenAll(this.receivers); 45 | } 46 | 47 | async Task ReceiveTask(string path) 48 | { 49 | var client = new ServiceBusClient(this.Settings.ConnectionString); 50 | var options = new ServiceBusReceiverOptions(); 51 | options.ReceiveMode = this.Settings.ReceiveMode; 52 | options.PrefetchCount = Settings.PrefetchCount; 53 | ServiceBusReceiver receiver = client.CreateReceiver(path, options); 54 | var semaphore = new DynamicSemaphoreSlim(this.Settings.MaxInflightReceives.Value + 1); 55 | var done = new SemaphoreSlim(1); done.Wait(); 56 | var sw = Stopwatch.StartNew(); 57 | long totalReceives = 0; 58 | await Task.Delay(TimeSpan.FromMilliseconds(Settings.WorkDuration)); 59 | this.Settings.MaxInflightReceives.Changing += (a, e) => AdjustSemaphore(e, semaphore); 60 | 61 | for (int j = 0; j < Settings.MessageCount && !this.CancellationToken.IsCancellationRequested; j ++) 62 | { 63 | var receiveMetrics = new ReceiveMetrics() { Tick = sw.ElapsedTicks }; 64 | 65 | var nsec = sw.ElapsedTicks; 66 | await semaphore.WaitAsync().ConfigureAwait(false); 67 | receiveMetrics.GateLockDuration100ns = sw.ElapsedTicks - nsec; 68 | 69 | if (Settings.ReceiveBatchCount <= 1) 70 | { 71 | nsec = sw.ElapsedTicks; 72 | // we're going to unblock the receives after 10 seconds if there's no pending message 73 | receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(10)).ContinueWith(async (t) => 74 | { 75 | receiveMetrics.ReceiveDuration100ns = sw.ElapsedTicks - nsec; 76 | if (t.IsFaulted || t.IsCanceled || t.Result == null) 77 | { 78 | if ((Exception)t.Exception is ServiceBusException sbException && sbException.Reason == ServiceBusFailureReason.ServiceBusy) 79 | { 80 | receiveMetrics.BusyErrors = 1; 81 | if (!this.CancellationToken.IsCancellationRequested) 82 | { 83 | await Task.Delay(3000, this.CancellationToken).ConfigureAwait(false); 84 | } 85 | } 86 | else 87 | { 88 | receiveMetrics.Errors = 1; 89 | } 90 | Metrics.PushReceiveMetrics(receiveMetrics); 91 | semaphore.Release(); 92 | if (Interlocked.Increment(ref totalReceives) >= Settings.MessageCount) 93 | { 94 | done.Release(); 95 | } 96 | } 97 | else 98 | { 99 | receiveMetrics.Receives = receiveMetrics.Messages = 1; 100 | nsec = sw.ElapsedTicks; 101 | 102 | // simulate work by introducing a delay, if needed 103 | if (Settings.WorkDuration > 0) 104 | { 105 | await Task.Delay(TimeSpan.FromMilliseconds(Settings.WorkDuration)); 106 | } 107 | receiver.CompleteMessageAsync(t.Result).ContinueWith(async (t1) => 108 | { 109 | receiveMetrics.CompleteDuration100ns = sw.ElapsedTicks - nsec; 110 | if (t1.IsFaulted) 111 | { 112 | if (t1.Exception?.GetType() == typeof(ServiceBusFailureReason)) 113 | { 114 | receiveMetrics.BusyErrors = 1; 115 | if (!this.CancellationToken.IsCancellationRequested) 116 | { 117 | await Task.Delay(3000, this.CancellationToken).ConfigureAwait(false); 118 | } 119 | } 120 | else 121 | { 122 | receiveMetrics.Errors = 1; 123 | } 124 | } 125 | else 126 | { 127 | receiveMetrics.Completions = receiveMetrics.CompleteCalls = 1; 128 | } 129 | Metrics.PushReceiveMetrics(receiveMetrics); 130 | semaphore.Release(); 131 | if (Interlocked.Increment(ref totalReceives) >= Settings.MessageCount) 132 | { 133 | done.Release(); 134 | } 135 | }).Fork(); 136 | }; 137 | 138 | }).Fork(); 139 | } 140 | else 141 | { 142 | nsec = sw.ElapsedTicks; 143 | // we're going to unblock the receives after 10 seconds if there's no pending message 144 | receiver.ReceiveMessagesAsync(Settings.ReceiveBatchCount, TimeSpan.FromSeconds(10)).ContinueWith(async (t) => 145 | { 146 | receiveMetrics.ReceiveDuration100ns = sw.ElapsedTicks - nsec; 147 | if (t.IsFaulted || t.IsCanceled || t.Result == null) 148 | { 149 | if ((Exception)t.Exception is ServiceBusException sbException && sbException.Reason == ServiceBusFailureReason.ServiceBusy) 150 | { 151 | receiveMetrics.BusyErrors = 1; 152 | if (!this.CancellationToken.IsCancellationRequested) 153 | { 154 | await Task.Delay(3000, this.CancellationToken).ConfigureAwait(false); 155 | } 156 | } 157 | else 158 | { 159 | receiveMetrics.Errors = 1; 160 | } 161 | Metrics.PushReceiveMetrics(receiveMetrics); 162 | semaphore.Release(); 163 | if (Interlocked.Increment(ref totalReceives) >= Settings.MessageCount) 164 | { 165 | done.Release(); 166 | } 167 | } 168 | else 169 | { 170 | receiveMetrics.Messages = t.Result.Count; 171 | receiveMetrics.Receives = 1; 172 | nsec = sw.ElapsedTicks; 173 | if (Settings.ReceiveMode == ServiceBusReceiveMode.PeekLock) 174 | { 175 | if (Settings.WorkDuration > 0) 176 | { 177 | // handle completes singly 178 | for (int i = 0; i < t.Result.Count; i++) 179 | { 180 | await Task.Delay(TimeSpan.FromMilliseconds(Settings.WorkDuration)); 181 | await receiver.CompleteMessageAsync(t.Result[i]).ContinueWith(async (t1) => 182 | { 183 | receiveMetrics.CompleteDuration100ns = sw.ElapsedTicks - nsec; 184 | if (t1.IsFaulted) 185 | { 186 | if ((Exception)t1.Exception is ServiceBusException sbException && sbException.Reason == ServiceBusFailureReason.ServiceBusy) 187 | { 188 | receiveMetrics.BusyErrors = 1; 189 | if (!this.CancellationToken.IsCancellationRequested) 190 | { 191 | await Task.Delay(3000, this.CancellationToken).ConfigureAwait(false); 192 | } 193 | } 194 | else 195 | { 196 | receiveMetrics.Errors = 1; 197 | } 198 | } 199 | else 200 | { 201 | receiveMetrics.CompleteCalls = 1; 202 | receiveMetrics.Completions = t.Result.Count; 203 | } 204 | Metrics.PushReceiveMetrics(receiveMetrics); 205 | semaphore.Release(); 206 | if (Interlocked.Increment(ref totalReceives) >= Settings.MessageCount) 207 | { 208 | done.Release(); 209 | } 210 | }); 211 | } 212 | } 213 | else 214 | { 215 | // batch complete 216 | await receiver.CompleteMessageAsync((ServiceBusReceivedMessage)t.Result.Select((m) => { return m; })).ContinueWith(async (t1) => 217 | { 218 | receiveMetrics.CompleteDuration100ns = sw.ElapsedTicks - nsec; 219 | if (t1.IsFaulted) 220 | { 221 | if ((Exception)t1.Exception is ServiceBusException sbException && sbException.Reason == ServiceBusFailureReason.ServiceBusy) 222 | { 223 | receiveMetrics.BusyErrors = 1; 224 | if (!this.CancellationToken.IsCancellationRequested) 225 | { 226 | await Task.Delay(3000, this.CancellationToken).ConfigureAwait(false); 227 | } 228 | } 229 | else 230 | { 231 | receiveMetrics.Errors = 1; 232 | } 233 | } 234 | else 235 | { 236 | receiveMetrics.CompleteCalls = 1; 237 | receiveMetrics.Completions = t.Result.Count; 238 | } 239 | Metrics.PushReceiveMetrics(receiveMetrics); 240 | semaphore.Release(); 241 | 242 | // count all the messages 243 | for (int k = 0; k < t.Result.Count; k++) 244 | { 245 | if (Interlocked.Increment(ref totalReceives) >= Settings.MessageCount) 246 | { 247 | done.Release(); 248 | } 249 | } 250 | }); 251 | } 252 | } 253 | else 254 | { 255 | if (Settings.WorkDuration > 0) 256 | { 257 | await Task.Delay(TimeSpan.FromMilliseconds(Settings.WorkDuration)); 258 | } 259 | Metrics.PushReceiveMetrics(receiveMetrics); 260 | semaphore.Release(); 261 | if (Interlocked.Increment(ref totalReceives) >= Settings.MessageCount) 262 | { 263 | done.Release(); 264 | } 265 | } 266 | } 267 | }).Fork(); 268 | 269 | } 270 | } 271 | await done.WaitAsync(); 272 | } 273 | 274 | static void AdjustSemaphore(Observable.ChangingEventArgs e, DynamicSemaphoreSlim semaphore) 275 | { 276 | if (e.NewValue > e.OldValue) 277 | { 278 | for (int i = e.OldValue; i < e.NewValue; i++) 279 | { 280 | semaphore.Grant(); 281 | } 282 | } 283 | else 284 | { 285 | for (int i = e.NewValue; i < e.OldValue; i++) 286 | { 287 | semaphore.Revoke(); 288 | } 289 | } 290 | } 291 | } 292 | } 293 | --------------------------------------------------------------------------------