Skip to content

Commit 7726685

Browse files
authored
Merge pull request #140 from bezzad/feature/inmemory_buffering_limitation
Feature/inmemory buffering limitation
2 parents 1c3377c + e3cc907 commit 7726685

15 files changed

+88
-42
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Downloader is compatible with .NET Standard 2.0 and above, running on Windows, L
5252
- Live streaming support, suitable for playing music at the same time as downloading.
5353
- Ability to download just a certain range of bytes of a large file.
5454
- Code is tiny, fast and does not depend on external libraries.
55+
- Control the amount of system memroy (RAM) that the Downloader consumes during downloading.
5556

5657
---
5758

@@ -93,6 +94,8 @@ var downloadOpt = new DownloadConfiguration()
9394
MaximumBytesPerSecond = 1024*1024*2,
9495
// the maximum number of times to fail
9596
MaxTryAgainOnFailover = 5,
97+
// release memory buffer after each 50 MB
98+
MaximumMemoryBufferBytes = 1024 * 1024 * 50,
9699
// download parts of file as parallel or not. Default value is false
97100
ParallelDownload = true,
98101
// number of parallel downloads. The default value is the same as the chunk count

src/Downloader.Test/Downloader.Test.csproj

+7-7
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,19 @@
2424
<PackageReference Include="AssertMessage.Fody" Version="2.1.0">
2525
<PrivateAssets>all</PrivateAssets>
2626
</PackageReference>
27-
<PackageReference Include="Fody" Version="6.6.4">
27+
<PackageReference Include="Fody" Version="6.7.0">
2828
<PrivateAssets>all</PrivateAssets>
2929
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
3030
</PackageReference>
31-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
32-
<PackageReference Include="Moq" Version="4.18.2" />
33-
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
34-
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
35-
<PackageReference Include="coverlet.collector" Version="3.2.0">
31+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
32+
<PackageReference Include="Moq" Version="4.18.4" />
33+
<PackageReference Include="MSTest.TestAdapter" Version="3.0.3" />
34+
<PackageReference Include="MSTest.TestFramework" Version="3.0.3" />
35+
<PackageReference Include="coverlet.collector" Version="6.0.0">
3636
<PrivateAssets>all</PrivateAssets>
3737
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
3838
</PackageReference>
39-
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
39+
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
4040
</ItemGroup>
4141

4242
<ItemGroup>

src/Downloader.Test/IntegrationTests/DownloadServiceTest.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ public void TestPackageSituationAfterDispose()
124124
Package.TotalFileSize = sampleDataLength * 64;
125125
Options.ChunkCount = 1;
126126
new ChunkHub(Options).SetFileChunks(Package);
127-
Package.BuildStorage(false);
127+
Package.BuildStorage(false, 1024 * 1024);
128128
Package.Storage.WriteAsync(0, sampleData, sampleDataLength);
129129
Package.Storage.Flush();
130130

@@ -145,7 +145,7 @@ public async Task TestPackageChunksDataAfterDispose()
145145
var dummyData = DummyData.GenerateOrderedBytes(chunkSize);
146146
Options.ChunkCount = 64;
147147
Package.TotalFileSize = chunkSize * 64;
148-
Package.BuildStorage(false);
148+
Package.BuildStorage(false, 1024 * 1024);
149149
new ChunkHub(Options).SetFileChunks(Package);
150150
for (int i = 0; i < Package.Chunks.Length; i++)
151151
{

src/Downloader.Test/UnitTests/DownloadPackageTest.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public virtual void Initial()
1919
{
2020
Config = new DownloadConfiguration() { ChunkCount = 8 };
2121
Data = DummyData.GenerateOrderedBytes(DummyFileHelper.FileSize16Kb);
22-
Package.BuildStorage(false);
22+
Package.BuildStorage(false, 1024 * 1024);
2323
new ChunkHub(Config).SetFileChunks(Package);
2424
Package.Storage.WriteAsync(0, Data, DummyFileHelper.FileSize16Kb);
2525
Package.Storage.Flush();

src/Downloader.Test/UnitTests/DownloadPackageTestOnFile.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ private void BuildStorageTest(bool reserveSpace)
5353
};
5454

5555
// act
56-
Package.BuildStorage(reserveSpace);
56+
Package.BuildStorage(reserveSpace, 1024*1024);
5757

5858
// assert
5959
Assert.IsInstanceOfType(Package.Storage.OpenRead(), typeof(FileStream));

src/Downloader.Test/UnitTests/RequestTest.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ public void GetRedirectUrlByLocationTest()
389389

390390
// assert
391391
Assert.AreNotEqual(url, redirectUrl);
392-
Assert.AreNotEqual(request.Address, redirectUrl);
392+
Assert.AreNotEqual(request.Address.ToString(), redirectUrl);
393393
Assert.AreEqual(redirectUrl, actualRedirectUrl.AbsoluteUri);
394394
}
395395

