Greg Aker

Session Cache in ExpressionEngine

Filed in: PHP, ExpressionEngine, tutorial

June 14, 2011

I think the session cache may quite possibly be one of the more under used tools in the third-party ExpressionEngine developers toolbox. The speed/performance increases from strategically caching items in this can be substantial. As with anything you do while developing your addon, benchmarking needs to be done every step of the way to ensure you aren't overdoing it and actually causing issues. I want to illustrate how adding something to the session cache can give a nice performance boost.

What is the session cache?

First, see the docs.

If you're a developer and haven't had a look at the EE core, I do suggest you do it. The session cache is a class variable in the session class, located at system/expressionengine/libraries/Session.php. With this, you can cache an item in there and have it available to you through the duration of the request. Note, this isn't persistent storage, therefore is active only for the duration of the request.

Let's look at a quick sample plugin. I'm performing a query in the constructor of my plugin. If you look through the template parser, you'll see plugins and modules are instantiated for every set of exp tags. So the end result is we should see the query being performed multiple times. If you try to 'cache' a query in a class variable of your addon, it will not work due to the object being newly instantiated for every exp tag. This is where the session cache comes in to save the day.

<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

$plugin_info = array(
    'pi_name'       => 'Awesome',
    'pi_version'    => '1.0',
    'pi_author'     => 'Greg Aker',
    'pi_author_url' => 'http://www.gregaker.net/',
    'pi_description'=> 'Plugin for EE tutorial',
    'pi_usage'      => Awesome::usage()
);

class Awesome {

    public $return_data;

    private $_super_admins;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->EE =& get_instance();

        $qry = $this->EE->db->get_where('members',
                    array('group_id' => 1));

        if ($qry->num_rows())
        {
            $this->_super_admins = $qry->result();
        }

        $qry->free_result();
    }

    // ----------------------------------------------------------------

    /**
     * Sauce function
     */
    public function admin_list()
    {
        if ( ! $this->_super_admins)
        {
            return $this->EE->TMPL->no_results();
        }

        $out = array();

        foreach ($this->_super_admins as $s)
        {
            $out[] = array(
                'member_id'     => $s->member_id,
                'username'      => $s->username,
                'screen_name'   => $s->screen_name,
                'email'         => $s->email,
                'url'           => $s->url
            );
        }

        $tagdata = $this->EE->TMPL->tagdata;

        return $this->EE->TMPL->parse_variables($tagdata, $out);
    }

    // ----------------------------------------------------------------

    /**
     * Admin posts
     */
    public function admin_posts()
    {
        // The superadmins query return nothing?  
        // There are bigger issues if that's the case, but we'll bail anyway.
        if ( ! $this->_super_admins)
        {
            return $this->EE->TMPL->no_results();
        }

        $admin_ids = array();

        // get author ids
        foreach ($this->_super_admins as $admin)
        {
            $admin_ids[] = (int) $admin->member_id;
        }

        $qry = $this->EE->db->select('entry_id, title, url_title, status')
                            ->where('site_id', (int) $this->EE->config->item('site_id'))
                            ->where_in('author_id', $admin_ids)
                            ->limit(25)
                            ->get('channel_titles');

        // no entries?  no results.
        if ( ! $qry->num_rows())
        {
            return $this->EE->TMPL->no_results();
        }

        $out = array();

        foreach ($qry->result() as $row)
        {
            $out[] = array(
                'entry_id'  => $row->entry_id,
                'title'     => $row->title,
                'url_title' => $row->url_title,
                'status'    => $row->status
            );
        }

        $tagdata = $this->EE->TMPL->tagdata;

        return $this->EE->TMPL->parse_variables($tagdata, $out);
    }

    // ----------------------------------------------------------------

    /**
     * Plugin Usage
     */
    public static function usage()
    {
        ob_start();
?>

Docs here

<?php
        $buffer = ob_get_contents();
        ob_end_clean();
        return $buffer;
    }
}

