Hi Hubr, I present to you a translation of the article "5 Reasons You Should Stop Using System.Drawing from ASP.NET"
Well, they did it.The corefx team finally agreed to numerous requests and included System.Drawing in .NET Core. (original article is from July 2017)
Outgoing package System.Drawing.Common will contain most of the System.Drawing functionality of the complete .NET Framework, and is intended to be used as a compatibility option for those who want to migrate to .NET Core but can’t because of dependencies. From this point of view, Microsoft is doing the right thing. There needs to be less friction because adopting .Net Core is a more worthwhile goal.
On the other hand, System.Drawing is one of the poorest and most deprived areas of the .Net Framework and many of us hoped that the introduction of .NET Core would mean the slow death of System.Drawing. And with that death should come the opportunity to do something better.
For example, the Mono team made a .NET-compatible wrapper for a cross-platform graphics library Skia From Google, called SkiaSharp To make installation easy, Nuget has come a long way in supporting native libraries for every platform. Skia is quite full-featured and its performance beats System.Drawing.
Command ImageSharp has also done a great job of replicating much of System.Drawing’s functionality, but with a better API and 100% C# implementation. They’re still not ready for productive use, but it looks like they’re pretty close to it. A small warning about this library, since we are talking about use in server applications : right now, the default configuration inside uses Parallel.For to speed up some operations, which means more worker threads from the ASP.NET pool will be used, eventually Reducing the overall throughput of the application Hopefully this behavior will be revised before the release, but even now a single configuration line is enough to make it more usable on the server.
In any case, if you’re drawing, plotting, or rendering text to images in a server application, it’s worth seriously considering changing System.Drawing to anything, whether you’re upgrading to .NET Core or not.
For my part, I’ve built a high-performance image processing pipeline for .NET and .NET Core that provides image quality that System.Drawing can’t provide, and does so in a highly scalable architecture designed specifically for use on a server. So far it’s only for Windows, but cross-platform is in the plans. If you’re using System.Drawing (or something else) to resize images on the server, you might want to consider MagicScaler As a substitute.
But the resurrection of System.Drawing, which makes the transition easier for some developers, is likely to kill much of the momentum that these projects gained, as developers were forced to look for alternatives. Unfortunately, in the .NET ecosystem, Microsoft libraries and packages will always win out, no matter how superior the alternatives may be.
This post is an attempt to fix some System.Drawing blunders in the hope that developers will explore alternatives even if System.Drawing remains as an option.
I’ll start with the oft-quoted disclaimer from the System.Drawing documentation. This disclaimer has been raised a couple of times in discussions on GitHub when discussing System.Drawing.Common
"Classes with the System.Drawing namespace are not supported for use in Windows services or ASP.NET. Attempting to use these classes with these types of applications can cause unexpected problems, such as reduced server performance and runtime errors."
Like many of you, I read this disclaimer a long time ago, and at the time I skipped it and still used System.Drawing in my ASP.NET application. Why? Because I love danger. Either that or there were no other viable options. And guess what? Nothing bad happened. I probably shouldn’t have said this, but I bet many of you have experienced the same thing. So why not keep using System.Drawing or libraries based on it?
Reason #1: GDI descriptors
If you’ve ever experienced problems using System.Drawing on a server, this is probably the case. If you haven’t yet, this is one of the most likely possible causes.
System.Drawing is, for the most part, a thin wrapper for the Windows GDI+ API. Many System.Drawing objects are supported GDI descriptors , and they have a quantitative limit per CPU and per user session. If this threshold is reached, you will get an ‘Out of memory’ exception and/or a GDI+ ‘generic’ error.
The problem is that in .NET, garbage collection and process termination can delay releasing these descriptors long enough for you to reach the limit, even under light load. If you forgot (or didn’t know you needed) to call Dispose() on an object that contains such descriptors, you run a very high risk of encountering such bugs in your environment. And like most bugs related to resource constraints or leaks, it is likely that this situation will successfully test and sting you in productive use. Naturally this will happen when your application is under the highest load, so that the maximum number of users will know about your embarrassment.
Processor and user session limits depends on the version of the operating system, and the CPU limit is configurable. But the version doesn’t matter, because GDI descriptors are internally represented by USHORT data type, so there is a hard limit of 65536 descriptors per user session, and even a well written application risks reaching this limit under sufficient load. When you assume that a more powerful server will allow you to serve more and more users in parallel on a single instance, this risk becomes more real. And really, who wants to build software with a known hard limit on scalability?
Reason #2: Parallelism
GDI+ has always had problems with parallelism, although many of them were related to architectural changes in Windows7 / Windows Server 2008 R2 , you’re still seeing some of them in the new versions. The most notable one is process locking that GDI+ does during the DrawImage() operation. If you resize images on the server using System.Drawing (or libraries that wrap it), the DrawImage() method probably underlies this code.
Moreover, when making several simultaneous calls to DrawImage(), all they will be blocked until all they are not executed. Even if response time is not an issue for you (why not? do you hate your users?) consider that any memory resources associated with these requests and any GDI descriptors held by the objects associated with these requests are tied to the execution time. It wouldn’t really take too much load on the server to start causing problems.
Of course there are workarounds for this particular problem. For example, some developers create an external process for each DrawImage() operation. But really, this workaround just adds more fragility, which you really shouldn’t do.
Reason #3: Memory
Consider the ASP.NET handler that generates the diagram. It should do something like this :
- Create a raster image as a canvas
- Draw several shapes on the bitmap using pens and/or brushes
- Draw text using one or more fonts
- Save a bitmap image as a PNG in a MemoryStream
Let’s say the diagram is 600 by 400 dots. That’s a total of 240, 000 points, multiplied by 4 bytes per point for the default RGBA format, for a total of 960, 000 bytes for the bitmap, plus some for drawing objects and the save buffer. Make it 1mb for the whole query. You probably won’t get memory problems for a scenario like that, and if anything you’ll encounter, it’s more likely the descriptor limit I mentioned earlier, since images, brushes, pens and fonts have their own descriptors.
The real problem comes when System.Drawing is used for imaging tasks. System.Drawing is primarily a graphics library, and graphics libraries are usually all built around the idea that everything is a bitmap image in memory. That’s fine as long as you think about the little things. But images can be really big, and they get bigger every day as cameras with more megapixels get cheaper all the time.
If you take the naive System.Drawing approach to building images, you get something like this for the resize handler :
- Create a raster image as the canvas for the receiver image.
- Load the source image into another bitmap image.
- Call DrawImage() with the "source image" parameter for the destination image, with resizing applied.
- Save the target raster image in JPEG format to the memory stream.
Assuming that the target image will be 600×400 as in the previous example, then again we have 1Mb for the target image and the memory stream. But let’s assume that someone loaded a 24 megapixel image from their fancy new mirrors, then we need 6000×4000 dots with 3 bytes each (72mb) for the decoded RGB source bitmap image. And we’ll use the HighQualityBicubic resampling from System.Drawing, because it’s the only one that looks good. Then we need to consider the other 6000×4000 dots with 4 bytes each, for PRGBA-conversion which takes place inside the called method , adding an extra 96mb of used memory. This adds up to 169mb (!) for a single image conversion request.
Now imagine you have more than one user doing these things. Now let’s remember that queries are blocked until all of them are fully executed. How long would it take for you to run out of memory? And even if you’re not worried about completely running out of all the available memory, remember that there are many better ways to use your server’s memory than to hold a bunch of pixels. Consider the effects of memory pressure on other parts of the application/system :
- ASP.NET cache can start dumping items that are expensive to recreate
- Garbage collector will run more often, slowing down the application
- IIS kernel cache or Windows file system cache can delete useful items
- Application pool can exceed memory limit and can be restarted
- Windows may start to swap memory to disk, slowing down the whole system
You really don’t want any of this, do you?
Libraries designed specifically for image processing tasks have a very different approach to this problem. They don’t need to load the source or target image entirely into memory. If you’re not going to paint on it, you don’t need a canvas/raster image. It’s done more like this :
- Create a stream for the JPEG encoder of the target image
- Load a single line from the source image and compress it horizontally
- Repeat as many times as needed to form a single line for the target file
- Shrink these lines vertically
- Repeat from step 2 until all lines of the source file have been processed
Using this method, the same image can be processed using only 1mb of memory and even much larger images will require a small increase in overhead.
I only know of one .NET library that is optimized like this, and I’ll give you a hint: it’s not System.Drawing.
Reason #4: CPU
Another side effect of System.Drawing being more graph-oriented than image-oriented is that DrawImage() is rather inefficient in terms of CPU usage. I’ve covered this in some detail at previous post , but this discussion can be summarized by the following facts :
- In System.Drawing, the HighQualityBicubic scale conversion only works with the PRGBA format. In almost all scenarios, this means an extra copy of the image. Not only does this use (significantly) more extra memory, but this behavior also burns up CPU cycles to convert and process the extra alpha channel.
- Even after the image is in its native format, the HighQualityBicubic scale conversion performs about 4 times as much computation as is needed to produce the correct conversion results.
These facts add a significant amount of wasted CPU cycles. In a pay-per-minute cloud environment, this directly contributes to the cost of hosting. And of course, your response time will suffer.
And think also about the extra electricity wasted and heat generated. Your use of System.Drawing for image processing tasks directly affects global warming. You are a monster.
Reason #5: Image processing is deceptively complicated
Performance aside, System.Drawing in many ways fails to process an image correctly. Using System.Drawing means either living with incorrect output, or learning all about ICC profile, color quantization, exif orientation, correction, and many other specific things. It’s a rabbit hole that most developers don’t have the time or inclination to explore.
Libraries like ImageResizer and ImageProcessor have gained a lot of fans by taking care of some of these details, but beware, they have System.Drawing inside, and they come with all the baggage that I detailed in this article.
Bonus reason : you can do better
If you, like me, have 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 just fine, and if I squinted right, it would be pretty clear. But then I put those glasses on, and the world became much more detailed than I would have guessed.
System.Drawing is much the same. It does the right thing if you filled in the settings correctly. but you’d be surprised how much better your images can look if you use better utilities.
I’ll just leave it here as an example. This is the best that System.Drawing can do compared to the default MagicScaler settings. Maybe your application would benefit from getting points…
Photo by Jakob Owens
Look around, explore alternatives, and please, for the love of kittens, stop using System.Drawing in ASP.NET