Skip to content

Commit c82592c

Browse files
author
Meyn
committed
Implemented Download Speed Measurement
1 parent d7112e3 commit c82592c

File tree

9 files changed

+465
-21
lines changed

9 files changed

+465
-21
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Requests
2+
{
3+
/// <summary>
4+
/// Represents an interface for requests that support speed reporting.
5+
/// </summary>
6+
public interface ISpeedReportable
7+
{
8+
/// <summary>
9+
/// Gets the SpeedReporter instance associated with the request, which is used to report speed values.
10+
/// </summary>
11+
SpeedReporter<long>? SpeedReporter { get; }
12+
}
13+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
namespace System
2+
{
3+
/// <summary>
4+
/// Provides an ISpeedReporter{T} that invokes callbacks for the latest reported speed value.
5+
/// </summary>
6+
/// <typeparam name="T">Specifies the type of the speed report value.</typeparam>
7+
public class SpeedReporter<T> : IProgress<T>
8+
{
9+
private readonly SynchronizationContext _synchronizationContext;
10+
private readonly Action<T>? _handler;
11+
private readonly SendOrPostCallback _invokeHandlers;
12+
private T _latestValue = default!;
13+
private long _lastReportTime;
14+
private int _isWaiting = 0;
15+
private int _isReporting = 0;
16+
17+
/// <summary>
18+
/// The timeout for the speed reporter in milliseconds.
19+
/// Note that the actual reporting interval may not be exactly the set timeout due to various factors.
20+
/// </summary>
21+
public int Timeout { get => _timeout; set => _timeout = value < 0 ? 0 : value; }
22+
private int _timeout = 0;
23+
24+
/// <summary>Initializes the <see cref="SpeedReporter{T}"/> with a default timeout.</summary>
25+
public SpeedReporter()
26+
{
27+
_synchronizationContext = SynchronizationContext.Current ?? SpeedReporterStatics.DefaultContext;
28+
_invokeHandlers = new SendOrPostCallback(InvokeHandlers);
29+
}
30+
31+
/// <summary>Initializes the <see cref="SpeedReporter{T}"/> with the specified callback and timeout.</summary>
32+
public SpeedReporter(Action<T> handler) : this()
33+
{
34+
ArgumentNullException.ThrowIfNull(handler);
35+
_handler = handler;
36+
}
37+
38+
/// <summary>Raised for each reported speed value.</summary>
39+
public event EventHandler<T>? SpeedChanged;
40+
41+
/// <summary>Reports a speed change.</summary>
42+
void IProgress<T>.Report(T value) => OnReport(value);
43+
44+
/// <summary>Reports a speed change.</summary>
45+
protected virtual void OnReport(T value)
46+
{
47+
_latestValue = value;
48+
if (Interlocked.CompareExchange(ref _isReporting, 1, 0) == 1)
49+
return;
50+
long currentTime = Environment.TickCount;
51+
if (currentTime - _lastReportTime > Timeout - 20)
52+
{
53+
_lastReportTime = currentTime;
54+
Post();
55+
}
56+
else if (Interlocked.CompareExchange(ref _isWaiting, 1, 0) == 0)
57+
{
58+
_latestValue = value;
59+
int delay = Timeout - (int)(currentTime - _lastReportTime);
60+
_lastReportTime = currentTime + delay;
61+
ScheduleDelayedPost(delay);
62+
}
63+
_isReporting = 0;
64+
}
65+
66+
private void ScheduleDelayedPost(int delay) => Task.Run(async () =>
67+
{
68+
await Task.Delay(delay);
69+
_lastReportTime = Environment.TickCount;
70+
_synchronizationContext.Post(_invokeHandlers, _latestValue);
71+
});
72+
73+
private void Post() => _synchronizationContext.Post(_invokeHandlers, _latestValue);
74+
75+
76+
/// <summary>Invokes the action and event callbacks.</summary>
77+
private void InvokeHandlers(object? state)
78+
{
79+
T value = _latestValue;
80+
_isWaiting = 0;
81+
_handler?.Invoke(value);
82+
SpeedChanged?.Invoke(this, value);
83+
}
84+
}
85+
86+
/// <summary>Holds static values for <see cref="SpeedReporter{T}"/>.</summary>
87+
internal static class SpeedReporterStatics
88+
{
89+
/// <summary>A default synchronization context that targets the ThreadPool.</summary>
90+
internal static readonly SynchronizationContext DefaultContext = new();
91+
}
92+
}

DownloadAssistant/Base/ThrottledStream.cs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ public long MaximumBytesPerSecond
5353
}
5454
}
5555

