PhotoSauce Blog

Every time you use System.Drawing from ASP.NET, something bad happens to a kitten.
I don’t know what, exactly... but rest assured, kittens hate it.

Well, they’ve gone and done it. The corefx team has finally acquiesced to the many requests that they include System.Drawing in .NET Core.

The upcoming System.Drawing.Common package will include most of the System.Drawing functionality from the full .NET Framework and is meant to be used as a compatibility option for those who wish to migrate to .NET core but were blocked by those dependencies. From that standpoint, Microsoft is doing the right thing. Reducing friction as far as .NET Core adoption is concerned is a worthy goal.

On the other hand, System.Drawing is one of the most poorly implemented and most developer-abused areas of the .NET Framework, and many of us were hoping that the uptake of .NET Core would mean a slow death for System.Drawing. And with that death would come the opportunity to build something better.

For example, the mono team have released a .NET-compatible wrapper for the Skia cross-platform graphics library from google, called SkiaSharp. Nuget has come a long way in supporting platform-native libraries, so installation is simple. Skia is quite full-featured, and its performance blows System.Drawing away.

The ImageSharp team have also done tremendous work, replicating a good deal of the System.Drawing functionality but with a nicer API and a 100% C# implementation. This one isn’t quite ready for production use yet, but it appears to be getting close. One word of warning with this library, though, since we’re talking about server apps: As of now, its default configuration uses Parallel.For internally to speed up some of its operations, which means it will tie up more worker threads from your ASP.NET thread pool, ultimately reducing overall application throughput. Hopefully this will be addressed before release, but it only takes one line of code to change that configuration to make it server-friendly.

Anyway, if you’re drawing, graphing, or rendering text to images in a server-side app, either of these would be worth a serious look as an upgrade from System.Drawing, whether you’re moving to .NET Core or not.

For my part, I’ve built a high-performance image processing pipeline for .NET and .NET Core that delivers image quality that System.Drawing can’t match and that does it in a highly scalable architecture designed specifically for server use. It’s Windows only for now, but cross-platform is on the roadmap. If you use System.Drawing (or anything else) to resize images on the server, you’d do well to evaluate MagicScaler as a replacement.

But the resurrection of System.Drawing, while easing the transition for some developers, will probably kill much of the momentum these projects have gained as developers were forced to search for alternatives. Unfortunately, in the .NET ecosystem, a Microsoft library/package will almost always win out over other options, no matter how superior those alternatives might be.

This post is an attempt to make clear some of the shortcomings of System.Drawing in the hopes that developers will evaluate the alternatives even though System.Drawing remains an option.

I’ll start with the oft-quoted disclaimer from the System.Drawing documentation. This disclaimer came up a couple of times in the GitHub discussion debating System.Drawing.Common.

"Classes within the System.Drawing namespace are not supported for use within a Windows or ASP.NET service. Attempting to use these classes from within one of these application types may produce unexpected problems, such as diminished service performance and run-time exceptions"

Like many of you, I read that disclaimer a long time ago, and then I went ahead and used System.Drawing in my ASP.NET apps anyway. Why? Because I like to live dangerously. Either that, or there just weren’t any other viable options. And you know what? Nothing bad happened. I probably shouldn’t have said that, but I’ll bet plenty of you have had the same experience. So why not keep using System.Drawing or the libraries built around it?

Reason #1: GDI Handles

If you ever did have a problem using System.Drawing on the server, this was probably it. And if you haven’t yet, this is the one you’re most likely to see.

System.Drawing is, for the most part, a thin wrapper around the Windows GDI+ API. Most System.Drawing objects are backed by a GDI handle, and there are a limited number of these available per process and per user session. Once that limit is reached, you’ll encounter out of memory exceptions and/or GDI+ ‘generic’ errors.

The problem is, .NET’s garbage collection and finalization process may delay the release of these handles for long enough that you can overrun the limit even under relatively light loads. If you forget (or don’t know) to call Dispose() on objects that hold one of those handles, you run a very real risk of encountering these errors in your environment. And like most resource-limit/leak bugs, it will probably get missed during testing and only bite you once you’ve gone live. Naturally, it will also occur when your app is under its heaviest load, so the max number of users will know your shame.

The per-process and per-session limits vary by OS version, and the per-process limit is configurable. But no matter the version, GDI handles are represented with a USHORT internally, so there’s a hard limit of 65,536 handles per user session, and even well-behaved apps are at risk of encountering this limit under sufficient load. When you consider the fact that more powerful servers allow us to serve more and more concurrent users from a single instance, this risk becomes more real. And really, who wants to build software with a known hard limit to its scalability?

Reason #2: Concurrency

