2016-10-12 10 views
8

ダウンロードマネージャでファイルをダウンロードするクラスがあります。このクラスは、ファイルをダウンロードし、指定されたパスに書き込む役割を担います。C#WebClient - ファイルをダウンロードした後のLOHの大幅な増加

ダウンロードするファイルのサイズは通常1から5 MBまで異なりますが、さらに大きくなることもあります。 WebClientクラスのインスタンスを使用して、インターネットからファイルを取得しています。

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

ダウンロードするたびに、ダウンロードしたアイテムのファイルサイズに比べて非常に大きなメモリが増加します。 〜3 MBのサイズのファイルをダウンロードした場合、メモリ使用量は約8 MB増加しています。

あなたがダウンロードは、ダウンロード後にクリアされていない多くのLOHを生産している見ることができるように。 GCを強制したり、設定をGCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;にしても、このメモリリークを防ぐことはできません。スナップショット1と2を比較すると

あなたは、メモリの量は、ダウンロードした結果であるかもしれないバイト配列によって生成されていることがわかります。いくつかのダウンロードを行う

このメモリリークがいかにひどい示しています。

私の意見では、これはどのような方法でWebClientインスタンスによって引き起こされると考えられます。しかし、私は本当にこの問題を引き起こしている正確なものを特定することはできません。 GCを強制しても問題はありません。ここでこの画面は強制GCせずにそれを示しています

は何がこの過熱を引き起こしていると私はそれをどのように修正することができますか?これは大きなバグであり、プロセスがメモリ不足になる100以上のダウンロードを想像しています。

編集


としては、私は、タグを設定し、MP3にM4Aに変換するための責任のセクションをコメントアウト示唆しました。

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

DownloadCompleted()方法は次のようになりましたになります:

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

結果7つのアイテムをダウンロードした後: コンバータはFFMPEGのちょうど呼び出しをあるしかし、それはメモリリークではありませんので、 これはメモリリークではないようです。

さらに、ダウンロード操作全体を処理しているので、DownloadManagerクラスも提出しています。たぶんこれが問題の原因になるかもしれない。

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

.NETバージョンは何ですか?あなたのコードから、NET CLR 1.0.3705 – Matt

+0

私は.NET Framework 4.5.2を使用しています – chris579

+0

WebClientにはリークがありません。明らかに、「Taglib」と「MusicFormatConverter」については、WebClientとは異なり、毎日何百万回もテストされていないクラスが懸念されるべきです。適切なメモリプロファイラを使用して先に進んでください。 –

答えて

2

最後に、数多くのプロファイリングとメモリチェックの後、問題は解決されました。

@SimonMourierは既に述べたように、この問題は,DownloadStringおよびDownloadFileの方法に関連しています。私が発見したように振る舞いである理由は明らかである戻り値の型について

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

: それらのバックエンドを見ると、あなたはそれらのすべてがこのシグネチャを持つWebClientクラスのプライベートDownloadBits方法を使用していることがわかります上記の方法を使用する場合、コンテンツはバイト配列で保存されます。したがって、ファイルサイズが> 85,000バイトの場合、これらのメソッドを使用することは推奨されません。これにより、メモリ制限に達するまでLOHを埋め込むことになります。ファイルが小さく、サイズが大きくなれば、LOHも倍数で増加しているので、これは重要ではないかもしれません。ここでさらにとして

私の最終的な解決策:

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

しかし助けに感謝します!

関連する問題