/* End of file pi.awesome.php */
/* Location: /system/expressionengine/third_party/awesome/pi.awesome.php */

So that's the first pass at my plugin. My EE template is pretty simple with only the following:

<h3>Admin List</h3>
{exp:awesome:admin_list}
{if no_results}
  <p>No Results</p>
{/if}

<p>Member id: {member_id}</p>
<p>Username: {username}</p>
<p>Screen Name: {screen_name}</p>
<p>Email: {email}</p>
<p>Url: {url}</p>
{/exp:awesome:admin_list}

<h3>Admin Posts</h3>
{exp:awesome:admin_posts}
{if no_results}
  <p>No Results</p>
{/if}

<p>Entry Id: {entry_id}</p>
<p>Title: {title}</p>
<p>Url Title: {url_title}</p>
<p>Status: {status}</p>

<hr>
{/exp:awesome:admin_posts}

So I'll throw siege at the template. Let's see what we get:

> siege -c 3 -b -t 10s http://localhost/EllisLab/ee2/index.php/test/
** SIEGE 2.70
** Preparing 3 concurrent users for battle.
The server is now under siege...

Lifting the server siege...      done.
Transactions:                352 hits
Availability:             100.00 %
Elapsed time:               9.36 secs
Data transferred:           0.90 MB
Response time:              0.08 secs
Transaction rate:          37.61 trans/sec
Throughput:             0.10 MB/sec
Concurrency:                2.98
Successful transactions:         352
Failed transactions:               0
Longest transaction:            0.17
Shortest transaction:           0.05

Not too shabby, but I'll bet we can do better. Since the query in the constructor is run for both tags, we're getting that member query twice. We can't count on it being available in a class var in another tag, so we can cache it in the session cache. Refactor the constructor to look like:

<?php

public function __construct()
{
    $this->EE =& get_instance();

    if ( ! isset($this->EE->session->cache['awesome']['sa_query']))
    {
        $qry = $this->EE->db->get_where('members',
                    array('group_id' => 1));

        if ($qry->num_rows())
        {
            $this->_super_admins = $qry->result();
        }

        $qry->free_result();

        if ( ! isset($this->EE->session->cache['awesome']))
        {
            $this->EE->session->cache['awesome'] = array();
        }

        $this->EE->session->cache['awesome']['sa_query'] = $this->_super_admins;
    }
    else 
    {
        $this->_super_admins = $this->EE->session->cache['awesome']['sa_query'];    
    }
}

Now, run siege again:

Lifting the server siege...      done.
Transactions:                392 hits
Availability:             100.00 %
Elapsed time:               9.97 secs
Data transferred:           1.00 MB
Response time:              0.08 secs
Transaction rate:          39.32 trans/sec
Throughput:             0.10 MB/sec
Concurrency:                2.98
Successful transactions:         392
Failed transactions:               0
Longest transaction:            0.16
Shortest transaction:           0.05

Not a bad boost. We're reducing load on MySQL by not running that query more than once. See how this might add up in the context of a more complicated template?

It occurred to me that using this might be a bit confusing to newer developers, so a few weeks ago I tweeted about how we added a getter/setter for the session cache that's due out in the ExpressionEngine 2.2 release. This should hopefully make things easier for people to start using. Let's see how that constructor will look when we all have access to the cache() and set_cache() methods.

<?php

public function __construct()
{
    $this->EE =& get_instance();

    if ( ! ($this->_super_admins = $this->EE->session->cache('awesome', 'sa_query')))
    {
        $qry = $this->EE->db->get_where('members',
                    array('group_id' => 1));

        if ($qry->num_rows())
        {
            $this->_super_admins = $qry->result();
        }

        $qry->free_result();

        $this->EE->session->set_cache('awesome', 'sa_query', $this->_super_admins);
    }
}

This code looks a lot more hot to me. It's less code, and in my opinion much easier to read.

Have you used the session cache before? If so, any good stories of it improving performance? Or you find issues with performance degrading? Let's hear 'em!