Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
108 changes: 107 additions & 1 deletion benchmarks/Yamux.Benchmark/Program.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Running;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Linq;
using System.Net;
using System.Net.Sockets;

Expand All @@ -27,9 +29,11 @@ public static async Task Main(string[] args)
}

[SimpleJob(RunStrategy.Throughput, launchCount: 1, warmupCount: 3, invocationCount: 5)]
[MemoryDiagnoser]
public class Yamux
{
private Memory<byte> _buffer;
private string? _goServerPath;

[Params(1, 50, 500)]
public int MBs = 50;
Expand Down Expand Up @@ -57,6 +61,12 @@ public void Setup()
_serverSock.Listen();

_port = ((IPEndPoint)_serverSock.LocalEndPoint!).Port;

var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir != null && !dir.EnumerateFiles("Yamux.sln").Any())
dir = dir.Parent;
var repoRoot = dir?.FullName ?? throw new InvalidOperationException("Cannot find Yamux.sln");
_goServerPath = Path.Combine(repoRoot, "benchmarks", "bin", "go-server.exe");
}

[GlobalCleanup]
Expand Down Expand Up @@ -201,6 +211,102 @@ Task RunChannelAsync(IReadOnlySessionChannel channel)

await Task.WhenAll(serverTask, clientTask);
}

[Benchmark]
public async Task CsharpToGoAsync()
{
using var goServer = GoServerProcess.Start(_goServerPath!);

var clientTask = Task.Run(async () =>
{
var sock = new Socket(SocketType.Stream, ProtocolType.Tcp);
await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, goServer.Port));

var opt = new SessionOptions
{
EnableKeepAlive = false,
DefaultChannelOptions = new SessionChannelOptions
{
MaxDataFrameSize = 1024 * 64,
}
};
var session = sock!.AsYamuxSession(true, options: opt, keepOpen: false);
session.Start();

List<Task> channels = new List<Task>();
int iterationsPerStream = (MBs * 32) / Streams;

for (int i = 0; i < Streams; i++)
{
channels.Add(Task.Run(async () =>
{
using var channel = await session.OpenChannelAsync(false);

for (int j = 0; j < iterationsPerStream; j++)
{
await channel.WriteAsync(_buffer);
}

var timeout = (await channel.WhenRemoteAckAsync(TimeSpan.FromSeconds(3)) == false);
if (timeout)
{
throw new TimeoutException("Timed out waiting for remote ack");
}

channel.Close();
}));
}

await Task.WhenAll(channels);

sock.Close();
});

await clientTask;
}
}

internal sealed class GoServerProcess : IDisposable
{
private readonly Process _process;
public int Port { get; }

public static GoServerProcess Start(string exePath)
{
var psi = new ProcessStartInfo(exePath)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
var proc = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start Go server");

var line = proc.StandardOutput.ReadLine() ?? "";
if (!line.StartsWith("LISTENING:"))
{
proc.Kill();
throw new InvalidOperationException($"Unexpected output: {line}");
}
var port = int.Parse(line.AsSpan("LISTENING:".Length));
return new GoServerProcess(proc, port);
}

private GoServerProcess(Process process, int port)
{
_process = process;
Port = port;
}

public void Dispose()
{
if (!_process.HasExited)
{
_process.Kill();
_process.WaitForExit(2000);
}
_process.Dispose();
}
}
}
}
19 changes: 19 additions & 0 deletions benchmarks/build-go-server.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
param([switch]$Force)

$ErrorActionPreference = "Stop"
$targetDir = Join-Path $PSScriptRoot "bin"
if (!(Test-Path $targetDir)) { New-Item -ItemType Directory -Path $targetDir -Force | Out-Null }

$serverExe = Join-Path $targetDir "go-server.exe"
if (!(Test-Path $serverExe) -or $Force) {
Push-Location (Join-Path $PSScriptRoot "go-server")
try {
go mod tidy
go build -o $serverExe .
Write-Host "Built: $serverExe"
} finally {
Pop-Location
}
} else {
Write-Host "Already exists: $serverExe (use -Force to rebuild)"
}
95 changes: 95 additions & 0 deletions benchmarks/compare.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
param([switch]$Full)

$ErrorActionPreference = "Stop"

Write-Host "=== Building Go benchmark ===" -ForegroundColor Cyan
Push-Location (Join-Path $PSScriptRoot "go-benchmark")
try {
go build -o (Join-Path $PSScriptRoot "bin\go-benchmark.exe") .
} finally {
Pop-Location
}

