Using a graphics library, be GD, ImageMagick or anything else, is convenient, but carries a price to pay: unlike most Drupal parts, which are generally database-bound, image generation is typically CPU-bound: generating many images on the fly can significantly increase the CPU load on a system, while Drupal setups are typically not optimized for this, and could result in problems if you are using Drupal on a shared hosting account. So what ?
The problem
In the two previous posts in this series, we saw how to build a progress bar widget for Forms API, first in text format, then in image format using the GD library. Is there a simple way to prevent deployment of such new Forms API widgets from overtaxing the CPU in our server ?
The solution
Drupal caching services
The canonical answer is, as in most things Drupal: "caching", and is luckily enough real simple to deploy in this scenario.
Drupal has for a long time included two functions to handle module-level caching cache_set
and its counterpart cache_get
. Initially (in the 4.x series) limited to just one global cache
table, caching has been expanded in Drupal 5 and later to allow per-module cache tables, to increase performance by grouping together cache references with similar access patterns in their own table.
Let's have a look at out current image generation function, from the previous tutorial step:
Redirecting GD output
The CPU-heavy part is the one that goes from imagecreatetruecolor()
to imagedestroy()
. So we would like to cache the results. But how can we do this, since imagepng()
outputs the image directly, instead of returning them, and there is no function in GD to return the image to a string ?
Here we turn to a more basic PHP mechanism: output buffering, which is essentially output redirection to a buffer. After a call to ob_start()
, PHP will push an output context, in which it will redirect every output from the standard output going to HTTP to a temporary buffer. Then, once we are done outputting our data (in this case the PNG image) to the buffer, a call to ob_get_clean()
will return the buffer, and end output buffering for that context and release it. Applied to our case, this looks like:
Nothing more ! At this point, $png
is a string containing the PNG file, which we could save to a file, with the associated security and cleanup problems, or we can save it to the Drupal cache, while not forgetting to release the image buffer used by GD:
Note: should you really want to use file-based caching, you could just use the optional second argument to imagepng
, which allows the program to specify a file name where GD should store the generated image, and skip the output buffering mechanism.
The first parameter will be the unique-by-construction "cid" parameter needed by the Drupal caching service as the primary key for the cache table. Note the differences in syntax for this function in Drupal versions:
- 4.6/4.7 :
cache_set($cid, $data, $expire = CACHE_PERMANENT, $headers = NULL)
- 5 adds the cache table argument:
cache_set($cid, $table = 'cache', $data, $expire = CACHE_PERMANENT, $headers = NULL)
- 6 and 7 reorder the arguments to match the Drupal coding style guide: since
$table
is optional, it is now passed after the mandatory arguments:
cache_set($cid, $data, $table = 'cache', $expire = CACHE_PERMANENT, $headers = NULL)
So we now know how to cache the image. But we also need to avoid recomputing it when it is available from cache. We can know this by querying the Drupal cache and checking for a zero return, which is always returned when no answer is available from the cache. When an answer is available, the result is a stdClass
object containing, among others, a data
field with the data fetched from the cache. So, putting it all together:
Going further
For this tutorial, we have been using the global cache
table for simplicity's sake, but in a real world example, you should typically create a specific cache table for your widget in your module .install
file, and use it in your cache operations, like cache_get("progress-bar-$value", "progress-bar")
instead of just cache_get("progress-bar-$value")
.
Finally, as on the other pages, for better readability, this example does not apply the Drupal coding standards, but your actual code implementing such a widget should typically apply them.