PHPTAL (re)visited

ludo, Wednesday 20 August 2003 23:30:20

As promised in a previous entry, I managed to give a look at PHPTAL. Well, not a serious, in-depth look at the implementation, more like a sort of I like TAL let's see if this is good enough look. Okay, okay, I can see your noses wrinkling, let me explain this a bit.

My involvement with PHP, which led me to develop the PEAR DB LDAP driver and maintaining for a while the Interbase driver, plus using it for lots of personal and work projects, came to an abrupt halt last year for two reasons: I got a new job for a major Italian bank managing a group of consultants, so less or no development; I fell in love with Python, so I began using it more and more for my personal projects.

Lately I started developing again at work, partly because I like it more than managing people (managing a development team is better, but unfortunately we delegate this sort of stuff to consultants), partly because the current economic climate (and plenty of available spare time at work) lets me convince the bosses from time to time to try and develop some of our projects in-house.

Thus my involvement with PHP began again, since some of the stuff we're rewriting or refactoring is already done in PHP, and very few things (none that I know of) can beat PHP web applications in speed of development/deployment and performance. Performance is in fact one of our major issues, since the applications I am working on are used daily by a good part of our 60.000 internal users. The other major issue related to PHP and its ancillary libraries/classes is reliability. We're not working on financial applications but on simple intranet stuff like our internal telephone directory. Simple stuff on which however some of our business processes depend, since the only way of timely reaching somebody is often to call him on his mobile phone, be it for troubleshooting an "important" application or process or to schedule a meeting, and one of the few ways to know who deals with what (if the what is something you don't know anything about in your usual tasks) is resorting to the telephone/organizational units directory.

Thus my good enough criteria means good enough for our loads, and reliable enough not to expose strange or random behaviour.

For templating we currently use a mix of the old PHPlib template class (used by our consultants), and my rewrite of it (in the new projects). It's simple, it's fast, it allows you to separate the logic from the presentation pretty well, although not as much as TAL does. So yesterday morning I spent a few minutes with a colleague trying to benchmark PHTAL and see if it is fast enough to try and develop something with it.

The results were pretty much what I expected: PHPTAL is too slow to be used for our applications. What I did not expect, however, was the order of magnitude of its slowness compared to what we are using (more on that later). Not satisfied with the (basic, but sufficient for our needs) performance tests, I did a quick and dirty reliability test by comparing PHPTAL with my reference TAL implementation, the Python library simpleTAL. I was pretty surprised to discover that simpleTAL is slightly faster than PHPTAL, and that it spits out warnings if you try to use the TAL templates used in the PHPTAL examples. This did not sound good for PHPTAL's quality.

So tonight I did a bit of reading around the PHPTAL documentation, and was pretty surprised to learn that PHPTAL requires a separate Types library that define new data types on top of the (perfectly complete, in PHP's context) native PHP ones. Urgh! I am allergic to too many abstractions (what Joel of Joel On Software fame calls leaky abstractions), and this looks definitely like a bad case of abstractitis. What's the need of a reference helper? Every decent PHP developer should know his way around references, they're not so hard (well, after you bang your head against the wall a few times in frustration but decide to stick with it). What's the need of creating an Iterator interface on top of PHP's very good, feature-rich, and fast arrays? Something like this does not look like sensible PHP code to me:

require_once 'Types/Iterator.php';
$i = $iterable->getNewIterator();
while ($i->isValid()) {
    $value =& $i->value();
    $i->next();
}

It is suspiciously similar in functionality to this:

$i = array('a'=>'b', 'c'=>'d');
foreach ($i as $k => $v)
    $value =& $v;

Two lines less, more clarity, more speed, less abstractions. Of course, this is only my opinion. Well, back to the original topic of this entry, testing.

For the tests, I tried to use one of the templates used in the PHPTAL documentation. Since it sports invalid TAL syntax according to simpleTAL (the Python library), I had to drop a row which was used only as a placeholder anyway. The resulting template looks like this:

<?xml version="1.0"?> 
<html>
<head>
    <title tal:content="title">place for the page title</title>
</head>
<body>
<h1 tal:content="title">sample title</h1>
<table>
<tr>
    <td>name</td>
    <td>phone</td>
</tr>
<tr tal:repeat="item users"> 
    <td tal:content="item/matricola">matricola</td>
    <td tal:content="string: ${item/cognome} ${item/nome}">item name</td>
    <td tal:content="item/telefono">item phone</td>
</tr>
</table>
</body> 
</html>

The data is in a separate php file that simply declares a "$users" array composed of 200 elements, each an associative array with the required fields. To time results, I used the very good PEAR Benchmark_Timer class by Sebstian Bergmann.

The sample PHPTAL code looks like this:

#!/usr/bin/php -q
<?php
require_once 'users.php';
require_once 'Benchmark/Timer.php';

$t =& new Benchmark_Timer(true);

require_once "HTML/Template/PHPTAL.php"; $t->setMarker('post-require');

$tpl =& new PHPTAL("tal_template.xml"); $t->setMarker('post-template');

$tpl->set("title", "Test Page"); $t->setMarker('post-set-title');

$tpl->setRef("users", $users); $t->setMarker('post-set-users');

$res = $tpl->execute(); $t->setMarker('post-execute');

if (PEAR::isError($res)) echo $res->toString(), "n"; ?>

Running this test the first time gives:

----------------------------------------------------------------------
marker             time index            ex time               perct
----------------------------------------------------------------------
Start              1061421217.91528700   -                       0.00%
----------------------------------------------------------------------
post-require       1061421217.93816100   0.022874                5.79%
----------------------------------------------------------------------
post-template      1061421217.93849100   0.000330                0.08%
----------------------------------------------------------------------
post-set-title     1061421217.93855700   0.000066                0.02%
----------------------------------------------------------------------
post-set-users     1061421217.93861100   0.000054                0.01%
----------------------------------------------------------------------
post-execute       1061421218.30995300   0.371342               94.04%
----------------------------------------------------------------------
Stop               1061421218.31017400   0.000221                0.06%
----------------------------------------------------------------------
total              -                     0.394887              100.00%
----------------------------------------------------------------------

subsequent runs use a cached version of the parsed template (something I don't like too much, it should be an option to cache, not an option not to cache) and give:

----------------------------------------------------------------------
marker             time index            ex time               perct
----------------------------------------------------------------------
Start              1061421220.91106600   -                       0.00%
----------------------------------------------------------------------
post-require       1061421220.93519000   0.024124               15.37%
----------------------------------------------------------------------
post-template      1061421220.93551900   0.000329                0.21%
----------------------------------------------------------------------
post-set-title     1061421220.93558300   0.000064                0.04%
----------------------------------------------------------------------
post-set-users     1061421220.93563700   0.000054                0.03%
----------------------------------------------------------------------
post-execute       1061421221.06779900   0.132162               84.20%
----------------------------------------------------------------------
Stop               1061421221.06802400   0.000225                0.14%
----------------------------------------------------------------------
total              -                     0.156958              100.00%
----------------------------------------------------------------------

Pretty slow, considering it's running on a sufficiently fast machine, and it's practically doing nothing. Our real application on top of that has lots of templating operations, LDAP searches, etc.

My second test tried to replicate the same functionality using my Template class. The template looks like this:

<html>
<head>
<title>Ludoo » PHPTAL (re)visited</title>
</head>
<body>
  <h1>Ludoo » PHPTAL (re)visited</h1>
  <table>
  <tr>
    <td>name</td>
    <td>phone</td>
  </tr>
  <!-- BEGIN row -->
  <tr> 
    <td>{row_matricola}</td>
    <td>{row_cognome} {row_nome}</td>
    <td>{row_telefono}</td>
  </tr>
  <!-- END row -->
  <!-- BEGIN dummy -->
  <tr> 
   <td>sample name</td>
   <td>sample phone</td>
  </tr>
  <tr>
    <td>sample name</td>
    <td>sample phone</td>
  </tr>
  <!-- END dummy -->
</table>
</body> 
</html>

The dummy row is there to serve the same purpose of the rows I removed from the TAL template after trying it with Python. I left them there since the speed difference is already enough. The code used for the second test is:

#!/usr/bin/php -q
<?php
require_once 'users.php';
require_once 'Benchmark/Timer.php';

$t =& new Benchmark_Timer(true);

require_once "Template.php"; $t->setMarker('post-require');

$tpl =& new Template('/home/ludo/tests'); $tpl->setFile('main', 'template.html'); $t->setMarker('post-template');

$tpl->setVar("title", "Test Page"); $t->setMarker('post-set-title');

$tpl->parseBlock('ROW', 'row', $users, 'main'); $t->setMarker('post-set-users');

$tpl->setBlock('main', 'dummy', 'DUMMY'); $tpl->setVar('DUMMY', '');

$res = $tpl->parse('MAIN', 'main'); $t->setMarker('post-execute');

// result may be an error if (PEAR::isError($res)) echo $res->toString(), "n"; ?>

Running this test gives:

----------------------------------------------------------------------
marker             time index            ex time               perct
----------------------------------------------------------------------
Start              1061421680.50660500   -                       0.00%
----------------------------------------------------------------------
post-require       1061421680.50944200   0.002837               22.93%
----------------------------------------------------------------------
post-template      1061421680.50970300   0.000261                2.11%
----------------------------------------------------------------------
post-set-title     1061421680.50974900   0.000046                0.37%
----------------------------------------------------------------------
post-set-users     1061421680.51790200   0.008153               65.88%
----------------------------------------------------------------------
post-execute       1061421680.51878400   0.000882                7.13%
----------------------------------------------------------------------
Stop               1061421680.51898000   0.000196                1.58%
----------------------------------------------------------------------
total              -                     0.012375              100.00%
----------------------------------------------------------------------

More than 10 times faster, and that's without caching anything. More than 30 times faster against the uncached version of PHPTAL.

To compare with Python, I timed the execution of the three scripts with the shell time command, since I was just interested in a rough overview of the relative speed.

Python:

/-(ludo@pippozzo)-(53/pts)-(01:24:37:Thu Aug 21)--
-($:~/tests)-- time ./test.py
real    0m0.591s
user    0m0.580s
sys     0m0.010s

PHPTAL:

/-(ludo@pippozzo)-(54/pts)-(01:24:38:Thu Aug 21)--
-($:~/tests)-- time ./test_tal.php >/dev/null

real 0m0.421s user 0m0.310s sys 0m0.110s

Template:

/-(ludo@pippozzo)-(56/pts)-(01:24:53:Thu Aug 21)--
-($:~/tests)-- time ./test_tpl.php >/dev/null

real 0m0.268s user 0m0.190s sys 0m0.080s

To sum it up, we're going to keep using our Template class. PHPTAL is a commendable effort, and something that I would definitely use in most of my PHP development, but it needs to be more lightweight, reliable and fast.

If you're interested in the Python script, here it is:

#!/usr/bin/env python
def test():
   from simpletal import simpleTAL, simpleTALES
    import sys, cPickle

users = cPickle.load(file('users.pickle', 'r'))

context = simpleTALES.Context() context.addGlobal("title", "Hello World") context.addGlobal("users", users)

template = simpleTAL.compileHTMLTemplate (file("tal_template.xml", 'r'))

template.expand(context, file('/dev/null', 'w'))

if __name__ == '__main__': test()

Readers' Comments

No user comments.