I wrote little WPF application with 2 threads - main thread is GUI thread and another thread is worker.
App has one WPF form with some controls. There is a button, allowing to select directory.
After selecting directory, application scans for .jpg files in that directory and checks if their thumbnails are in hashtable. if they are, it does nothing. else it's adding their full filenames to queue for worker.
Worker is taking filenames from this queue, loading JPEG images (using WPF's JpegBitmapDecoder and BitmapFrame), making thumbnails of them (using WPF's TransformedBitmap) and adding them to hashtable.
Everything works fine, except memory consumption by this application when making thumbnails for big images (like 5000x5000 pixels). I've added textboxes on my form to show memory consumption (GC.GetTotalMemory() and Process.GetCurrentProcess().PrivateMemorySize64) and was very surprised, cuz GC.GetTotalMemory() stays close to 1-2 Mbytes, while private memory size constantly grows, especially when loading new image (~ +100Mb per image).
Even after loading all images, making thumbnails of them and freeing original images, private memory size stays at ~700-800Mbytes. My VirtualBox is limited to 512Mb of physical memory and Windows in VirtualBox starts to swap alot to handle this huge memory consumption. I guess I'm doing something wrong, but I don't know how to investigate this problem, cuz according to GC, allocated memory size is very low.
Attaching code of thumbnail loader class:
class ThumbnailLoader
{
Hashtable thumbnails;
Queue<string> taskqueue;
EventWaitHandle wh;
Thread[] workers;
bool stop;
object locker;
int width, height, processed, added;
public ThumbnailLoader()
{
int workercount,i;
wh = new AutoResetEvent(false);
thumbnails = new Hashtable();
taskqueue = new Queue<string>();
stop = false;
locker = new object();
width = height = 64;
processed = added = 0;
workercount = Environment.ProcessorCount;
workers=new Thread[workercount];
for (i = 0; i < workercount; i++) {
workers[i] = new Thread(Worker);
workers[i].IsBackground = true;
workers[i].Priority = ThreadPriority.Highest;
workers[i].Start();
}
}
public void SetThumbnailSize(int twidth, int theight)
{
width = twidth;
height = theight;
if (thumbnails.Count!=0) AddTask("#resethash");
}
public void GetProgress(out int Added, out int Processed)
{
Added = added;
Processed = processed;
}
private void AddTask(string filename)
{
lock(locker) {
taskqueue.Enqueue(filename);
wh.Set();
added++;
}
}
private string NextTask()
{
lock(locker) {
if (taskqueue.Count == 0) return null;
else {
processed++;
return taskqueue.Dequeue();
}
}
}
public static string FileNameToHash(string s)
{
return FormsAuthentication.HashPasswordForStoringInConfigFile(s, "MD5");
}
public bool GetThumbnail(string filename,out BitmapFrame thumbnail)
{
string hash;
hash = FileNameToHash(filename);
if (thumbnails.ContainsKey(hash)) {
thumbnail=(BitmapFrame)thumbnails[hash];
return true;
}
AddTask(filename);
thumbnail = null;
return false;
}
private BitmapFrame LoadThumbnail(string filename)
{
FileStream fs;
JpegBitmapDecoder bd;
BitmapFrame oldbf, bf;
TransformedBitmap tb;
double scale, dx, dy;
fs = new FileStream(filename, FileMode.Open);
bd = new JpegBitmapDecoder(fs, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
oldbf = bd.Frames[0];
dx = (double)oldbf.Width / width;
dy = (double)oldbf.Height / height;
if (dx > dy) scale = 1 / dx;
else scale = 1 / dy;
tb = new TransformedBitmap(oldbf, new ScaleTransform(scale, scale));
bf = BitmapFrame.Create(tb);
fs.Close();
oldbf = null;
bd = null;
GC.Collect();
return bf;
}
public void Dispose()
{
lock(locker) {
stop = true;
}
AddTask(null);
foreach (Thread worker in workers) {
worker.Join();
}
wh.Close();
}
private void Worker()
{
string curtask,hash;
while (!stop) {
curtask = NextTask();
if (curtask == null) wh.WaitOne();
else {
if (curtask == "#resethash") thumbnails.Clear();
else {
hash = FileNameToHash(curtask);
try {
thumbnails[hash] = LoadThumbnail(curtask);
}
catch {
thumbnails[hash] = null;
}
}
}
}
}
}