GDI+ has always had issues with concurrency, and although many of those were addressed with architectural changes in Windows 7/Windows Server 2008 R2, you will still encounter some of them in newer versions. Most prominent is a process-wide lock held by GDI+ during any DrawImage() operation. If you’re resizing images on the server using System.Drawing (or the libraries that wrap it), DrawImage() is likely at the core of that code.

What’s more, when you issue multiple concurrent DrawImage() calls, all of them will block until all of them complete. Even if the response time isn’t an issue for you (why not? do you hate your users?), consider that any memory resources tied up in those requests and any GDI handles held by objects related to those requests are tied up for the duration. It actually doesn’t take very much load on the server for this to cause problems.

There are, of course, workarounds for this specific issue. Some developers spawn an external process for each DrawImage() operation, for example. But really, these workarounds just add extra fragility to something you really shouldn’t be doing in the first place.

Reason #3: Memory

Consider an ASP.NET handler that generates a chart. It might go something like this:

  1. Create a Bitmap as a canvas
  2. Draw some shapes on that Bitmap using Pens and/or Brushes
  3. Draw some text using one or more Fonts
  4. Save the Bitmap as PNG to a MemoryStream

Let’s say the chart is 600x400 pixels. That’s a total of 240,000 pixels, multiplied by 4 bytes per pixel for the default RGBA format, so 960,000 bytes for the Bitmap, plus some memory for the drawing objects and the save buffer. We’ll call it 1MB for that request. You’re probably not going to run into memory issues in this scenario, and if you do, you might be bumping up against that handle limit I mentioned earlier because of all those Bitmaps and Pens and Brushes and Fonts.

The real problem comes when you use System.Drawing for imaging tasks. System.Drawing is primarily a graphics library, and graphics libraries tend to be built around the idea that everything is a bitmap in memory. That’s fine if you’re thinking small. But images can be really big, and they’re getting bigger every day as high-megapixel cameras get cheaper.

If you take System.Drawing’s naive approach to imaging, you’ll end up with something like this for an image resizing handler:

  1. Create a Bitmap as a canvas for the destination image.
  2. Load the source image into another Bitmap.
  3. DrawImage() the source onto the destination, resized/resampled.
  4. Save the destination Bitmap as JPEG to a MemoryStream.

We’ll assume the same 600x400 output as before, so we have 1MB again for the destination image and Stream. But let’s imagine someone has uploaded a 24-megapixel image from their fancy new DSLR, so we’ll need 6000x4000 pixels times 3 bytes per pixel (72MB) for the decoded RGB source Bitmap. And we’d use System.Drawing’s HighQualityBicubic resampling because that’s the only one that looks good, so we need to add another 6000x4000 times 4 bytes per pixel for the PRGBA conversion that it uses internally, making another 96MB. That’s 169MB(!) for a single image resizing request.

Now imagine you have more than one user doing the same thing. Now remember that those requests will block until they’re all complete. How many does it take before you run out of memory? And even if you’re not concerned about running completely out of memory, remember there are lots of ways your server memory could be put to better use than holding on to a bunch of pixels. Consider the impact of memory pressure on other parts of the app/system:

  • The ASP.NET cache may start dumping items that are expensive to re-create
  • The garbage collector will run more frequently, slowing the app down
  • The IIS kernel cache or Windows file system caches may have to remove useful items
  • The App Pool may overrun its configured memory limit and get recycled
  • Windows may have to start paging memory to disk, slowing the entire system

None of those are things you want, right?

A library designed specifically for imaging tasks will approach this problem in a very different way. It has no need to load either the source or destination image completely into memory. If you’re not going to draw on it, you don’t need a canvas/bitmap. It goes more like this:

  1. Create a Stream for the output JPEG encoder
  2. Load a single line from the source image and shrink it horizontally.
  3. Repeat for as many lines from the source as required to create a single line of output
  4. Shrink intermediate lines vertically and write a single output line to the encoder
  5. Goto 2. Repeat until all lines are processed.

Using this method, the same image resizing task can be performed using around 1MB of memory total, and even larger images incur only a small incremental overhead.

I know of only one .NET library that is optimized in this way, and I’ll give you a hint: it’s not System.Drawing.

Reason #4: CPU

Another side-effect of the fact that System.Drawing is more graphics-focused than imaging-focused is that DrawImage() is quite inefficient CPU-wise. I have covered this in quite a bit of detail in a previous post, but that discussion can be summarized with the following facts:

  • System.Drawing’s HighQualityBicubic scaler works only in PRGBA pixel format. In almost all cases, this means an extra copy of the image. Not only does this use (considerably) more RAM, it also burns CPU cycles on the conversion and the processing of the extra alpha channel.
  • Even after the image is in its native format, the HighQualityBicubic scaler performs roughly 4x as many calculations as are necessary to obtain the correct resampling results.

