Tony Landis home

How I sped up Smarty by 5x on Lighttpd

In this article I will discuss how I obtained a 5x speedup on a Smarty driven website on a Lighttpd Web server. This is achieved by enabling Lighttpd to a directly access and serve the cached files directly from the file system, rather than calling into Smarty.

Why is this approach so much faster?

Smarty’s caching engine does a great job at compiling the templates at the correct interval and this creates a drastic speedup compared to recompiling the template on each page request.

However, even when Smarty is serving up cached pages, there is a lot of overhead added to each request when compared to the Web server directly serving the cached page. This is because PHP is still being loaded, the Smarty library is being included, and a small amount of logic is being performed within Smarty before the cached page is finally being passed along.

Is this caching technique right for every situation?

In cases where the cache life must be very short due to frequent changes to the data being rendered, the approach I will explain in this article may not be viable. However, in these cases it would be still possible to use a push method to remove cache when the data changes, versus adding the overhead of checking for changes on each page request. In my opinion, this push approach to caching reduces the cost to the lowest possible value, so if the need for performance is of the utmost importance, then it makes sense to implement this approach.

In my case, the data changes occur infrequently and a combination of clearing the cache on a scheduled interval plus a method to manually force a recompilation of a specific page is adequate, and a worthwhile trade-off for the performance increase. Also, I am a performance junkie.

Implementation Notes

Since Smarty is a flexible library, every implementation is unique. So while I cannot give imperitive instructions on how you can implement this lighttpd cache, can explain how I did it.

This was my lighttpd configuration for the site prior to implementing the cache. It has a few basic rewrite rules so request for a (htm|html) file gets passed to the index.php. This file acts as a handler to determine the actual smarty template to load, enabling the use of friendly URLs.

REXML could not parse this XML/HTML: 
<pre><code><span style="color: #333333;">$</span></strong>
<strong>server.document-root</strong> = "/var/www/site/html"
<strong>url.rewrite</strong> = (
    "/(.*)\.(htm|html)(\?.*)??$" =&gt; "/index.php?p=$1",
    "/(.*)/(\?.*)??$" =&gt; "/index.php?p=$1",
	"/(.*)/(.*)/$" =&gt; "/index.php?p=$1/$2"
)

}</span></code></pre>

And after implementing the cache, this is the lighttpd host configuration:

REXML could not parse this XML/HTML: 
<pre><code><span style="color: #333333;"><strong>$</strong>HTTP</span>["host"] =~ "www.site.com" {
    <strong>server.document-root</strong> = "/var/www/site/html"
    <strong>magnet.attract-physical-path-to</strong> = ("/var/www/site/html/rewrite.lua")
}</span>

Below is the code for rewrite.lua (referenced in the lighttpd conf above) which implements lighttpd mod_magnet to handle the rewrite rules. It checks the file system to determine if a cached file exists, and if so, it serves that file. Otherwise, it rewrites to the index.php handler so smarty can generate the new cache file.

The cache_path variable in the rewrite.lua script is my smarty cache dir $smarty->cache_dir, plus the $smarty->cache_id (mine is blank, be sure to append it to the cache_path variable as a subdir of your smarty cache_dir)

cache_path = "/var/www/site/tmp"
cache_code = "compile"

-- render and cache to the filesystem
function cache_gen()
lighty.env["physical.path"] = lighty.env["physical.doc-root"] .. "/index.php"
lighty.env["uri.query"] = "p=" .. string.gsub(lighty.env["uri.path"], "\.(htm|html)$", "")
--print ("CACHE: " .. lighty.env["uri.query"])
end

-- load the cached copy from the filesystem
function cache_load()
lighty.env["physical.path"] = cache_path .. lighty.env["uri.path"]
--print ("LOAD: " .. cache_path .. lighty.env["uri.path"])
end

-- add index file if directory listing requested
if(lighty.env["uri.path"] == "" or string.find(lighty.env["uri.path"], "/", -1)) then
lighty.env["uri.path"] = lighty.env["uri.path"] .. "index.html"
end

-- process htm|html file requests
if(string.find(lighty.env["uri.path"], "htm", -4)) then
exists = lighty.stat(cache_path .. lighty.env["uri.path"])

cache_code_passed = false
if(lighty.env["uri.query"] ) then
cache_code_passed = string.find(lighty.env["uri.query"], cache_code)
end

if(exists and not cache_code_passed) then
action = cache_load()
else
action = cache_gen()
end
end
view raw gistfile1.lua This Gist brought to you by GitHub.

At this point, lighttpd is rewriting all requests to my index.php handler since it will not find a cached copy in the the cache_path dir. I now have to work with the index.php handler file so Smarty will save the compiled HTML files into the cache_path defined in rewrite.lua. To do this, I wrote a custom cache handler function for smarty and stuck it in the index.php file. So far, my Smarty setup is looking like this:

<?php

require "../includes/smarty/Smarty.class.php";
$smarty = new Smarty;
$smarty->template_dir = 'templates';
$smarty->compile_dir = 'compile';

$smarty->cache_dir = '/var/www/site/tmp';
$smarty->caching = true;
$smarty->force_compile = true;
$smarty->compile_id = '';

$smarty->cache_handler_func = 'server_rewrite_cache_handler';
function server_rewrite_cache_handler($action, &$smarty_obj, &$cache_content, $tpl_file=null, $cache_id=null, $compile_id=null, $exp_time=null)
{
$cache_file = $smarty_obj->cache_dir . '/' . $compile_id . $cache_id;
if(!is_file($cache_file)) {
$base_file = basename($cache_file);
$base_dir = dirname($cache_file);
if(!is_dir($base_dir)) mkdir($base_dir, 0777, true);
}
    switch ($action) {
        case 'read':
         if(!is_file($cache_file)) return false; else return true;
            break;
        case 'write':
         return file_put_contents($cache_file, $cache_content);
            break;
        case 'clear':
         return @unlink($cache_file);
            break;
        default:
            return false;
            break;
    }
}

$_p = $_REQUEST['p']; // detect the actual file path requested so I can save the cached copy with the correct path and filename
$page = ''; // my logic here to determine the correct smarty template to display
$smarty->display($page, $_p);

?>
view raw gistfile1.php This Gist brought to you by GitHub.

The main thing here is that the caching is enabled, each request will recompile the template, the compile_id is blank, and the cache_dir matches what is set for cache_path in rewrite.lua.

The function server_rewrite_cache_handler() overrides the default smarty cache read/write/clear logic so that the file is saved in the correct directory structure that matches the request that was rewritten from lighttpd.

The one last thing is to disable several lines of code in the smarty/internals/core.write_cache_file.php, as by default Smarty will add some serialized data to the top of the cache data it passes to our custom cache handler function. The changes are shown below and occur around line 65 and 66 of the core.write_cache_file.php file.

REXML could not parse this XML/HTML: 
<pre><code><span style="color: #333333;">#$_cache_info = serialize($smarty-&gt;_cache_info);
#$params['results'] = strlen($_cache_info) ."\n". $_cache_info.$params['results'];
</span></span>

That is all, you should see a drastic speedup at this point.

blog comments powered by Disqus