diff --git a/src/Starward/Controls/InstallGameController.xaml.cs b/src/Starward/Controls/InstallGameController.xaml.cs index e4613bb6b..be2a8b486 100644 --- a/src/Starward/Controls/InstallGameController.xaml.cs +++ b/src/Starward/Controls/InstallGameController.xaml.cs @@ -134,7 +134,6 @@ private void UpdateSpeedState() long totalBytes = 0; long finishedBytes = 0; bool determinate = false; - _installGameManager.UpdateSpeedState(); foreach (var model in InstallServices) { model.UpdateState(); diff --git a/src/Starward/Pages/Setting/DownloadSettingPage.xaml.cs b/src/Starward/Pages/Setting/DownloadSettingPage.xaml.cs index cffec065c..8676606ef 100644 --- a/src/Starward/Pages/Setting/DownloadSettingPage.xaml.cs +++ b/src/Starward/Pages/Setting/DownloadSettingPage.xaml.cs @@ -5,7 +5,6 @@ using Starward.Services.Download; using System; using System.IO; -using System.Threading.RateLimiting; using System.Threading.Tasks; using Windows.Storage; using Windows.System; @@ -70,15 +69,9 @@ partial void OnDefaultInstallPathChanged(string? value) private int speedLimit = AppConfig.SpeedLimitKBPerSecond; partial void OnSpeedLimitChanged(int value) { - InstallGameManager.SpeedLimitBytesPerSecond = value == 0 ? int.MaxValue : value * 1024; - InstallGameManager.rateLimiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions - { - TokensPerPeriod = InstallGameManager.SpeedLimitBytesPerSecond, - ReplenishmentPeriod = TimeSpan.FromSeconds(1), - TokenLimit = InstallGameManager.SpeedLimitBytesPerSecond, - QueueProcessingOrder = QueueProcessingOrder.OldestFirst, - AutoReplenishment = true - }); + int speed = value == 0 ? int.MaxValue : value * 1024; + InstallGameManager.SpeedLimitBytesPerSecond = speed; + InstallGameManager.GlobalRateLimiter = InstallGameManager.GetRateLimiter(speed); AppConfig.SpeedLimitKBPerSecond = value; } diff --git a/src/Starward/Services/Download/InstallGameManager.cs b/src/Starward/Services/Download/InstallGameManager.cs index b24da6a75..beec78791 100644 --- a/src/Starward/Services/Download/InstallGameManager.cs +++ b/src/Starward/Services/Download/InstallGameManager.cs @@ -4,9 +4,10 @@ using Starward.Messages; using System; using System.Collections.Concurrent; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Threading.RateLimiting; +using System.Threading; +using System.Threading.Tasks; namespace Starward.Services.Download; @@ -22,14 +23,7 @@ private InstallGameManager() _services = new(); int speed = AppConfig.SpeedLimitKBPerSecond * 1024; SpeedLimitBytesPerSecond = speed == 0 ? int.MaxValue : speed; - rateLimiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions - { - TokensPerPeriod = SpeedLimitBytesPerSecond, - ReplenishmentPeriod = TimeSpan.FromSeconds(1), - TokenLimit = SpeedLimitBytesPerSecond, - QueueProcessingOrder = QueueProcessingOrder.OldestFirst, - AutoReplenishment = true - }); + GlobalRateLimiter = GetRateLimiter(SpeedLimitBytesPerSecond); } @@ -38,27 +32,18 @@ private InstallGameManager() - public static long DownloadBytesInSecond; - - public static int SpeedLimitBytesPerSecond { get; set; } - public static RateLimiter rateLimiter; + public static TokenBucketRateLimiter GlobalRateLimiter; - private long _lastTimeStamp; + public static bool IsEnableSpeedLimit => SpeedLimitBytesPerSecond != int.MaxValue; - public void UpdateSpeedState() - { - long ts = Stopwatch.GetTimestamp(); - if (ts - _lastTimeStamp >= Stopwatch.Frequency) - { - DownloadBytesInSecond = 0; - } - } - + // BUFFER_SIZE越大限速时保留速度也会越大,可以用来抵消迷之原因造成的超速¿ + // speedLimit<=2MB/s → 4Bytes else 16KB + public static int BUFFER_SIZE => (SpeedLimitBytesPerSecond <= (1 << 21)) ? (1 << 4) : (1 << 10); public event EventHandler InstallTaskAdded; @@ -70,6 +55,37 @@ public void UpdateSpeedState() + public static TokenBucketRateLimiter GetRateLimiter(int speedLimitBytesPerSecond) + { + // 小于speedLimitBytesPerSecond的最大能被BUFFER_SIZE整除的值 + var speedLimitBytesPerPeriod = Math.Max(speedLimitBytesPerSecond / 25 / BUFFER_SIZE * BUFFER_SIZE, BUFFER_SIZE); + return new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions + { + TokenLimit = speedLimitBytesPerPeriod, + // 0.04: 将每秒切割为上面的25份,间隔越小速度越精准。 + // 因补充令牌逻辑运行耗时远大于期望,若间隔极小,将无法达到最高限速。 + ReplenishmentPeriod = TimeSpan.FromSeconds(Math.Max(BUFFER_SIZE / (double)speedLimitBytesPerSecond, 0.04)), + TokensPerPeriod = speedLimitBytesPerPeriod, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + AutoReplenishment = true + }); + } + + + + public static async Task GetLeaseAsync(TokenBucketRateLimiter rateLimiter, int length, CancellationToken cancellationToken) + { + RateLimitLease lease; + do + { + lease = await 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); + } + + + public bool TryGetInstallService(GameBiz gameBiz, [NotNullWhen(true)] out InstallGameService? service) { if (_services.TryGetValue(gameBiz, out var model)) diff --git a/src/Starward/Services/Download/InstallGameService.cs b/src/Starward/Services/Download/InstallGameService.cs index beb6c7a24..4225632b2 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; @@ -623,6 +622,10 @@ public void ClearState() + protected List _taskItems; + public List TaskItems => _taskItems; + + protected void StartTask(InstallGameState state) { @@ -713,10 +716,9 @@ protected void StartTask(InstallGameState state) } State = state; _cancellationTokenSource = new CancellationTokenSource(); - for (int i = 0; i < Environment.ProcessorCount; i++) - { - _ = ExecuteTaskItemAsync(_cancellationTokenSource.Token); - } + _taskItems = Enumerable.Range(0, Environment.ProcessorCount) + .Select(_ => ExecuteTaskItemAsync(_cancellationTokenSource.Token)) + .ToList(); } @@ -1068,6 +1070,14 @@ protected void Finish() + public long HTTP_BUFFER_SIZE = InstallGameManager.BUFFER_SIZE; + + + + public bool IsEnableSpeedLimit = InstallGameManager.IsEnableSpeedLimit; + + + protected int _totalCount; public int TotalCount => _totalCount; @@ -1189,7 +1199,9 @@ 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); @@ -1202,24 +1214,39 @@ 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(buffer, cancellationToken).ConfigureAwait(false)) != 0) + while ((length = await hs.ReadAsync(IsEnableSpeedLimit ? httpBuffer : buffer, cancellationToken).ConfigureAwait(false)) != 0) { - RateLimitLease lease; - do + if (IsEnableSpeedLimit) { - lease = await InstallGameManager.rateLimiter.AcquireAsync(buffer.Length, cancellationToken).ConfigureAwait(false); - if (!lease.IsAcquired) + await InstallGameManager.GetLeaseAsync(InstallGameManager.GlobalRateLimiter, length, cancellationToken).ConfigureAwait(false); + int remainingSpace = buffer.Length - bufferOffset; + if (length > remainingSpace) { - await Task.Delay(1, cancellationToken).ConfigureAwait(false); + Buffer.BlockCopy(httpBuffer, 0, buffer, bufferOffset, remainingSpace); + bufferOffset += remainingSpace; + await fs.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); + bufferOffset = 0; + Buffer.BlockCopy(httpBuffer, remainingSpace, buffer, bufferOffset, length - remainingSpace); + bufferOffset += length - remainingSpace; } - } while (!lease.IsAcquired); - await fs.WriteAsync(buffer.AsMemory(0, length), cancellationToken).ConfigureAwait(false); + else + { + Buffer.BlockCopy(httpBuffer, 0, buffer, bufferOffset, length); + bufferOffset += length; + } + } + else + await fs.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); 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 145dedccb..720a5f22a 100644 --- a/src/Starward/Services/Download/InstallGameStateModel.cs +++ b/src/Starward/Services/Download/InstallGameStateModel.cs @@ -3,7 +3,11 @@ using Starward.Core; using Starward.Models; using System; +using System.Linq; +using System.Collections.Generic; using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; namespace Starward.Services.Download; @@ -95,6 +99,10 @@ internal InstallGameStateModel(InstallGameService service) public double _speedBytesPerSecond; + private List _recentSpeed = []; + + private readonly SynchronizationContext uiContext = SynchronizationContext.Current!; + [RelayCommand] @@ -102,8 +110,12 @@ private void ContinueOrPause() { if (ButtonGlyph is PlayGlyph) { - Service.Continue(); - InstallStarted?.Invoke(this, EventArgs.Empty); + Task.Run(() => + { + Task.WhenAll(Service.TaskItems).Wait(); + Service.Continue(); + InstallStarted?.Invoke(this, EventArgs.Empty); + }); } else if (ButtonGlyph is PauseGlyph) { @@ -123,6 +135,18 @@ private void Cancel() public void UpdateState() { + if (Service.HTTP_BUFFER_SIZE != InstallGameManager.BUFFER_SIZE || Service.IsEnableSpeedLimit != InstallGameManager.IsEnableSpeedLimit) + { + Interlocked.Exchange(ref Service.HTTP_BUFFER_SIZE, InstallGameManager.BUFFER_SIZE); + Service.IsEnableSpeedLimit = InstallGameManager.IsEnableSpeedLimit; + Service.Pause(); + Task.Run(() => + { + Task.WhenAll(Service.TaskItems).Wait(); + Service.Continue(); + InstallStarted?.Invoke(this, EventArgs.Empty); + }); + } try { IsContinueOrPauseButtonEnabled = true; @@ -215,6 +239,7 @@ private void ComputeSpeed(InstallGameState state) if (ts - _lastTimestamp >= Stopwatch.Frequency) { long bytes = Service.FinishBytes; + double averageSpeed = 0; _speedBytesPerSecond = Math.Clamp((double)(bytes - _lastFinishedBytes) / (ts - _lastTimestamp) * Stopwatch.Frequency, 0, long.MaxValue); _lastFinishedBytes = bytes; _lastTimestamp = ts; @@ -225,22 +250,26 @@ private void ComputeSpeed(InstallGameState state) } else { - if (_speedBytesPerSecond >= MB) + if (_speedBytesPerSecond == 0) { - SpeedText = $"{_speedBytesPerSecond / MB:F2} MB/s"; + RemainingTimeText = null; } else { - SpeedText = $"{_speedBytesPerSecond / KB:F2} KB/s"; + _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; + RemainingTimeText = TimeSpan.FromSeconds(seconds).ToString(@"hh\:mm\:ss"); } - if (_speedBytesPerSecond == 0) + if (_speedBytesPerSecond >= MB) { - RemainingTimeText = null; + SpeedText = $"{averageSpeed / MB:F2} MB/s"; } else { - var seconds = (Service.TotalBytes - Service.FinishBytes) / _speedBytesPerSecond; - RemainingTimeText = TimeSpan.FromSeconds(seconds).ToString(@"hh\:mm\:ss"); + SpeedText = $"{averageSpeed / KB:F2} KB/s"; } } } @@ -252,7 +281,7 @@ private void ComputeSpeed(InstallGameState state) private void _service_StateChanged(object? sender, InstallGameState e) { - UpdateState(); + uiContext.Post(_ => UpdateState(), null); }