Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
6d12815
chore: flush hardware metrics on scenario complete
guitarrapc Feb 10, 2026
cfb9018
chore: span
guitarrapc Feb 10, 2026
8d529ea
chore: IQR filter should not early return on zero data
guitarrapc Feb 10, 2026
7e18699
chore: Add OutlierIqr to fallback IQR -> MAD when iqr value is almost 0
guitarrapc Feb 10, 2026
e9693f6
chore: move
guitarrapc Feb 10, 2026
7b24f09
chore: use TimeProvider injected by DI
guitarrapc Feb 10, 2026
dc2732a
chore: comment out log
guitarrapc Feb 10, 2026
a98bbba
chore: metaverse
guitarrapc Feb 10, 2026
a3e428c
chore: metaverse 2
guitarrapc Feb 17, 2026
0272c25
chore: metaverse scenario
guitarrapc Feb 17, 2026
a16132f
chore: handle TimeProvider
guitarrapc Feb 17, 2026
ddedb2c
chore: remove Broadcast (no limit)
guitarrapc Feb 17, 2026
6e5f1c2
chore: don't need TimeProvider
guitarrapc Feb 17, 2026
3ab5335
chore: add Metaverse Scenario
guitarrapc Feb 17, 2026
1014061
chore: launchprofile
guitarrapc Feb 17, 2026
2c9fe75
chore: broadcast expect sender
guitarrapc Feb 17, 2026
ca7150a
Merge branch 'main' into chore/bench
guitarrapc Feb 17, 2026
ec39308
chore: log
guitarrapc Feb 17, 2026
0d423e5
chore: launchsettings
guitarrapc Feb 17, 2026
9c593be
chore: rename
guitarrapc Feb 17, 2026
8185fe4
chore: broadcast hub handling
guitarrapc Feb 17, 2026
7d59a88
chore: add benchmark config
guitarrapc Feb 17, 2026
b5b88b0
chore: comment out
guitarrapc Feb 17, 2026
094b563
chore: reconcider BroadcastMetrics
guitarrapc Feb 17, 2026
13c21cf
chore: MetaverseBroadcast now works
guitarrapc Feb 17, 2026
e5540b0
chore: no remove
guitarrapc Feb 17, 2026
9d4fcf1
chore: update config
guitarrapc Feb 17, 2026
3ad1f19
chore: update config
guitarrapc Feb 17, 2026
031356c
chore: jitter
guitarrapc Feb 18, 2026
416c243
chore: adjust benchmark parameter
guitarrapc Feb 18, 2026
b309347
chore: Metaverse bench config
guitarrapc Feb 18, 2026
5a54ae0
chore: benchmark config
guitarrapc Feb 18, 2026
c196071
chore: DoubleBuffering to avoid memory allocation on frequent Metadat…
guitarrapc Feb 18, 2026
3c4e99c
chore: replace DoubleBuffer to BufferPool
guitarrapc Feb 18, 2026
8ef1932
chore: channelxstream not match
guitarrapc Feb 18, 2026
ecc2dd3
Merge branch 'main' into chore/bench
guitarrapc Feb 18, 2026
490ff06
Revert "chore: replace DoubleBuffer to BufferPool"
guitarrapc Feb 18, 2026
19bf3d5
chore: add GC
guitarrapc Feb 18, 2026
5c41183
chore: remove TotalAllocation
guitarrapc Feb 18, 2026
55a41ed
chore: warning
guitarrapc Feb 18, 2026
0000b40
chore: add GC Count
guitarrapc Feb 18, 2026
dfc3b25
chore: clear
guitarrapc Feb 18, 2026
a66f255
fix: get delta for gc count
guitarrapc Feb 18, 2026
3c725e4
chore: remove 30fps 1500 channels
guitarrapc Feb 18, 2026
d0e6c83
Merge branch 'main' into chore/bench
guitarrapc Mar 4, 2026
297d921
chore: format
guitarrapc Mar 4, 2026
e1180f6
chore: update links
guitarrapc Mar 4, 2026
b64539d
chore: add metaverse benchmark
guitarrapc Mar 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/benchmark.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ on:
type: choice
options:
- workflow_dispatch_broadcast
- workflow_dispatch_metaversebroadcast
- workflow_dispatch_memorypack_h2
- workflow_dispatch_memorypack_h2c
- workflow_dispatch_memorypack_h3
Expand Down
15 changes: 4 additions & 11 deletions perf/BenchmarkApp/PerformanceTest.Client/BroadcastScenario.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ public class BroadcastScenario : IScenario, IPerTestBroadcastHubReceiver
{
IPerfTestService client = default!;
IPerTestBroadcastHub hubClient = default!;
readonly TimeProvider timeProvider = TimeProvider.System;
PerformanceTestRunningContext context = default!;
int connectionId;
long begin;

protected virtual int TargetFps => 0; // 0 means maximum speed

Expand All @@ -25,23 +22,20 @@ public async ValueTask PrepareAsync(GrpcChannel channel)
// So most times MoveNext won't wait at all, and it may wait occasionally.
public async ValueTask RunAsync(int connectionId, PerformanceTestRunningContext ctx, CancellationToken cancellationToken)
{
var start = TimeProvider.System.GetTimestamp();
context = ctx;
this.connectionId = connectionId;
begin = timeProvider.GetTimestamp();
await hubClient.JoinGroupAsync();

// Only the first client triggers broadcast after warmup completes
if (connectionId == 0)
{
// Wait for warmup to complete
await ctx.WaitForReadyAsync();

// Use exact duration to match client-side metrics collection period
var duration = TimeSpan.FromSeconds(ctx.DurationSeconds);
_ = client.BroadcastAsync(duration, TargetFps);
}

while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(100, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
Expand All @@ -52,6 +46,7 @@ public async Task CompleteAsync()
{
if (hubClient is not null)
{
await hubClient.LeaveGroupAsync();
await hubClient.DisposeAsync();
}
}
Expand All @@ -60,8 +55,6 @@ public void OnMessage(BroadcastPositionMessage message)
{
// Collect only Count
context.Increment();
//context.LatencyThrottled(connectionId, timeProvider.GetElapsedTime(begin), 100); // avoid OOM
//begin = timeProvider.GetTimestamp();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using Grpc.Net.Client;
using MagicOnion.Client;
using PerformanceTest.Shared;

namespace PerformanceTest.Client;

public class MetaverseBroadcastScenario : IScenario, IMetaverseBroadcastHubReceiver
{
const int clientFps = 15;
static Random random = new(76); // Fixed seed for reproducibility

IMetaverseBroadcastHub hubClient = default!;
PerformanceTestRunningContext context = default!;
int connectionId;

protected virtual int TargetFps => 30; // Default 30 FPS

public async ValueTask PrepareAsync(GrpcChannel channel)
{
hubClient = await StreamingHubClient.ConnectAsync<IMetaverseBroadcastHub, IMetaverseBroadcastHubReceiver>(channel, this);
}

public async ValueTask RunAsync(int connectionId, PerformanceTestRunningContext ctx, CancellationToken cancellationToken)
{
context = ctx;
this.connectionId = connectionId;
await hubClient.JoinAsync(TargetFps);

// Wait for warmup to complete
await ctx.WaitForReadyAsync();

// jitter to avoid all clients sending updates at the same time
await Task.Delay(random.Next(0, 500), cancellationToken);

if (connectionId == 0)
{
await hubClient.StartBroadcast(TargetFps);
}

// Simulate client position updates periodically (fixed to 15 FPS for all scenarios to isolate broadcast performance)
var interval = TimeSpan.FromMilliseconds(1000.0 / clientFps);
using var updateTimer = new PeriodicTimer(interval);

try
{
while (await updateTimer.WaitForNextTickAsync(cancellationToken))
{
// Send random position update
var position = new BroadcastPositionMessage(
connectionId,
new Vector3(
(float)(random.NextDouble() * 1000),
(float)(random.NextDouble() * 100),
(float)(random.NextDouble() * 1000)
)
);

await hubClient.UpdatePositionAsync(position);
}
}
catch (OperationCanceledException)
{
// Expected on cancellation
}

if (connectionId == 0)
{
await hubClient.StopBroadcast();
}
}

public async Task CompleteAsync()
{
if (hubClient is not null)
{
await hubClient.LeaveAsync();
await hubClient.DisposeAsync();
}
}

public void OnBroadcastAllPositions(AllClientsPositionMessage message)
{
// Collect metrics for each broadcast received
context.Increment();
}
}

public class MetaverseBroadcast15FpsScenario : MetaverseBroadcastScenario
{
protected override int TargetFps => 15;
}

public class MetaverseBroadcast30FpsScenario : MetaverseBroadcastScenario
{
protected override int TargetFps => 30;
}

public class MetaverseBroadcast60FpsScenario : MetaverseBroadcastScenario
{
protected override int TargetFps => 60;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ public class PerformanceTestRunningContext
public TimeSpan Timeout { get; }
public int DurationSeconds { get; }

public PerformanceTestRunningContext(int connectionCount, (int WarmupSec, int RunSec) serverTimeout, DatadogMetricsRecorder recoder, ScenarioType scenario)
public PerformanceTestRunningContext(int connectionCount, (int WarmupSec, int RunSec) serverTimeout, DatadogMetricsRecorder recoder, ScenarioType scenario, TimeProvider timeProvider)
{
Timeout = TimeSpan.FromSeconds(serverTimeout.WarmupSec + serverTimeout.RunSec + 3); // add some sec for safely complete serverstreaming
DurationSeconds = serverTimeout.RunSec;
stopwatch = new Stopwatch();
latencyPerConnection = new(connectionCount);
errorsPerConnection = 0;
locks = new(connectionCount);
profileService = new ProfileService(TimeProvider.System, recoder, scenario);
profileService = new ProfileService(timeProvider, recoder, scenario);

for (var i = 0; i < connectionCount; i++)
{
Expand Down
21 changes: 13 additions & 8 deletions perf/BenchmarkApp/PerformanceTest.Client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ async Task Main(

for (var i = 1; i <= rounds; i++)
{
WriteLog($"Round: {i}");
WriteLog($"Scenario {scenario}, Round: {i}");
if (!resultsByScenario.TryGetValue(scenario2, out var results))
{
results = new List<PerformanceResult>(10000);
Expand All @@ -99,9 +99,11 @@ async Task Main(
var result = await RunScenarioAsync(scenario2, config, config.ChannelList, controlServiceClient, datadog);
results.Add(result);

WriteLog($"Interval 1s for next scenario...");
WriteLog($"Interval 1s for next run/scenario...");
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); // interval
}

await controlServiceClient.NotifyCompleteScenarioAsync();
}
WriteLog($"All scenario complete");

Expand Down Expand Up @@ -200,14 +202,16 @@ async Task<PerformanceResult> RunScenarioAsync(ScenarioType scenario, ScenarioCo
ScenarioType.StreamingHubLargePayload32K => () => new StreamingHubLargePayload32KScenario(),
ScenarioType.StreamingHubLargePayload64K => () => new StreamingHubLargePayload64KScenario(),
ScenarioType.ServerStreaming => () => new ServerStreamingScenario(),
ScenarioType.Broadcast => () => new BroadcastScenario(),
ScenarioType.Broadcast60Fps => () => new Broadcast60FpsScenario(),
ScenarioType.Broadcast30Fps => () => new Broadcast30FpsScenario(),
ScenarioType.Broadcast15Fps => () => new Broadcast15FpsScenario(),
ScenarioType.MetaverseBroadcast60Fps => () => new MetaverseBroadcast60FpsScenario(),
ScenarioType.MetaverseBroadcast30Fps => () => new MetaverseBroadcast30FpsScenario(),
ScenarioType.MetaverseBroadcast15Fps => () => new MetaverseBroadcast15FpsScenario(),
_ => throw new Exception($"Unknown Scenario: {scenario}"),
};

var ctx = new PerformanceTestRunningContext(connectionCount: config.Channels, serverTimeout: (config.Warmup, config.Duration), datadog, scenario);
var ctx = new PerformanceTestRunningContext(connectionCount: config.Channels, serverTimeout: (config.Warmup, config.Duration), datadog, scenario, config.TimeProvider);
using var cts = new CancellationTokenSource();
var cleanIndex = 0;
var threadBefore = ThreadPool.ThreadCount;
Expand Down Expand Up @@ -488,12 +492,12 @@ private static IReadOnlyList<PerformanceResult> RemoveOutlinerByIQR(IReadOnlyLis
var mean = data.Select(x => x.Latency.Mean).OrderBy(x => x).ToArray();

// get outliner for rps
var lowerBoundRps = OutlinerHelper.GetLowerBound(rps);
var upperBoundRps = OutlinerHelper.GetUpperBound(rps);
var lowerBoundRps = OutlierIqr.GetLowerBound(rps);
var upperBoundRps = OutlierIqr.GetUpperBound(rps);

// get outliner for mean
var lowerBoundMean = OutlinerHelper.GetLowerBound(mean);
var upperBoundMean = OutlinerHelper.GetUpperBound(mean);
var lowerBoundMean = OutlierIqr.GetLowerBound(mean);
var upperBoundMean = OutlierIqr.GetUpperBound(mean);

// compute tuple in range
var filteredData = data
Expand All @@ -514,6 +518,7 @@ public class ScenarioConfiguration
public int Streams { get; }
public int Channels { get; }
public bool Verbose { get; }
public TimeProvider TimeProvider { get; } = TimeProvider.System;

private readonly bool clientAuth;
private bool useHttp3;
Expand Down
Loading
Loading