Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

opt: downloader #1019

Merged
merged 9 commits into from
Sep 15, 2024
Merged
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);
}
Loading