From 4c2f2a026c6bf2d392b4bc21a86b87c245c4be9a Mon Sep 17 00:00:00 2001 From: Eric-Joker <1277243150@qq.com> Date: Wed, 7 Aug 2024 15:25:49 +0800 Subject: [PATCH] Improve more precise speed limits Co-authored-by: DismissedLight <1686188646@qq.com> --- .../Pages/Setting/DownloadSettingPage.xaml.cs | 3 +- .../Services/Download/InstallGameManager.cs | 27 ++-------- .../Services/Download/InstallGameService.cs | 52 ++++++------------- .../Download/InstallGameStateModel.cs | 26 ++++------ .../TokenBucketRateLimiterExtension.cs | 25 +++++++++ 5 files changed, 58 insertions(+), 75 deletions(-) create mode 100644 src/Starward/Services/Download/TokenBucketRateLimiterExtension.cs diff --git a/src/Starward/Pages/Setting/DownloadSettingPage.xaml.cs b/src/Starward/Pages/Setting/DownloadSettingPage.xaml.cs index 1ba1cbfd1..86363f8f8 100644 --- a/src/Starward/Pages/Setting/DownloadSettingPage.xaml.cs +++ b/src/Starward/Pages/Setting/DownloadSettingPage.xaml.cs @@ -5,6 +5,7 @@ using Starward.Services.Download; using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using Windows.Storage; using Windows.System; @@ -70,7 +71,7 @@ partial void OnDefaultInstallPathChanged(string? value) partial void OnSpeedLimitChanged(int value) { int speed = value <= 0 ? int.MaxValue : value * 1024; - InstallGameManager.SpeedLimitBytesPerSecond = speed; + Interlocked.Exchange(ref InstallGameManager.SpeedLimitBytesPerSecond, speed); AppConfig.SpeedLimitKBPerSecond = value; InstallGameManager.SetRateLimit(); } diff --git a/src/Starward/Services/Download/InstallGameManager.cs b/src/Starward/Services/Download/InstallGameManager.cs index cc3aefca0..d1dd1a0c9 100644 --- a/src/Starward/Services/Download/InstallGameManager.cs +++ b/src/Starward/Services/Download/InstallGameManager.cs @@ -5,9 +5,8 @@ using System; using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; -using System.Threading.RateLimiting; using System.Threading; -using System.Threading.Tasks; +using System.Threading.RateLimiting; namespace Starward.Services.Download; @@ -32,18 +31,13 @@ private InstallGameManager() - public static int SpeedLimitBytesPerSecond { get; set; } + public static long SpeedLimitBytesPerSecond; public static TokenBucketRateLimiter RateLimiter { get; private set; } - public static bool IsEnableSpeedLimit => SpeedLimitBytesPerSecond != int.MaxValue; - - - // BUFFER_SIZE越大限速时保留速度也会越大,可以用来抵消迷之原因造成的超速¿ - // speedLimit<=2MB/s → 16Bytes else 1KB - public static int BUFFER_SIZE => (SpeedLimitBytesPerSecond <= (1 << 21)) ? (1 << 4) : (1 << 10); + public static bool IsEnableSpeedLimit => Interlocked.Read(ref SpeedLimitBytesPerSecond) != int.MaxValue; public event EventHandler InstallTaskAdded; @@ -54,27 +48,20 @@ private InstallGameManager() - public static event EventHandler LimitStateChanged; - - - public static void SetRateLimit() { - // 小于speedLimitBytesPerSecond的最大能被BUFFER_SIZE整除的值 - var speedLimitBytesPerPeriod = Math.Max(SpeedLimitBytesPerSecond / 25 / BUFFER_SIZE * BUFFER_SIZE, BUFFER_SIZE); + var speedLimitBytesPerPeriod = (int)SpeedLimitBytesPerSecond / 25; RateLimiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions { TokenLimit = speedLimitBytesPerPeriod, // 0.04: 将每秒切割为上面的25份,间隔越小速度越精准。 // 因补充令牌逻辑运行耗时远大于期望,若间隔极小,将无法达到最高限速。 - ReplenishmentPeriod = TimeSpan.FromSeconds(Math.Max(BUFFER_SIZE / (double)SpeedLimitBytesPerSecond, 0.04)), + ReplenishmentPeriod = TimeSpan.FromSeconds(0.04), TokensPerPeriod = speedLimitBytesPerPeriod, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, AutoReplenishment = true }); - if (LimitStateChanged != null && LimitStateChanged.GetInvocationList().Length > 0) - Task.Run(() => LimitStateChanged.Invoke(null, EventArgs.Empty)); } @@ -105,8 +92,6 @@ public void AddInstallService(InstallGameService service) model.InstallFailed += Model_InstallFailed; model.InstallCanceled -= Model_InstallCanceled; model.InstallCanceled += Model_InstallCanceled; - LimitStateChanged -= model._manager_LimitStateChanged; - LimitStateChanged += model._manager_LimitStateChanged; InstallTaskAdded?.Invoke(this, model); } @@ -121,7 +106,6 @@ private void Model_InstallFinished(object? sender, EventArgs e) model.InstallFinished -= Model_InstallFinished; model.InstallFailed -= Model_InstallFailed; model.InstallCanceled -= Model_InstallCanceled; - LimitStateChanged -= model._manager_LimitStateChanged; InstallTaskRemoved?.Invoke(this, model); WeakReferenceMessenger.Default.Send(new InstallGameFinishedMessage(model.GameBiz)); NotificationBehavior.Instance.Success(Lang.InstallGameManager_DownloadTaskCompleted, $"{InstallTaskToString(model.Service.InstallTask)} - {model.GameBiz.ToGameName()} - {model.GameBiz.ToGameServer()}", 0); @@ -150,7 +134,6 @@ private void Model_InstallCanceled(object? sender, EventArgs e) model.InstallFinished -= Model_InstallFinished; model.InstallFailed -= Model_InstallFailed; model.InstallCanceled -= Model_InstallCanceled; - LimitStateChanged -= model._manager_LimitStateChanged; InstallTaskRemoved?.Invoke(this, model); } } diff --git a/src/Starward/Services/Download/InstallGameService.cs b/src/Starward/Services/Download/InstallGameService.cs index 8aaefe35f..d924a108e 100644 --- a/src/Starward/Services/Download/InstallGameService.cs +++ b/src/Starward/Services/Download/InstallGameService.cs @@ -21,7 +21,6 @@ using System.Text; using System.Text.Json.Nodes; using System.Threading; -using System.Threading.RateLimiting; using System.Threading.Tasks; using Vanara.PInvoke; @@ -1073,14 +1072,6 @@ protected void Finish() - public long HTTP_BUFFER_SIZE = InstallGameManager.BUFFER_SIZE; - - - - public bool IsEnableSpeedLimit = InstallGameManager.IsEnableSpeedLimit; - - - protected int _totalCount; public int TotalCount => _totalCount; @@ -1203,9 +1194,7 @@ protected async Task DownloadItemAsync(InstallGameItem item, CancellationToken c { file_target = item.WriteAsTempFile ? file_tmp : file; } - var httpBuffer = ArrayPool.Shared.Rent((int)Interlocked.Read(ref HTTP_BUFFER_SIZE)); var buffer = ArrayPool.Shared.Rent(BUFFER_SIZE); - int bufferOffset = 0; try { using var fs = File.Open(file_target, FileMode.OpenOrCreate); @@ -1218,43 +1207,34 @@ protected async Task DownloadItemAsync(InstallGameItem item, CancellationToken c response.EnsureSuccessStatusCode(); using var hs = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); int length; - while ((length = await hs.ReadAsync(IsEnableSpeedLimit ? httpBuffer : buffer, cancellationToken).ConfigureAwait(false)) != 0) + while ((length = await hs.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0) { - if (IsEnableSpeedLimit) + if (InstallGameManager.IsEnableSpeedLimit) { - RateLimitLease lease; - do + int totalWritten = 0; + while (totalWritten < length) { - lease = await InstallGameManager.RateLimiter.AcquireAsync(length, cancellationToken).ConfigureAwait(false); - if (!lease.IsAcquired && lease.TryGetMetadata(MetadataName.RetryAfter, out TimeSpan retryAfter)) - await Task.Delay((int)Math.Max(Math.Sqrt(retryAfter.TotalMilliseconds), 1), cancellationToken).ConfigureAwait(false); - } while (!lease.IsAcquired); - int remainingSpace = buffer.Length - bufferOffset; - if (length > remainingSpace) - { - Buffer.BlockCopy(httpBuffer, 0, buffer, bufferOffset, remainingSpace); - await fs.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); - Buffer.BlockCopy(httpBuffer, remainingSpace, buffer, 0, length - remainingSpace); - bufferOffset = length - remainingSpace; - } - else - { - Buffer.BlockCopy(httpBuffer, 0, buffer, bufferOffset, length); - bufferOffset += length; + int remaining = length - totalWritten; + if (TokenBucketRateLimiterExtension.TryAcquire(InstallGameManager.RateLimiter, remaining, out int tokensAcquired, out _)) + await Task.Delay(1, cancellationToken).ConfigureAwait(false); + else + { + await fs.WriteAsync(buffer.AsMemory(totalWritten, tokensAcquired), cancellationToken).ConfigureAwait(false); + totalWritten += tokensAcquired; + Interlocked.Add(ref _finishBytes, tokensAcquired); + } } } else + { await fs.WriteAsync(buffer.AsMemory(0, length), cancellationToken).ConfigureAwait(false); - Interlocked.Add(ref _finishBytes, length); + Interlocked.Add(ref _finishBytes, length); + } } - // Write any remaining data in buffer - if (bufferOffset > 0) - await fs.WriteAsync(buffer.AsMemory(0, bufferOffset), cancellationToken).ConfigureAwait(false); } } finally { - ArrayPool.Shared.Return(httpBuffer); ArrayPool.Shared.Return(buffer); } } diff --git a/src/Starward/Services/Download/InstallGameStateModel.cs b/src/Starward/Services/Download/InstallGameStateModel.cs index ef7e53b6b..ab41a43d1 100644 --- a/src/Starward/Services/Download/InstallGameStateModel.cs +++ b/src/Starward/Services/Download/InstallGameStateModel.cs @@ -244,8 +244,16 @@ private void ComputeSpeed(InstallGameState state) } else { - _recentSpeed.RemoveAll(value => Math.Abs(value - _speedBytesPerSecond) / _speedBytesPerSecond > 0.25); - _recentSpeed.RemoveRange(0, Math.Max(_recentSpeed.Count - 9, 0)); + if (InstallGameManager.IsEnableSpeedLimit) + { + _recentSpeed.RemoveAll(value => Math.Abs(value - _speedBytesPerSecond) / _speedBytesPerSecond > 0.05); + _recentSpeed.RemoveRange(0, Math.Max(_recentSpeed.Count - 59, 0)); + } + else + { + _recentSpeed.RemoveAll(value => Math.Abs(value - _speedBytesPerSecond) / _speedBytesPerSecond > 0.25); + _recentSpeed.RemoveRange(0, Math.Max(_recentSpeed.Count - 9, 0)); + } _recentSpeed.Add(_speedBytesPerSecond); averageSpeed = _recentSpeed.Average(); var seconds = (Service.TotalBytes - Service.FinishBytes) / averageSpeed; @@ -270,7 +278,6 @@ private void ComputeSpeed(InstallGameState state) private void _service_StateChanged(object? sender, InstallGameState e) { uiContext.Post(_ => UpdateState(), null); - } @@ -281,17 +288,4 @@ private void Service_InstallFailed(object? sender, Exception e) } - - public void _manager_LimitStateChanged(object? sender, EventArgs e) - { - if (Service.State is InstallGameState.Download && Service.HTTP_BUFFER_SIZE != InstallGameManager.BUFFER_SIZE || Service.IsEnableSpeedLimit != InstallGameManager.IsEnableSpeedLimit) - { - Service.Pause(); - Task.WhenAll(Service.TaskItems).Wait(); - Service.HTTP_BUFFER_SIZE = InstallGameManager.BUFFER_SIZE; - Service.IsEnableSpeedLimit = InstallGameManager.IsEnableSpeedLimit; - Service.Continue(); - InstallStarted?.Invoke(this, EventArgs.Empty); - } - } } diff --git a/src/Starward/Services/Download/TokenBucketRateLimiterExtension.cs b/src/Starward/Services/Download/TokenBucketRateLimiterExtension.cs new file mode 100644 index 000000000..f48602834 --- /dev/null +++ b/src/Starward/Services/Download/TokenBucketRateLimiterExtension.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading; +using System.Threading.RateLimiting; +using System.Runtime.CompilerServices; + +namespace Starward.Services.Download; + +internal static class TokenBucketRateLimiterExtension +{ + // IMPORTANT: acquired can be none 0 values if false is returned + public static bool TryAcquire(this TokenBucketRateLimiter rateLimiter, int permits, out int acquired, out TimeSpan retryAfter) + { + lock (PrivateGetLock(rateLimiter)) + acquired = Math.Min(permits, (int)Volatile.Read(ref PrivateGetTokenCount(rateLimiter))); + return rateLimiter.AttemptAcquire(acquired).TryGetMetadata(MetadataName.RetryAfter, out retryAfter); + } + + // private object Lock → _queue + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_Lock")] + private static extern object PrivateGetLock(TokenBucketRateLimiter rateLimiter); + + // private double _tokenCount; + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_tokenCount")] + private static extern ref double PrivateGetTokenCount(TokenBucketRateLimiter rateLimiter); +}