Write-Host "=== Running Go benchmark ===" -ForegroundColor Cyan
$goResults = @{}
$goOutput = & (Join-Path $PSScriptRoot "bin\go-benchmark.exe") 2>&1
$goOutput | ForEach-Object { Write-Host $_ -ForegroundColor Gray }

# Parse Go results
$goOutput | Select-String '^(Go\w+)\s+(\d+)\s+(\d+)\s+([\d.]+[a-z]?)\s+([\d.]+)' | ForEach-Object {
$method = $_.Matches.Groups[1].Value
$mbs = $_.Matches.Groups[2].Value
$streams = $_.Matches.Groups[3].Value
$mbps = $_.Matches.Groups[5].Value
$goResults["$method|$mbs|$streams"] = [double]$mbps
}

Write-Host "`n=== Running C# benchmark (filtered) ===" -ForegroundColor Cyan
$csResults = @{}
$tempFile = Join-Path $env:TEMP "cs-bench-out.txt"

if ($Full) {
$csOutput = dotnet run -c Release --project (Join-Path $PSScriptRoot "..\Yamux.Benchmark") -- --filter *CsharpToGo* 2>&1
} else {
$csOutput = dotnet run -c Release --project (Join-Path $PSScriptRoot "..\Yamux.Benchmark") -- --filter *CsharpToGo* 2>&1
}
$csOutput | Out-File $tempFile

# Parse C# results - look for Mean values
$currentMethod = ""
$currentMBs = 0
$currentStreams = 0
Get-Content $tempFile | ForEach-Object {
if ($_ -match 'CsharpToGoAsync.*MBs:\s*(\d+).*Streams:\s*(\d+)') {
$currentMBs = [int]$Matches[1]
$currentStreams = [int]$Matches[2]
$currentMethod = "CsharpToGo"
}
if ($_ -match 'Mean\s*=\s*([\d.]+)\s*ms') {
$meanMs = [double]$Matches[1]
if ($currentMethod -eq "CsharpToGo" -and $currentMBs -gt 0) {
$totalMB = $currentMBs * $currentStreams
$mbps = ($totalMB) / ($meanMs / 1000.0)
$csResults["$currentMethod|$currentMBs|$currentStreams"] = [double]$mbps
}
}
}

Write-Host "`n============================================" -ForegroundColor Green
Write-Host " SIDE-BY-SIDE: C# Yamux vs Go Yamux" -ForegroundColor Green
Write-Host "============================================" -ForegroundColor Green
Write-Host ""

# Find all unique parameter combinations
$allKeys = @{}
$goResults.Keys | ForEach-Object { $allKeys[$_] = $true }
$csResults.Keys | ForEach-Object { $allKeys[$_] = $true }

Write-Host ("{0,-10} {1,5} {2,8} {3,15} {4,15} {5,15}" -f "Method", "MBs", "Streams", "Go MB/s", "C# MB/s", "Ratio C#/Go")
Write-Host ("-" * 70)

$sortedKeys = $allKeys.Keys | Sort-Object
foreach ($key in $sortedKeys) {
$parts = $key -split '\|'
$method = $parts[0]
$mbs = $parts[1]
$streams = $parts[2]

$goVal = $goResults[$key]
$csVal = $csResults[$key]

$goStr = if ($goVal) { "{0,13:F1}" -f $goVal } else { " N/A" }
$csStr = if ($csVal) { "{0,13:F1}" -f $csVal } else { " N/A" }

$ratio = ""
if ($goVal -and $csVal -and $goVal -gt 0) {
$ratio = "{0,13:F2}x" -f ($csVal / $goVal)
} elseif ($goVal) {
$ratio = " -"
}

Write-Host ("{0,-10} {1,5} {2,8} {3,15} {4,15} {5,15}" -f $method, $mbs, $streams, $goStr, $csStr, $ratio)
}

Write-Host ""
Write-Host "Ratio > 1.0 means C# is faster. Ratio < 1.0 means Go is faster."
5 changes: 5 additions & 0 deletions benchmarks/go-benchmark/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/paulb/Yamux/benchmarks/go-benchmark

go 1.19

require github.com/hashicorp/yamux v0.1.1
2 changes: 2 additions & 0 deletions benchmarks/go-benchmark/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
Loading
Loading