Skip to content

Commit

Permalink
Add Request Response Benchmark (#110)
Browse files Browse the repository at this point in the history
  • Loading branch information
Havret authored May 25, 2024
1 parent 4ec1f49 commit c9e5c53
Show file tree
Hide file tree
Showing 15 changed files with 431 additions and 0 deletions.
21 changes: 21 additions & 0 deletions ArtemisNetCoreClient.sln
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArtemisNetCoreClient", "src
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArtemisNetCoreClient.Tests", "test\ArtemisNetCoreClient.Tests\ArtemisNetCoreClient.Tests.csproj", "{0C9852DE-7477-4B3A-93FD-74EEBA8195CA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", "{F1AC22BC-1EC4-46A7-96B6-6C117EF29DB9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PingPong_ArtemisNetCoreClient", "benchmark\PingPong_ArtemisNetCoreClient\PingPong_ArtemisNetCoreClient.csproj", "{410586C1-5DE7-4EE3-BB96-6163A4A5D5BE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PingPong", "PingPong", "{12B2FE3E-96BE-4AC2-8D93-9371BB3B1A14}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PingPong_NMS.AMQP", "benchmark\PingPong_NMS.AMQP\PingPong_NMS.AMQP.csproj", "{F5B9E0A9-B86E-440F-B68B-AAB3F69CF86F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -18,5 +26,18 @@ Global
{0C9852DE-7477-4B3A-93FD-74EEBA8195CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0C9852DE-7477-4B3A-93FD-74EEBA8195CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0C9852DE-7477-4B3A-93FD-74EEBA8195CA}.Release|Any CPU.Build.0 = Release|Any CPU
{410586C1-5DE7-4EE3-BB96-6163A4A5D5BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{410586C1-5DE7-4EE3-BB96-6163A4A5D5BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{410586C1-5DE7-4EE3-BB96-6163A4A5D5BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{410586C1-5DE7-4EE3-BB96-6163A4A5D5BE}.Release|Any CPU.Build.0 = Release|Any CPU
{F5B9E0A9-B86E-440F-B68B-AAB3F69CF86F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5B9E0A9-B86E-440F-B68B-AAB3F69CF86F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5B9E0A9-B86E-440F-B68B-AAB3F69CF86F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5B9E0A9-B86E-440F-B68B-AAB3F69CF86F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{12B2FE3E-96BE-4AC2-8D93-9371BB3B1A14} = {F1AC22BC-1EC4-46A7-96B6-6C117EF29DB9}
{410586C1-5DE7-4EE3-BB96-6163A4A5D5BE} = {12B2FE3E-96BE-4AC2-8D93-9371BB3B1A14}
{F5B9E0A9-B86E-440F-B68B-AAB3F69CF86F} = {12B2FE3E-96BE-4AC2-8D93-9371BB3B1A14}
EndGlobalSection
EndGlobal
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,29 @@ var message = await consumer.ReceiveMessageAsync();

- [ActiveMQ Artemis address model explained with examples in .NET](https://havret.io/activemq-artemis-address-model) (April 19, 2022)[^1]

## Performance

```sh
OS=macOS 14.5 (23F79) [Darwin 23.5.0]
Apple M1 Pro, 1 CPU, 10 logical and 10 physical cores
.NET SDK=8.0.100
[Host] : .NET 8.0.0 (8.0.23.53103), Arm64 RyuJIT
DefaultJob : .NET 8.0.0 (8.0.23.53103), Arm64 RyuJIT
```

### Request-Reply (PingPong) Benchmark

This benchmark compares the performance of two client libraries — ArtemisNetCoreClient and NMS.AMQP — in a request-reply messaging scenario. The test setup involves two components, `Ping` and `Pong`:

- **`Ping`**: Sends a message to `Pong` and starts a timer.
- **`Pong`**: Receives the message and responds back immediately.

The cycle of messages between `Ping` and `Pong` provides metrics on round-trip time, throughput, and system efficiency when under load. Each test cycle involved sending and receiving 10,000 messages, with performance measured by recording the time taken to process these messages and calculating the throughput in messages per second. This benchmark is designed to demonstrate how each client library handles intensive message exchanges, offering insights into their suitability for high-demand environments.

<div align="center">
<img src="./readme/PingPong_Benchmark.svg" alt="Benchmark Results Diagram"/>
</div>

## Running the tests

To run the tests, you need an Apache ActiveMQ Artemis server. The server can be hosted in a Docker container.
Expand Down
99 changes: 99 additions & 0 deletions benchmark/PingPong_ArtemisNetCoreClient/Ping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System.Diagnostics;
using ActiveMQ.Artemis.Core.Client;

namespace PingPong_ArtemisNetCoreClient
{
public class Ping : IAsyncDisposable
{
private readonly IConnection _connection;
private readonly ISession _session;
private readonly IProducer _producer;
private readonly IConsumer _consumer;
private readonly Stopwatch _stopwatch;
private int _numberOfMessages;
private int _skipMessages;
private int _counter;
private TaskCompletionSource<Stats> _tsc;
private readonly Task _consumerLoopTask;
private readonly CancellationTokenSource _cts;

private Ping(IConnection connection, ISession session, IProducer producer, IConsumer consumer)
{
_connection = connection;
_session = session;
_producer = producer;
_consumer = consumer;
_stopwatch = new Stopwatch();

_cts = new CancellationTokenSource();

_consumerLoopTask = ConsumerLoop();
}

public static async Task<Ping> CreateAsync(Endpoint endpoint)
{
var connectionFactory = new ConnectionFactory();
var connection = await connectionFactory.CreateAsync(endpoint);
var session = await connection.CreateSessionAsync();

var producer = await session.CreateProducerAsync(new ProducerConfiguration { Address = "ping" });
var consumer = await session.CreateConsumerAsync(new ConsumerConfiguration { QueueName = "pong" });
return new Ping(connection, session, producer, consumer);
}

public Task<Stats> Start(int numberOfMessages, int skipMessages)
{
_numberOfMessages = numberOfMessages;
_skipMessages = skipMessages;
_stopwatch.Start();
_tsc = new TaskCompletionSource<Stats>();
var pingMessage = new Message { Body = "Ping"u8.ToArray() };
_producer.SendMessageAsync(pingMessage);
return _tsc.Task;
}

private Task ConsumerLoop()
{
return Task.Run(async () =>
{
try
{
while (!_cts.IsCancellationRequested)
{
var msg = await _consumer.ReceiveMessageAsync(_cts.Token);
if (_skipMessages > 0)
_skipMessages--;
else
_counter++;
if (_counter == _numberOfMessages)
{
_stopwatch.Stop();
_tsc.TrySetResult(new Stats { MessagesCount = _counter, Elapsed = _stopwatch.Elapsed });
}
else
{
var pingMessage = new Message { Body = "Ping"u8.ToArray() };
await _producer.SendMessageAsync(pingMessage, _cts.Token);
}
await _consumer.AcknowledgeAsync(msg.MessageDelivery, _cts.Token);
}
}
catch (OperationCanceledException)
{
}
});
}

public async ValueTask DisposeAsync()
{
await _cts.CancelAsync();
_cts.Dispose();
await _consumerLoopTask;
await _consumer.DisposeAsync();
await _producer.DisposeAsync();
await _session.DisposeAsync();
await _connection.DisposeAsync();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>PingPong_ArtemisNetCoreClient</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\ArtemisNetCoreClient\ArtemisNetCoreClient.csproj" />
</ItemGroup>

</Project>
66 changes: 66 additions & 0 deletions benchmark/PingPong_ArtemisNetCoreClient/Pong.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using ActiveMQ.Artemis.Core.Client;

namespace PingPong_ArtemisNetCoreClient
{
public class Pong : IAsyncDisposable
{
private readonly IConnection _connection;
private readonly ISession _session;
private readonly IProducer _producer;
private readonly IConsumer _consumer;
private readonly Task _consumerLoopTask;
private readonly CancellationTokenSource _cts;

private Pong(IConnection connection, ISession session, IProducer producer, IConsumer consumer)
{
_connection = connection;
_session = session;
_producer = producer;
_consumer = consumer;
_cts = new CancellationTokenSource();

_consumerLoopTask = ConsumerLoop();
}

public static async Task<Pong> CreateAsync(Endpoint endpoint)
{
var connectionFactory = new ConnectionFactory();
var connection = await connectionFactory.CreateAsync(endpoint);
var session = await connection.CreateSessionAsync();
var producer = await session.CreateProducerAsync(new ProducerConfiguration { Address = "pong" });
var consumer = await session.CreateConsumerAsync(new ConsumerConfiguration { QueueName = "ping" });
return new Pong(connection, session, producer, consumer);
}

private Task ConsumerLoop()
{
return Task.Run(async () =>
{
try
{
while (!_cts.IsCancellationRequested)
{
var pingMsg = await _consumer.ReceiveMessageAsync(_cts.Token);
var pongMessage = new Message { Body = "Pong"u8.ToArray() };
await _producer.SendMessageAsync(pongMessage, _cts.Token);
await _consumer.AcknowledgeAsync(pingMsg.MessageDelivery, _cts.Token);
}
}
catch (OperationCanceledException)
{
}
});
}

public async ValueTask DisposeAsync()
{
await _cts.CancelAsync();
_cts.Dispose();
await _consumerLoopTask;
await _consumer.DisposeAsync();
await _producer.DisposeAsync();
await _session.DisposeAsync();
await _connection.DisposeAsync();
}
}
}
25 changes: 25 additions & 0 deletions benchmark/PingPong_ArtemisNetCoreClient/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using ActiveMQ.Artemis.Core.Client;

namespace PingPong_ArtemisNetCoreClient;

class Program
{
static async Task Main()
{
var endpoint = new Endpoint
{
Host = "localhost",
Port = 61616,
User = "artemis",
Password = "artemis"
};

for (int i = 0; i < 10; i++)
{
await using var ping = await Ping.CreateAsync(endpoint);
await using var pong = await Pong.CreateAsync(endpoint);
var start = await ping.Start(skipMessages: 100, numberOfMessages: 10000);
Console.WriteLine(start);
}
}
}
12 changes: 12 additions & 0 deletions benchmark/PingPong_ArtemisNetCoreClient/Stats.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace PingPong_ArtemisNetCoreClient;

public class Stats
{
public int MessagesCount { get; init; }
public TimeSpan Elapsed { get; init; }

public override string ToString()
{
return $"Sent {MessagesCount} msgs in {Elapsed.TotalMilliseconds:F2}ms -- {MessagesCount / Elapsed.TotalSeconds:F2} msg/s";
}
}
67 changes: 67 additions & 0 deletions benchmark/PingPong_NMS.AMQP/Ping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.Diagnostics;
using Apache.NMS;

namespace PingPong_NMS.AMQP
{
public class Ping: IDisposable
{
private readonly IConnection _connection;
private readonly ISession _session;
private readonly IMessageProducer _messageProducer;
private readonly IMessageConsumer _messageConsumer;
private readonly Stopwatch _stopwatch;
private TaskCompletionSource<Stats>? _tsc;
private int _numberOfMessages;
private int _skipMessages;
private int _counter;

public Ping(IConnectionFactory connectionFactory)
{
_connection = connectionFactory.CreateConnection();
_session = _connection.CreateSession();
_messageProducer = _session.CreateProducer(_session.GetQueue("ping"));
_messageConsumer = _session.CreateConsumer(_session.GetQueue("pong"));
_messageConsumer.Listener += OnMessage;
_stopwatch = new Stopwatch();
_connection.Start();
}

private void OnMessage(IMessage message)
{
if (_skipMessages > 0)
_skipMessages--;
else
_counter++;

if (_counter == _numberOfMessages)
{
_stopwatch.Stop();
_tsc.SetResult(new Stats { MessagesCount = _counter, Elapsed = _stopwatch.Elapsed });

Check warning on line 39 in benchmark/PingPong_NMS.AMQP/Ping.cs

View workflow job for this annotation

GitHub Actions / linux

Dereference of a possibly null reference.
}
else
{
var pingMessage = _messageProducer.CreateBytesMessage("Ping"u8.ToArray());
_messageProducer.Send(pingMessage);
}
}

public Task<Stats> Start(int numberOfMessages, int skipMessages)
{
_numberOfMessages = numberOfMessages;
_skipMessages = skipMessages;
_tsc = new TaskCompletionSource<Stats>(TaskCreationOptions.RunContinuationsAsynchronously);
_stopwatch.Start();
var pingMessage = _messageProducer.CreateBytesMessage("Ping"u8.ToArray());
_messageProducer.Send(pingMessage);
return _tsc.Task;
}

public void Dispose()
{
_messageConsumer.Dispose();
_messageProducer.Dispose();
_session.Dispose();
_connection.Dispose();
}
}
}
14 changes: 14 additions & 0 deletions benchmark/PingPong_NMS.AMQP/PingPong_NMS.AMQP.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Apache.NMS.AMQP" Version="2.2.0" />
</ItemGroup>

</Project>
Loading

0 comments on commit c9e5c53

Please sign in to comment.