2016-10-12 1 views
8

Ich habe eine Klasse verantwortlich für das Herunterladen von Dateien in einem Download-Manager. Diese Klasse ist verantwortlich für das Herunterladen der Datei und das Schreiben in den angegebenen Pfad.C# WebClient - Große Zunahme von LOH nach dem Herunterladen von Dateien

Die Größe der herunterzuladenden Dateien variiert normalerweise von 1 bis 5 MB, könnte aber auch viel größer sein. Ich verwende eine Instanz der WebClient-Klasse, um die Datei aus dem Internet zu erhalten.

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

Jeder Download verursacht einen sehr großen Speicher Anstieg im Vergleich mit der Dateigröße des heruntergeladenen Artikels. Wenn ich eine Datei mit einer Größe von ~ 3 MB herunterlade, erhöht sich die Speicherbelegung um ca. 8 MB.

Wie Sie der Download produziert viel LOH sehen kann, die nicht nach dem Download gelöscht. Selbst das Erzwingen des GC oder die Einstellung GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; hilft nicht, dieses Speicherleck zu verhindern.

Vergleich Snapshot 1 und 2 sehen Sie, dass die Größe des Speichers von Byte-Arrays erzeugt wird, die das Download-Ergebnis sein könnte.

mehr Downloads Doing zeigt, wie schrecklich dieses Speicherleck ist.

Meiner Meinung nach wird dies durch die WebClient-Instanz in irgendeiner Weise verursacht. Ich kann jedoch nicht wirklich feststellen, was genau dieses Problem verursacht. Es ist nicht einmal wichtig, wenn ich den GC erzwinge. Dieser Bildschirm zeigt hier ohne gezwungen gc:

Was diese Überhitzung verursacht, und wie kann ich es beheben? Dies ist ein schwerwiegender Fehler, und wenn man sich 100 oder mehr Downloads vorstellt, würde der Prozess keinen Speicher mehr haben.

bearbeiten


Wie vorgeschlagen, dass ich aus dem Abschnitt zu einem MP3-Tags, die zur Einstellung und zum Umwandeln des M4A verantwortlich kommentiert. Doch der Wandler gerade einen Anruf von FFMPEG ist so sollte es nicht ein Speicherverlust sein:

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

Die DownloadCompleted() Methode sieht nun wie folgt aus:

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

Das Ergebnis nach dem Download 7 Artikel: Es scheint, dass dies nicht das Speicherleck war.

Als Ergänzung ich auch die DownloadManager Klasse einreichen, da es den gesamten Download-Vorgang behandelt. Vielleicht könnte dies die Ursache des Problems sein.

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

Was ist Ihre .NET-Version? Aus Ihrem Code heißt es: NET CLR 1.0.3705 – Matt

+0

Ich verwende .NET Framework 4.5.2 – chris579

+0

WebClient hat kein Leck. Natürlich sollten Sie sich viel mehr Gedanken über "Taglib" und "MusicFormatConverter" machen, Klassen, die im Gegensatz zum WebClient * millionenmal täglich getestet werden. Verwenden Sie einen anständigen Speicherprofiler, um weiter zu kommen. –

Antwort

2

Endlich, nach Dutzenden von Profiling und Speicherprüfung ist das Problem jetzt gelöst.

Wie @SimonMourier bereits erklärt, diese Frage für die Gestaltung der UploadFile, DownloadString und DownloadFile Methoden DownloadData, verwandt ist. Ein Blick in das Backend von ihnen können Sie sehen, dass alle von ihnen sind die private DownloadBits Methode in der WebClient Klasse mit dieser Signatur:

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

In Bezug auf den Rückgabetyp ist es klar, warum das Verhalten ist, wie ich entdecken: Bei Verwendung der oben genannten Methoden wird der Inhalt in einem Byte-Array gespeichert. Daher wird es nicht empfohlen, diese Methoden zu verwenden, wenn die Dateigröße> 85.000 Bytes beträgt, da dies dazu führen würde, dass die LOH gefüllt wird, bis das Speicherlimit erreicht ist. Dies ist möglicherweise nicht wichtig, wenn die Dateien klein sind, aber mit wachsender Größe wächst auch der LOH um ein Vielfaches.

Als Ergänzung hier meine endgültige Lösung:

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

aber danke für die Hilfe!

Verwandte Themen