Measures your internet speed using Cloudflare's global edge network with 6 concurrent connections.
Measures your internet speed using Cloudflare's global edge network with multiple concurrent connections.
@* Gateway (Direct) Option - Primary *@
diff --git a/src/NetworkOptimizer.Web/Services/CloudflareSpeedTestService.cs b/src/NetworkOptimizer.Web/Services/CloudflareSpeedTestService.cs
index fee7f85a..a57f9d0b 100644
--- a/src/NetworkOptimizer.Web/Services/CloudflareSpeedTestService.cs
+++ b/src/NetworkOptimizer.Web/Services/CloudflareSpeedTestService.cs
@@ -1,6 +1,5 @@
using System.Diagnostics;
using System.Net;
-using System.Runtime;
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore;
@@ -22,8 +21,8 @@ public partial class CloudflareSpeedTestService
private const string DownloadPath = "__down?bytes=";
private const string UploadPath = "__up";
- // Concurrency and duration settings (matching cloudflare-speed-cli defaults)
- private const int Concurrency = 6;
+ // Concurrency and duration settings
+ private const int Concurrency = 8;
private static readonly TimeSpan DownloadDuration = TimeSpan.FromSeconds(10);
private static readonly TimeSpan UploadDuration = TimeSpan.FromSeconds(10);
private const int DownloadBytesPerRequest = 10_000_000; // 10 MB per request (matches cloudflare-speed-cli)
@@ -35,6 +34,7 @@ public partial class CloudflareSpeedTestService
private readonly IDbContextFactory _dbFactory;
private readonly INetworkPathAnalyzer _pathAnalyzer;
private readonly IConfiguration _configuration;
+ private readonly Iperf3ServerService _iperf3ServerService;
// Observable test state (polled by UI components)
private readonly object _lock = new();
@@ -82,13 +82,15 @@ public CloudflareSpeedTestService(
IHttpClientFactory httpClientFactory,
IDbContextFactory dbFactory,
INetworkPathAnalyzer pathAnalyzer,
- IConfiguration configuration)
+ IConfiguration configuration,
+ Iperf3ServerService iperf3ServerService)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
_dbFactory = dbFactory;
_pathAnalyzer = pathAnalyzer;
_configuration = configuration;
+ _iperf3ServerService = iperf3ServerService;
}
///
@@ -98,7 +100,7 @@ public record CloudflareMetadata(string Ip, string City, string Country, string
///
/// Run a full Cloudflare WAN speed test with progress reporting.
- /// Uses 6 concurrent connections per direction, similar to cloudflare-speed-cli.
+ /// Uses multiple concurrent connections per direction, similar to cloudflare-speed-cli.
/// Test state is tracked on the service so UI components can navigate away and
/// poll CurrentProgress / LastCompletedResult when they return.
///
@@ -119,6 +121,14 @@ public record CloudflareMetadata(string Ip, string City, string Country, string
try
{
+ // Pause iperf3 server during WAN speed test to free pipe handles.
+ // GC compaction while iperf3 pipe readers are active causes AccessViolationException.
+ if (_iperf3ServerService.IsRunning)
+ {
+ _logger.LogInformation("Pausing iperf3 server for WAN speed test");
+ await _iperf3ServerService.PauseAsync();
+ }
+
_logger.LogInformation("Starting Cloudflare WAN speed test ({Concurrency} concurrent connections)", Concurrency);
// Wrap progress to always update service-level state (for navigate-away polling)
@@ -162,10 +172,6 @@ void Report(string phase, int percent, string? status)
downloadMbps, downloadBytes, Concurrency, dlLatencyMs);
Report("Download complete", 55, $"Down: {downloadMbps:F1} Mbps");
- // Reclaim download phase memory before starting upload
- GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, blocking: true, compacting: true);
- GC.WaitForPendingFinalizers();
-
// Phase 4: Upload (55-95%) - concurrent connections + latency probes
Report("Testing upload", 56, null);
var (uploadBps, uploadBytes, ulLatencyMs, ulJitterMs) = await MeasureThroughputAsync(
@@ -237,11 +243,6 @@ void Report(string phase, int percent, string? status)
Report("Complete", 100, $"Down: {downloadMbps:F1} / Up: {uploadMbps:F1} Mbps");
lock (_lock) _lastCompletedResult = result;
- // Reclaim upload phase memory and return to OS
- GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, blocking: true, compacting: true);
- GC.WaitForPendingFinalizers();
- GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, blocking: true, compacting: true);
-
// Trigger background path analysis with Cloudflare-reported WAN IP and pre-resolved WAN group
var cfWanIp = metadata.Ip;
var resolvedWanGroup = result.WanNetworkGroup;
@@ -288,6 +289,8 @@ void Report(string phase, int percent, string? status)
}
finally
{
+ // Resume iperf3 server after WAN speed test completes (or fails/cancels)
+ await _iperf3ServerService.ResumeAsync();
lock (_lock) _isRunning = false;
}
}
@@ -562,6 +565,8 @@ private static string ColoToCityName(string colo)
Interlocked.Add(ref totalBytes, bytesWritten));
using var uploadResponse = await workerClient.PostAsync(url, content, linked.Token);
Interlocked.Increment(ref requestCount);
+ // Drain response body to enable TCP connection reuse (HTTP/1.1)
+ await uploadResponse.Content.CopyToAsync(Stream.Null, linked.Token);
if (!uploadResponse.IsSuccessStatusCode)
{
Interlocked.Increment(ref errorCount);
diff --git a/src/NetworkOptimizer.Web/Services/Iperf3ServerService.cs b/src/NetworkOptimizer.Web/Services/Iperf3ServerService.cs
index f7da174b..3f83e51f 100644
--- a/src/NetworkOptimizer.Web/Services/Iperf3ServerService.cs
+++ b/src/NetworkOptimizer.Web/Services/Iperf3ServerService.cs
@@ -18,6 +18,10 @@ public class Iperf3ServerService : BackgroundService
private Process? _iperf3Process;
private const int Iperf3Port = 5201;
+ // Pause/resume support (used during WAN speed tests to free pipe handles)
+ private volatile bool _isPaused;
+ private TaskCompletionSource? _resumeTcs;
+
public Iperf3ServerService(
ILogger logger,
ClientSpeedTestService clientSpeedTestService,
@@ -43,6 +47,47 @@ public Iperf3ServerService(
///
public string? FailureMessage { get; private set; }
+ ///
+ /// Pause the iperf3 server (kills the process and prevents restart).
+ /// Used during WAN speed tests to free pipe handles that interfere with GC compaction.
+ ///
+ public async Task PauseAsync()
+ {
+ if (_isPaused) return;
+ _isPaused = true;
+ _resumeTcs = new TaskCompletionSource();
+
+ // Kill current iperf3 process
+ if (_iperf3Process is { HasExited: false })
+ {
+ try { _iperf3Process.Kill(entireProcessTree: true); }
+ catch (Exception ex) { _logger.LogDebug(ex, "Error killing iperf3 process during pause"); }
+ }
+
+ // Also kill orphans to ensure port is free when we resume
+ await KillOrphanedIperf3ProcessesAsync();
+
+ _logger.LogInformation("iperf3 server paused");
+ }
+
+ ///
+ /// Resume the iperf3 server after a pause.
+ /// Kills any orphaned iperf3 processes and waits for the port to be released.
+ ///
+ public async Task ResumeAsync()
+ {
+ if (!_isPaused) return;
+
+ // Kill any orphaned iperf3 still holding the port, then wait for OS to release it
+ await KillOrphanedIperf3ProcessesAsync();
+ await Task.Delay(1000);
+
+ _isPaused = false;
+ _resumeTcs?.TrySetResult();
+ _resumeTcs = null;
+ _logger.LogInformation("iperf3 server resumed");
+ }
+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Check if iperf3 server mode is enabled
@@ -60,6 +105,22 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
while (!stoppingToken.IsCancellationRequested)
{
+ // Wait if paused (e.g., during WAN speed test to free pipe handles)
+ if (_isPaused && _resumeTcs != null)
+ {
+ _logger.LogDebug("iperf3 server paused, waiting for resume signal");
+ try
+ {
+ await _resumeTcs.Task.WaitAsync(stoppingToken);
+ }
+ catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
+ {
+ break;
+ }
+ consecutiveImmediateExits = 0;
+ continue;
+ }
+
try
{
var ranSuccessfully = await RunIperf3ServerAsync(stoppingToken);
diff --git a/src/cfspeedtest/speedtest/throughput.go b/src/cfspeedtest/speedtest/throughput.go
index c19496fb..8b725453 100644
--- a/src/cfspeedtest/speedtest/throughput.go
+++ b/src/cfspeedtest/speedtest/throughput.go
@@ -126,6 +126,7 @@ func MeasureThroughput(ctx context.Context, isUpload bool, cfg Config) (*Through
continue
}
}
+ io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
diff --git a/src/cfspeedtest/speedtest/types.go b/src/cfspeedtest/speedtest/types.go
index 7f939eea..08b2b5ff 100644
--- a/src/cfspeedtest/speedtest/types.go
+++ b/src/cfspeedtest/speedtest/types.go
@@ -51,7 +51,7 @@ type Config struct {
// DefaultConfig returns sensible defaults matching the C# service.
func DefaultConfig() Config {
return Config{
- Streams: 6,
+ Streams: 8,
Duration: 10 * time.Second,
DownloadSize: 10_000_000, // 10 MB
UploadSize: 5_000_000, // 5 MB