Understanding performance measurement in Laravel Octane
We have said that introducing Laravel Octane in your application allows for a performance boost, mainly because the objects and the various instances of the classes used by the framework are no longer initialized at every single HTTP request but at the start of the application server. As a result, for each HTTP request, framework objects are reused. Reusing framework objects saves time in serving the HTTP request.
While, on a logical and understandable level, this can have a positive impact in terms of performance, the goal of this part is to get pragmatic feedback on this performance boost by trying to recover some metrics and values.
In order to provide a rough indication of the benefits and improved response time performance for a request, let us try to perform a simple performance test.
To do this, we are going to install a tool to generate and execute some HTTP-concurrent requests. There are several such tools, one of which is wrk (https://github.com/wg/wrk).
If you have a macOS environment, you could use the brew
command (provided by Homebrew) to install the wrk
tool. To install the tool, use brew install
as shown:
brew install wrk
With wrk
, you can generate concurrent requests for a defined amount of time.
We will conduct two tests for comparison: one test will be conducted with a classical web application on nginx (http://octane.test
), and the other one with an application served by an application server on Laravel Octane (http://octane.test:8000).
The two URLs are resolved as shown:
http://octane.test/
is resolved with local address127.0.0.1
and will reply nginxhttp://octane.test:8000/
is resolved with local address127.0.0.1
and port8000
is bound by Swoole
The wrk
execution will use 4 threads, open 20 connections, and take 10 seconds of tests.
So, to test NGINX, launch the wrk
command with these arguments:
wrk -t4 -c20 -d10s http://octane.test
You will see the following output:
Running 10s test @ http://octane.test 4 threads and 20 connections Thread Stats Avg Stdev Max +/- Stdev Latency 51.78ms 61.33ms 473.05ms 88.54% Req/Sec 141.79 68.87 313.00 66.50% 5612 requests in 10.05s, 8.47MB read Non-2xx or 3xx responses: 2 Requests/sec: 558.17 Transfer/sec: 863.14KB
To test Laravel Octane (RoadRunner), use the following command:
wrk -t4 -c20 -d10s http://octane.test:8000
You will see the following output:
Running 10s test @ http://octane.test:8000 4 threads and 20 connections Thread Stats Avg Stdev Max +/- Stdev Latency 134.58ms 178.24ms 1.09s 79.75% Req/Sec 222.72 192.63 1.02k 73.72% 7196 requests in 10.02s, 8.06MB read Requests/sec: 718.51 Transfer/sec: 823.76KB
This test is very basic because there are no special server-side logic or query databases involved, but it is good to run the test to understand the raw difference in bootstrapping basic objects for Laravel (application container, requests, etc.) and perceive their flavor.
The difference is not so great (7,196 requests versus 5,612 requests) – around 22% – but consider that this difference grows if you add new packages and libraries (more code to be bootstrapped for each request).
Consider also that RoadRunner and Swoole provide other additional tools for improving performances such as enabling concurrency and executing concurrent tasks. The additional tools will be shown later in Chapters 2 and 3.
To better explain why Laravel Octane allows you to achieve this improvement, let me demonstrate how and when service providers are instanced and loaded into a service container.
Typically, in a classic Laravel application service, providers are loaded in each request.
Create a new service provider named MyServiceProvider
in the app/Providers
directory:
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; class MyServiceProvider extends ServiceProvider { public function __construct($app) { echo "NEW - " . __METHOD__ . PHP_EOL; parent::__construct($app); } public functionregister
() { echo "REGISTER - " . __METHOD__ . PHP_EOL; } public functionboot
() { echo "BOOT - " . __METHOD__ . PHP_EOL; } }
The new service provider simply shows a message when the service provider is created, registered, and booted.
The lifecycle of a service provider starts with three phases: creation, registration, and boot.
The register()
and boot()
methods are needed for dependency resolution. First of all, every service provider is registered. Once they are all registered, they could be booted. If a service provider needs another service in the boot
method, you can be sure that it is ready to be used because it is already registered.
Then, you have to register the service provider, so in config/app.php
in the providers
array, add App\Providers\MyServiceProvider::class
.
In a classical Laravel web application, for every HTTP request, the MyServiceProvider
service provider is instanced, and the construct
, register
, and boot
methods are called every time, showing this output:
NEW - App\Providers\MyServiceProvider::__construct REGISTER - App\Providers\MyServiceProvider::register BOOT - App\Providers\MyServiceProvider::boot
With Laravel Octane, something different happens.
For a better understanding, we are going to launch the Laravel Octane server with two parameters:
workers
: The number of workers that should be available to handle requests. We are going to set this number to2
.max-requests
: The number of requests to process before reloading the server. We are going to set this number to a maximum limit of5
for each worker.
To start the Octane server with two workers and reload the server after processing five requests, we enter the following command:
php artisan octane:start --workers=2 --max-requests=5
After launching Octane, try to perform more than one request with the browser accessing this URL: http://127.0.0.1:8000
.
The following is the output:
NEW - App\Providers\MyServiceProvider::__construct REGISTER - App\Providers\MyServiceProvider::register BOOT - App\Providers\MyServiceProvider::boot 200 GET / ...................................................... 113.62 ms NEW - App\Providers\MyServiceProvider::__construct REGISTER - App\Providers\MyServiceProvider::register BOOT - App\Providers\MyServiceProvider::boot 200 GET / ....................................................... 85.49 ms 200 GET / ........................................................ 7.57 ms 200 GET / ........................................................ 6.96 ms 200 GET / ........................................................ 6.40 ms 200 GET / ........................................................ 7.27 ms 200 GET / ........................................................ 3.97 ms 200 GET / ........................................................ 5.17 ms 200 GET / ........................................................ 8.41 ms worker stopped 200 GET / ........................................................ 4.84 ms worker stopped
The first 2 requests take around 100 milliseconds (ms), the next requests take 10 ms, and the register()
and boot()
methods are called on the first two requests.
So we can see the first two requests (two because we have two workers) are a bit slower (113.62 ms and 85.49 ms) than the next requests (from the third to the tenth request, where we have a response time of less than 10 ms).
Another important thing to mention is that the register
and boot
methods are called for the first two requests until the tenth request (two workers multiplied by five max requests). This behavior is repeated for subsequent requests.
And so, installing Laravel Octane in your web application allows you to improve the response time of your application.
All this without having involved certain tools such as concurrency management provided by application servers such as Swoole and RoadRunner.