These facts add up to considerable wasted CPU cycles. In a pay-per-minute cloud environment, this directly contributes to higher hosting costs. And of course your response times will suffer.

And think of all the extra electricity wasted and heat generated. Your use of System.Drawing for imaging tasks is directly contributing to global warming. You monster.

Reason #5: Imaging is deceptively complicated

Performance aside, System.Drawing doesn’t get imaging right in many ways. Using System.Drawing means either living with incorrect output or learning all about ICC Profiles, Color Quantizers, Exif Orientation correction, and many more domain-specific topics. It’s a rabbit hole most developers have neither the time nor inclination to explore.

Libraries like ImageResizer and ImageProcessor have gained many fans by taking care of some of these details, but beware, they’re System.Drawing on the inside, and they come with all the baggage I've detailed in this post.

Bonus Reason: You can do better

If, like me, you’ve had to wear glasses at some point in your life, you probably remember what it was like the first time you put them on. I thought I could see ok, and if I squinted just right, things were pretty clear. But then I slid those glasses on, and the world became a lot more detailed than I knew it could.

System.Drawing is a lot like that. It does ok if you get the settings just right, but you might be surprised how much better your images could look if you used a better tool.

I’ll just leave this here as an example. This is the very best System.Drawing can do versus MagicScaler’s default settings. Maybe your app would benefit from getting glasses…

System.Drawing System.Drawing
MagicScaler MagicScaler

So look around, evaluate the alternatives, and please, for the love of kittens, stop using System.Drawing in ASP.NET.


Comment by Bruno Martinez

I think the guidance about avoiding blocking on many threads per request in ASP.NET doesn't apply here. You wouldn't be blocking but actually using CPU cycles. SQL Server also uses multiple CPUs per query, even though it may be able to put other queries in the other CPUs.

Bruno Martinez
Comment by Clinton Ingram

@Bruno, thanks for bringing that up. I’ll admit, the Hanselman blog post I linked wasn't a great reference for the issue because it specifically addressed threads that were blocked by waiting on external requests. It's true that threads busy on a CPU-bound task wouldn't be wasted in the same way.

However, using multiple ASP.NET thread pool threads for a single CPU-bound request does hurt scalability. Essentially, if you do this, you're allowing a single request to tie up all CPU resources, which is great for the speed of that one request but bad for all others and for the system as a whole. Any additional load on the server once all CPUs are busy leads to extra context switches, which lowers overall performance. And once all thread pool threads are exhausted, ASP.NET will add more to the pool, resulting in more stack memory allocated, even more context switches, and even worse performance.

This post describes the problem in a generic sense if you’re not familiar.

As for the SQL Server comparison, it’s quite a different situation because SQL Server knows more about what work has to be done and what resources there are to do it, so it can make intelligent decisions as far as how much to parallelize and when. For a comparable SQL example, you’d have to do something like creating a managed UDF that uses one thread per core internally. That would wreak havoc with SQL’s own thread management, and performance would suffer there as well.

Clinton Ingram
Comment by Altex

GDI+ objects are NOT backed by GDI handles at all. Use GDIView and check for yourself. I created 1M fonts, brushes, and Graphics objects from bitmaps, 0 (zero) GDI handles. GDI+ is not GDI.

GDI+ doesn't lock the process. There is no code in Windows gdiplus library to lock the process, in fact GDI+ is not thread safe and not synchronized. This is also simple to verify, which I did running 1K threads using DrawImage in tight loops and dumping the thread ids. The complaints you read around are probably because of bad code and a misunderstanding of thread synchronization.

Comment by Clinton Ingram

The behavior you see is completely dependent on undocumented implementation details in GDI+. For example, depending how you test, you may or may not be able to run DrawImage operations in parallel. Using the HighQualityBicubic interpolation mode (which is most common in real-world scenarios) most definitely causes the DrawImage() operations to be serialized. And GDI+ objects may or may not allocate a handle depending on the backing device context. Again, it depends what you're doing/testing. That unpredictable behavior is one of the main reasons it's not the best tool for the job.

Clinton Ingram
Comment by butek

May be I utilize the 3d party libraries wrong?

Comment by Clinton Ingram

@butek from what I can see in your example, the System.Drawing output is sharper and the file sizes smaller. System.Drawing defaults to a quality setting of 75, so that explains the smaller file size, and its HighQualityBicubic interolation setting introduces more sharpness than the defaults of SkiaSharp or ImageSharp. So yes, that's all about the settings used. You will find that you get sharper/cleaner output still with MagicScaler's defaults.

Most of the points in this post are related to performance and scalability rather than image quality or compression, though. While changing settings will allow you to modify sharpness and output size, fundamental differences in architecture cannot be be easily seen in a simple one-image test.

Clinton Ingram