using System.Diagnostics; using System.Net; using System.Net.Sockets; using MeWeb.Shared; namespace MeWeb.Core.Net; public class SpeedReport { public double DownMbps { get; set; } public double UpMbps { get; set; } public int DownBytes { get; set; } public int UpBytes { get; set; } public bool Limited { get; set; } public int RoundTripMs { get; set; } } public static class SpeedCheck { public static async Task Run(HttpClient https, LinkKind kind, CancellationToken ct) { var mobile = kind == LinkKind.Mobile; var report = new SpeedReport { DownBytes = mobile ? 2 * 1024 * 1024 : 12 * 1024 * 1024, UpBytes = mobile ? 1024 * 1024 : 4 * 1024 * 1024 }; report.RoundTripMs = await RoundTrip(ct); try { using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); timeout.CancelAfter(TimeSpan.FromSeconds(30)); var clock = Stopwatch.StartNew(); using (var response = await https.GetAsync(Endpoints.Base + Endpoints.Speed.GET_M_Down(report.DownBytes), HttpCompletionOption.ResponseHeadersRead, timeout.Token)) { if ((int)response.StatusCode == 429) { report.Limited = true; return report; } response.EnsureSuccessStatusCode(); await using var body = await response.Content.ReadAsStreamAsync(timeout.Token); var buffer = new byte[64 * 1024]; var warm = report.DownBytes / 4; long total = 0; long warmBytes = 0; var warmAt = TimeSpan.Zero; int read; while ((read = await body.ReadAsync(buffer, timeout.Token)) > 0) { total += read; if (warmBytes > 0 || total < warm) continue; warmBytes = total; warmAt = clock.Elapsed; } var steady = clock.Elapsed - warmAt; report.DownMbps = warmBytes > 0 && total > warmBytes && steady > TimeSpan.FromMilliseconds(50) ? Mbps(total - warmBytes, steady) : Mbps(total, clock.Elapsed); } var payload = new byte[report.UpBytes]; Random.Shared.NextBytes(payload); clock.Restart(); using (var response = await https.PostAsync(Endpoints.Base + Endpoints.Speed.POST_Up, new ByteArrayContent(payload), timeout.Token)) { if (response.IsSuccessStatusCode) report.UpMbps = Mbps(payload.Length, clock.Elapsed); } } catch (Exception) when (!ct.IsCancellationRequested) { } return report; } static async Task RoundTrip(CancellationToken ct) { try { var addresses = await Dns.GetHostAddressesAsync(Endpoints.Host, ct); if (addresses.Length == 0) return 0; var samples = new List(); for (var i = 0; i < 3; i++) { using var client = new TcpClient(addresses[0].AddressFamily); using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); timeout.CancelAfter(TimeSpan.FromSeconds(3)); var clock = Stopwatch.StartNew(); await client.ConnectAsync(addresses[0], 443, timeout.Token); samples.Add(clock.Elapsed.TotalMilliseconds); } return (int)Math.Round(samples.Order().ElementAt(samples.Count / 2)); } catch (Exception) when (!ct.IsCancellationRequested) { return 0; } } static double Mbps(long bytes, TimeSpan elapsed) => elapsed.TotalSeconds <= 0 ? 0 : Math.Round(bytes * 8 / elapsed.TotalSeconds / 1_000_000, 1); }