Skip to content

Commit

Permalink
opt: downloader (#1019)
Browse files Browse the repository at this point in the history
* refactor: download limiter

* opt: Decrease the time interval for replenishing tokens

* fix: crash by NumberBox possibly being -21e

* Improve more precise speed limits

Co-authored-by: DismissedLight <[email protected]>

* opt: reduce downloader writes

* refactor: simplify rate limiting

---------

Co-authored-by: DismissedLight <[email protected]>
Co-authored-by: Scighost <[email protected]>
  • Loading branch information
3 people authored Sep 15, 2024
1 parent 0731321 commit 93c028d
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 50 deletions.
1 change: 1 addition & 0 deletions src/Starward/Pages/Setting/DownloadSettingPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
Spacing="12">
<NumberBox MinWidth="100"
Minimum="0"
Maximum="2097151"
Value="{x:Bind SpeedLimit, Mode=TwoWay}" />
<TextBlock VerticalAlignment="Center" Text="KB/s" />
</StackPanel>
Expand Down
5 changes: 4 additions & 1 deletion src/Starward/Pages/Setting/DownloadSettingPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,8 +70,10 @@ partial void OnDefaultInstallPathChanged(string? value)
private int speedLimit = AppConfig.SpeedLimitKBPerSecond;
partial void OnSpeedLimitChanged(int value)
{
InstallGameManager.SetRateLimit(value * 1024);
int speed = value <= 0 ? int.MaxValue : value * 1024;
Interlocked.Exchange(ref InstallGameManager.SpeedLimitBytesPerSecond, speed);
AppConfig.SpeedLimitKBPerSecond = value;
InstallGameManager.SetRateLimit();
}


Expand Down
49 changes: 25 additions & 24 deletions src/Starward/Services/Download/InstallGameManager.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging;
using Starward.Core;
using Starward.Helpers;
using Starward.Messages;
using System;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.RateLimiting;

namespace Starward.Services.Download;
Expand All @@ -19,7 +20,9 @@ internal class InstallGameManager
private InstallGameManager()
{
_services = new();
SetRateLimit(AppConfig.SpeedLimitKBPerSecond * 1024);
int speed = AppConfig.SpeedLimitKBPerSecond * 1024;
SpeedLimitBytesPerSecond = speed == 0 ? int.MaxValue : speed;
SetRateLimit();
}


Expand All @@ -31,32 +34,13 @@ private InstallGameManager()



public static TokenBucketRateLimiter RateLimiter { get; private set; }

public static long SpeedLimitBytesPerSecond;


public static void SetRateLimit(int bytesPerSecond)
{
if (bytesPerSecond <= 0)
{
bytesPerSecond = int.MaxValue;
}
else if (bytesPerSecond < (1 << 14))
{
bytesPerSecond = 1 << 14;
}
RateLimiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
{
TokenLimit = bytesPerSecond,
ReplenishmentPeriod = TimeSpan.FromSeconds(1),
TokensPerPeriod = bytesPerSecond,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
AutoReplenishment = true
});
}

public static TokenBucketRateLimiter RateLimiter { get; private set; }


public static bool IsEnableSpeedLimit => Interlocked.Read(ref SpeedLimitBytesPerSecond) != int.MaxValue;


public event EventHandler<InstallGameStateModel> InstallTaskAdded;
Expand All @@ -68,6 +52,23 @@ public static void SetRateLimit(int bytesPerSecond)



public static void SetRateLimit()
{
var speedLimitBytesPerPeriod = (int)SpeedLimitBytesPerSecond / 25;
RateLimiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions
{
TokenLimit = speedLimitBytesPerPeriod,
// 0.04: 将每秒切割为上面的25份,间隔越小速度越精准。
// 因补充令牌逻辑运行耗时远大于期望,若间隔极小,将无法达到最高限速。
ReplenishmentPeriod = TimeSpan.FromSeconds(0.04),
TokensPerPeriod = speedLimitBytesPerPeriod,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
AutoReplenishment = true
});
}