@@ -408,7 +408,7 @@ public void GetRedirectUrlWithoutLocationTest()
408408

409409
// assert
410410
Assert.AreNotEqual(url, redirectUrl);
411-
Assert.AreNotEqual(request.Address, redirectUrl);
411+
Assert.AreNotEqual(request.Address.ToString(), redirectUrl);
412412
Assert.AreEqual(redirectUrl, actualRedirectUrl.AbsoluteUri);
413413
}
414414

src/Downloader/ConcurrentStream.cs

+30-17
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ namespace Downloader
88
{
99
public class ConcurrentStream : IDisposable
1010
{
11-
private readonly SemaphoreSlim _queueCheckerSemaphore = new SemaphoreSlim(0);
11+
private readonly SemaphoreSlim _queueConsumerLocker = new SemaphoreSlim(0);
1212
private readonly ManualResetEventSlim _completionEvent = new ManualResetEventSlim(true);
13+
private readonly ManualResetEventSlim _stopWriteNewPacketEvent = new ManualResetEventSlim(true);
1314
private readonly ConcurrentBag<Packet> _inputBag = new ConcurrentBag<Packet>();
14-
private int? _resourceReleaseThreshold;
15-
private long _packetCounter = 0;
15+
private long _maxMemoryBufferBytes = 0;
1616
private bool _disposed;
1717
private Stream _stream;
1818
private string _path;
@@ -29,7 +29,7 @@ public string Path
2929
}
3030
}
3131
}
32-
32+
3333
public byte[] Data
3434
{
3535
get
@@ -49,24 +49,35 @@ public byte[] Data
4949

5050
public long Length => _stream?.Length ?? 0;
5151

52-
public ConcurrentStream(Stream stream)
52+
public long MaxMemoryBufferBytes
53+
{
54+
get => _maxMemoryBufferBytes;
55+
set
56+
{
57+
_maxMemoryBufferBytes = (value <= 0) ? long.MaxValue : value;
58+
}
59+
}
60+
61+
public ConcurrentStream(Stream stream, long maxMemoryBufferBytes = 0)
5362
{
5463
_stream = stream;
64+
MaxMemoryBufferBytes = maxMemoryBufferBytes;
5565
Initial();
5666
}
5767

58-
public ConcurrentStream(string filename, long initSize)
68+
public ConcurrentStream(string filename, long initSize, long maxMemoryBufferBytes = 0)
5969
{
6070
_path = filename;
6171
_stream = new FileStream(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
72+
MaxMemoryBufferBytes = maxMemoryBufferBytes;
6273

6374
if (initSize > 0)
6475
_stream.SetLength(initSize);
6576

6677
Initial();
6778
}
6879

69-
public ConcurrentStream()
80+
public ConcurrentStream() // parameterless constructor for deserialization
7081
{
7182
_stream = new MemoryStream();
7283
Initial();
@@ -88,44 +99,46 @@ public Stream OpenRead()
8899

89100
public void WriteAsync(long position, byte[] bytes, int length)
90101
{
102+
_stopWriteNewPacketEvent.Wait();
91103
_inputBag.Add(new Packet(position, bytes, length));
92104
_completionEvent.Reset();
93-
_queueCheckerSemaphore.Release();
105+
_queueConsumerLocker.Release();
106+
ReleaseQueue(length);
94107
}
95108

96109
private async Task Watcher()
97110
{
98111
while (!_disposed)
99112
{
100-
await _queueCheckerSemaphore.WaitAsync().ConfigureAwait(false);
113+
await _queueConsumerLocker.WaitAsync().ConfigureAwait(false);
101114
if (_inputBag.TryTake(out var packet))
102115
{
103116
await WritePacket(packet).ConfigureAwait(false);
104-
ReleasePackets(packet.Data.Length);
105117
packet.Dispose();
106118
}
107119
}
108120
}
121+
109122
private async Task WritePacket(Packet packet)
110123
{
111124
if (_stream.CanSeek)
112125
{
113126
_stream.Position = packet.Position;
114127
await _stream.WriteAsync(packet.Data, 0, packet.Length).ConfigureAwait(false);
115-
_packetCounter++;
116128
}
117129

118130
if (_inputBag.IsEmpty)
119131
_completionEvent.Set();
120132
}
121133

122-
private void ReleasePackets(int packetSize)
134+
private void ReleaseQueue(int packetSize)
123135
{
124-
_resourceReleaseThreshold ??= 1024 * 1024 * 50 / packetSize; // 50MB / a packet size
125-
126136
// Clean up RAM every _resourceReleaseThreshold packet
127-
if (_packetCounter % _resourceReleaseThreshold == 0)
128-
GC.Collect();
137+
if (MaxMemoryBufferBytes < packetSize * _inputBag.Count)
138+
{
139+
_stopWriteNewPacketEvent.Set();
140+
Flush();
141+
}
129142
}
130143

131144
public void Flush()
@@ -141,7 +154,7 @@ public void Dispose()
141154
{
142155
Flush();
143156
_disposed = true;
144-
_queueCheckerSemaphore.Dispose();
157+
_queueConsumerLocker.Dispose();
145158
_stream.Dispose();
146159
}
147160
}

src/Downloader/DownloadConfiguration.cs

+28-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ public class DownloadConfiguration : ICloneable, INotifyPropertyChanged
99
private int _bufferBlockSize;
1010
private int _chunkCount;
1111
private long _maximumBytesPerSecond;
12+
private int _maximumTryAgainOnFailover;
13+
private long _maximumMemoryBufferBytes;
1214
private bool _checkDiskSizeBeforeDownload;
13-
private int _maxTryAgainOnFailover;
1415
private bool _parallelDownload;
1516
private int _parallelCount;
1617
private int _timeout;
@@ -26,7 +27,7 @@ public class DownloadConfiguration : ICloneable, INotifyPropertyChanged
2627
public DownloadConfiguration()
2728
{
2829
RequestConfiguration = new RequestConfiguration(); // default requests configuration
29-
_maxTryAgainOnFailover = int.MaxValue; // the maximum number of times to fail.
30+
_maximumTryAgainOnFailover = int.MaxValue; // the maximum number of times to fail.
3031
_parallelDownload = false; // download parts of file as parallel or not
3132
_parallelCount = 0; // number of parallel downloads
3233
_chunkCount = 1; // file parts to download
@@ -117,10 +118,10 @@ public long MaximumBytesPerSecond
117118
/// </summary>
118119
public int MaxTryAgainOnFailover
119120
{
120-
get => _maxTryAgainOnFailover;
121+
get => _maximumTryAgainOnFailover;
121122
set
122123
{
123-
_maxTryAgainOnFailover = value;
124+
_maximumTryAgainOnFailover = value;
124125
OnPropertyChanged();
125126
}
126127
}
@@ -249,6 +250,29 @@ public bool ReserveStorageSpaceBeforeStartingDownload
249250
}
250251
}
251252

253+
/// <summary>
254+
/// Gets or sets the maximum amount of memory, in bytes, that the Downloader library is allowed
255+
/// to allocate for buffering downloaded content. Once this limit is reached, the library will
256+
/// stop downloading and start writing the buffered data to a file stream before continuing.
257+
/// The default value for is 0, which indicates unlimited buffering.
258+
/// </summary>
259+
/// <example>
260+
/// The following example sets the maximum memory buffer to 50 MB, causing the library to release
261+
/// the memory buffer after each 50 MB of downloaded content:
262+
/// <code>
263+
/// MaximumMemoryBufferBytes = 1024 * 1024 * 50
264+
/// </code>
265+
/// </example>
266+
public long MaximumMemoryBufferBytes
267+
{
268+
get => _maximumMemoryBufferBytes;
269+
set
270+
{
271+
_maximumMemoryBufferBytes = value;
272+
OnPropertyChanged();
273+
}
274+
}
275+
252276
public object Clone()
253277
{
254278
return MemberwiseClone();

src/Downloader/DownloadPackage.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ public void Validate()
5050
}
5151
}
5252

