Mam klasę odpowiedzialną za pobieranie plików w menedżerze pobierania. Ta klasa jest odpowiedzialna za pobranie pliku i zapisanie go do podanej ścieżki.C# WebClient - Duży wzrost LOH po pobraniu plików
Rozmiar plików do pobrania różni się zwykle od 1 do 5 MB, ale może być znacznie większy. Używam instancji klasy WebClient, aby pobrać plik z Internetu.
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);
}
}
Każde pobranie powoduje bardzo duży wzrost pamięci w porównaniu z rozmiarem pliku pobranego elementu. Jeśli pobierzesz plik o rozmiarze ~ 3 MB, zużycie pamięci wzrasta o około 8 MB.
Jak widać pobieranie produkuje dużo LOH który nie jest czyszczony po pobraniu. Nawet wymuszenie ustawienia GC lub ustawienia GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
nie pomaga w zapobieganiu wyciekom pamięci.
Porównując Snapshot 1 i 2 widać, że ilość pamięci jest produkowany przez tablicami bajtów, które mogą być wynikiem pobrania.
robi kilka pobieranie pokazuje, jak straszne to wyciek pamięci.
Moim zdaniem jest to spowodowane przez instancję WebClient w jakikolwiek sposób. Jednak nie mogę naprawdę określić, co dokładnie powoduje ten problem. Nie ma to znaczenia, jeśli zmuszę GC. Ekran ten pokazuje go tutaj bez wymuszonego GC:
Co jest przyczyną tego przegrzać i jak mogę to naprawić? To poważny błąd i wyobrażamy sobie, że 100 lub więcej pobrań w procesie zabraknie pamięci.
Edit
Jak sugeruje ja wykomentowane sekcji odpowiedzialnej za ustalanie tagi i konwersji M4A do MP3. Jednak przetwornica jest po prostu wezwanie FFMPEG więc nie powinno być przeciek pamięci:
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);
}
}
Sposób DownloadCompleted()
wygląda teraz tak:
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);
}
}
Wynik po pobraniu 7 pozycji: Wygląda na to, że nie był to wyciek pamięci.
Jako dodatek, przesyłam również klasę DownloadManager
, która obsługuje całą operację pobierania. Może to może być źródłem problemu.
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
Jaka jest twoja wersja .NET? Z Twojego kodu wynika, że: NET CLR 1.0.3705 – Matt
Używam .NET Framework 4.5.2 – chris579
WebClient nie ma przecieku. Najwyraźniej powinieneś być bardziej zaniepokojony "Taglib" i "MusicFormatConverter", klasami, które w przeciwieństwie do WebClient * nie są * testowane miliony razy każdego dnia. Użyj porządnego profilera pamięci, aby osiągnąć sukces. –