public bool TryGetInstallService(GameBiz gameBiz, [NotNullWhen(true)] out InstallGameService? service)
{
if (_services.TryGetValue(gameBiz, out var model))
Expand Down
35 changes: 20 additions & 15 deletions src/Starward/Services/Download/InstallGameService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Starward.Core;
using Starward.Core.HoYoPlay;
using Starward.Services.InstallGame;
Expand All @@ -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;

Expand Down Expand Up @@ -625,6 +624,12 @@ public void ClearState()




protected List<Task> _taskItems;
public List<Task> TaskItems => _taskItems;



protected void StartTask(InstallGameState state)
{
if (_concurrentExecuteThreadCount > 0) return;
Expand Down Expand Up @@ -715,7 +720,6 @@ protected void StartTask(InstallGameState state)
_finishBytes = 0;
}
State = state;

_ = RunTasksAsync(); //不需要ConfigureAwait,因为返回值丢弃,且无需调用“.GetAwaiter().OnCompleted()”
return;

Expand Down Expand Up @@ -1164,13 +1168,14 @@ protected async Task ExecuteTaskItemAsync(CancellationToken cancellationToken =
_installItemQueue.Enqueue(item);
return;
}
catch (Exception ex)
{
_logger.LogError(ex, nameof(ExecuteTaskItemAsync));
_installItemQueue.Enqueue(item);
OnInstallFailed(ex);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, nameof(ExecuteTaskItemAsync));
OnInstallFailed(ex);
}
finally
{
Interlocked.Decrement(ref _concurrentExecuteThreadCount);
Expand All @@ -1182,7 +1187,7 @@ protected async Task ExecuteTaskItemAsync(CancellationToken cancellationToken =

protected async Task DownloadItemAsync(InstallGameItem item, CancellationToken cancellationToken = default)
{
const int BUFFER_SIZE = 1 << 10;
const int BUFFER_SIZE = 1 << 14;
string file = item.Path;
string file_tmp = item.Path + "_tmp";
string file_target;
Expand Down Expand Up @@ -1214,12 +1219,12 @@ protected async Task DownloadItemAsync(InstallGameItem item, CancellationToken c
int length;
while ((length = await hs.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0)
{
RateLimitLease lease = await InstallGameManager.RateLimiter.AcquireAsync(length, cancellationToken).ConfigureAwait(false);
while (!lease.IsAcquired)
{
await Task.Delay(1, cancellationToken).ConfigureAwait(false);
lease = await InstallGameManager.RateLimiter.AcquireAsync(length, cancellationToken).ConfigureAwait(false);
}
int totalTokens = 0;
while (InstallGameManager.IsEnableSpeedLimit && totalTokens < length)
if (!TokenBucketRateLimiterExtension.TryAcquire(InstallGameManager.RateLimiter, length - totalTokens, out int tokensAcquired, out _))
await Task.Delay(1, cancellationToken).ConfigureAwait(false);
else
totalTokens += tokensAcquired;
await fs.WriteAsync(buffer.AsMemory(0, length), cancellationToken).ConfigureAwait(false);
Interlocked.Add(ref _finishBytes, length);
}
Expand Down
42 changes: 32 additions & 10 deletions src/Starward/Services/Download/InstallGameStateModel.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Starward.Core;
using Starward.Models;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;

namespace Starward.Services.Download;

Expand Down Expand Up @@ -95,15 +98,21 @@ internal InstallGameStateModel(InstallGameService service)

public double _speedBytesPerSecond;

private List<double> _recentSpeed = [];



[RelayCommand]
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)
{
Expand Down Expand Up @@ -215,6 +224,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;
Expand All @@ -225,22 +235,34 @@ 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";
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;
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";
}
}
}
Expand Down
24 changes: 24 additions & 0 deletions src/Starward/Services/Download/TokenBucketRateLimiterExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Threading;
using System.Threading.RateLimiting;
using System.Runtime.CompilerServices;

namespace Starward.Services.Download;

internal static class TokenBucketRateLimiterExtension
{
public static bool TryAcquire(this TokenBucketRateLimiter rateLimiter, int permits, out int acquired, out TimeSpan retryAfter)
{
acquired = Math.Min(permits, (int)Volatile.Read(ref PrivateGetTokenCount(rateLimiter)));
lock (PrivateGetLock(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);
}

0 comments on commit 93c028d

Please sign in to comment.