http://drupal.org/node/1747970 allows to write PHP files securely.

Twig is really fast if templates are compiled and run from the Cache.

This works really well, but it needs to be integrated with the way Drupal allows reading/writing PHP files.

Follow-up from: #1696786: Integrate Twig into core: Implementation issue

@todos here:

* Think about how to clear out the PHP cache files again
* Fix class name TwigEnvironment, it is confusing
* Fix documentation and run through docs review
* Benchmark
* Merge back into main issue as separate patch

Comments

fabianx’s picture

*DONE*

http://drupalcode.org/sandbox/pixelmord/1750250.git/commit/7b49741

Patch out of that diff is coming once Twig is in core.

fabianx’s picture

podarok’s picture

+class TwigEnvironment extends \Twig_Environment {

this is so weird ^(
its like
green grin greeen grean

fabianx’s picture

Status: Active » Needs review
StatusFileSize
new47.22 KB

Here is a first patch that includes the #1696786: Integrate Twig into core: Implementation issue inline with it, so only the relevant bits can be discussed.

I hope dreditor supports that.

Properties:

* Drupal PHP Storage is used
* The template cache can virtually be cleaned by cc all as all last modified times are stored in the cache.
** Such no mtime() method needs to be implemented on the drupal_php_storage() stores.

moshe weitzman’s picture

I didn't test it out, but the cache code looks good to me. It is so small, that I recommend incorporating it into #1696786: Integrate Twig into core: Implementation issue. Benchmarks with cache enabled would be nice.

webchick’s picture

Well, I think Fabian's hoping for a bit of dedicated discussion around this portion before incorporating it into the "mega" patch.

webchick’s picture

Btw, for those wanting to review the relevant changes, cmd+F for "2/2".

fabianx’s picture

See: http://drupal.org/node/1811418 for how to rework a patch that is having two patches in one.

podarok’s picture

Status: Needs review » Reviewed & tested by the community

#4 patch 2/2 looks good
and I agree with #5

RTBC?

webchick’s picture

Status: Reviewed & tested by the community » Needs review

Er, no. :) #3 is a good point, and there is at least one glaring "@todo document me" thing remaining. ;)

Status: Needs review » Needs work

The last submitted patch, twig_in_core-Twig-enable-drupal-cache-1806546-4.diff, failed testing.

podarok’s picture

latest HEAD w/o patch
drupal minimal profile install
apachebench second pass

root@pubuntu:/var/www/podarok/drupal# ab -c5 -n1000 http://127.0.0.1/podarok/drupal/index.php/node/1
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        Apache/2.2.22
Server Hostname:        127.0.0.1
Server Port:            80

Document Path:          /podarok/drupal/index.php/node/1
Document Length:        4991 bytes

Concurrency Level:      5
Time taken for tests:   80.829 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      5595000 bytes
HTML transferred:       4991000 bytes
Requests per second:    12.37 [#/sec] (mean)
Time per request:       404.143 [ms] (mean)
Time per request:       80.829 [ms] (mean, across all concurrent requests)
Transfer rate:          67.60 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.4      0       7
Processing:   126  404  82.5    405     699
Waiting:      117  377  77.0    380     676
Total:        126  404  82.5    405     699

Percentage of the requests served within a certain time (ms)
  50%    405
  66%    438
  75%    460
  80%    470
  90%    506
  95%    541
  98%    576
  99%    630
 100%    699 (longest request)

the same with #4 patch

 ab -c5 -n1000 http://127.0.0.1/podarok/drupal/index.php/node/1
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        Apache/2.2.22
Server Hostname:        127.0.0.1
Server Port:            80

Document Path:          /podarok/drupal/index.php/node/1
Document Length:        4991 bytes

Concurrency Level:      5
Time taken for tests:   53.281 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      5595000 bytes
HTML transferred:       4991000 bytes
Requests per second:    18.77 [#/sec] (mean)
Time per request:       266.407 [ms] (mean)
Time per request:       53.281 [ms] (mean, across all concurrent requests)
Transfer rate:          102.55 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.3      0       8
Processing:   129  266  46.0    286     423
Waiting:      119  249  43.4    268     396
Total:        129  266  46.0    286     427

Percentage of the requests served within a certain time (ms)
  50%    286
  66%    301
  75%    306
  80%    308
  90%    318
  95%    327
  98%    337
  99%    347
 100%    427 (longest request)
podarok’s picture

#12 shows speed-up +~51%
did I miss something?

fabianx’s picture

Status: Needs work » Needs review
fabianx’s picture

#13 Nice. Did you clear caches after applying patch and loading page once?

Did you repeat the tests several times?

podarok’s picture

#15
yup
logs displayed here second or third passes for better stability results
server with LoadAverage ~0 :)

cache cleared before testing

Crell’s picture

+++ b/core/lib/Drupal/Core/Template/TwigEnvironment.php
@@ -0,0 +1,56 @@
+        $storage = drupal_php_storage('twig');

Is this not injectable yet?

+++ b/core/lib/Drupal/Core/Template/TwigEnvironment.php
@@ -0,0 +1,56 @@
+        $obj = cache()->get($cid);

This probably isn't injectable until #1764474: Make Cache interface and backends use the DIC lands, but that's really close. When that happens, this should be injected. That would then enable unit testing of this class, I think.

I agree, though, that this is small enough to just bake into the Twig patch itself. Especially given the impressive numbers above (yowza!)

Status: Needs review » Needs work

The last submitted patch, twig_in_core-Twig-enable-drupal-cache-1806546-4.diff, failed testing.

swentel’s picture

Are you sure about those benchmarks ? Trying to run the patch gives me fatal error here , unless I'm doing something completely stupid.

moshe weitzman’s picture

Considering that Drupal spends less than 50% of its time in the theme layer, I'm pretty sure the benchmark is wrong.

fabianx’s picture

Re-rolling patch in #4. I am not sure why it fails, but it works locally for me ...

I guess if it really fatals out then that is the reason for the "speedup".

fabianx’s picture

+class TwigEnvironment extends \Twig_Environment {

this is so weird ^(
its like
green grin greeen grean

Any thoughts on how to call this class? I was using the same convention as for the others.

fabianx’s picture

Status: Needs work » Needs review

Testbot: Get to work!

fabianx’s picture

Issue summary: View changes

Add proper link

fabianx’s picture

Issue tags: +Needs tests
+++ b/core/lib/Drupal/Core/Template/TwigEnvironment.php
@@ -0,0 +1,56 @@
+        $storage = drupal_php_storage('twig');

Is this not injectable yet?

This is injected internally, but I am using the public API as per http://drupal.org/node/1747970.

+++ b/core/lib/Drupal/Core/Template/TwigEnvironment.php
@@ -0,0 +1,56 @@
+        $obj = cache()->get($cid);

This probably isn't injectable until #1764474: Make Cache interface and backends use the DIC lands, but that's really close. When that happens, this should be injected. That would then enable unit testing of this class, I think.

Yes, I would think so. However I thought the cache() wrapper would remain? Could you clarify how you think this class should be using the DIC?

On what kind of Unit tests did you think?

I agree, though, that this is small enough to just bake into the Twig patch itself. Especially given the impressive numbers above (yowza!)

I think this needs more reviews, more testing and more thought. I would rather have a little longer discussion here, proper benchmarks, etc., than "blow up" the main thread with discussions about everything.

fabianx’s picture

Okay, I found the problem.

-    $twig = new \TwigEnvironment($loader, array(
+    $twig = new TwigEnvironment($loader, array(

definitely helps ;-) and I agree with #3.

For Benchmarking:

* Enable stark theme
* Benchmark :-)

fabianx’s picture

And here is the new patch.

Status: Needs review » Needs work

The last submitted patch, twig_in_core-Twig-enable-drupal-cache-1806546-25.diff, failed testing.

podarok’s picture

ab -c5 -n1000 http://127.0.0.1/podarok/drupal/index.php/node/1
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        Apache/2.2.22
Server Hostname:        127.0.0.1
Server Port:            80

Document Path:          /podarok/drupal/index.php/node/1
Document Length:        10297 bytes

Concurrency Level:      5
Time taken for tests:   118.582 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      10901000 bytes
HTML transferred:       10297000 bytes
Requests per second:    8.43 [#/sec] (mean)
Time per request:       592.908 [ms] (mean)
Time per request:       118.582 [ms] (mean, across all concurrent requests)
Transfer rate:          89.77 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       2
Processing:   228  592  99.3    595     914
Waiting:      210  551  93.6    553     840
Total:        228  592  99.3    595     914

Percentage of the requests served within a certain time (ms)
  50%    595
  66%    632
  75%    654
  80%    667
  90%    707
  95%    748
  98%    794
  99%    837
 100%    914 (longest request)

with patch #26
template caching works

./sites/default/files/php/twig
./sites/default/files/php/twig/1#52#fd#c5e80be8e75f270e9d036fe7995b.php
./sites/default/files/php/twig/1#52#fd#c5e80be8e75f270e9d036fe7995b.php/82b09f53afd2c13ae4b17656e5b8ac1ca6dd1fde631e5aaf4eeb67076396e747.php
ab -c5 -n1000 http://127.0.0.1/podarok/drupal/index.php/node/1
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        Apache/2.2.22
Server Hostname:        127.0.0.1
Server Port:            80

Document Path:          /podarok/drupal/index.php/node/1
Document Length:        10476 bytes

Concurrency Level:      5
Time taken for tests:   128.091 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      11080000 bytes
HTML transferred:       10476000 bytes
Requests per second:    7.81 [#/sec] (mean)
Time per request:       640.455 [ms] (mean)
Time per request:       128.091 [ms] (mean, across all concurrent requests)
Transfer rate:          84.47 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:   243  639 104.7    638     940
Waiting:      223  595  99.4    592     898
Total:        243  639 104.7    638     940

Percentage of the requests served within a certain time (ms)
  50%    638
  66%    675
  75%    699
  80%    719
  90%    768
  95%    810
  98%    870
  99%    890
 100%    940 (longest request)
swentel’s picture

Frontpage, memory and ab -c1 -n100 - done on bartik

- Before: age execution time was 111.33 ms. Memory used at: devel_boot()=1.19 MB, devel_shutdown()=5.07 MB, PHP peak=5.25 MB.
Requests per second: 10.07 [#/sec] (mean)
- After: Page execution time was 115.26 ms. Memory used at: devel_boot()=1.19 MB, devel_shutdown()=5.35 MB, PHP peak=5.5 MB.
Requests per second: 9.65 [#/sec] (mean)

Node 1, memory and ab -n5 c1000

- Before: Page execution time was 65.1 ms. Memory used at: devel_boot()=1.19 MB, devel_shutdown()=4.75 MB, PHP peak=5 MB.
Requests per second: 15.97 [#/sec] (mean)
- After: Page execution time was 65.89 ms. Memory used at: devel_boot()=1.19 MB, devel_shutdown()=5.02 MB, PHP peak=5.25 MB.
Requests per second: 15.38 [#/sec] (mean)

So with the compilation cache, the difference is *much* less then without, but it's still a tiny little bit slower, and eating a little more memory. I haven't done xhprof, but my guess this is because of the autoloading and two theme engines being enabled.

So I'd really prefer to enable cache by default for the first commit. I'd post this patch on #1696786: Integrate Twig into core: Implementation issue and mark this one duplicate.

fabianx’s picture

Regardless of the exceptions throwing test (the test is broken).

Here are the first performance results:

I optimized the code paths and now can get with 100 nods tested on /node with xhprof:

Core: 2,064,251
Core + optimized Twig: 2,161,547

=> around 100ms slower
=> around 5% slower

The overhead now only for the node.tpl.php:

Node.tpl.php: 1,053,896
Node.twig (compiled): 1,095,220

=> around 42 ms slower => 4-5% slower

The overhead comes mostly from our hugest gain:

TwigTemplate::getAttribute() which allows the nice twig syntax with the '.':

Twig_Template::getAttribute: 25 ms

The rest of the overhead is from the twig_render() function:

twig_render: 14ms (4 ms the is_? questions, the rest overhead of the function).

It gets called 1600 times ...

The rest is mainly overhead added by twig that comes from the conversion.

So, while there is overhead, I think it justifies the gains :-).

fabianx’s picture

Currently done already:

* Removed the __callStatic magic and made this explicit in functions. It can be kept, but is rather slow due to call_user_func_array.

Next steps for optimizing this:

* Remove the _convert_to_reference and do on runTime (40 ms)

* Extend function definition and make references explicit:

new ..._Function(..., array ( 'expects_reference' => TRUE);

* Parse for functions needing Reference in the NodeVisitor and change the getContext Node to getReferenceContext, which returns a TwigReference.

* Functions need to implement to check for instance of TwigReference

* Change the NodeVisitor to re-include show/hide tags

TBC

Crell’s picture

Yes, I would think so. However I thought the cache() wrapper would remain? Could you clarify how you think this class should be using the DIC?

cache() is for legacy support in procedural code. Instead, the cache object for the appropriate bin should be passed into the object's constructor, and saved to an object propertly. Then instead of $obj = cache()->get($cid);, you call $this->cache->get($id);

That makes it possible to pass in a completely fake cache object, such a memory-backed cache object or a cache object that is hard coded to return values relevant for testing, without modifying the global environment.

fabianx’s picture

Title: Twig: Enable twig cache by using the Drupal PHP Loading/Writing » Twig: Performance Testing / Enable twig cache by using the Drupal PHP Loading/Writing

Changed title so we can do performance in here.

fabianx’s picture

@Crell: Thank you very much for this explanation. That makes sense.

So the call in TwigFactory would give the cache and drupal_php_storage objects to the TwigEnvironment and the TwigFactory would get the objects from the DIC, where the cache is set as parameter?

fabianx’s picture

New numbers:

With optimization and 100 nodes rendered the slowdown is now 90 ms, but its barely testable ...

Around 10-15 ms of that are the getting from DIC ...

The rest is:

27 ms: get Attribute
18 ms: get ContextReference // creation of TwigReference objects on demand
23 ms: twig_render tests and getReference
1 ms: hide call to getReference

Without making calls to render() explicit again it is almost not possible to optimize that more.

But given that this is 70-100 ms for 100 calls, this is a slowdown of 0.7 - 1 ms per render call; the rest is DIC overhead ... (static, once, O(1))

Patch is coming once cleaned up ...

AB numbers with 100 nodes

Same codebase:

Core with node.tpl.php: 599-603 ms
Core with node.twig: 609-613 ms

=> around 10 ms slowdown with 100 nodes => 0.01 ms slowdown per render call

I think that is totally acceptable :-D.

fabianx’s picture

diff --git a/core/lib/Drupal/Core/Template/TwigNodeVisitor.php b/core/lib/Drupal/Core/Template/TwigNodeVisitor.php
index f08f071..446cc72 100644
--- a/core/lib/Drupal/Core/Template/TwigNodeVisitor.php
+++ b/core/lib/Drupal/Core/Template/TwigNodeVisitor.php
@@ -59,7 +59,7 @@ function enterNode(\Twig_NodeInterface $node, \Twig_Environment $env) {
    * @see twig_render
    */
   function leaveNode(\Twig_NodeInterface $node, \Twig_Environment $env) {
-    if ($node instanceof \Twig_Node_Print) {
+    if ($node instanceof \Twig_Node_Print && $this->isReference) {
       $this->isReference = FALSE;
 
       $class = get_class($node);
@@ -72,7 +72,10 @@ function leaveNode(\Twig_NodeInterface $node, \Twig_Environment $env) {
     if ($this->isReference) {
       if ( $node instanceof \Twig_Node_Expression_Name) {
         $name = $node->getAttribute('name');
-        return new TwigNodeExpressionNameReference($name, $node->getLine());
+        if (in_array($name, array('content', 'title_prefix', 'title_suffix'))) {
+          return new TwigNodeExpressionNameReference($name, $node->getLine());
+        }
+        $this->isReference = FALSE;
       }
       elseif ($node instanceof \Twig_Function_Function) {
         // Do something!

If ever we need to compile time optimize this templates more, here is some code for that.

fabianx’s picture

Okay, here are the newest Apache AB numbers with one optimization that might need to be removed:

ab -c 5 -n 100 http://127.0.0.1/node/ # test takes 60 seconds

Core with node.tpl.php: 599-602 ms
Core with node.twig: 607-609 ms

0.82% - 1.64% slowdown :-D

I found a performance optimization for render() for the case that an empty array is submitted. In that case $var == null, which is very cheap to test.

To further make this faster the TwigCompiler would need to know what are references and what are not.

For critical code paths like theme link or theme field we could remove the reference logic like:

{% autorender off %}
{% autorender on %}

But that would be premature opt. at this point ...

Patch coming soon ...

fabianx’s picture

And here is the new Patch, which will still fail as I did not fix the Test, yet.

Changes here:

Interpret "needs reference at compile time

Change from call like $func(&arg) to $func($arg), where the definition of the function defines that it needs a reference.

New code:

$twig->addFunction($function, new TwigReferenceFunction($php_function));

This move the processing from runtime to compile time as much as possible.

The logic is simple:

If there is a function that wants a reference (instanceof TwigReferenceFunction), then change the "name" identifier to our own getContextReference(), which returns a TwigReference object.

This optimizes all calls that are not hide / show or printing something like all if statements.

Further it would allow us to have a critical section, where this "auto render" logic is off; note that hide / show / explicit render would still work.

Like:

{% autorender off %}
{% autorender on %}

Very similar to autoescape.

Optimization of getContextReference

In the ideal case getContextReference would only be called for variables that are "render arrays". As that information is not known at compile time, we save it as much as we can.

** Save resolved references as ReferenceObjects to $context['_references'][$key].

** Save information about references or no references to $this->is_reference or $this->is_no_reference.

Note: This is debatable as a render array could once be a value and once an array. I don't think besides it being NULL (need to check for that) / empty that this ever happens, but this needs debate.

This saves 1000 is_array calls, which are rather slow given that this is a critical path.

Edit: Just adding

if (!$context[item]) {
  return $context[item];
}

before saves this case.

Lots of micro optimizations

  • ** Use clone instead of new for TwigReference objects
  • ** Check for $arg == null to not process empty values or arrays
  • ** Save $cacheO, $storage once from cache() / drupal_php_storage() (fix later via proper DIC passing)
  • ** Use instanceof in twig_render / twig_hide / twig_show directly.

Todos here

* Debate / Remove / Fix for NULL case the is_reference / is_no_reference
* Fix the failing test
* Add proper docs to all classes *sigh*

And thats it.

It is _really_ fast now.

Status: Needs review » Needs work

The last submitted patch, twig_in_core-Twig-enable-drupal-cache-1806546-38.diff, failed testing.

fabianx’s picture

Fixed non rooted class names.

And another one for the testbot ...

fabianx’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, twig_in_core-Twig-enable-drupal-cache-1806546-40.diff, failed testing.

fabianx’s picture

Status: Needs work » Needs review
StatusFileSize
new60.93 KB

Only for testbot ...

fabianx’s picture

Yahoo! That last patch 3/3 needs a chx review :)

+++ b/core/includes/file.incundefined
@@ -1441,7 +1441,7 @@ function file_scan_directory($dir, $mask, $options = array(), $depth = 0) {
   $files = array();
-  if (is_dir($dir) && $handle = opendir($dir)) {
+  if (is_dir($dir) && $handle = @opendir($dir)) {

@chx: Is that an acceptable fix?

podarok’s picture

+++ b/core/includes/theme.incundefined
@@ -468,6 +474,28 @@ function _theme_process_registry(&$cache, $name, $type, $theme, $path) {
+          $render_engines = array(
+            '.twig' => 'twig',
+            '.tpl.php' => 'phptemplate'

can we convert this into config?

podarok’s picture

+++ b/core/lib/Drupal/Core/Template/TwigFactory.phpundefined
@@ -0,0 +1,86 @@
+    $twig = new \Twig_Environment($loader, array(

possibly we have to move into ...files/ folder

+++ b/core/lib/Drupal/Core/Template/TwigFactory.phpundefined
@@ -0,0 +1,86 @@
+    $functions = array(
+      'hide' => 'twig_hide',
+      'render' => 'twig_render',
+      'show' => 'twig_show',
+      'unset' => 'unset'
+    );
+    $filters = array(
+      't' => 't'

possibly we have to move this to config()

+++ b/core/lib/Drupal/Core/Template/TwigTemplate.phpundefined
@@ -0,0 +1,58 @@
+   * @todo Document me!
+   */ ¶

trailing whitespace :*

fabianx’s picture

@podarok: Thanks for your feedback.

Please be aware that this issue is only for discussion of PATCH 2/2 (and sometimes 3/3) to keep this focused. 1/2 was RTBC already in itself.

To answer the questions: No, we cannot move any of these to config as these are necessary for the double engine support (will be removed anyway) or for the engine itself.

Modules can extend this anyway, but this should not happen as config.

fabianx’s picture

ab -n100 http://127.0.0.1/node/ (on a dedicated VM)

TWIG

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:   555  583  12.8    582     621
Waiting:      541  566  12.1    565     603
Total:        555  583  12.8    582     621

Node.tpl.php

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:   565  591  10.1    591     625
Waiting:      552  574   9.7    574     608
Total:        565  591  10.1    591     625

So in this case twig was even faster, but normally it is now the same and the rest difference is measurement error.

The overhead is described in detail above. We can improve superfluous render calls in follow up issue.

Merging into main branch and closing this as duplicate.

Just putting last patch in, so difference is clear.

fabianx’s picture

Status: Needs review » Closed (duplicate)
fabianx’s picture

Issue summary: View changes

add todos