C # WebClient - Big increase in LOH after file uploads

I have a class responsible for downloading files in the download manager. This class is responsible for loading the file and writing it to the given path.

The size of the downloaded files usually varies from 1 to 5 MB, but can also be much larger. I am using an instance of the WebClient class to get a file from the Internet.

public class DownloadItem { #region Events public delegate void DownloadItemDownloadCompletedEventHandler(object sender, DownloadCompletedEventArgs args); public event DownloadItemDownloadCompletedEventHandler DownloadItemDownloadCompleted; protected virtual void OnDownloadItemDownloadCompleted(DownloadCompletedEventArgs e) { DownloadItemDownloadCompleted?.Invoke(this, e); } public delegate void DownloadItemDownloadProgressChangedEventHandler(object sender, DownloadProgressChangedEventArgs args); public event DownloadItemDownloadProgressChangedEventHandler DownloadItemDownloadProgressChanged; protected virtual void OnDownloadItemDownloadProgressChanged(DownloadProgressChangedEventArgs e) { DownloadItemDownloadProgressChanged?.Invoke(this, e); } #endregion #region Fields private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private WebClient _client; #endregion #region Properties public PlaylistItem Item { get; } public string SavePath { get; } public bool Overwrite { get; } #endregion public DownloadItem(PlaylistItem item, string savePath, bool overwrite = false) { Item = item; SavePath = savePath; Overwrite = overwrite; } public void StartDownload() { if (File.Exists(SavePath) && !Overwrite) { OnDownloadItemDownloadCompleted(new DownloadCompletedEventArgs(true)); return; } OnDownloadItemDownloadProgressChanged(new DownloadProgressChangedEventArgs(1)); Item.RetreiveDownloadUrl(); if (string.IsNullOrEmpty(Item.DownloadUrl)) { OnDownloadItemDownloadCompleted(new DownloadCompletedEventArgs(true, new InvalidOperationException("Could not retreive download url"))); return; } // GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; using (_client = new WebClient()) { _client.Headers.Add("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)"); try { _client.DownloadDataCompleted += (sender, args) => { Task.Run(() => { DownloadCompleted(args); }); }; _client.DownloadProgressChanged += (sender, args) => OnDownloadItemDownloadProgressChanged(new DownloadProgressChangedEventArgs(args.ProgressPercentage)); _client.DownloadDataAsync(new Uri(Item.DownloadUrl)); } catch (Exception ex) { Logger.Warn(ex, "Error downloading track {0}", Item.VideoId); OnDownloadItemDownloadCompleted(new DownloadCompletedEventArgs(true, ex)); } } } private void DownloadCompleted(DownloadDataCompletedEventArgs args) { // _client = null; // GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; // GC.Collect(2, GCCollectionMode.Forced); if (args.Cancelled) { OnDownloadItemDownloadCompleted(new DownloadCompletedEventArgs(true, args.Error)); return; } try { File.WriteAllBytes(SavePath, args.Result); using (var file = TagLib.File.Create(SavePath)) { file.Save(); } try { MusicFormatConverter.M4AToMp3(SavePath); } catch (Exception) { // ignored } OnDownloadItemDownloadCompleted(new DownloadCompletedEventArgs(false)); } catch (Exception ex) { OnDownloadItemDownloadCompleted(new DownloadCompletedEventArgs(true, ex)); Logger.Error(ex, "Error writing track file for track {0}", Item.VideoId); } } public void StopDownload() { _client?.CancelAsync(); } public override int GetHashCode() { return Item.GetHashCode(); } public override bool Equals(object obj) { var item = obj as DownloadItem; return Item.Equals(item?.Item); } } 

Each download causes a very large increase in memory compared to the file size of the loaded item. If I upload a ~ 3 MB file, the memory usage will increase by about 8 MB.

As you can see, loading creates a lot of LOH, which does not clear after loading. Even forcing GC on or setting GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; do not help prevent memory leak.

Comparing snapshots 1 and 2, you can see that the amount of memory created by byte arrays, which may be the result of loading.

Performing multiple downloads shows how terrible this memory leak is.

In my opinion, this is caused by an instance of WebClient. However, I can’t determine exactly what exactly causes this problem. It doesn't even matter if I can force the GC. This screen here shows it without a forced gc:

What causes this overheating and how can I fix it? This is a basic error and represents 100 or more downloads that the process will end out of memory.

Edit


As I said, I commented on the section responsible for tagging and converting M4A to MP3. However, the converter is just a FFMPEG call, so it should not be a memory leak:

