2016-10-12 13 views
8

Tôi có một lớp chịu trách nhiệm tải xuống tệp trong trình quản lý tải xuống. Lớp này chịu trách nhiệm tải xuống tệp và ghi nó vào đường dẫn đã cho.C# WebClient - Mức tăng lớn của LOH sau khi tải xuống các tệp

Kích thước của tệp để tải xuống thay đổi bình thường từ 1 đến 5 MB nhưng cũng có thể lớn hơn nhiều. Tôi đang sử dụng một thể hiện của lớp WebClient để lấy tệp từ 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); 
    } 
} 

Mọi lượt tải xuống đều làm tăng bộ nhớ rất lớn so với kích thước tệp của mục đã tải xuống. Nếu tôi tải xuống một tệp có kích thước ~ 3 MB thì mức sử dụng bộ nhớ sẽ tăng khoảng 8 MB.

Như bạn có thể thấy việc tải xuống được sản xuất nhiều LOH mà không được xóa sau khi tải về. Ngay cả việc buộc GC hoặc thiết lập GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; không giúp ngăn chặn rò rỉ bộ nhớ này.

So sánh Snapshot 1 và 2 bạn có thể thấy rằng dung lượng bộ nhớ được sản xuất bởi các mảng byte mà có thể là kết quả download.

Thực hiện một số lần tải xuống cho thấy mức độ rò rỉ bộ nhớ này khủng khiếp đến mức nào.

Theo tôi, điều này gây ra bởi cá thể WebClient theo bất kỳ cách nào. Tuy nhiên tôi không thể thực sự xác định chính xác những gì đang gây ra vấn đề này. Nó thậm chí không quan trọng nếu tôi buộc GC. Màn hình này cho thấy đây đó mà không cần gc buộc:

gì đang gây ra quá nóng này và làm thế nào tôi có thể sửa chữa nó? Đây là một lỗi lớn và tưởng tượng 100 hoặc nhiều lượt tải xuống quá trình sẽ hết bộ nhớ.

Sửa


Như đề nghị tôi nhận xét ra phần trách nhiệm thiết lập các thẻ và chuyển đổi M4A sang MP3. Tuy nhiên công cụ chuyển đổi chỉ là một tiếng gọi của FFMPEG vì vậy nó không phải là một rò rỉ bộ nhớ:

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

Phương pháp DownloadCompleted() bây giờ trông như thế này:

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

Kết quả sau khi tải 7 hạng mục: Có vẻ như đây không phải là rò rỉ bộ nhớ.

Ngoài ra, tôi cũng đang nộp lớp DownloadManager vì nó đang xử lý toàn bộ quá trình tải xuống. Có lẽ đây có thể là nguồn gốc của vấn đề.

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

Phiên bản .NET của bạn là gì? Từ mã của bạn, nó nói: NET CLR 1.0.3705 – Matt

+0

Tôi đang sử dụng .NET Framework 4.5.2 – chris579

+0

WebClient không bị rò rỉ. Rõ ràng bạn nên quan tâm nhiều hơn về "Taglib" và "MusicFormatConverter", các lớp không giống như WebClient là * không * được kiểm tra hàng triệu lần mỗi ngày. Sử dụng một profiler bộ nhớ phong nha để có được trước. –

Trả lời

2

Cuối cùng, sau hàng chục lần kiểm tra lỗi và bộ nhớ được giải quyết ngay bây giờ.

Vì @SimonMourier đã nêu vấn đề này liên quan đến thiết kế của các phương pháp UploadFile, DownloadData, DownloadStringDownloadFile. Nhìn vào backend của họ, bạn có thể thấy rằng tất cả trong số họ đang sử dụng DownloadBits method private trong lớp WebClient với chữ ký này:

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

Về kiểu trả rõ ràng lý do tại sao hành vi cũng giống như tôi phát hiện ra: Khi sử dụng các phương pháp được đề cập ở trên, nội dung được lưu trong một mảng byte. Do đó, không nên sử dụng các phương thức này nếu kích thước tệp> 85.000 byte vì điều này sẽ dẫn đến việc lấp đầy LOH cho đến khi đạt tới giới hạn bộ nhớ. Điều này có thể không quan trọng nếu các tập tin là nhỏ nhưng với kích thước ngày càng tăng, LOH cũng đang phát triển bởi một bội số.

Như một sự bổ sung ở đây giải pháp cuối cùng của tôi:

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

Tuy nhiên nhờ sự giúp đỡ!

Các vấn đề liên quan