Converting the Symfony Stopwatch Component from PHP to Zephir
What is Zephir?
During the last HackDay at Marmelab, I wanted to give Zephir a try. Zephir is a new language which eases the creation of C extensions for PHP. The Zephir syntax is very close to PHP but it is both static and dynamic typed.
Why the Symfony Stopwatch Component?
I choose to convert the Symfony Stopwatch Component because it is the lightest component of the Symfony Framework, and Zephir is still very alpha.
Code extract from the official Zephir documentation
1. Bootstrapping the Zephir project
First of all, I renamed all PHP files by changing their extension from *.php
to *.zep
, then lowercased the full pathname as Zephir recognizes only lowercase filenames. I also split stopwatch.zep
into 2 files, as it contains 2 classes (Stopwatch
and Section
).
More details in the Zephir documentation: http://zephir-lang.com/language.html#organizing-code-in-files-and-namespaces
2. Declaring variables
Zephir handles both dynamic and static variables but we have to declare these variables before using them. The keyword var
is used for dynamic typed variable, whereas uint
is used for static variable (I'm sure you can guess the type it refers to).
--- stopwatchevent.zep
+++ stopwatchevent.zep
@@ -173,6 +173,9 @@
*/
public function getDuration()
{
+ uint $total;
+ var $period;
+
$total = 0;
foreach ($this->periods as $period) {
$total += $period->getDuration();
More details in the Zephir documentation: http://zephir-lang.com/language.html#variable-declarations
3. Variable Assignment
In Zephir each variable assignment is prepended with let
, just like in bash.
--- stopwatchperiod.zep
+++ stopwatchperiod.zep
@@ -30,9 +30,9 @@
public function __construct($start, $end)
{
- $this->start = (integer) $start;
- $this->end = (integer) $end;
- $this->memory = memory_get_usage(true);
+ let $this->start = (integer) $start;
+ let $this->end = (integer) $end;
+ let $this->memory = memory_get_usage(true);
}
Furthermore, and at the time I'm writing this post, Zephir cannot handle chained assignment, so let's unchain these ones.
--- stopwatch.zep
+++ stopwatch.zep
@@ -30,7 +30,8 @@
public function __construct()
{
- $this->sections = $this->activeSections = array('root' => new Section('root'));
+ let $this->activeSections = array('root' => new Section('root'));
+ let $this->sections = activeSections;
}
More details in the Zephir documentation: http://zephir-lang.com/types.html#types
4. Ternary Operator
Zephir does not support ternary operators (yet?), so we have to bypass this limitation by converting ternary expressions to if
statements.
--- stopwatch.zep
+++ stopwatch.zep
@@ -173,7 +173,11 @@
public function __construct($origin = null)
{
- $this->origin = is_numeric($origin) ? $origin : null;
+ if (is_numeric($origin)) {
+ $this->origin = $origin;
+ } else {
+ $this->origin = null;
+ }
}
More details in the Zephir documentation: http://zephir-lang.com/control.html#if-statement
5. Using Strings
As in C, Zephir handles string
and char
differently. If we use a string we have to enclose it in "
, If we use a char the delimiting character is '
.
--- stopwatch.zep
+++ stopwatch.zep
@@ -273,7 +273,7 @@
public function stopEvent($name)
{
if (!isset($this->events[$name])) {
- throw new \LogicException(sprintf('Event "%s" is not started.', $name));
+ throw new \LogicException(sprintf("Event \"%s\" is not started.", $name));
}
return $this->events[$name]->stop();
More details in the Zephir documentation: http://zephir-lang.com/types.html#string
6. Fully Qualified Namespaces
Because Zephir does not recognize relative namespaces, we have to translate usages to fully qualified namespaces.
--- stopwatchevent.zep
+++ stopwatchevent.zep
@@ -101,7 +101,7 @@
throw new \LogicException('stop() called but start() has not been called before.');
}
- $this->periods[] = new StopwatchPeriod(arraypop($this->started), $this->getNow());
+ $this->periods[] = new \Symfony\Component\Stopwatch\StopwatchPeriod(arraypop($this->started), $this->getNow());
return $this;
}
7. Type Hints
In order to optimize memory consumption, we can tell Zephir which type a method parameter requires, and which type a function returns.
--- section.zep
+++ section.zep
@@ -270,7 +270,7 @@
*
* @throws \LogicException When the event has not been started
*/
- public function stopEvent($name)
+ public function stopEvent(string $name) -> <Symfony\Component\Stopwatch\StopwatchEvent>
{
if (!isset($this->events[$name])) {
throw new \LogicException(sprintf('Event "%s" is not started.', $name));
8. Bypassing "identical to null" Statement
Another thing Zephir cannot parse yet is a comparison with null
using an identical operator. One easy way to allow compilation in this case is to use the is_null()
function instead.
--- stopwatch.zep
+++ stopwatch.zep
@@ -44,7 +44,7 @@
{
$current = end($this->activeSections);
- if (null !== $id && null === $current->get($id)) {
+ if (!isnull($id) && isnull($current->get($id))) {
throw new \LogicException(sprintf('The section "%s" has been started at an other level and can not be opened.', $id));
}
9. Initializing Properties in a Constructor
Zephir doesn't handle property assignment, so we have to initialize these properties within the __constuct()
method. This is theexact opposite of what has been done to Symfony recently.
--- section.zep
+++ section.zep
@@ -149,7 +149,7 @@
/**
* @var StopwatchEvent[]
*/
- private $events = array();
+ private $events;
/**
* @var null|float
@@ -173,6 +173,7 @@
*/
public function _construct($origin = null)
{
+ $this->events = [];
$this->origin = isnumeric($origin) ? $origin : null;
}
Let's play with it
1. Compiling step
Once all the translation of the Symfony Stopwatch Component is done, we can compile our fresh new Zephir project. This step will create the symfony.so
file corresponding to the resulting PHP extension.
cd /root/
zephir init symfony
cd symfony/
zephir compile
2. Enabling the new Symfony module
Now that the symfony.so
module exists, we have to update PHP configuration in order to enable it.
echo "extension=/root/symfony/ext/modules/symfony.so" > /etc/php5/mods-available/symfony.ini
php5enmod symfony
3. Is it working?
If everything works fine, then pasting the following code in your terminal will print out 2 durations (~200 ms. and 400 ms.). You can notice that we do not include the usual /vendor/autoload.php
....
php << 'EOF'
<?php
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\Stopwatch\StopwatchEvent;
use Symfony\Component\Stopwatch\StopwatchPeriod;
$stopwatch = new Stopwatch();
$stopwatch->start('eventA');
usleep(200000);
$stopwatch->start('eventB');
usleep(200000);
$eventA = $stopwatch->stop('eventA');
$eventB = $stopwatch->stop('eventB');
printf("Duration of event A: %u ms.\n", $eventA->getDuration());
printf("Duration of event B: %u ms.\n", $eventB->getDuration());
EOF
4. Is it really working?
You can go further and run the original testsuite. But before doing it, you have to skip test doing Mocking
against the current component, because it is not possible to mock a loaded extension.
--- /root/vendor/symfony/stopwatch/Symfony/Component/Stopwatch/Tests/StopwatchTest.php
+++ /root/vendor/symfony/stopwatch/Symfony/Component/Stopwatch/Tests/StopwatchTest.php
@@ -48,6 +48,7 @@ class StopwatchTest extends \PHPUnit_Framework_TestCase
public function testIsNotStartedEvent()
{
+ $this->markTestSkipped();
$stopwatch = new Stopwatch();
$sections = new \ReflectionProperty('Symfony\Component\Stopwatch\Stopwatch', 'sections');
Then we can run the test suite with the following command:
phpunit --colors /root/vendor/symfony/stopwatch/Symfony/Component/Stopwatch/
Unfortunately, the test suite fails because 2 LogicException
are thrown and I still haven't found how to bypass this error. But because we are positive minded, let's turn this problem into an exercise. Go fork the repository and try solving this issue!
PHPUnit 3.7.28 by Sebastian Bergmann.
...............S...EE.
Time: 1.35 seconds, Memory: 2.75Mb
There were 2 errors:
Symfony\Component\Stopwatch\Tests\StopwatchTest::testSection
LogicException: Event "section.child" is not started.
/root/vendor/symfony/stopwatch/Symfony/Component/Stopwatch/Tests/StopwatchTest.php:115
Symfony\Component\Stopwatch\Tests\StopwatchTest::testReopenASection
LogicException: Event "section.child" is not started.
/root/vendor/symfony/stopwatch/Symfony/Component/Stopwatch/Tests/StopwatchTest.php:139
FAILURES!
Tests: 22, Assertions: 30, Errors: 2, Skipped: 1.
Conclusion
This extension is much faster on a vagrant box than the original component (written in PHP), probably because Vagrant's default filesystem is quite slow. Runing the extension within a docker container is slightly slower with the extension (this is weird). Benchmarking is not an easy task and as the Symfony Stopwatch Component is a really simple one, I don't think benchmarking is relevant here.
Zephir is far from being production ready, but development is very active and still full of promises. You really should keep an eye on this project. The source code of the translation of Symfony Stopwatch Component from PHP to Zephir is available on my repository on github.