PHP xcache performance tuning tutorial on Lighttpd

A tutorial on php + xcache for serving webpages directly from server RAM

A typical PHP application and most of the frameworks uses “The Loop” method to code the website. Whenever request comes to website, It’s typically sent to index page to handle every parameters.

// The world's simplest index page, also called The Loop
get_header();
if (have_action()) :
	if(found_action()):
		include(action);
	else:
		header("404");
else :
	  include("homepage");
endif;
get_footer();

Above code describes typical PHP application routing for template based websites. This style of coding can be converted into enterprise PHP framework with use of design patterns (i.e interfaces like pear, Template engine like smarty, use of MVC).

Xcache and Lighttpd

Xcache optimize PHP performance by keeping most frequently used code into server RAM. i.e. Once code is cached, repeated requests are directly served without recompiling. There are several PHP accelerators are available that goes well on different environments i.e. APC, memcache, xcache, static cache etc.

Accelerator on Lighttpd No of Requests Concurrent connections Serving Time
static File Cache 20000 5 43 seconds
memcache 20000 5 19 seconds
xcache 20000 5 13 seconds

I made some ab (apache benchmark) test on lighttpd with xcache, memcache and static file cache & I have to conclude that Lighttpd and Xcache combination gives best php performance.

Best Pairs of Accelerator + Web Server

  • APC + Apache = Best performance
  • Memcached + Nginx = Best performance
  • Xcache + Lighttpd = Best performance

The Loop Framework with Xcache

We can use xcache variables to store entire web page and serve it from RAM.

Xcache Functions

  • xcache_isset($key) - Checks for $key exist in xcache
  • xcache_set(”key”,”value”,$timeout) - Sets variable in xcache for $timeout seconds
  • xcache_get(”key”) - Gets variable from xcache
  • xcache_unset(”key”) - Removes variable from xcache

Using xcache with php to optimize website.

// Init Caching
include("xcache.php"); // Including Xcache Library... (find code below)
define('HTML_CACHE',true); // set this to false when you need to turn off xcache feature
$cacheConfig['cachepage'] = true; // for mobile useragent you may turn it off for fetching and displaying ads.
init_caching();

get_header();
if (have_action()) :
	if(found_action()):
		include(action);
	else:
		header("404");
else :
	  include("homepage");
endif;
get_footer();  

// Finish Caching
finish_caching();

xcache.php

