Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# OutOfMemoryException in System.Drawing.Bitmap

Is there any way to get more detail at runtime about an OutOfMemoryException? Or, can this exception somehow not be caught by the enclosing try/catch and instead a try/catch higher up the call stack? I cannot reproduce using WinDBG so it must be something I can log from the application.

I apologize for the long explanation, but there are a lot of possible causes to eliminate, which I explain.

I have read up on all the possibilities for an OutofMemoryException and basically eliminated all of them. Normally, application runs great, but occasionally on only certain computers, I am getting an OutOfMemoryException. As these reports are in the field on not reproducible locally, I only have logs to go by. But I have a fair amount of detail.

What is strange:

  • Anything that might logically be allocating memory in the vicinity is in a try/catch, but the exception is treated as unhandled (and caught much higher up the call stack)
  • There are no StringBuffers in use
  • The exception happens even after rebooting and restarting the application.
  • The exception occurs after only a couple minutes, and only about 30MiB of memory allocated, in chunks no more than 1.5MiB.
  • Verified the application (built for "any" processor) is running as 64 bit.
  • no shortage of disk space (270Gb free) and pagefile is enabled.
  • does not appear to be a possible LoH fragmentation issue.

This has happened a couple times in different parts of the application recently. The first time, I concluded there was a corrupt .NET assembly, as the exception was occurring right when it would first load the System.Web.Serialization assembly. I could determine it was happening right during a method call where that assembly was used for the first time. Reimaging the computer (to be identical of original setup) and updating windows resolved this issue.

But, it seems highly unlikely to me that the second case, different client, happening within a few days, is also corruption. This one is happening in a location where no assemblies would be loaded. I'm rethinking the first error now. What I do know:

  • It's happening in a thread pool thread (System.Timers.Timer, [Statthread])
  • Small number of threads active (< 5)
  • It happens around the time a 1MiB file is downloaded. This is read into a MemoryStream, so that could be as big as 2MiB. That is then fed to a System.Drawing.Bitmap constructor, resulting in a Bitmap that would be about 8MiB. However, that is all in a try/catch that catches System.Exception. The only thing not in the try/catch is returning the byte[] reference, which should just be a reference copy, not any memory allocation.
  • No other significant memory allocations have been done before this time. Reviewing heap in my local version, which should be running identically, shows just the app icon and a couple dozen objects that would be on Small Object Heap.
  • it is repeatable on a specific system with specific input. However, these systems are cloned from one another. Only obvious variation would be the order of windows updates.
  • The assembly I'm running is signed. Isn't there a checksum that ensures it isn't corrupted? Same for all the system assemblies? I don't see how this instance could be explained by corrupted dlls, or even data.
  • Viewing the call stack at time of exception is surprisingly unhelpful. I indicate in the code below where exception is thrown.
  • There is some use of COM objects. However, under normal conditions we run the app for weeks without memory problems, and when we get these exceptions, they are almost immediate, after only using around 20 relatively lightweight COM objects (IUPnPDevice)

    // Stack Trace indicates this method is throwing the OutOfMemoryException
    // It isn't CAUGHT here, though, so not in the try/catch.
    //
    internal void Render(int w, int h)
    {
        if (bitmap != null)
        {
            bitmap.Dispose();
            bitmap = null;
        }
    
        if (!String.IsNullOrEmpty(url))
        {
        // this information is printed successfully to log, with correct url
        // exception occurs sometime AFTER this somehow.
            Logger.Default.LogInfo("Loading {0}", url);
    
        // when file contents changed (to go from 1MiB to 500MiB, the error went away)
            byte[] data = DownloadBinaryFile(url);
            if (data != null)
            {
                try
                {
                    Bitmap bmp;
                    using (var ms = new MemoryStream(data))
                    {
                        bmp = new Bitmap(ms);
                    }
                    bitmap = bmp;
                }
                catch (Exception)
                {
                    // We do not catch anything here.
                    Logger.Default.LogWarning("WARNING: Exception loading image {0}", url);
                }
            }
    
            //
            // if we had any errors, just skip this slide
            //
            if (bitmap == null)
            {
                return;
            }
    
            // QUESTION EDIT:
            // in the problematic version, there was actually an unnecessary
            // call here that turns out to be where the exception was raised:
            using( Graphics g = Graphics.FromImage(bitmap)) {
            }
    
        }
    }
    
    // calling this would trigger loading of the System.Web assembly, except
    // similar method has been called earlier that read everything. Only
    // class being used first time is the BinaryReader, which is in System.IO 
    // and already loaded.
    internal static byte[] DownloadBinaryFile(String strURL, int timeout = 30000)
    {
        try
        {
            HttpWebRequest myWebRequest = HttpWebRequest.Create(strURL) as HttpWebRequest;
    
            myWebRequest.KeepAlive = true;
            myWebRequest.Timeout = timeout;
            myWebRequest.ReadWriteTimeout = timeout;
            myWebRequest.UserAgent = "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)";
    
            Encoding encode = System.Text.Encoding.GetEncoding("utf-8");
    
            using (HttpWebResponse myWebResponse = myWebRequest.GetResponse() as HttpWebResponse)
            {
                if (myWebResponse.StatusCode != HttpStatusCode.OK)
                {
                    Logger.Default.LogWarning("WARNING: Response {0} loading {1}", myWebResponse.StatusCode, strURL);
                    return null;
                }
    
                using (Stream receiveStream = myWebResponse.GetResponseStream())
                {
                    using (BinaryReader readStream = new BinaryReader(receiveStream))
                    {
                        // this extension method uses MemoryStream, but seems irrelevant since we don't catch anything here.
                        return readStream.ReadAllBytes();
                    }
                }
            }
        }
        catch (Exception e)
        {
            // we do not catch anything here.
            Logger.Default.LogError("ERROR: Exception {0} loading {1}", e.Message, strURL);
        }
        return null;
    }
    

