Edit: Added code (Exception on line 095, 5th time it's hit.)
public DataTable ParseBarcodes(String[] files, BarcodeZoneScan[] scanParameters)
{
message = null;
//gmseBitmap img = null;
gmseBitmap rotImg = null;
gmseBitmap parseImage = null;
gmseBitmap tempImage = null;
DataTable codes = new DataTable();
codes.Columns.Add("PageNumber");
codes.Columns.Add("Text");
codes.Columns.Add("Type");
codes.Columns.Add("RegionName");
try
{
gmseBarcodeInfoCollection bcc;
gmseBarcodeReaderParameter param = new gmseBarcodeReaderParameter();
gmseLicense.License = "plaintext license key ommited";
String dvImageName;
int searchCount = 0;
for (int dvCount = 0; dvCount < files.Length; dvCount++)
{
if (cancelled) //If cancelled, end the loops
{
dvCount = files.Length;
break;
}
dvImageName = files[dvCount].ToString();
using (gmseBitmap img = new gmseBitmap(dvImageName))
{
int framecount = img.GetFrameCount();
for (int e = 0; e < framecount; e++)
{
for (int j = 0; j < scanParameters.Length; j++)
{
if (scanParameters[j].Range == PageRange.All ||//All
(scanParameters[j].Range == PageRange.Even && (searchCount == 0 || searchCount % 2 == 0)) || //even
(scanParameters[j].Range == PageRange.Odd && (searchCount != 0 && searchCount % 2 != 0)) ||
(scanParameters[j].Range == PageRange.First && searchCount == 0))
{
//Setup what barcodes are going to be search for
param.BarcodeType = 0;
if (scanParameters[j].BarcodeTypes == BarcodeType.All) //All
{
param.BarcodeType = (int)gmseBarcodeType.All;
}
else
{
if ((scanParameters[j].BarcodeTypes & BarcodeType.Code39) != 0) //Code 39
param.BarcodeType = param.BarcodeType | (int)gmseBarcodeType.Code39;
if ((scanParameters[j].BarcodeTypes & BarcodeType.Code11) != 0) //Code 11
param.BarcodeType = param.BarcodeType | (int)gmseBarcodeType.Code11;
if ((scanParameters[j].BarcodeTypes & BarcodeType.Code93) != 0) //Code 93
param.BarcodeType = param.BarcodeType | (int)gmseBarcodeType.Code93;
if ((scanParameters[j].BarcodeTypes & BarcodeType.Code128) != 0) //Code 128
param.BarcodeType = param.BarcodeType | (int)gmseBarcodeType.Code128;
if ((scanParameters[j].BarcodeTypes & BarcodeType.Ean8) != 0) //EAN 8
param.BarcodeType = param.BarcodeType | (int)gmseBarcodeType.EAN8;
if ((scanParameters[j].BarcodeTypes & BarcodeType.Ean13) != 0) // EAN 13
param.BarcodeType = param.BarcodeType | (int)gmseBarcodeType.EAN13;
if ((scanParameters[j].BarcodeTypes & BarcodeType.I2of5) != 0) //I2of5
param.BarcodeType = param.BarcodeType | (int)gmseBarcodeType.i2of5;
}
param.IgnoreCheckSum = 1;
param.ReadMode = gmseBarcodeReadMode.WholeBitmap;
using (rotImg = new gmseBitmap(img.ExtractFrame(e)))
{
// do some basic image enhancement for better results
rotImg.ChangePixelFormat(System.Drawing.Imaging.PixelFormat.Format32bppArgb);
rotImg.SelectActiveFrame(e);
if (scanParameters[j].WholePage)
{
parseImage = rotImg.ExtractFrame(e);
}
else
{
using (tempImage = rotImg.ExtractFrame(e))
{
Rectangle convertedRect = returnConvertedRectangle(tempImage, scanParameters[j].Dimensions);
if (convertedRect.IntersectsWith(new Rectangle(0, 0, tempImage.Width, tempImage.Height)))
{
//GC.Collect(); //Test so I can see what objects are still alive in dump
parseImage = tempImage.CopyRectangle(convertedRect); //Exception here
}
}
}
}
//rotImg.Dispose();
//rotImg = null;
if (parseImage != null)
{
//Now we will apply the image enhancements:
if (scanParameters[j].Enhancements != ImageEnhancement.None)
{
rotImg = EnhanceImage(parseImage, scanParameters[j].Enhancements);
parseImage.Dispose();
parseImage = null;
}
if ((scanParameters[j].BarcodeScanDirection & ScanDirection.LeftToRight) != 0 && !cancelled)
{
if (parseImage == null)
{
tempImage = new gmseBitmap(rotImg.Image, 1);
}
else
{
tempImage = new gmseBitmap(parseImage.Image, 1);
}
bcc = tempImage.ReadBarcodes(param);
foreach (gmseBarcodeInfo bc in bcc)
{
addBarcode(codes, new object[] { searchCount, bc.Text, gmseBarcodeTypeConvert(bc.BarcodeType), scanParameters[j].ZoneName });
}
tempImage.Dispose();
tempImage = null;
}
if ((scanParameters[j].BarcodeScanDirection & ScanDirection.RightToLeft) != 0 && !cancelled)
{
if (parseImage == null)
{
tempImage = new gmseBitmap(rotImg.Image, 1);
}
else
{
tempImage = new gmseBitmap(parseImage.Image, 1);
}
tempImage.RotateFlip(RotateFlipType.Rotate180FlipNone);
bcc = tempImage.ReadBarcodes(param);
foreach (gmseBarcodeInfo bc in bcc)
{
addBarcode(codes, new object[] { searchCount, bc.Text, gmseBarcodeTypeConvert(bc.BarcodeType), scanParameters[j].ZoneName });
}
tempImage.Dispose();
tempImage = null;
}
if ((scanParameters[j].BarcodeScanDirection & ScanDirection.TopToBottom) != 0 && !cancelled)
{
if (parseImage == null)
{
tempImage = new gmseBitmap(rotImg.Image, 1);
}
else
{
tempImage = new gmseBitmap(parseImage.Image, 1);
}
tempImage.RotateFlip(RotateFlipType.Rotate90FlipNone);
bcc = tempImage.ReadBarcodes(param);
foreach (gmseBarcodeInfo bc in bcc)
{
addBarcode(codes, new object[] { searchCount, bc.Text, gmseBarcodeTypeConvert(bc.BarcodeType), scanParameters[j].ZoneName });
}
tempImage.Dispose();
tempImage = null;
}
if ((scanParameters[j].BarcodeScanDirection & ScanDirection.BottomToTop) != 0 && !cancelled)
{
if (parseImage == null)
{
tempImage = new gmseBitmap(rotImg.Image, 1);
}
else
{
tempImage = new gmseBitmap(parseImage.Image, 1);
}
tempImage.RotateFlip(RotateFlipType.Rotate270FlipNone);
bcc = tempImage.ReadBarcodes(param);
foreach (gmseBarcodeInfo bc in bcc)
{
addBarcode(codes, new object[] { searchCount, bc.Text, gmseBarcodeTypeConvert(bc.BarcodeType), scanParameters[j].ZoneName });
}
tempImage.Dispose();
tempImage = null;
}
if (parseImage != null)
{
parseImage.Dispose();
parseImage = null;
}
if (rotImg != null)
{
rotImg.Dispose();
rotImg = null;
}
}
}
}
searchCount++;
if (cancelled) //If cancelled, end the loops
{
e = framecount;
dvCount = files.Length;
}
}
} //end using img
//img.Dispose();
//img = null;
}
}
catch (Exception ex)
{
message = ex.Message;
}
finally
{
if (img != null)
{
img.Dispose();
img = null;
}
if (rotImg != null)
{
rotImg.Dispose();
rotImg = null;
}
if (tempImage != null)
{
tempImage.Dispose();
tempImage = null;
}
if (parseImage != null)
{
parseImage.Dispose();
parseImage = null;
}
}
if (!String.IsNullOrEmpty(message))
throw new Exception(message);
return codes;
}
We use this GMSE Imaging plugin to assist in OCR reading barcodes from scans, it deals with skew by rotating the image by 10 degrees until it gets a read. A bug was discovered where scanning different sized sheets would throw an error.
I traced it from our main program to one of our DLLs, where I found it was catching an OutOfMemoryException.
The original TIF is 300kb, but there is a fair amount of copying done to rotate the images. (between 4 bitmaps) However I have followed the program through and monitored the locals and it appears that each bitmap is being disposed and assigned null correctly before the method at fault loops.
I've also tried adding GC.Collect()
at the end of my loop.
I am on a 32bit W7 machine, which I have read has 2GB limit per object, with copious amounts of RAM so nothing lacking that that respect. Been watching it on Task Manager and my RAM usage only goes from 1.72GB to 1.78GB.
This has been a tricky one to research, as OoM seems to be an unusual occurring error. I was wondering if anyone had any advice in dealing with this kind of exception? I'm not a Visual Studio master, is there an easy way of monitoring resources/memory usage?
Or knows of any utilities I can use to assist?
Dumping the error message here, not sure how useful the code snippets would be in this situation...
System.OutOfMemoryException was caught
Message=Out of memory.
Source=System.Drawing
StackTrace:
at System.Drawing.Bitmap.Clone(Rectangle rect, PixelFormat format)
at gmse.Imaging.gmseBitmap.CopyRectangle(Rectangle r)
at ImagingInterface.ImagingFunctions.ParseBarcodes(String[] files, BarcodeZoneScan[] scanParameters) in C:\Working\Scan.backup\Global Dlls\v2.6.0.02\ScanGlobalDlls\ImagingInterface\ImagingFunctions.cs:line 632
(currently reading more into GC/Memory management http://msdn.microsoft.com/en-us/library/ee851764.aspx )
Working on a step of this guide, using SOS debugger in the Immediate window, with the aim of pinpointing whether the exception is generated from managed or unmanaged code.
Steps from above have indicated it's a problem with the managed code, as exception type from SOS is shown.
Exception object: 39594518
Exception type: System.OutOfMemoryException
Message: <none>
InnerException: <none>
StackTrace (generated):
The Heapdump I took doesn't seem to be thousands of bitmaps like I had kinda expected. Not 100% sure how to interpret the dump so seeing what I can find on it.
Not sure where to move from here right now! (searches..)
edit:
I have been trying to apply the lessons in this blog to my problem.
Started with PerfMon
This graph shows my program from execution to where it catches the exception.
The first two sharp peaks occur after triggering parsing of the scanned image, the last drop off occurs when the exception is caught.
Q: Compare the curves for Virtual Bytes, Private Bytes and #Bytes in all Heaps, do they follow eachother or do they diverge? What is the significance of #Bytes in all heaps diverging from? (As its flat on mine)
Examined Memory with !address -summary MEM_IMAGE corresponded PrivateBytes(113MB) pretty much spot on.
Q: Where is most of the memory going (which RegionType)? RegionUsageFree 87.15% RegionUsageIsVAF 5.64% (Busy 43.89%) [memory allocated through VirtualAlloc] RegionUsageImage 5.54% (Busy 43.13%) [Memory that is mapped to a file that is part of an executable image.]
In WinDbg with SOS loaded, I did a !DumpHeap
//...
7063424c 1201 28824 System.Collections.ArrayList
706228d4 903 28896 System.EventHandler
7062f640 1253 30072 System.RuntimeType
6ec2be78 833 31216 System.Windows.Forms.PropertyStore+IntegerEntry[]
6ec2b0a4 654 34008 System.Windows.Forms.CreateParams
7063547c 318 35472 System.Collections.Hashtable+bucket[]
6ec2aa5c 664 37184 System.Windows.Forms.Control+ControlNativeWindow
70632938 716 40400 System.Int32[]
6c546700 48 49728 System.Data.RBTree`1+Node[[System.Data.DataRow, System.Data]][]
70634944 85 69600 System.Byte[]
6ec2b020 931 85972 System.Windows.Forms.PropertyStore+ObjectEntry[]
6c547758 156 161616 System.Data.RBTree`1+Node[[System.Int32, mscorlib]][]
705e6c28 2107 238912 System.Object[]
00305ce8 18 293480 Free
7062f9ac 5842 301620 System.String
Total 35669 objects
And here are the top memory hogging objects. I was hoping something would stick out like a sore thumb, like a giant amount of bitmaps or something. Is anything here scream out "I'm acting unusually!" to anyone? (I am trying to examine the top ones individually for suspect things, but would be nice to narrow down the possible culprits a bit more)
This page (Address summary explained) has been a big help. However C# is my first language, so I have no prior experience debugging memory issues. Would like to know if I am on the right track (Is GC an issue at all?) as I haven't found anything that's given me any clear indications yet.
Answer: Problem was caused in 3rd party library. Nothing I can do. Found out through deliberation and some tests with stripped down code involving just the method producing the error.
Bounty awarded to what I felt I learnt the most from.
Okay, the added info helps. The problem is not that your program uses too much memory, it uses too little. The garbage collected heap has very little data in it. That's not uncommon for a program that manipulates bitmaps. The Bitmap class is a very small wrapper around GDI+ functions, it uses only a handful of bytes in the GC heap. So you can create an enormous amount of bitmaps before you fill up the gen #0 heap and trigger a garbage collection. This is also visible from Perfmon, you want to look at the .NET CLR Memory, Gen 0 Collections counter. A healthy program triggers a collection about 10 times per second when it is doing work.
Not getting collections is fine, but there's something else that doesn't work when there are no collections. The finalizer thread never runs. Finalizers are important to release unmanaged resources other than memory. Like operating system handles and any unmanaged memory pointers held by managed objects. Bitmap has those.
First thing to do is to run Taskmgr.exe, Processes tab. Click View + Select Columns and tick Handles, USER objects and GDI objects. Observe these counters while your program is running. If you see one climbing up without bound then you have a problem that could cause GDI+ to generate an OOM exception. GDI objects being the common cause.
Carefully review your code and check that you are calling Dispose() on any Image or Bitmap that you no longer use. Beware of the subtle ones, like assigning the Image property of a PictureBox. You'd have to dispose the old one if it isn't null. That's painful of course and it is too easy to miss one. So use a simple strategy, count the number of bitmaps you created and, say, on the hundredth call GC.Collect + GC.WaitForPendingFinalizers() to trigger a collection and a finalizer sweep.