// code for xcache.php
global $cacheConfig;
$cacheConfig = Array();
$cacheConfig['timeout'] = 10800; // 3 hours
$cacheConfig['page'] = md5("http://".$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']);
$cacheConfig['savepage'] = false;
$cacheConfig['cachepage'] = false;

function cacheEnabled() {
	if (!defined('HTML_CACHE') || (defined('HTML_CACHE') && HTML_CACHE != true)) {
		return false;
	}else{
		return true;
	}
}

function init_caching() {
	if (!cacheEnabled()) {
		return;
	}
	global $cacheConfig;
	if ($cacheConfig['cachepage'] == true) {
	startHTMLCache();
	}
}

function finish_caching() {
	if (!cacheEnabled() ){
		return;
	}
	global $cacheConfig;
	if ($cacheConfig['savepage'] == true) {
	endHTMLCache();
	}
}

function endHTMLCache() {
		global $cacheConfig;
		$content = ob_get_contents();
		ob_end_clean();
		xcache_set($cacheConfig['page'],$content,$cacheConfig['timeout']);
		echo $content;
		exit;
}

function startHTMLCache() {
		global $cacheConfig;
		if (xcache_isset($cacheConfig['page']))	{
			echo xcache_get($cacheConfig['page']);
			exit;
		}
  		else {
    	  $cacheConfig['savepage'] = true;
		   ob_start();
  	 	}
}

StickyTable - jQuery fixed table header plugin

One of my client wanted to display financial reports data with better accessibility and printing usability. He asked me to retain table header fixed on the top while scrolling financial reports.  He also wanted to have MS Access VBA style multicolumn drop-down component for large customer database. In this post I am writing about that fixed table header component which I have coded in jQuery.

By default, If you print any HTML table…

  • the <TH> row get printed on top of every pages that contains the table.
  • <TFOOT> row get printed on every page’s footer.
  • <THEAD> row get printed on every page’s header.

For printing we have chosen fixed width fonts i.e. Courier New.

Demo tutorial page: Demo for jQuery stickyTable plugin

Download: jQuery stickyTable plugin zip file

Initializing jQuery stickyTable plugin

$(document).ready(function(){
	$('#report_table').stickyTable();
});

Above function binds StickyTable plugin to supplied table.

jQuery.stickyTable.js plugin source

(function($){
$.fn.stickyTable = function() {
  if ($(this).length == 0) {
    throw new Error('jQuery.stickyTable ERRORnnAttribute id="'+ $(this).selector.replace("#","") +'" NOT FOUND in your webpage.');
    return;
  }

  function equilize_width() {
    var ths = $('#report_table')[0].rows[0].cells;
    var thd = $('#stickyfixed')[0].rows[0].cells;
    for(var j=0; j < ths.length; j++) {
      $(thd[j]).width($(ths[j]).width()-1);
    }
    $('#stickyfixed').width($('#report_table').width()+4);
  }

  $(this).parent().append("<div id='stickyfixed_container' class='hide'><table id='stickyfixed' class='report_table' border='1'></table>&ly;/div>");

  $('#report_table tr:first').clone().appendTo('#stickyfixed');
  equilize_width();
  var cutoffTop      = $('#report_table').offset().top;
  var cutoffBottom  = $('#report_table').height(); + cutoffTop - $($('#report_table')[0].rows[0].cells[0]).height();
  var no_fixed_support = false;
  if ($('#stickyfixed').css('position') == "absolute") {
    // IE 6 hack
    no_fixed_support = true;
  }
  $('#report_table tr').hover(function() {
      $(this).addClass("highlight");
    },function() {
      $(this).removeClass("highlight");
  });

  //Handling windows resize
  $(window).resize(function(){
    equilize_width();
  });

  $(window).scroll(function(){
    var currentPosition = $(document).scrollTop();
      if (currentPosition > cutoffTop && currentPosition < cutoffBottom) {
        $('#stickyfixed_container').show();
          if (no_fixed_support == true) {
            // IE 6 hack
            document.getElementById('stickyfixed').style.top = parseInt(currentPosition) + "px";
          }
      } else {
    $('#stickyfixed_container').hide();
   }
  });
  return $(this);
 };
})(jQuery);

SCREEN.CSS

/* CSS for screen */
body,*{font-family:Tahoma}
h1{padding:0px;margin:5px;font-size:22px}
h1{padding:0px;margin:5px;font-size:22px}
#header{padding:10px;font-size:24pt;font-weight:bold;font-family:Arial,sans-serif;border-bottom:1px solid #DDD;text-align:left;margin:0px}
#report *{font-family:Tahoma;font-size:8pt}
table.report_table{border-collapse:collapse;border-spacing: 2px;border:2px solid #FFF;width:100%;overflow:scroll;background-color:#FFF;font-size:10pt;}
table.report_table th{padding:5px;background-color:#3B5998;color:#FFF}
table.report_table tr td{padding:5px;background-color:#F3F3F3;border:1px solid #FFF;}
table.report_table tr.highlight td{padding:5px;background-color:#B0D4F7;color:#000;}
table#stickyfixed{z-index:10;position:fixed;_position:absolute;top:0;font-size:10pt;}
table#stickyfixed th{}
.hide{display:none;}

PRINT.CSS

/* CSS for print */
body,*{font-family:Courier New;font-size:11px}
h1{display:none;font-family:Times New Roman;padding:0px;margin:5px;font-size:18px}
#report *{font-family:Courier New}
table.report_table tr th{font-size:14pt}
table.report_table tr td h3{font-size:12pt}
table.report_table td{overflow:hidden;white-space:nowrap;}
#member_links{display:none}
table#report_form{display:none;}
table#stickyfixed{display:none;}

Upgrading php 5.1 to php 5.3 with xcache rebuild on centos 5

Upgrading PHP 5.3 on CentOS 5

CentOS 5 comes with php 5.1 version. There is no official PHP 5.2+ release for  upgrade since last 3 years. So, It was hard for any php developer to work with new php functions like json_encode, json_decode and powerful frameworks like symfony and cakephp 2

As a result, Developer had to implement alternative functions to integrate twitter, myspace OAuth API. Today wordpress has officially said bye bye to php 4 and mysql 4. So finally, I gathered some courage to mess with my current php installation.

I followed the following steps to upgrade php 5.3 on centos 5.

Adding webtatic repository to yum for php 5.3 upgrade

su
cd ~
rpm -ivh http://repo.webtatic.com/yum/centos/5/`uname  -i`/webtatic-release-5-0.noarch.rpm

Updating PHP with yum

yum --enablerepo=webtatic update php

Up to this point, everything had executed perfectly, including dependency resolving.

After upgrading PHP 5.3, when I tried to browse my site homepage, I got blank page!

I thought now I went into deep trouble, I shouldn’t had taken the risk but then I thought lets check all sites hosted on my server. Fortunately they were working correctly. So as computer engineer, I started digging into problem with basics. Just added 2 lines of error debugging code into my header file.

error_reporting(E_ALL);
ini_set("display_errors",1);

Got xcache error: function xcache_isset() not defined on line blah… So I got it. I checked for php version.

php -v

Everything was there but xcache was missing. Finally I got the clue, I have to rebuild xcache with newer version of php. So I rebuild the xcache.

XCache 1.3.0 rebuild commands for PHP 5.3 on CentOS 5

wget http://xcache.lighttpd.net/pub/Releases/1.3.0/xcache-1.3.0.tar.gz
tar -xzvf xcache-1.3.0.tar.gz
cd xcache-1.3.0/
phpize --clean
phpize
./configure --enable-xcache && make
make install

That’s it. After restarting lighttpd, my homepage started working correctly.

Google App Engine powered python mailer

Third party highly customizable email marketing infrastructure is quiet costly compared to owning dedicate email server. Companies providing low cost email marketing packages has limitation on daily/monthly mail count and bandwidth.  Even there is no guaranty of any hosted mailing server, If internet users mark sent mail/IP as spam, server get black listed. So It’s wise to use trusted mailing infrastructure in budget cost.

Google App Engine allows daily 2000 mail resource usage for free. For more quota one can easily upgrade membership and have reliable and trustful emailing infrastructure.

Google App Engine Mailer Logic Diagram

Features

  1. Django template based mailer
  2. IP address validation/filtering.
  3. Generating PHP API on the fly for remote requests.
  4. Task Queue based mail sending module.

Summary of Application

  • First of all it ask for admin’s google account credentials whenever we point browser to http://appid.appspot.com
  • After successful login, It shows dashboard containing existing created mailer templates.
    Dash Board Screen
  • Admin can add, edit, delete templates. It also allows python django template variables. i.e. Dear {{username}}
    Mailer Edit Screen
  • To get Google App Engine request curl api address, one need to go inside edit mailer.
    Google App Engine Mailer
  • It also allows admin to configure our app for limited IP addresses.
    Mailer IP filter screen

Here is a demo code for Google App Engine Mail task queue

app.yaml

application: your_app
version: 1
runtime: python
api_version: 1

handlers:
- url: /picknsend*
  script: picknsend.py

queue.yaml

queue:
- name: limitedemail
  rate: 2000/d

Setting up task queue

try:
  from google.appengine.api.labs import taskqueue
except ImportError:
  from google.appengine.api import taskqueue # for official inclusion of taskqueue.

 # schedule task queue for sending mail
      q = taskqueue.Queue('limitedemail')
      t = taskqueue.Task(url='/picknsend', method='POST')
      q.add(t)

picknsend.py

#!/usr/bin/env python
# /picknsend task queue job
import os
from google.appengine.ext import webapp
from google.appengine.ext.webapp import util
from google.appengine.ext import db
from google.appengine.api import mail

class MailQueue(db.Model):
  mail_to = db.StringProperty(required=True)
  mail_subject = db.StringProperty(required=True)
  mail_body = db.TextProperty(required=True)
  mail_datetime = db.DateTimeProperty(auto_now_add=True)
  mail_sent = db.BooleanProperty(default=False)

class PickAndSendMail(webapp.RequestHandler):
  def post(self):
    self.sendmail()

  def sendmail(self):
    m = MailQueue.gql("WHERE mail_sent=False ORDER BY mail_datetime asc").fetch(1)
    for i in m:
      BODY    = i.mail_body
      TO      = i.mail_to
      SUBJECT = i.mail_subject
    mail.send_mail('your_email@gmail.com',TO,SUBJECT,BODY)
	for i in m:
      i.mail_sent = True  # marking... mail gone
      i.put()      

def main():
  application = webapp.WSGIApplication([('/picknsend*', PickAndSendMail)], debug=True)
  util.run_wsgi_app(application)

if __name__ == '__main__':
  main()

Above code is not our entire application. It’s just one of the sample module that Google App Engine developer can refer for development.

Drupal 6.x clean urls with lighttpd 1.4 + windows 7

While experimenting with drupal module development on my lighttpd server (Windows 7 Ultimate), I encounter some wired things.

  1. It stuck on installation page, after taking mysql host, username and password, nothing happened…

    SOLUTION:

    I changed permission of sites/default/settings.php & supplied mysql credentials manually. So it moved to next steps and completed installation.

  2. Okay… trouble were not just over. After completing installation, when I went to admin”/settings/clean-urls”I realized lighttpd + drupal + windows = Trouble to developer!

    Drupal clean urls

  3. Clean URL option was disabled! I couldn’t change it (although I could do it with firefox developer toolbar). So I started searching the drupal community for lighttpd rules. Bunch of pages had number of answers like mod_magnet, lua, mod_rewrite and blah .. blah.. Windows 7 and centos 5.0 have some trouble with lua and mod_magnet so I had to take  mod_rewrite option.

    SOLUTION:


    It did trick when I took code from other site and paste it into settings.php I could able to enable option with the code.

    if (strpos($_SERVER['SERVER_SOFTWARE'], 'LightTPD') !== false) {
    $_lighty_url = $base_url.$_SERVER['REQUEST_URI'];
    $_lighty_url = @parse_url($_lighty_url);
    if ($_lighty_url['path'] != '/index.php' && $_lighty_url['path'] != '/') {
        $_SERVER['QUERY_STRING'] = $_lighty_url['path'];
        parse_str($_lighty_url['path'], $_lighty_query);
        foreach ($_lighty_query as $key => $val)
            $_GET[$key] = $_REQUEST[$key] = $val;
    	}
        $_GET['q'] = $_REQUEST['q'] = substr($_lighty_url['path'], 1);
    }
    }

    but I didn’t know, blind copy paste will make things much uneasier.

  4. While reading drupal view module documents, I figured out I can save my lots of time in most of the projects. So I installed view module and tried to enable frontpage module by clicking enable link inside admin/build/views. But I got the classic 403 error “You are not authorized to access this page.” I tried to search, for the same but couldn’t find the solution anywhere.Drupal View Build 403 errorSo, I open that core module, and checked for the error function in /modules/views/includes/admin.inc and I found that…
    /**
     * Page callback for the Views enable page.
     */
    function views_ui_enable_page($view) {
    
      if (isset($_GET['token']) && drupal_valid_token($_GET['token'], 'views-enable')) {
        $views_status = variable_get('views_defaults', array());
        $views_status[$view->name] = FALSE; // false is enabled
        variable_set('views_defaults', $views_status);
        views_invalidate_cache();
        menu_rebuild();
        drupal_goto('admin/build/views');
      }
      else {
        return drupal_access_denied();
      }
    }

    was creating the problem. Our copy/pasted code wasn’t handling token and destination parameters, causing failure at above function. So I reopen sites/default/settings.php file. and edit the rewrite handling code as follow.

    SOLUTION:

    if (strpos($_SERVER['SERVER_SOFTWARE'], 'LightTPD') !== false) {
    $_lighty_url = $base_url.$_SERVER['REQUEST_URI'];
    $_lighty_url = @parse_url($_lighty_url);
    if ($_lighty_url['path'] != '/index.php' && $_lighty_url['path'] != '/') {
        $_SERVER['QUERY_STRING'] = $_lighty_url['path'];
        parse_str($_lighty_url['path'], $_lighty_query);
        foreach ($_lighty_query as $key => $val)
            $_GET[$key] = $_REQUEST[$key] = $val;
    
            // Note: Lighttpd won't send the querystring attached with clean url
    
    	if(isset($_lighty_url['query']) && $_lighty_url['query'] != ""){
    		parse_str($_lighty_url['query'],$myGET);
    		foreach($myGET as $k=>$v) {
    			$_GET[$k] = $v;
    		}
    	}
        $_GET['q'] = $_REQUEST['q'] = substr($_lighty_url['path'], 1);
    }
    }

    Above final code made my drupal installation on windows running under lighttpd 1.4 for further trouble and solutions… I’ll keep posting.