56+
/// <summary>
57+
/// Gets the current transfer rate in bytes per second.
58+
/// </summary>
59+
public long CurrentBytesPerSecond { get; private set; }
60+
61+
/// <summary>
62+
/// Reports and monitors data transfer speed using <see cref="IProgress{T}"/>.
63+
/// </summary>
64+
public IProgress<long>? SpeedReporter { get; set; }
65+
5666
/// <summary>
5767
/// Gets a value indicating whether the current stream supports reading.
5868
/// </summary>
@@ -298,34 +308,41 @@ public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, Cancella
298308
/// <param name="bufferSizeInBytes">The buffer size in bytes.</param>
299309
protected async Task ThrottleAsync(int bufferSizeInBytes)
300310
{
301-
if (_maximumBytesPerSecond <= 0 || bufferSizeInBytes <= 0)
311+
if (SpeedReporter == null && (_maximumBytesPerSecond <= 0 || bufferSizeInBytes <= 0))
302312
return;
303313

304314
_byteCount += bufferSizeInBytes;
305315
long elapsedMilliseconds = CurrentMilliseconds - _start;
306316

307317
if (elapsedMilliseconds > 0)
308318
{
309-
long bps = _byteCount * 1000L / elapsedMilliseconds;
319+
CurrentBytesPerSecond = _byteCount * 1000L / elapsedMilliseconds;
320+
SpeedReporter?.Report(CurrentBytesPerSecond);
310321

311-
if (bps > _maximumBytesPerSecond)
322+
if (_maximumBytesPerSecond > 0 && CurrentBytesPerSecond > _maximumBytesPerSecond)
312323
{
313324
long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond;
314325
int toSleep = (int)(wakeElapsed - elapsedMilliseconds);
315326

316327
if (toSleep > 1)
317328
{
318-
try
319-
{
320-
await Task.Delay(toSleep);
321-
}
322-
catch { }
329+
await Task.Delay(toSleep);
323330
Reset();
324331
}
325332
}
333+
else
334+
Reset();
326335
}
327336
}
328337

338+
/// <inheritdoc/>
339+
public override void Close()
340+
{
341+
CurrentBytesPerSecond = 0;
342+
SpeedReporter?.Report(0);
343+
base.Close();
344+
}
345+
329346
/// <summary>
330347
/// Will reset the bytecount to 0 and reset the start time to the current time.
331348
/// </summary>

DownloadAssistant/Options/GetRequestOptions.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,19 @@ public record GetRequestOptions : WebRequestOptions<string>
5151
public uint MinReloadSize { get; set; } = 1048576 * 2; //2Mb
5252

5353
/// <summary>
54-
/// Gets or sets the maximum of bytes that can be downloaded by the <see cref="GetRequest"/> per second.
54+
/// Configures the maximum download rate in bytes per second for the <see cref="GetRequest"/>.
5555
/// </summary>
5656
/// <value>
57-
/// The maximum bytes per second.
57+
/// The limit for bytes per second, or null for unlimited speed.
5858
/// </value>
5959
public long? MaxBytesPerSecond { get => _maxBytesPerSecond; init => _maxBytesPerSecond = value > 1 ? value : null; }
6060
private readonly long? _maxBytesPerSecond = null;
6161

62+
/// <summary>
63+
/// Initializes a speed reporter to monitor and report the download speed in bytes per second.
64+
/// </summary>
65+
public SpeedReporter<long>? SpeedReporter { get; init; }
66+
6267
/// <summary>
6368
/// Gets or sets the minimum content byte of the Request.
6469
/// </summary>
@@ -137,6 +142,7 @@ protected GetRequestOptions(GetRequestOptions options) : base(options)
137142
WriteMode = options.WriteMode;
138143
BufferLength = options.BufferLength;
139144
MaxBytesPerSecond = options.MaxBytesPerSecond;
145+
SpeedReporter = options.SpeedReporter;
140146
MinByte = options.MinByte;
141147
MaxByte = options.MaxByte;
142148
SupportsHeadRequest = options.SupportsHeadRequest;

DownloadAssistant/Options/LoadRequestOptions.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ public string Filename
4848
public long? MaxBytesPerSecond { get => _maxBytesPerSecond; set => _maxBytesPerSecond = value > 1 ? value : null; }
4949
private long? _maxBytesPerSecond = null;
5050

51+
/// <summary>
52+
/// Determines whether a speed reporter should be initialized to monitor and report the download speed in bytes per second.
53+
/// </summary>
54+
public bool CreateSpeedReporter { get; init; } = false;
55+
56+
/// <summary>
57+
/// The timeout for the speed reporter in milliseconds. This property can only be used when <see cref="CreateSpeedReporter"/> is true.
58+
/// Note that the actual reporting interval may not be exactly the set timeout due to various factors.
59+
/// </summary>
60+
public int SpeedReporterTimeout { get => _speedReporterTimeout; init => _speedReporterTimeout = value < 0 ? 0 : value; }
61+
private int _speedReporterTimeout = 0;
62+
5163
/// <summary>
5264
/// Gets or sets a value indicating whether the server supports the HEAD request. Default is true.
5365
/// </summary>
@@ -130,6 +142,8 @@ protected LoadRequestOptions(LoadRequestOptions options) : base(options)
130142
_temporaryPath = options.TempDestination;
131143
_destinationPath = options.DestinationPath;
132144
DeleteFilesOnFailure = options.DeleteFilesOnFailure;
145+
SpeedReporterTimeout = options.SpeedReporterTimeout;
146+
CreateSpeedReporter = options.CreateSpeedReporter;
133147
WriteMode = options.WriteMode;
134148
BufferLength = options.BufferLength;
135149
MaxBytesPerSecond = options.MaxBytesPerSecond;

0 commit comments

Comments
 (0)