Super-slow ColdFusion image processing
November 8, 2010 · Chris Peters
I was excited to finally take some time to add the ability for people to upload screenshots in the CFWheels Site Directory when I ran into a big problem with performance.
I was excited to finally take some time to add the ability for people to upload screenshots in the CFWheels Site Directory when I ran into a big problem. But when I started sending large screenshots to ColdFusion’s ImageScaleToWidth()
, it would take 30+ seconds to do the processing. Unacceptable.
As it turns out, this slowness was a combination of processing a PNG on my Linux server. When I tried uploading a JPEG of the same size, it would process in under 5 seconds. I don’t know if PNG processing causes similar issues on Windows or Mac systems, so post in the comments if you have experienced similar issues on those OSes.
How I tried to fix it
Here’s where the fun starts. I tried a number of things to get around this constraint and try to allow for users to still upload PNGs without the server timing out.
Tweaking image quality settings
There is an interpolation
argument that you can pass into the image functions with different options for algorithms or canned settings like highestQuality
, highQuality
, and mediumQuality
. I had to get the settings so low that the images started looking jacked up. Not exactly how I want people visiting cfwheels.org to think…
Image CFC
This library has been great through the years as it adds an easy API for such awesome functionality. The only problem is that the underlying Java libraries it uses do a fairly poor job at maintaining quality while resizing images. Image quality is one area where the built-in ColdFusion functionality really shines.
Directory watcher event gateway
I don’t know why I went down this route, but I decided to try implementing an instance of the Directory Watcher event gateway. I found a really helpful article by Ray Camden that took most of the work out of this while helping me learn how to actually do it.
The watcher would check the image queue periodically for new files and resize them from there. It was pretty cool, but not really as good as <cfthread>
…
Offload the resizing task using <cfthread>
I had tried <cfthread>
earlier in the project to split the task into 3 parts (full, medium, and thumb). I then called <cfthread action="join">
at the end of that bit. As it turned out, the thread would cause the page load to wait until the slowest operation was done, which didn’t help my page load time very much at all. (It may have actually ended up being worse.)
But as it turns out, if you simply don’t call action="join"
in your script, CF will take that thread past your page load and continue processing in the background. All I had to do was remove that line and update my database with a status indicating that the images couldn’t be shown until the process was done. (The threaded process updates the flag in the database at the end.)
Here’s a little code from my siteScreenshot
CFWheels model to give you an idea of what happens:
<cffunction name="setImageSizes" access="private" hint="Resizes image in queue, moves new resized files to 'production,' and removes `tempUploadId` from DB."> | |
<cfset var loc = {}> | |
<cfif isUploadingNewFile()> | |
<!--- Read image ---> | |
<cflog file="siteScreenshotResize" text="Reading #this.imageFile#"> | |
<cfset loc.image = ImageRead("#variables.UPLOAD_QUEUE_DIRECTORY##this.imageFile#")> | |
<!--- Turn on antialiasing to improve image quality ---> | |
<cfset ImageSetAntialiasing(loc.image, "on")> | |
<cfthread | |
action="run" | |
name="loc[#CreateUuid()#]" | |
priority="low" | |
image="#loc.image#" | |
dsn="#get('dataSourceName')#" | |
thisId="#this.id#" | |
> | |
<!--- Resize full size ---> | |
<cfset resizeImage(image, "full", 1)> | |
<!--- Resize medium size ---> | |
<cfset resizeImage(image, "medium", 0.9)> | |
<!--- Resize thumbnail ---> | |
<cfset resizeThumbnail(image, "thumb", 0.8)> | |
<!--- Update DB to reflect new status | |
(Use query because model doesn't seem to be working inside of thread) ---> | |
<cfquery datasource="#dsn#"> | |
UPDATE | |
sitescreenshots | |
SET | |
tempuploadid = NULL | |
WHERE | |
id = | |
<cfqueryparam cfsqltype="cf_sql_integer" value="#thisId#"> | |
</cfquery> | |
</cfthread> | |
</cfif> | |
</cffunction> |