Skip to content

Commit

Permalink
opt: Decrease the time interval for replenishing tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
Eric-Joker committed Aug 5, 2024
1 parent 0155e51 commit 0a2c034
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 57 deletions.
1 change: 0 additions & 1 deletion src/Starward/Controls/InstallGameController.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
13 changes: 3 additions & 10 deletions src/Starward/Pages/Setting/DownloadSettingPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down
62 changes: 39 additions & 23 deletions src/Starward/Services/Download/InstallGameManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}


Expand All @@ -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<InstallGameStateModel> InstallTaskAdded;
Expand All @@ -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))
Expand Down
53 changes: 40 additions & 13 deletions src/Starward/Services/Download/InstallGameService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 @@ -623,6 +622,10 @@ public void ClearState()



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



protected void StartTask(InstallGameState state)
{
Expand Down Expand Up @@ -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();
}


Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -1189,7 +1199,9 @@ protected async Task DownloadItemAsync(InstallGameItem item, CancellationToken c
{
file_target = item.WriteAsTempFile ? file_tmp : file;
}
var httpBuffer = ArrayPool<byte>.Shared.Rent((int)Interlocked.Read(ref HTTP_BUFFER_SIZE));
var buffer = ArrayPool<byte>.Shared.Rent(BUFFER_SIZE);
int bufferOffset = 0;
try
{
using var fs = File.Open(file_target, FileMode.OpenOrCreate);
Expand All @@ -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<byte>.Shared.Return(httpBuffer);
ArrayPool<byte>.Shared.Return(buffer);
}
}
Expand Down
49 changes: 39 additions & 10 deletions src/Starward/Services/Download/InstallGameStateModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -95,15 +99,23 @@ internal InstallGameStateModel(InstallGameService service)

public double _speedBytesPerSecond;

private List<double> _recentSpeed = [];

private readonly SynchronizationContext uiContext = SynchronizationContext.Current!;



[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 All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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";
}
}
}
Expand All @@ -252,7 +281,7 @@ private void ComputeSpeed(InstallGameState state)

private void _service_StateChanged(object? sender, InstallGameState e)
{
UpdateState();
uiContext.Post(_ => UpdateState(), null);
}


Expand Down

0 comments on commit 0a2c034

Please sign in to comment.