So, after all of that, I return to my opening question. Are there any known properties on the OutOfMemoryException object I can inspect, or calls I can make after the exception is thrown, to narrow this down?

And..is there any reason an OutOfMemoryException would not be caught by the first try/catch, but would be caught further up the call stack?

like image 927
Garr Godfrey Avatar asked Oct 18 '17 06:10

Garr Godfrey


People also ask

What C is used for?

C programming language is a machine-independent programming language that is mainly used to create many types of applications and operating systems such as Windows, and other complicated programs such as the Oracle database, Git, Python interpreter, and games and is considered a programming foundation in the process of ...

Is C language easy?

Compared to other languages—like Java, PHP, or C#—C is a relatively simple language to learn for anyone just starting to learn computer programming because of its limited number of keywords.

What is C in C language?

What is C? C is a general-purpose programming language created by Dennis Ritchie at the Bell Laboratories in 1972. It is a very popular language, despite being old. C is strongly associated with UNIX, as it was developed to write the UNIX operating system.

What is C full form?

History: The name C is derived from an earlier programming language called BCPL (Basic Combined Programming Language). BCPL had another language based on it called B: the first letter in BCPL.


1 Answers

Thank you all. The answer is somewhat curious and I had some details wrong which made it hard to figure out. The error is here:

            Bitmap bmp;
            using (var ms = new MemoryStream(data))
            {
                bmp = new Bitmap(ms);
            }
            bitmap = bmp;

In the remarks in documentation on the Bitmap constructor, I found this:

You must keep the stream open for the lifetime of the Bitmap.

Obviously, closing the MemoryStream immediately after constructing was violating this. A garbage collection between this and when I actually used the Bitmap was apparently creating the error. (EDIT: actually, it seems that a boundary exists around 1MiB where the FromStream function will decompress only so much of a JPEG file initially. For JPEG < 1MiB, the entire image is decompressed and it doesn't actually use the stream after the initialization. For larger JPEG, it will not read beyond the first 1MiB until those pixels are needed)

It's hard for me to imagine why Microsoft did it this way. I wouldn't want to keep the original stream open, either (which is an http connection) so only solution I see is to clone the bitmap:

// Create a Bitmap object from a file.
using (var ms = new MemoryStream(data))
{
   bmp = new Bitmap(ms);
   Rectangle cloneRect = new Rectangle(0, 0, bmp.Width, bmp.Height);
   System.Drawing.Imaging.PixelFormat format = bmp.PixelFormat;
   this.bitmap = bmp.Clone(cloneRect, bmp.PixelFormat);    
}

What led to my long frustrating search and the exclusion of a key piece of information was that the code executing on a client machine was a slightly older version, with only a subtle change. Graphics.FromImage() was being called on the bitmap in the previous version, but that had been removed. Still, that version functioned very well the vast majority of the time.

like image 109
Garr Godfrey Avatar answered Sep 30 '22 01:09

Garr Godfrey