Reducing system load for graphical widgets

Submitted by Frederic Marand on

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:

<?php
/**
* Output a progress bar
*
* It displays the value/range ratio on top of the progress bar itself.
*
* @param int $usValue
* @return void
*/
function progress_bar_view($usValue = 0)
  {
 
// [... sanitize $usValue into $value ...]
 
 
drupal_set_header('Content-type: image/png');
 
 
// Build in-memory image
 
$image = imagecreatetruecolor($width, $height);
  [...]

 
// Output it
 
imagepng($image);

 
// Release memory
 
imagedestroy($image);

 
// Tell drupal not to output anything else
 
return NULL;
  }
?>

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:

<?php
    ob_start
();
   
imagepng($image);
   
$png = ob_get_clean();
?>

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:

<?php
    ob_start
();
   
imagepng($image);
   
$png = ob_get_clean();
   
cache_set("progress-bar-$value", 'cache', $png);
   
imagedestroy($image);       
?>

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:

<?php
function progress_bar_view($usValue = 0)
  {
 
// [... sanitize $usValue into $value ...]
 
 
drupal_set_header('Content-type: image/png');

 
// Check whether the data is available from cache
 
if (($png = cache_get("progress-bar-$value")) == 0)
    {
   
// No cached data, we must build the image
   
$image = imagecreatetruecolor($width, $height);
   
// [...]

    // And we can capture it with OB
   
ob_start();
   
imagepng($image);
   
$png = ob_get_clean();

   
// Then save it to cache   
   
cache_set("progress-bar-$value", 'cache', $png);

   
// And release it
   
imagedestroy($image);       
    }
  else
// Cache returned a valid answer, we can use it
   
{
   
$png = $png->data;
    }

  print
$png; // output without Drupal adding anything
 
return NULL;
  }
?>

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.