53-
public void BuildStorage(bool reserveFileSize)
53+
public void BuildStorage(bool reserveFileSize, long maxMemoryBufferBytes = 0)
5454
{
5555
if (string.IsNullOrWhiteSpace(FileName))
56-
Storage = new ConcurrentStream();
56+
Storage = new ConcurrentStream() { MaxMemoryBufferBytes = maxMemoryBufferBytes };
5757
else
58-
Storage = new ConcurrentStream(FileName, reserveFileSize ? TotalFileSize : 0);
58+
Storage = new ConcurrentStream(FileName, reserveFileSize ? TotalFileSize : 0, maxMemoryBufferBytes);
5959
}
6060
}
6161
}

src/Downloader/DownloadService.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ private async Task<Stream> StartDownload()
178178
await _singleInstanceSemaphore.WaitAsync();
179179
Package.TotalFileSize = await _requestInstance.GetFileSize().ConfigureAwait(false);
180180
Package.IsSupportDownloadInRange = await _requestInstance.IsSupportDownloadInRange().ConfigureAwait(false);
181-
Package.BuildStorage(Options.ReserveStorageSpaceBeforeStartingDownload);
181+
Package.BuildStorage(Options.ReserveStorageSpaceBeforeStartingDownload, Options.MaximumMemoryBufferBytes);
182182
ValidateBeforeChunking();
183183
_chunkHub.SetFileChunks(Package);
184184