 class MusicFormatConverter { public static void M4AToMp3(string filePath, bool deleteOriginal = true) { if(string.IsNullOrEmpty(filePath) || !filePath.EndsWith(".m4a")) throw new ArgumentException(nameof(filePath)); var toolPath = Path.Combine("tools", "ffmpeg.exe"); var convertedFilePath = filePath.Replace(".m4a", ".mp3"); File.Delete(convertedFilePath); var process = new Process { StartInfo = { FileName = toolPath, #if !DEBUG WindowStyle = ProcessWindowStyle.Hidden, #endif Arguments = $"-i \"{filePath}\" -acodec libmp3lame -ab 128k \"{convertedFilePath}\"" } }; process.Start(); process.WaitForExit(); if(!File.Exists(convertedFilePath)) throw new InvalidOperationException("File was not converted successfully!"); if(deleteOriginal) File.Delete(filePath); } } 

Now the DownloadCompleted() method looks like this:

 private void DownloadCompleted(DownloadDataCompletedEventArgs args) { // _client = null; // GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; // GC.Collect(2, GCCollectionMode.Forced); if (args.Cancelled) { OnDownloadItemDownloadCompleted(new DownloadCompletedEventArgs(true, args.Error)); return; } try { File.WriteAllBytes(SavePath, args.Result); /* using (var file = TagLib.File.Create(SavePath)) { file.Save(); } try { MusicFormatConverter.M4AToMp3(SavePath); } catch (Exception) { // ignore } */ OnDownloadItemDownloadCompleted(new DownloadCompletedEventArgs(false)); } catch (Exception ex) { OnDownloadItemDownloadCompleted(new DownloadCompletedEventArgs(true, ex)); Logger.Error(ex, "Error writing track file for track {0}", Item.VideoId); } } 

Result after loading 7 elements: This does not seem to be a memory leak.

As an add-on, I'm sending the DownloadManager class, as it handles the entire download operation. Perhaps this could be the source of the problem.

 public class DownloadManager { #region Fields private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private readonly Queue<DownloadItem> _queue; private readonly List<DownloadItem> _activeDownloads; private bool _active; private Thread _thread; #endregion #region Construction public DownloadManager() { _queue = new Queue<DownloadItem>(); _activeDownloads = new List<DownloadItem>(); } #endregion #region Methods public void AddToQueue(DownloadItem item) { _queue.Enqueue(item); StartManager(); } public void Abort() { _thread?.Abort(); _queue.Clear(); _activeDownloads.Clear(); } private void StartManager() { if(_active) return; _active = true; _thread = new Thread(() => { try { while (_queue.Count > 0 && _queue.Peek() != null) { DownloadItem(); while (_activeDownloads.Count >= Properties.Settings.Default.ParallelDownloads) { Thread.Sleep(10); } } _active = false; } catch (ThreadInterruptedException) { // ignored } }); _thread.Start(); } private void DownloadItem() { if (_activeDownloads.Count >= Properties.Settings.Default.ParallelDownloads) return; DownloadItem item; try { item = _queue.Dequeue(); } catch { return; } if (item != null) { item.DownloadItemDownloadCompleted += (sender, args) => { if(args.Error != null) Logger.Error(args.Error, "Error downloading track {0}", ((DownloadItem)sender).Item.VideoId); _activeDownloads.Remove((DownloadItem) sender); }; _activeDownloads.Add(item); Task.Run(() => item.StartDownload()); } } #endregion 
+8
garbage-collection c # memory memory-leaks webclient
source share
1 answer

Finally, after dozens of profiles and memory are verified, the problem is now resolved.

As @SimonMourier already reported, this issue is related to the design of the UploadFile , DownloadData , DownloadString and DownloadFile methods. Looking at the backend of them, you can see that they all use the private DownloadBits method in the WebClient class with this signature:

 private byte[] DownloadBits(WebRequest request, Stream writeStream, CompletionDelegate completionDelegate, AsyncOperation asyncOp) 

As for the return type, it’s clear why the behavior is similar to what I found: When using the above methods, the contents are stored in an array of bytes. Therefore, it is not recommended to use these methods if the file size is> 85,000 bytes, as this will cause the LOH to fill until the memory limit is reached. This may not matter if the files are small, but as the size grows, the LOH also grows by several.

As a complement here is my final solution:

 public class DownloadItem : DownloadManagerItem { #region Fields private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private WebClient _webClient; #endregion #region Properties public string SavePath { get; } public bool Overwrite { get; } public DownloadFormat DownloadFormat { get; } #endregion public DownloadItem(PlaylistItem item, string savePath, DownloadFormat downloadFormat, bool overwrite = false) : base(item) { SavePath = savePath; Overwrite = overwrite; DownloadFormat = downloadFormat; } public override void StartDownload() { if (File.Exists(SavePath) && !Overwrite) { OnDownloadItemDownloadCompleted(new DownloadCompletedEventArgs(true)); return; } OnDownloadItemDownloadProgressChanged(new DownloadProgressChangedEventArgs(1)); Item.RetreiveDownloadUrl(); if (string.IsNullOrEmpty(Item.DownloadUrl)) { OnDownloadItemDownloadCompleted(new DownloadCompletedEventArgs(true, new InvalidOperationException("Could not retreive download url"))); return; } using (_webClient = new WebClient()) { _webClient.Headers.Add("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)"); try { _webClient.OpenReadCompleted += WebClientOnOpenReadCompleted; _webClient.OpenReadAsync(new Uri(Item.DownloadUrl)); } catch (Exception ex) { Logger.Warn(ex, "Error downloading track {0}", Item.VideoId); OnDownloadItemDownloadCompleted(new DownloadCompletedEventArgs(true, ex)); } } } private void WebClientOnOpenReadCompleted(object sender, OpenReadCompletedEventArgs openReadCompletedEventArgs) { _webClient.Dispose(); if (openReadCompletedEventArgs.Cancelled) { OnDownloadItemDownloadCompleted(new DownloadCompletedEventArgs(true, openReadCompletedEventArgs.Error)); return; } if (!Overwrite && File.Exists(SavePath)) return; var totalLength = 0; try { totalLength = int.Parse(((WebClient)sender).ResponseHeaders["Content-Length"]); } catch (Exception) { // ignored } try { long processed = 0; var tmpPath = Path.GetTempFileName(); using (var stream = openReadCompletedEventArgs.Result) using (var fs = File.Create(tmpPath)) { var buffer = new byte[16 * 1024]; int read; while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) { fs.Write(buffer, 0, read); processed += read; OnDownloadItemDownloadProgressChanged(new DownloadProgressChangedEventArgs(processed, totalLength)); } } File.Move(tmpPath, SavePath); OnDownloadItemDownloadCompleted(new DownloadCompletedEventArgs(false)); } catch (Exception ex) { OnDownloadItemDownloadCompleted(new DownloadCompletedEventArgs(true, ex)); } } public override void StopDownload() { _webClient?.CancelAsync(); } public override void Dispose() { _webClient?.Dispose(); } public override int GetHashCode() { return Item.GetHashCode(); } public override bool Equals(object obj) { var item = obj as DownloadItem; return Item.Equals(item?.Item); } } 

However, thanks for the help!

+2
source share

All Articles