I have the following class which is a tool for retrieving MD5 hashes of files that are input into it. They can potentially be very large files and I require cancellation and progress report, hence why i rolled my own solution rather than just passing in a filestream to HashAlgorithm
.
I have a producer - consumer set up so that I can be reading in file blocks at the same time as calculating the MD5 hasher. I find the best performance is using 16mb blocks, which I allow a max pre-read of 3 blocks. Considering I have up to 5 of these running in parallel (5 drives) then memory usage can max out at 240mb assuming that the GC collects immediately. Obviously this doesn't happen so i see memory usage jump to approx 700mb or more in some instances.
How can I reduce this erratic memory behaviour?
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
namespace MD5Library
{
public class FileBlock
{
public byte[] Data;
public int Length;
public FileBlock(int inBufferSize)
{
Data = new byte[inBufferSize];
}
}
public class MD5Hasher
{
protected int bufferSize = 1024 * 1024 * 16; //16mb at a time
public void MD5Hasher()
{
}
public void MD5Hasher(int _bufferSize)
{
bufferSize = _bufferSize;
}
public Task<string> Go(string inFile, CancellationToken ct, IProgress<ProgressReport> prg)
{
FileInfo fi = new FileInfo(inFile);
// Queue with capacity 3x the buffer
BlockingCollection<FileBlock> BigBuffer = new BlockingCollection<FileBlock>(3);
Task readFileTask = new Task(() => readFiles(fi, ct, BigBuffer));
Task<string> computeHashTask = new Task<string>(() => calcHash(fi, BigBuffer, ct, prg));
readFileTask.Start();
computeHashTask.Start();
return computeHashTask;
}
private async void readFiles(FileInfo fi, CancellationToken ct, BlockingCollection<FileBlock> BigBuffer)
{
int bytesRead;
using (FileStream stream = new FileStream(fi.FullName, FileMode.Open))
{
do
{
FileBlock fileBlock = new FileBlock(bufferSize);
fileBlock.Length = await stream.ReadAsync(fileBlock.Data, 0, bufferSize, ct);
bytesRead = fileBlock.Length;
//Infinite timeout
BigBuffer.TryAdd(fileBlock, -1, ct);
} while (bytesRead != 0);
BigBuffer.CompleteAdding();
}
}
private string calcHash(FileInfo fi, BlockingCollection<FileBlock> BigBuffer, CancellationToken ct, IProgress<ProgressReport> prg)
{
long _fileSize = fi.Length;
long _bytesProcessed = 0;
using (HashAlgorithm hashAlgorithm = MD5.Create())
{
foreach (var b in BigBuffer.GetConsumingEnumerable(ct))
{
hashAlgorithm.TransformBlock(b.Data, 0, b.Length, b.Data, 0);
_bytesProcessed += b.Length;
// Report progress
ProgressReport p = new ProgressReport();
p.setProgress(_bytesProcessed, _fileSize);
prg.Report(p);
}
hashAlgorithm.TransformFinalBlock(new byte[0], 0, 0);
return HashToString(hashAlgorithm.Hash);
}
}
public int BufferSize
{
get
{
return bufferSize;
}
}
public string HashToString(byte[] inHash)
{
string hex = "";
foreach (byte b in inHash)
{
hex += b.ToString("x2");
}
return hex;
}
}
public class ProgressReport
{
public int PercentDone
{
get;
set;
}
public string InfoText
{
get;
set;
}
public void setProgress(long _progress, long _total)
{
PercentDone = Convert.ToInt32((_progress * 100) / _total);
InfoText = PercentDone + "% complete."; ;
}
}
//Borrowed from Stephen Cleary: http://nitoprograms.blogspot.co.uk/2012/02/reporting-progress-from-async-tasks.html
public class PropertyProgress<T> : IProgress<T>, INotifyPropertyChanged
{
private readonly SynchronizationContext context;
private T progress;
public PropertyProgress(T initialProgress = default(T))
{
this.context = SynchronizationContext.Current ?? new SynchronizationContext();
this.progress = initialProgress;
}
public T Progress
{
get
{
return this.progress;
}
private set
{
this.progress = value;
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs("Progress"));
}
}
}
void IProgress<T>.Report(T value)
{
this.context.Post(_ => { this.Progress = value; }, null);
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
Usage:
string source = @"c:\largefile.dat";
MD5Hasher md5async = new MD5Hasher();
PropertyProgress<ProgressReport> p = new PropertyProgress<ProgressReport>();
CancellationTokenSource cts = new CancellationTokenSource();
Console.Writeline(await md5async.Go(source, cts.Token, p));