src/Downloader/Downloader.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<PackageProjectUrl>https://github.com/bezzad/Downloader</PackageProjectUrl>
1111
<RepositoryUrl>https://github.com/bezzad/Downloader</RepositoryUrl>
1212
<PackageTags>download-manager, downloader, download, idm, internet, streaming, download-file, stream-downloader, multipart-download</PackageTags>
13-
<PackageReleaseNotes>Added task async method for the download cancel operation. #133</PackageReleaseNotes>
13+
<PackageReleaseNotes>Added task async method for the download cancel operation. #132</PackageReleaseNotes>
1414
<SignAssembly>true</SignAssembly>
1515
<AssemblyOriginatorKeyFile>Downloader.snk</AssemblyOriginatorKeyFile>
1616
<Copyright>Copyright (C) 2019-2022 Behzad Khosravifar</Copyright>

src/Samples/Downloader.Sample/DownloadList.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
// "Url": "https://c2rsetup.officeapps.live.com/c2r/downloadVS.aspx?sku=community&channel=Release&version=VS2022&source=VSLandingPage&includeRecommended=true&cid=2030:9bf2104738684908988ca7dcd5dafed1"
99
//},
1010
{
11-
"FileName": "D:\\TestDownload\\LocalFile100MB_Raw.dat",
12-
"Url": "http://localhost:3333/dummyfile/file/size/104857600"
11+
"FileName": "D:\\TestDownload\\LocalFile1GB_Raw.dat",
12+
"Url": "http://localhost:3333/dummyfile/file/size/1073741824"
1313
},
1414
{
1515
"FolderPath": "D:\\TestDownload",

src/Samples/Downloader.Sample/Downloader.Sample.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9-
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
9+
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
1010
<PackageReference Include="ShellProgressBar" Version="5.2.0" />
1111
</ItemGroup>
1212

src/Samples/Downloader.Sample/Helper.cs

+5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
using System;
2+
using System.Diagnostics;
23

34
namespace Downloader.Sample
45
{
56
public static class Helper
67
{
8+
private static Process CurrentProcess = Process.GetCurrentProcess();
9+
710
public static string CalcMemoryMensurableUnit(this long bytes)
811
{
912
return CalcMemoryMensurableUnit((double)bytes);
@@ -51,12 +54,14 @@ public static void UpdateTitleInfo(this DownloadProgressChangedEventArgs e, bool
5154
string bytesReceived = e.ReceivedBytesSize.CalcMemoryMensurableUnit();
5255
string totalBytesToReceive = e.TotalBytesToReceive.CalcMemoryMensurableUnit();
5356
string progressPercentage = $"{e.ProgressPercentage:F3}".Replace("/", ".");
57+
string usedMemory = CurrentProcess.WorkingSet64.CalcMemoryMensurableUnit();
5458

5559
Console.Title = $"{progressPercentage}% - " +
5660
$"{speed}/s (avg: {avgSpeed}/s) - " +
5761
$"{estimateTime} {timeLeftUnit} left - " +
5862
$"Active Chunks: {e.ActiveChunks} - " +
5963
$"[{bytesReceived} of {totalBytesToReceive}] " +
64+
$"[{usedMemory} memory buffer] " +
6065
(isPaused ? " - Paused" : "");
6166
}
6267
}

src/Samples/Downloader.Sample/Program.cs

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ private static DownloadConfiguration GetDownloadConfiguration()
117117
ChunkCount = 8, // file parts to download, default value is 1
118118
MaximumBytesPerSecond = 1024 * 1024 * 10, // download speed limited to 10MB/s, default values is zero or unlimited
119119
MaxTryAgainOnFailover = 5, // the maximum number of times to fail
120+
MaximumMemoryBufferBytes = 1024 * 1024 * 50, // release memory buffer after each 50 MB
120121
ParallelDownload = true, // download parts of file as parallel or not. Default value is false
121122
ParallelCount = 4, // number of parallel downloads. The default value is the same as the chunk count
122123
Timeout = 3000, // timeout (millisecond) per stream block reader, default value is 1000

0 commit comments

Comments
 (0)