Skip to content

Commit

Permalink
fix: handle changing speed limits properly
Browse files Browse the repository at this point in the history
  • Loading branch information
anna-is-cute committed Oct 29, 2023
1 parent 50fb9c9 commit f2ab8d0
Show file tree
Hide file tree
Showing 4 changed files with 32 additions and 23 deletions.
4 changes: 2 additions & 2 deletions Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ internal class Configuration : IPluginConfiguration {
public byte[]? OneClickSalt;
public string? OneClickHash;
public string? OneClickCollection;
public long MaxKibsPerSecond;
public long AltMaxKibsPerSecond;
public ulong MaxKibsPerSecond;
public ulong AltMaxKibsPerSecond;
public SpeedLimit LimitNormal = SpeedLimit.On;
public SpeedLimit LimitInstance = SpeedLimit.Default;
public SpeedLimit LimitCombat = SpeedLimit.Default;
Expand Down
6 changes: 3 additions & 3 deletions Ui/Tabs/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ ref this.Plugin.Config.DefaultCollection
);

if (ImGui.CollapsingHeader("Download speed limits")) {
anyChanged |= ImGuiHelper.InputLongVertical(
anyChanged |= ImGuiHelper.InputULongVertical(
"Max download speed in KiB/s (0 for unlimited)",
"##max-download-speed",
ref this.Plugin.Config.MaxKibsPerSecond
);

anyChanged |= ImGuiHelper.InputLongVertical(
anyChanged |= ImGuiHelper.InputULongVertical(
"Alternate max download speed in KiB/s (0 for unlimited)",
"##alt-download-speed",
ref this.Plugin.Config.AltMaxKibsPerSecond
Expand Down Expand Up @@ -250,4 +250,4 @@ ref this.Plugin.Config.OneClickCollection
this.Plugin.SaveConfig();
}
}
}
}
41 changes: 25 additions & 16 deletions Util/GloballyThrottledStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
namespace Heliosphere.Util;

internal class GloballyThrottledStream : Stream {
private static long _maxBytesPerSecond;
private static ulong _maxBytesPerSecond;

internal static long MaxBytesPerSecond {
get => _maxBytesPerSecond;
internal static ulong MaxBytesPerSecond {
get => Interlocked.Read(ref _maxBytesPerSecond);
set => Interlocked.Exchange(ref _maxBytesPerSecond, value);
}

Expand All @@ -17,9 +17,9 @@ internal static long MaxBytesPerSecond {
/// The fill level of the leaky bucket. Number of bytes read multiplied by
/// <see cref="Stopwatch.Frequency"/>.
/// </summary>
private static long _bucket;
private static ulong _bucket;

private static long _lastRead = Stopwatch.GetTimestamp();
private static ulong _lastRead = (ulong) Stopwatch.GetTimestamp();

private Stream Inner { get; }
private ConcurrentDeque<DownloadTask.Measurement> Entries { get; }
Expand Down Expand Up @@ -57,19 +57,28 @@ public override void Flush() {
this.Inner.Flush();
}

private static long Leak(long mbps) {
var now = Stopwatch.GetTimestamp();
private static ulong Leak(ulong mbps) {
var now = (ulong) Stopwatch.GetTimestamp();
var then = Interlocked.Exchange(ref _lastRead, now);
var leakAmt = (now - then) * mbps;
var leakAmt = checked(now - then) * mbps;

long bucket;
ulong bucket;
Mutex.Wait();
try {
if (_bucket > 0) {
_bucket = Math.Max(0, _bucket - leakAmt);
_bucket = leakAmt > _bucket
? 0
: checked(_bucket - leakAmt);
}

bucket = mbps * Stopwatch.Frequency - _bucket;
var mul = mbps * (ulong) Stopwatch.Frequency;
if (_bucket > mul) {
// by changing the speed limit, we have now overfilled the
// bucket. remove excess
_bucket = mul;
}

bucket = checked(mul - _bucket);
} finally {
Mutex.Release();
}
Expand All @@ -78,7 +87,7 @@ private static long Leak(long mbps) {
}

public override int Read(byte[] buffer, int offset, int count) {
var mbps = Interlocked.Read(ref _maxBytesPerSecond);
var mbps = MaxBytesPerSecond;

int amt;
if (mbps == 0) {
Expand All @@ -87,18 +96,18 @@ public override int Read(byte[] buffer, int offset, int count) {
// available capacity in the bucket * freq
var bucket = Leak(mbps);
// number of bytes the bucket has space for
var bytes = (int) (bucket / Stopwatch.Frequency);
var bytes = (int) (bucket / (ulong) Stopwatch.Frequency);

// let's not do a million tiny reads
// wait until between 1 and 65536 bytes are available, depending on
// the buffer size and the speed limit
var exp = (int) Math.Truncate(Math.Log2(Math.Min(count, mbps)));
var exp = (int) Math.Truncate(Math.Log2(Math.Min(count, (int) mbps)));
var lessThan = Math.Pow(2, Math.Clamp(exp, 0, 16));

while (bytes < lessThan) {
Thread.Sleep(TimeSpan.FromMilliseconds(5));
bucket = Leak(mbps);
bytes = (int) (bucket / Stopwatch.Frequency);
bytes = (int) (bucket / (ulong) Stopwatch.Frequency);
}

// read how many bytes are available or the buffer size, whichever
Expand All @@ -111,7 +120,7 @@ public override int Read(byte[] buffer, int offset, int count) {
if (mbps != 0) {
Mutex.Wait();
try {
_bucket += read * Stopwatch.Frequency;
_bucket += (ulong) read * (ulong) Stopwatch.Frequency;
} finally {
Mutex.Release();
}
Expand Down
4 changes: 2 additions & 2 deletions Util/ImGuiHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -298,15 +298,15 @@ internal static bool InputTextVertical(string title, string id, ref string input
return ImGui.InputText(id, ref input, max, flags);
}

internal static bool InputLongVertical(string title, string id, ref long input) {
internal static bool InputULongVertical(string title, string id, ref ulong input) {
ImGui.TextUnformatted(title);
ImGui.SetNextItemWidth(-1);
var text = input.ToString();
if (!ImGui.InputText(id, ref text, 100, ImGuiInputTextFlags.CharsDecimal)) {
return false;
}

if (!long.TryParse(text, out var parsed)) {
if (!ulong.TryParse(text, out var parsed)) {
return false;
}

Expand Down

0 comments on commit f2ab8d0

Please sign in to comment.