Skip to content

Commit

Permalink
feat: add speed limits
Browse files Browse the repository at this point in the history
  • Loading branch information
anna-is-cute committed Oct 28, 2023
1 parent 26a4d85 commit a4f438e
Show file tree
Hide file tree
Showing 9 changed files with 357 additions and 10 deletions.
18 changes: 18 additions & 0 deletions Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ internal class Configuration : IPluginConfiguration {
public byte[]? OneClickSalt;
public string? OneClickHash;
public string? OneClickCollection;
public long MaxKibsPerSecond;
public long AltMaxKibsPerSecond;
public SpeedLimit LimitNormal = SpeedLimit.On;
public SpeedLimit LimitInstance = SpeedLimit.Default;
public SpeedLimit LimitCombat = SpeedLimit.Default;
public SpeedLimit LimitParty = SpeedLimit.Default;

public Configuration() {
}
Expand All @@ -44,6 +50,12 @@ internal Configuration(Configuration other) {
this.OneClickSalt = other.OneClickSalt;
this.OneClickHash = other.OneClickHash;
this.OneClickCollection = other.OneClickCollection;
this.MaxKibsPerSecond = other.MaxKibsPerSecond;
this.AltMaxKibsPerSecond = other.AltMaxKibsPerSecond;
this.LimitNormal = other.LimitNormal;
this.LimitInstance = other.LimitInstance;
this.LimitCombat = other.LimitCombat;
this.LimitParty = other.LimitParty;
}

private void Redact() {
Expand All @@ -62,4 +74,10 @@ internal static Configuration CloneAndRedact(Configuration other) {

return redacted;
}

internal enum SpeedLimit {
Default,
On,
Alternate,
}
}
46 changes: 43 additions & 3 deletions DownloadTask.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Diagnostics;
using System.Net.Http.Headers;
using System.Text;
using Blake3;
using Dalamud.Interface.Internal.Notifications;
using DequeNet;
using gfoidl.Base64;
using Heliosphere.Exceptions;
using Heliosphere.Model;
Expand Down Expand Up @@ -44,6 +46,33 @@ internal class DownloadTask : IDisposable {
internal uint StateData { get; private set; }
internal uint StateDataMax { get; private set; }
internal Exception? Error { get; private set; }
private ConcurrentDeque<Measurement> Entries { get; } = new();

private const double Window = 5;
internal double BytesPerSecond {
get {
if (this.Entries.Count == 0) {
return 0;
}

var total = 0u;
var removeTo = 0;
foreach (var entry in this.Entries) {
if (Stopwatch.GetElapsedTime(entry.Ticks) > TimeSpan.FromSeconds(Window)) {
removeTo += 1;
continue;
}

total += entry.Data;
}

for (var i = 0; i < removeTo; i++) {
this.Entries.TryPopLeft(out _);
}

return total / Window;
}
}

private bool _disposed;
private string? _oldModName;
Expand Down Expand Up @@ -461,7 +490,10 @@ StateCounter counter
var chunk = chunks[i];

// get the content of this multipart chunk as a stream
await using var stream = await content.ReadAsStreamAsync(this.CancellationToken.Token);
await using var stream = new GloballyThrottledStream(
await content.ReadAsStreamAsync(this.CancellationToken.Token),
this.Entries
);

// now we're going to read each file in the chunk out,
// decompress it, and write it to the disk
Expand Down Expand Up @@ -662,7 +694,10 @@ await Retry(3, $"Error downloading {baseUri}/{hash}", async _ => {
resp.EnsureSuccessStatusCode();
await using var file = File.Create(path);
await using var stream = await resp.Content.ReadAsStreamAsync(this.CancellationToken.Token);
await using var stream = new GloballyThrottledStream(
await resp.Content.ReadAsStreamAsync(this.CancellationToken.Token),
this.Entries
);
await new DecompressionStream(stream).CopyToAsync(file, this.CancellationToken.Token);
return false;
Expand Down Expand Up @@ -1190,6 +1225,11 @@ internal DownloadOptions(Dictionary<string, List<string>> options) {
private struct FullDownloadOptions {
public bool Full;
}

internal struct Measurement {
internal long Ticks;
internal uint Data;
}
}

internal enum State {
Expand Down Expand Up @@ -1293,4 +1333,4 @@ public override long Position {
get => this._inner.Position;
set => this._inner.Position = value;
}
}
}
54 changes: 52 additions & 2 deletions Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.RegularExpressions;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.IoC;
Expand Down Expand Up @@ -51,11 +52,17 @@ public class Plugin : IDalamudPlugin {
[PluginService]
internal ICommandManager CommandManager { get; init; }

[PluginService]
internal ICondition Condition { get; init; }

[PluginService]
internal IDutyState DutyState { get; init; }

[PluginService]
internal IFramework Framework { get; init; }

[PluginService]
private IPluginLog PluginLog { get; init; }
internal IPartyList PartyList { get; init; }

internal Configuration Config { get; }
internal DownloadCodes DownloadCodes { get; }
Expand Down Expand Up @@ -85,7 +92,6 @@ public Plugin() {
Instance = this;
GameFont = new GameFont(this);
PluginInterface = this.Interface!;
Log = this.PluginLog!;

this.Sentry = SentrySdk.Init(o => {
o.Dsn = "https://[email protected]/4";
Expand Down Expand Up @@ -167,6 +173,8 @@ public Plugin() {
this.LinkPayloads = new LinkPayloads(this);
this.CommandHandler = new CommandHandler(this);

this.Framework!.Update += this.CalculateSpeedLimit;

if (startWithAvWarning || checkTask is { Status: TaskStatus.RanToCompletion, Result: true }) {
this.PluginUi.OpenAntiVirusWarning();
}
Expand All @@ -175,6 +183,7 @@ public Plugin() {
}

public void Dispose() {
this.Framework.Update -= this.CalculateSpeedLimit;
this.CommandHandler.Dispose();
this.LinkPayloads.Dispose();
this.Server.Dispose();
Expand All @@ -186,6 +195,7 @@ public void Dispose() {
GameFont.Dispose();
ImageLoadSemaphore.Dispose();
DownloadSemaphore.Dispose();
GloballyThrottledStream.Shutdown();

foreach (var wrap in this.CoverImages.Deconstruct().Values) {
wrap.Dispose();
Expand All @@ -196,6 +206,46 @@ internal void SaveConfig() {
this.Interface.SavePluginConfig(this.Config);
}

private void CalculateSpeedLimit(IFramework framework) {
var limit = this.CalculateLimit();
GloballyThrottledStream.MaxBytesPerSecond = limit switch {
Configuration.SpeedLimit.On => this.Config.MaxKibsPerSecond * 1_024,
Configuration.SpeedLimit.Alternate => this.Config.AltMaxKibsPerSecond * 1_024,
Configuration.SpeedLimit.Default => this.Config.LimitNormal switch {
Configuration.SpeedLimit.On => this.Config.MaxKibsPerSecond * 1_024,
Configuration.SpeedLimit.Default => this.Config.MaxKibsPerSecond * 1_024,
Configuration.SpeedLimit.Alternate => this.Config.AltMaxKibsPerSecond * 1_024,
_ => 0,
},
_ => 0,
};
}

private Configuration.SpeedLimit CalculateLimit() {
if (this.Condition[ConditionFlag.InCombat]) {
return this.Config.LimitCombat;
}

if (this.DutyState.IsDutyStarted) {
return this.Config.LimitInstance;
}

// if (this.Condition.Any(
// ConditionFlag.BoundByDuty,
// ConditionFlag.BoundByDuty56,
// ConditionFlag.BoundByDuty95,
// ConditionFlag.BoundToDuty97
// )) {
// return this.Config.LimitInstance;
// }

if (this.PartyList.Length > 0) {
return this.Config.LimitParty;
}

return this.Config.LimitNormal;
}

internal async Task AddDownloadAsync(DownloadTask task, CancellationToken token = default) {
bool wasAdded;
using (var guard = await this.Downloads.WaitAsync(token)) {
Expand Down
10 changes: 9 additions & 1 deletion Ui/DownloadStatusWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ private void DrawRealDownloads(Guard<List<DownloadTask>>.Handle guard) {
continue;
}

var bps = task.BytesPerSecond;
var speed = bps switch {
>= 1_073_741_824 => $"{bps / 1_073_741_824:N2} GiB/s",
>= 1_048_576 => $"{bps / 1_048_576:N2} MiB/s",
>= 1_024 => $"{bps / 1_024:N2} KiB/s",
_ => $"{bps:N2} B/s",
};

var packageName = task switch {
{ PackageName: not null, VariantName: null } => $"{task.PackageName} - ",
{ PackageName: not null, VariantName: not null } => $"{task.PackageName} ({task.VariantName}) - ",
Expand All @@ -85,7 +93,7 @@ private void DrawRealDownloads(Guard<List<DownloadTask>>.Handle guard) {
ImGui.ProgressBar(
(float) task.StateData / task.StateDataMax,
new Vector2(ImGui.GetContentRegionAvail().X, 25 * ImGuiHelpers.GlobalScale),
$"{packageName}{task.State.Name()}: {task.StateData:N0} / {task.StateDataMax:N0}"
$"{packageName}{task.State.Name()}: {task.StateData:N0} / {task.StateDataMax:N0} ({speed})"
);

ImGuiHelper.Tooltip("Hold Ctrl and click to cancel.");
Expand Down
35 changes: 35 additions & 0 deletions Ui/Tabs/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,41 @@ internal void Draw() {
ref this.Plugin.Config.DefaultCollection
);

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

anyChanged |= ImGuiHelper.InputLongVertical(
"Alternate download speed in KiBs (0 for unlimited)",
"##max-download-speed",
ref this.Plugin.Config.AltMaxKibsPerSecond
);

void DrawLimitCombo(string title, string id, ref Configuration.SpeedLimit limit) {
ImGui.TextUnformatted(title);
if (!ImGui.BeginCombo(id, Enum.GetName(limit))) {
return;
}

foreach (var option in Enum.GetValues<Configuration.SpeedLimit>()) {
if (ImGui.Selectable(Enum.GetName(option), option == limit)) {
limit = option;
anyChanged = true;
}
}

ImGui.EndCombo();
}

DrawLimitCombo("Speed limit (default)", "##speed-limit-normal", ref this.Plugin.Config.LimitNormal);
DrawLimitCombo("Speed limit (in instance)", "##speed-limit-instance", ref this.Plugin.Config.LimitInstance);
DrawLimitCombo("Speed limit (in combat)", "##speed-limit-combat", ref this.Plugin.Config.LimitCombat);
DrawLimitCombo("Speed limit (in party)", "##speed-limit-party", ref this.Plugin.Config.LimitParty);
}

if (ImGui.CollapsingHeader("One-click install")) {
anyChanged |= ImGui.Checkbox("Enable", ref this.Plugin.Config.OneClick);

Expand Down
Loading

0 comments on commit a4f438e

Please sign in to comment.