2016-10-12 20 views
8

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 
+0

Jaka jest twoja wersja .NET? Z Twojego kodu wynika, że: NET CLR 1.0.3705 – Matt

+0

Używam .NET Framework 4.5.2 – chris579

+0

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. –

Odpowiedz

2

Wreszcie po kilkudziesięciu profilach i sprawdzeniu pamięci problem został rozwiązany.

Jak @SimonMourier już wspomniano ten problem jest związany z projektowaniem metod UploadFile, DownloadData, DownloadString i DownloadFile. Patrząc w backend z nich można zobaczyć, że wszyscy korzystają z prywatnej DownloadBits metodę w klasie WebClient z tym podpisem:

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

sprawie rodzaju powrotnej jest jasne, dlaczego zachowanie jest jak odkryłem: Podczas korzystania z wyżej wymienionych metod zawartość jest zapisywana w tablicy bajtów. Dlatego nie zaleca się stosowania tych metod, jeśli rozmiar pliku wynosi> 85 000 bajtów, ponieważ mogłoby to spowodować wypełnienie LOH do osiągnięcia limitu pamięci. Może to nie być ważne, jeśli pliki są małe, ale ze wzrostem rozmiaru LOH rośnie również o wiele.

Jako dodatek tutaj mojego ostatecznego rozwiązania:

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); 
    } 
} 

Jednak dzięki za pomoc!

Powiązane problemy