Posts

Journey of Profiling a Slow Development Setup

While working on the migration from Vagrant to Docker with Compose in our development setup, we were seeing extremely slow responses in the browser and I immediately made a mistake to assume I know the problem without measuring.

A story in 7 acts.

1. Making the wrong assumptions

From common experience, the first thing to blame was Docker Volume Performance. So I optimized the obvious issues, that you can find in the many Docker on Mac Performance blog posts around the web:

  • Set the volume to :delegated or :cached (Edit: Turns out this is moot, but you’ll find it on the internet everywhere)
  • Do not share the Composer and NPM/Yarn dependency folders into the container
  • Enable the new experimental VirtioFS filesystem in Docker Desktop for Mac
  • Regenerate the Symfony cache via docker-compose exec to make sure the cache generation is not holding up the request.

2. Reality breaks assumptions: Why is cache generation quick?

At that point I had a realization:

Using „docker-compose exec app php /app/bin/console cache:clear“ generated the cache in 3-4 seconds. If volume performance was the problem, then this CLI script should also be slow. Yet 3-4 seconds is pretty quick for generating the cache of our large Symfony application.

So it is most likely not be Docker volumes that are slow, but something in the Nginx, PHP-FPM or our application stack.

3. Identify where slowness comes from

To identify where to dig next, I executed the front controller „index.php“ script from the CLI as well.

$ docker-compose exec app php /app/web/index.php

This gave a long 60 second response, which leads me to conclude that our PHP application is slow and not Nginx or PHP-FPM. I call this divide and conquer debugging, splitting up the potential causes and eliminating them one by one.

4. Identify what causes the slowness

I installed the tideways CLI into the Docker container next and re-ran the index.php script from the CLI with:

docker-compose exec app tideways run php /app/web/index.php

Clicking on the output response to find out where the 60 seconds are coming from: Our own code making a HTTP request to a host that is not available using file_get_contents:

This is only a debug call, so its using file_get_contents and its default timeout of 60 seconds is what is causing the problem.

5. Setting Timeouts on HTTP calls

A case of not following our own advice of always setting timeouts on HTTP calls. We have a blog post on „Using HTTP Client Timeouts in PHP“ and Tideways itself warns when the default socket timeout is not adjusted to a lower value through an observation.

So changing the code to use a timeout:

private function queryMailcatchMails() : array
{
    $context = stream_context_create([
        'http' => ['timeout' => 1.0, 'ignore_errors' => true],
    ]);
    $data = @file_get_contents("http://mailcatcher.local:1025/api/messages", false, $context);

    // ...
}

Still the same slow response! Turns out, since mailcatcher.local is a domain that does not resolve with DNS, PHP is hanging in dns resolving, which for some reason is not affected by the stream timeout settings!

6. Rewriting with Symfony HttpClient

So I did a rewrite with Symfony HTTP Cilent for the code, since we use that in many other places of the code base already:

private function queryMailcatchMails() : array
{
    $client = HttpClient::create();
    try {
        $response = $client->request(
            'GET',
            'http://mailcatcher.local:1025',
            options: ['timeout' => 1, 'max_duration' => 1]
        );
        $file = $response->getContent();
    } catch (TransportExceptionInterface $e) {
        return ['count' => 0, 'last' => []];
    }

    // ...
}

That did not work either, because of a bug in Symfony HttpClient only seting CURLOPT_TIMEOUT but not CURLOPT_CONNECTIONTIMEOUT. We still have 20 seconds spent in HTTP requests.

I have filed a bug report and will spend some time proposing a PR in the next few weeks.

7. Fixing the underlying problem: New hostname

This journey started with the migration from Vagrant to Docker, the mailcatcher.local domain was used in Vagrant, but it did not exist in Docker (yet). Once the mailcatcher service was running in the Docker Compose setup and the code changed to talk to the right HTTP host everything was fast again.

Here you can see the comparison of the old vs the new code in a callgraph comparison in Tideways: curl_multi_exec is now 19 seconds or 95% faster than before.

The moral of the story: don’t make assumptions without measuring, otherwise you might spend a lot of time optimizing something that is not slow.

In our case the Docker volumes were quick and response anyways, the problem was in the application and specifically the unavailable HTTP host.

Refactoring with Deprecations

Deprecating old code and replacing it with new and improved APIs is an established process in software development. In the core of PHP APIs are provided to trigger and to get notified of deprecations.

  1. If a feature in PHP is deprecated, the language triggers a notification message with the E_DEPRECATED level.
  2. As a developer, library or framework author, deprecations can be triggered directly from PHP code using the function trigger_error() with the E_DEPRECATED or E_USER_DEPRECATED levels.

As a PHP application developer you can then hook into all triggered deprecations using a user defined error handler. You can use this API to collect deprecations and fix them.

A Refactoring Workflow using Deprecations

A common workflow to slowly and safely improve your code, without potentially breaking usages of your old code is a strategy of introducing new APIs and deprecating the old ones. Both APIs are then maintained until all usages of the old API is replaced with the new API.

A checklist of this workflow looks as follows:

  1. Introduce a new API that improves upon the old API.
  2. Add a call to trigger_error in the old API (unless its called by the new API)
  3. Roll out this change to production and start logging the deprecations with Tideways.
  4. Replace ocurring deprecations where you haven’t replaced the old with the new API
  5. Remove the old API as soon as you are confident the old API is not used anymore, potentially by not seeing the deprecation message anymore for several days.

If you can replace the old API with the new one in a single step, then you don’t need the indirection through using deprecations. But when an API is used wide spread throughout the code base this approach allows you to do a gradual migration instead of a big bang refactoring that introduces the risk of bugs.

Example: Deprecating an API

Lets see an example. We have a method that allows querying from a database table using many parameters to influence the query conditions, the example is directly taken from Tideways own code-basis:

<?php

interface TracepointRuleRepository
{
    /**
     * @return array<TracepointRule>
     */
    public function findAllTracepointRules(
        int $applicationId,
        bool $activeOnly = false,
        string $service = '',
        string $environment = '') : array;
}

We have recently added two new parameters to this API for $service and $environment. Because many optional parameters are a code smell, we decided to refactor this code to what we call the “Criteria API Pattern” in our team. More generally the pattern is called Parameter Object.

First we introduce the Criteria object:

<?php

class TracepointCriteria extends Struct
{
    public bool $activeOnly = false;
    public string $service = '';
    public string $environment = '';
}

Then we introduce a new method on the interface and the coresponding implementation:

<?php

interface TracepointRuleRepository
{
    /**
     * @return array<TracepointRule>
     */
    public function findMatching(int $applicationId, TracepointCriteria $criteria) : array;
}

The important part is not to change the original old API and keep it around. Instead we modify this code to trigger a deprecation:

<?php

class TracepointRuleOrmRepository implements TracepointRuleRepository
{
    /**
     * @return array<TracepointRule>
     */
    public function findAllTracepointRules(
        int $applicationId,
        bool $activeOnly = false,
        string $service = '',
        string $environment = '') : array
    {
        @trigger_error("findAllTracepointRules() is deprecated, use findByCriteria() instead.", E_USER_DEPRECATED);

        // original implementation
    }
}

After enabling deprecations tracking in Tideways, these deprecations now look like in the following screenshot:

A deprecation in Tideways

You can see the stacktrace to this deprecation and if you start fixing occurrences, until the deprecation is not triggered again.

For the new implementation of the findMatching method there are now three approaches we can take:

  1. Completly implement the new method from scratch and keep the old method.
  2. Implement the new method from scratch, but call the newly implement method from the old, deprecated method.
  3. Call the deprecated code from the new method. This is the most tricky, because we have to avoid that the deprecation is being triggered. We can do this by passing a parameter that controls the behavior.
<?php

class TracepointRuleOrmRepository implements TracepointRuleRepository
{
    /**
     * @return array<TracepointRule>
     */
    public function findAllTracepointRules(
        int $applicationId,
        bool $activeOnly = false,
        string $service = '',
        string $environment = '',
        bool $triggerDeprecation = true) : array
    {
        if ($triggerDeprecation) {
            @trigger_error("findAllTracepointRules() is deprecated, use findByCriteria() instead.", E_USER_DEPRECATED);
        }

        // original implementation
    }

    public function findMatching(int $applicationId, TracepointCriteria $criteria) : array
    {
        return $this->findAllTracepointRules(
            $applicationId,
            $criteria->activeOnly,
            $criteria->service,
            $criteria->environment,
            false
        );
    }
}

Shopware 6.4.11 NavigationLoader performance improvements evaluated

During Shopware Community Unconference 2022, we heard a lot about an internal performance week at Shopware, where they took tie to optimize different parts of the core of the Shopware platform. These were mostly performance problems that also affect our customers. Naturally, we are super excited about them their release in version 6.4.11. In this blog post we will look at one improvement to category navigation loading.

Comparing two Callgraphs of NavigationLoader

If you have a category-heavy Shopware 6 store, fetching and rendering the navigation menu would be a slow operation, we have seen from 100-500ms depending on the number of categories for different customers. Even smaller category trees required a disproportionate amount of time to render. We verified this with a demo shop, which the following numbers are taken from.

Comparing the performance of the NavigationLoader::loadTree function before and after the update to 6.4.11 shows roughly 15% performance gain for a store with 3097 categories (generated by the framework:demodata command).

We can see the gain by searching for all calls to the NavigationLoader class in the callgraph table view.

The whole tree fetching takes 163ms in the old code and just 14ms in the new code. What was changed?

Reducing Loop Complexity to Improve Performance

The commit to fix this performance bottleneck is not straightforward by glancing at it, but with a bit of time we can see that instead of an O(N^2) loop through all categories to build the navigation tree, it is now iterated only O(2N) times. See the Big O Notation for complexity of an algorithm if you are unfamiliar with this notation.

A quadratic complexity means that for each category all categories are iterated once again. For 2 categories we have 4 iterations, for 1,000 categories we have 1,000,000 iterations. This perfectly explains why the performance gets so much slower for large numbers of categories and you can see evidence of this in the Tideways Callgraph screenshot above.

CategoryEntity::getParentId was called 277409 times more often in the old code. This is a very simple getter, but the number of calls in the old algorithm made up for 41ms of execution, 25% of the 163ms the NavigationLoader took in total.

The new algorithm only iterates over all the categories twice.

The benefit of this performance improvement cannot be understated because it is a positive effect on all uncached pages in the store: Homepage, category pages, product pages, everything.

How was this data generated and measured?

Using the Ansible automation of the Shopware 6 Benchmarking Tool, we have set up a 2 machine cluster on Digitalocean for this test, with one web node including Redis and one database node with MySQL, both having 4 Cores, 8 GB RAM, and SSDs. The shown numbers should not be considered as absolute improvements, instead, you should see a similar performance improvement in relation to the total request time.

Talk: Best Practices Running Shopware 6 in Production

Earlier this month the Shopware organized their Community Day (SCD) and I presented a talk on Shopware 6 Best Practices for Production. I have now published the slides to this talk for your reference. Topics include:

  • Setting up Shopware 6 Cronjobs and Queues
  • Configure Caching
  • Http Cache
  • Advanced Topics around Queues
  • Plugins and Performance
  • Scaling to Multiple Application Servers
  • Using Elasticsearch for Product and Category Indexing and Searching

Log all tasks the Shopware 6 queue processes

The Shopware 6 software architecture heavily relies on a message queue to process tasks in the background. This is a fundamental change of how Shopware works compared to version 5, which did not have a message queue.

To fully leverage the benefits of Shopware 6 you should make use of the message queue as much as possible. Shopware can already do the following tasks in the background out of the box:

  • Store and Index Imported Products / Categories via the Sync API
  • Indexing of all entities
  • Warmup product and category pages in the HTTP Cache
  • Generate Thumbnails for Images
  • Generate Sitemap
  • Product Exports (Google Shopping Feed, …)
  • several others…

This has a lot of performance benefits, but it also introduces an operational risk: What if there is a bug, an error or any kind of problem in the queue? Will I see this and can I debug it?

You can make use of the canonical log line pattern to essentially generate yourself a more powerful equivalent of the webservers “access.log” for every task that the message queue processes. The output will look like this:

2021-08-15 10:39:58 ProductIndexing data=1791edec1f04464a8a4074423501e781 duration=0.190
2021-08-15 11:30:48 UpdateThumbnails mediaIds=102ac62ba27347a688030a05c1790db7 duration=0.015
2021-08-15 11:32:16 DeleteFile files=shirt_red_600x600_800x800.jpg duration=0.002
2021-08-15 12:08:25 WarmUp route=frontend.detail.page domain=http://shopware6.tideways.io cache_id=cc3e1493bc874b62b16c11cd291826f9 duration=0.003
2021-08-15 12:08:25 WarmUp route=frontend.navigation.page domain=http://shopware6.tideways.io cache_id=cc3e1493bc874b62b16c11cd291826f9 duration=0.002

This can easily be achieved, because the message queue is based on the Symfony Messenger component and it can be extended via so called Middleware.

We create a middleware class with the following structure:

namespace Shopware\Production\Messenger;

use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\StackInterface;

class TaskLoggingMiddleware implements MiddlewareInterface
{
    function handle(Envelope $envelope, StackInterface $stack): Envelope
    {
        return $stack->next()->handle($envelope, $stack);
    }
}

And we register it as a service that is a message queue middleware with this Symfony container service definition:

# config/packages/framework.yaml
framework:
  messenger:
    buses:
      messenger.bus.shopware:
        middleware:
          - "Shopware\\Core\\Framework\\MessageQueue\\Midleware\\RetryMiddleware"
          - "Shopware\\Production\\Messenger\\TaskLoggingMiddleware"

services:
- "Shopware\\Production\\Messenger\\TaskLoggingMiddleware": ~

This will run the middleware whenever a task is processed in the Shopware Message queue. Next up is the implementation of logging what is happening:

  • map each task into a task name
  • extract arguments from all known message task types to enhance the log output
  • handle errors in tasks
  • write the task processing to a file

On the macro level I use the following implementation for handle:

public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
    $message = $envelope->getMessage();
    $taskName = $this->getTaskName($message);
    $args = $this->extractArgumentsFromMessage($message);

    $start = microtime(true);
    try {
        return $stack->next()->handle($envelope, $stack);
    } catch (HandlerFailedException $e) {
        $args = $this->addExceptionToArgs($e, $args);

        throw $e;
    } finally {
        $this->logTaskProcessing($taskName, $args, $start);
    }
}

The try catch finally makes sure that regardless of an Exception, we always “logTaskProcessing” to store information about the processed task. We have two methods for mapping the message to a human readable task name and extract the arguments at the beginning and when an Exception occurs, we extract the exception class and message into the arguments.

The interesting part is “extractArgumentsFromMessage”, which has special code for a lot of the different Shopware internal messages, for example “EntityIndexingMessage”, “WarmUpMessage” or “ScheduledTask”.

private function extractArgumentsFromMessage($message)
{
    if ($message instanceof EntityIndexingMessage) {
        $data = $message->getData();

        if (is_array($data)) {
            return ['data' => implode(',', $data)];
        }
    } else if ($message instanceof ScheduledTask) {
        return ['taskId' => $message->getTaskId()];
    } else if ($message instanceof DeleteImportExportFile ||
              $message instanceof DeleteFileMessage) {
       return ['files' => implode(',', array_map(
           function ($f) { return basename($f); },
           $message->getFiles())
       )];
    } else if ($message instanceof GenerateThumbnailsMessage) {
       return ['mediaIds' => implode(',', $message->getMediaIds())];
    } else if ($message instanceof WarmUpMessage) {
       return [
           'route' => $message->getRoute(),
           'domain' => $message->getDomain(), 
           'cache_id' => $message->getCacheId()
       ];
    }

    return [];
}

When you add your own messages to Shopware, you should extend this method to add arguments to your tasks as well for the log file.

You can find the full code of the TaskLoggingMiddleware in this Gist. We are working to turn this into a Composer package in the near future.

Combining this with Tideways, you can even get aggregated performance insights and profiling for Message Queue on top using the “tideways/symfony-messenger-middleware” package.

Related content:

Innocent looking array_unique – 2 Stories of performance hogs in Shopware 6 and Tideways own backend code base

This post tells a story of a performance mistake that is quickly made even by experienced developers: The expectation that a built-in PHP function has better performance than a better suited data structure written in PHP.

The protagonist in these stories is array_unique, a function that takes a list of values and removes duplicate entries to return a list of each value occurring only once.

Take these two code examples, the first from Shopware 6.3:

$this->tags = array_unique(array_merge($this->tags, array_values($tags)));

The second one from our own Tideways backend code:

$versions[$row['version']]['organizations'][] = $row['organization_id'];
$versions[$row['version']]['unique'] = count(array_unique(
    $versions[$row['version']]['organizations']
));

On a first glance these snippets look innocent: append new values to an array and then make sure they aren’t duplicates of previously known values by applying array_unique.

With the help of Tideways Profiler, we found both are executed in loops and perform the unique operation many thousands of times.

The algorithmic complexity of array_unique was improved in PHP 7.2, but it is still “just” O(n), meaning for larger arrays the operation can take a while. Here is the code of array_unique translated to how it would look like in PHP:

function array_unique(array $values) {
    $seen = [];
    $uniqueValues = [];
    
    foreach ($values as $value) {
        if (!isset($seen[$value])) {
            $uniqueValues[] = $value;
            $seen[$value] = true;
        }
    }
    
    return $uniqueValues;
}

This is an efficient enough algorithm, but only if you don’t repeatedly do the operation over and over again. There are two solutions to this performance problem:

  • Calculate array_unique only once at the end of the loop, while appending duplicates to the array during the loop. This strategy is simple, but might cause memory problems if the array is very large or the elements are long strings.
  • Incrementally calculate uniqueness by keeping the array of $seen values around over all merges of new elements.

The second solution is easy to implement with PHP Arrays by storing the values as keys and not appending them as values. Technically this means using the $seen array from the code example as data structure:

Both code examples from the beginning of the post can be rewritten by storing the values as array keys:

foreach ($tags as $tag) {
     $this->tags[$tag] = true;
}

The list of unique tags is then fetched using array_keys($this->tags) later. We found that this change was already made from Shopware 6.3 to 6.4 in the following commit.
We rewrote our own Tideways backend code to make use of the same approach, using array keys:

$versions[$row['version']]['organizations'][$row['organization_id']] = true;
$versions[$row['version']]['unique'] = count(
    $versions[$row['version']]['organizations']
);

This benefits from using the O(1) complexity of adding an element to an array (hashmap) and array_unique is not needed anymore.

Looking at a comparison of both optimizations in Tideways Callgraph Profiler shows how massive the gains are for these changes.

In Shopware 6.3, we replaced the old CacheTagCollection code with the code from 6.4 and the performance improved from 7,3 minutes to 51 seconds for the Product Export, an improvement of 88% of which 4,08 minutes (54%) can be attributed to the removal of array_unique and another 40 seconds to the use of array_merge in the same line. Conclusion: Upgrade to Shopware 6.4!

In the case of Tidways own backend code, removing array_unique reduces 925ms from the original request that takes 1,87 seconds, a rough 50% improvement.

And here is the moral of our story: array_unique should generally be called for any array only once, not over and over again.

Intrigued by PHP performance story-telling? We are currently looking for a Developer Advocate to write about bottlenecks just like this.

Five Challenges for Running Reliable PHP Background Processes

BONUS: We have discussed this topic with an expert in the PHP community in our podcast:

PHP isn’t typically thought of as a solution when creating worker or background processes, jobs that typically can last for an extended period. These can be tasks such as image processing, file repair, and mass email batch jobs.

Typically, PHP is linked with HTTP requests, requests which are short in duration and stateless in nature. However, just because of this enduring association, it doesn’t mean that PHP can’t be used for background processes. On the contrary.

If you’re considering developing background processes in PHP, then this post is for you. I’m going to step you through five points that you need to keep in mind. Hopefully, by the end, you’ll have less stress and effort as a result.

Use Angel Processes

Background jobs can die.

They can die for many reasons, including out of memory exceptions, connection errors, as well as a number of other conditions. Given that this could happen and to avoid manual intervention to restart the job, it’s helpful to make use of an angel process.

An angel process offers three key advantages:

  • Can start a job, if it’s not already started
  • Can restart a failed job
  • Provides debugging and logging capabilities. For example, Supervisor and Systemctl can centralize job logging and show a job’s status and history.

The angel process isn’t responsible for anything else. Consequently, the logic should be quite rudimentary, and its load should be quite light at any given time. However, there are a couple of conditions that you need to be aware of, when writing them.

Handle Failed Jobs

No matter what language you write your jobs in, you have to know when they fail and be able to restart them when they do. PHP is no exception.

So, how do you do this? You could roll your own solution, such as a shell script, but there are many existing solutions already available. As a result, you should never need to write your own.

Here is a selection of them, delimited by operating system:

Package Description Operating System
Supervisor Available for UNIX/Linux systems, Supervisor provides a simple, centralized, efficient, and extensible approach to controlling many processes. It’s been tested on Linux, Mac OS X, Solaris, and FreeBSD. You may be able to use it on Windows in combination with Cygwin.
Systemd Available for UNIX/Linux systems, systemd was designed as a replacement for UNIX System V and the BSD init system. Initially developed at Red Hat, it includes features such as on-demand starting of daemons, snapshot support, and process tracking. Linux
Gearman To quote the official documentation, it: ” provides a generic application framework to farm out work to other machines or processes that are better suited to do the work.” Written in C, it’s available for Linux and Windows.
The Windows Service Control Manager It provides functionality similar to Supervisor and Systemd for Windows servers. Windows

For more information about supervisord, check out this post on Servers for Hackers. And for more information on Gearman and PHP, check out this post by Matthew Weier O’Phinney. If you’re considering using Gearman, be aware that it requires an angel process for its own workers. It’s too much to cover in this post, however I just wanted you to be aware of this fact.

Monitor Background Jobs

As background jobs can last for quite some time, it’s helpful to be able to inspect their state when required. There’s often nothing worse than knowing that a job is still active, yet to have no information about how much or how little progress its made, how long it takes to complete, and any problems that it has encountered during processing.

Given that, it’s handy, though arguably not essential, to have some form of monitoring available. This could be something quite rudimentary, such as printing out a list of the currently active jobs, their start time, expected completion time, and the number of failures that they have encountered to the terminal.

The output might look like the following from Supervisor:

$ sudo supervisorctl status alertActionWorker:alertActionWorker00                 RUNNING   pid 2349, uptime 0:01:35 createErrorWorker:createErrorWorker00                 RUNNING   pid 2345, uptime 0:01:35 createProfileWorker:createProfileWorker00             RUNNING   pid 2346, uptime 0:01:35 notificationWorker:notificationWorker00               RUNNING   pid 2350, uptime 0:01:35 recordMeasurementsWorker:recordMeasurementsWorker00   RUNNING   pid 2339, uptime 0:01:35 

It could also go as far as a comprehensive dashboard, such as the monitoring in Tideways:

Tideways Background Jobs MonitoringTideways Background Jobs Monitoring

Whichever way you go, ensure that your job is storing some form of statistical information that can be periodically polled and collated.

Many vendors provide built-in functionality to store this information, such as Microsoft’s Azure platform. Alternatively, you can store your own in one of the open-source databases, such as PostgreSQL, MySQL, or SQLite.

Tune Memory Usage and Restart Jobs Periodically

PHP wasn’t designed with long-running processes and tasks in mind. It was designed for shorter tasks based around the nature of an HTTP request. As such, long-running tasks can be problematic, mostly because of memory consumption.

There are a couple of things that you can do to help reduce memory consumption. These include:

  • Garbage Collection Analysis: Performing garbage collection analysis to help identify how your worker process uses variables and how PHP’s garbage collector interacts with your script.
  • Avoid Global and Class Static Variables.
  • Use cursor-like behavior for iterating over information, such as arrays, lists, and collections. By doing this, only a limited amount of memory is used at any one time.

By doing these three things, the likelihood of writing code that leaks memory is reduced, which in turn reduces the likelihood of needing to kill a process arbitrarily.

Implement Exponential Backoff When Auto Restarting

Long-running jobs will inevitably encounter some form of network collision or outages that temporarily prevent them from continuing to execute. These can be caused by too many users being active at one point in time, one or more servers or network devices between the client and the end server being down, or even something as drastic as a DDOS attack.

The question is, what to do when these occur? One common approach is to implement Exponential Backoff in your application. Exponential Backoff is a way of spacing out repeated worker restarts for longer intervals until either:

  1. A maximum number of retries is reached.
  2. A maximum backoff time is reached.

It’s important to note that the time between the restarts is a random interval, up to a maximum defined value. Because value is random, it helps avoid the possibility that another background process may choose the same interval and begin executing at the same time as the current client, resulting in the same outage occurring.

The net effect of implementing an Exponential Backoff algorithm is that processes:

  1. Can eventually complete successfully while not colliding with other processes.
  2. Avoid creating excessive server load.

Conclusion

That’s been a broad introduction to five challenges which PHP developers face when developing worker (background) processes. While not extensive, it’s provided an excellent introduction to them with the hope that these challenges can be avoided in the applications that you and your team are developing.

New Feature: Support for GraphQL Queries in Timeline Profiler

We have just released the PHP Extension version 5.0.62 which includes a new feature, instrumentation for GraphQL queries made using the webonyx/graphql-php package, the most widespread GraphQL server package for PHP, for example by Magento 2.

The following screenshot shows how GraphQL spans are rendered in the Tidways Timeline Profiler, here using a Magento 2 GraphQL endpoint responding to a query for the cart:

GraphQL queries are parsed and anonymized in the tideways-daemon running on your machines in a similar way to how SQL queries are anonymized.

Each resolver has its own sub-span under the GraphQL query, so that you can see what part of the query took how long. This allows you to see exactly what SQL and Redis calls are made in resolvers due to the GraphQL query and can help pinpointing performance problems in certain GraphQL queries.

For this SQL query we can see the list of parent spans containing the “Cart” resolver inside the “Cart” GraphQL query.

Check out the 5.0.62 release (changelog), which contains a few other improvements to Magento 2 instrumentation that greatly increase the monitoring and profiling insights into this e-commerce platform.

New Feature: Max Memory Monitoring

The PHP INI setting memory_limit is important to configure right for two reasons:

  • Set too low and an increasing number of requests fail due to memory limit reached errors.
  • Set too high reduces the theoretical number of PHP processes that can run on a single server.

To help you finding the right memory_limit setting Tideways now offers two additional ways to see the maximum memory in use:

  1. on the service level for the currently selected time period
  2. for every bucket in the performance graph, visible via the tooltip when hovering.

I have recorded a short three minute video demonstrating this feature:

Max Memory Monitoring If you want to know more, setting the right memory limit was also a topic Matt and I interviewed our guest Arne Blankerts about in episode 8 of the Undercover ElePHPant podcast about PHP-FPM tuning and we wrote a blog post on PHP-FPM tuning with more details.

An Introduction to PHP-FPM Tuning

BONUS: We have discussed this topic with an expert in the PHP community in our podcast:

PHP-FPM (or Fast Process Manager) offers several advantages over mod_php, with two of the most notable being that it is more flexible to configure and currently the preferred mode of running PHP by many in the community. However, if you’re using your package manager’s default configuration settings, then you’re likely not getting the most out of it.

In this post, I’m going to give a brief overview on how to improve PHP-FPM’s performance, by discussing PHP-FPM’s three process manager types, and which one is best to use under which circumstance.

PHP-FPM can use one of three process management types:

  • static
  • dynamic; and
  • ondemand

Let’s look at what each one is in a little bit of detail.

Static

Static ensures a fixed number of child processes are always available to handle user requests. This is set with pm.max_children. In this mode requests don’t need to wait for new processes to startup, which makes it the fastest approach.

Assuming that you wanted to use static configuration with 10 child processes always available, you would configure it in /etc/php/8.3/fpm/pool.d/www.conf (assuming you’re using Debian/Ubunut’s PHP-FPM’s default configuration file) as follows:

pm = static
pm.max_children = 10

To see if the configuration change has been effective, after restarting PHP-FPM, run pstree -c -H <PHP-FPM process id> -S <PHP-FPM process id>. This will show that there are ten processes available, as in the example below.

php-fpm8.3-+-php-fpm8.3
           |-php-fpm8.3
           |-php-fpm8.3 
           |-php-fpm8.3 
           |-php-fpm8.3 
           |-php-fpm8.3 
           |-php-fpm8.3 
           |-php-fpm8.3 
           |-php-fpm8.3 
           |-php-fpm8.3 
           |-php-fpm8.3

Dynamic

In this mode, PHP-FPM dynamically manages the number of available child processes and ensures that at least one child process is always available.

This configuration uses five configuration options; these are:

  • pm.max_children: The maximum number of child processes allowed to be spawned.
  • pm.start_servers: The number of child processes to start when PHP-FPM starts.
  • pm.min_spare_servers: The minimum number of idle child processes PHP-FPM will create. More are created if fewer than this number are available.
  • pm.max_spare_servers: The maximum number of idle child processes PHP-FPM will create. If there are more child processes available than this value, then some will be killed off.
  • pm.process_idle_timeout: The idle time, in seconds, after which a child process will be killed.

Now the fun part comes; how do you calculate the values for each setting? Sebastian Buckpesch offers the following formula:

Setting Value
max_children (Total RAM – Memory used for Linux, DB, etc.) / process size
start_servers Number of CPU cores x 4
min_spare_servers Number of CPU cores x 2
max_spare_servers Same as start_servers

We also need to set pm.process_idle_timeout, which is the number of seconds after which an idle process will be killed. If you don’t want to calculate this from scratch, we wrapped all this into a simple to use calculator tool to get the optimal FPM configuration settings.

Let’s say that our server has two CPUs, each with four cores, and 8GB RAM. If we assume that Linux and related daemons are using around 2GB (use free -hl to get a more specific value), that leaves us with around 6192MB.

Now, how much memory is each process using? As a rule of thumb, you need to look at the memory_limit configured in PHP. To calculate a more accurate number, there’s a Python script called ps_mem.py. After running it, using sudo python ps_mem.py | grep php-fpm, you’ll get output similar to the following:

28.4 MiB +  33.8 MiB =  62.2 MiB    php-fpm8.3 (11) 

The first column is private memory. The second column is shared memory. The third column is the total RAM used. The fourth column is the process name.

From the above, you can see that the process size is 62.2MiB. So, feeding all of that information into our formula, we arrive at the following:

# Round the result up. (8192 - 2000) / 62.2 

Based on that, we come to the following settings values:

Setting Value
max_children 100
start_servers 32
min_spare_servers 16
max_spare_servers 32

We’ll leave the pm.process_idle_timeout to the default of 10s. Assuming that we’re happy with these settings, then we’d configure it as follows:

pm = dynamic
pm.max_children = 100
pm.start_servers = 32
pm.min_spare_servers = 16
pm.max_spare_servers = 32 pm.max_requests = 200

You can also use memory monitoring tools such as Tideways on a regular basis to monitor how much memory your application is using.

ondemand

ondemand has PHP-FPM fork processes when requests are received. To configure PHP-FPM to use it, we need to set pm to dynamic, and provide values for:

  • max_children
  • process_idle_timeout
  • max_requests

max_requests sets the number of requests each child process should execute before respawning. The documentation suggests that this setting is helpful for working around memory leaks.

Assuming that we take the same settings as for dynamic, we’d configure it as follows:

pm = ondemand
pm.max_children = 100
pm.process_idle_timeout = 10s
pm.max_requests = 200

Which Configuration Is Right For You?

Honestly? The answer is: “it depends“, as it always depends on the type of applications that you are running. However, here are some suggestions as to which configuration to choose. To nail the right secondary configuration values for your FPM configuration, we recommend you to look into our simple calculator.

Low Traffic Site

If you have a low traffic site, such as one hosting a backend control panel, such as cPanel, then use ondemand. Memory will be saved as child processes will only be spawned when they’re needed and killed off when they’re no longer required. As it’s a backend, users can wait a moment or two longer while a thread spawns to handle their request.

High Traffic Site

If you have a high traffic website, then use static and adjust the settings based on your needs over time and your available hardware resources. It might seem like overkill to have a large number of child processes always ready to receive requests.

However, high-traffic sites need to respond as quickly as possible. Therefore, it’s essential to use static so that a sufficient number of child processes are ready to do so.

By using ondemand, child processes will likely consume too much memory being spawned and killed, and the startup delay will have a performance hit.

Using dynamic likely won’t be as bad, depending on the configuration. However, you may end up with a configuration that effectively mirrors static.

Using Multiple Pools for Frontend/Backend

Now for one final recommendation: serve the frontend and backend of your website using different pools. Say you have an e-commerce site, perhaps powered by Magento. You can look at the application as being composed of two parts:

  • A frontend where customers can browse and make purchases
  • A backend, where admin staff manage the shop (such as adding/removing products, categories, and tags, and reviewing ratings)

When viewed this way, it makes sense to have one pool that serves the frontend and another that serves the backend and to configure each appropriately.

For what it’s worth, you could split up any application into multiple parts using this strategy, if it makes sense to do so. Here’s how to do so.

In /etc/php/8.3/fpm/pool.d/www.conf, add the following configuration:

; frontend
[frontend]
listen = /var/run/php-fpm-frontend.sock
user = www-data
group = www-data
listen.owner = www-data
listen.group = www-data
pm = static
pm.max_children = 5;
 
; backend
[backend]
listen = /var/run/php-fpm-backend.sock
user = www-data
group = www-data
listen.owner = www-data
listen.group = www-data
pm = ondemand
pm.max_children = 5
pm.process_idle_timeout = 10s

This creates two pools, one for the frontend, and one for the backend. Both have the same user and group, but have different process manager configurations and are connected to via different sockets.

The frontend pool uses a static configuration with a small maximum number of child processes. The backend pool uses the ondemand configuration, also with a small number of configurations. These numbers are arbitrary, as they’re for the purposes of an example.

With that saved, for your NGINX vhost file, use the following configuration:

server {
    listen 80;
    server_name test-site.localdomain;
    
    root /var/www/test-site/public;
    
    access_log /var/log/nginx/test-site.access.log;
    error_log /var/log/nginx/test-site.error.log error;
    
    index index.php;
    
    set $fpm_socket "unix:/var/run/php-fpm-frontend.sock";
    
    if ($uri ~* "^/api/") { 
        set $fpm_socket "unix:/var/run/php-fpm-backend.sock";
    }
     
    location / {
        try_files $uri $uri/ /index.php;
        
        location ~ .php$ {
            fastcgi_split_path_info ^(.+.php)(/.+)$;
            fastcgi_pass $fpm_socket;
            fastcgi_index index.php;
            include fastcgi.conf;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        }
    }
}

This creates a virtual host configuration that sends requests either to the frontend or backend pool, based on the location requested. Any requests to /api are sent to the backend pool, and all other requests are routed to the frontend.

In Conclusion

That’s been a rapid introduction to tuning PHP-FPM for better performance. We’ve looked at the three different process manager configurations, their related settings, and discussed when each configuration makes sense. We then finished up by looking at worker pools.

Four Logging Best Practices for Production Applications

BONUS: We have discussed this topic with an expert in the PHP community in our podcast:

Logging is an essential part of just about any PHP-based application; whether in a script or a larger application. However, how little is too little and how much is too much to log?

If we don’t log enough information, when something goes wrong, as it invariably does, then we won’t have enough information available to determine what went wrong so that we can fix the problem. However, if we have too much information, then we’ll be unable to filter out the white noise.

In this post, I’m going to step through four best practices for logging in production, practices that you can apply to ensure you have the right amount of information as and when you need it.

Know The Right Log Level to Use

If you’ve spent any amount of time around logging libraries, you’ll know that they support several different logging levels. Most of them follow, to one extent or another, RFC 5424. And, if you’ve had a chance to review PSR-3: Logger Interface, then you’ll know that there are eight levels.

However, you may not know when to use each level. If so, here’s a broad overview of when to use each level.

  • Emergency: This level is for when the system is unusable.
  • Alert: This level is for when action must be taken immediately.
  • Critical: This level is for reporting conditions that critically affect the application.
  • Error: This level is for problems that prevent your application from continuing normally. These types of problems can include the database being down or inaccessible.
  • Warning: Use this level for alerts about something going wrong, but the application can continue functioning. This might include socket timeouts or retries, and invalid logins attempts.
  • Notice: Use this level for alerts about issues that start to indicate something is going wrong with your application, yet which aren’t too significant.
  • Info: Use this level for general messages, but not for debugging information.
  • Debug: Use this level for tracing information.

Know What To Write In a Log Message

The previous section gave a broad overview of when to use each log level. However, what do you write in a log message?

You don’t want the message to be too extensive. You don’t want to be too brief, either.

Splunk has an excellent set of suggestions. Of those suggestions, I’ve found the following five to be the most pertinent:

  • Create events that humans can read: Make sure that whatever it is that you’re logging, that it’s intelligible. If the information needs to be stored in a binary format, because it’s too extensive, then ensure that tools are available that can decode that format.
  • Use timestamps: It’s essential to know when an event occurred, especially as you’re going to be reading many log entries at a time to track down the source of the error. Make sure that log messages can be read in the correct order so that they don’t exacerbate the problem.
  • Use unique identifiers (IDs): Unique identifiers can help information be filtered better than timestamps alone. They can help users filter by an application component, node, section, or other attributes.
  • Use developer-friendly formats: There are a plethora of formats that developers are commonly familiar with, that they know how to parse, and are well suited to storing information in a structured way.
  • Identify the source: Where did the error originate? Ensure that you identify the source of the error, such as the class, function, and file.

Centralize Logging

No matter the size of your application, logs should be stored in a centrally available location. If not, then you run the risk of not having all the relevant information at hand when something goes wrong.

If this seems strange, consider the following scenario. Your application is quite successful so has been scaled horizontally, with multiple nodes available to handle requests which sit behind a load balancer.

If the code is written such that log entries are written to the node that the request is handled by, what happens when one or nodes become unavailable or inaccessible? Alternatively, when an error occurs:

  • How long will it take to retrieve all of the log information from each node in the cluster?
  • How will you know on which node the error occurred?

To avoid situations such as these, I strongly advocate for centralized logging. You can use a dedicated, internal, logging server or a commercial logging solution, such as Loggly, Papertrail, or Amazon Elasticsearch Service.

It should be relatively trivial to either log to a centralized location, or to the operating system’s log service and configure it to forward the log messages out to a central location. This should happen regardless of the logging solution, the logging library (such as Monolog), and whether you’re deploying to a Linux or Windows server.

For example, you could log to Rsyslog on Linux which forwards those log messages to a central log server. On Windows, you could configure it to forward log messages to a central collector service.

Regardless of whether the central server is using an open-source tool such as ELK or a commercial service such as Loggly, the application’s logs can be centrally accessed and backed up, in case of some form of a natural disaster.

Understand the Difference Between Domain Messages and Framework Messages

When you’re logging, make sure that you distinguish between these two types of messages. Domain messages relate to the events within the business logic of your application. Framework (and library) messages are those that relate to libraries, frameworks, and other low-level code which your application uses.

Here’s an example of a domain message. It shows a domain “event” that was triggered within the application.

$logger->info("User $userId with $email registered for newsletter $name"); 

And here’s an example of a framework message. It shows the matched route when the given URL was requested, and which controller was dispatched to handle it.

$logger->info("Route $foo matched URL $url: Executing controller $controllerClass"); 

With that said, in production it might make most sense to have two logger services:

  • One for domain messages and one for framework messages, which would log up to INFO level; and
  • One for framework/library messages, which would log up to either WARNING or ERROR.

Here’s a small, for example purposes only, snippet on how to configure two different Monolog logger instances, one for domain messages, and one for framework messages.

// Create the logger for domain messages $domainLogger = new Logger('domain_logger');

// Create the logger for framework messages $frameworkLogger = new Logger('framework_logger');

// Now add some handlers, which log up to a certain log-level $domainLogger->pushHandler(new StreamHandler(__DIR__.'/domain.log', Logger::INFO)); $frameworkLogger->pushHandler(new StreamHandler(__DIR__.'/framework.log', Logger::ERROR)); 

Conclusion

And that’s four logging best practices that you can use when developing your PHP applications. From the right log level to use to centralizing logging, each should help you store more meaningful information in the right way.

PHP & Shared Nothing Architecture: The Benefits and Downsides

BONUS: We have discussed this topic with an expert in the PHP community in our podcast:

There are several ways (architectures) in which applications can work, with the most notable being:

  • Shared-Nothing
  • Shared-Disk
  • Shared-Memory
  • Shared-Everything

In this post, we’re going to look at what Shared-Nothing Architecture is, along with its benefits and downsides, concerning PHP and its impact on performance.

Several different languages can be used for web-based application development, most of which you are likely already familiar. The most popular of these are JavaScript, Ruby, Python, Java, Go, R, and, of course, PHP. Of these languages, however, only PHP uses a Shared-Nothing Architecture by default.

If these terms are new to you, let’s briefly step through them.

A Shared-Nothing Architecture

A Shared-Nothing Architecture is a distributed-computing architecture in which nodes do not share (independently access) memory or storage with any other. The intent is to eliminate contention among nodes.

Shared-Nothing ArchitectureShared-Nothing Architecture

A Shared-Disk Architecture

A Shared-Disk Architecture is quite similar to shared-nothing, except that every node interacts with a shared disk, typically a database (such as MySQL, PostgreSQL, and Oracle), or a shared filesystem typically provided by a storage area network (SAN) or network-attached storage (NAS).

A Shared-Memory Architecture

Similar to shared-disk, the Shared-Memory Architecture, as the name implies, is where each node makes use of a shared-memory resource. This could be a cache provided by Redis, Memcache or APCu.

A Shared-Everything Architecture

As the name implies, this is where each node shares all resources with every other node.

Shared-Everything ArchitectureShared-Everything Architecture

Shared Nothing = PHP By Default

If you’ve ever considered how PHP works, by default, then you’ll know that it is based around a shared-nothing model. This is because, for every request a node starts from scratch — with no memory shared from the prior request of the same node. It brings all the required objects into memory, performs the required processing, and then releases all the resources when the request has been fulfilled. This approach lies in stark contrast to languages such as Java, which persist objects across multiple requests.

The Benefits

However, why is this model a good one? Let’s consider the benefits. A Shared Nothing architecture has several key benefits; these are:

  • It Eliminates Single Points of Failure
  • Its Simpler to Scale
  • It Makes Upgrades Simpler
  • It’s a Drastically Simpler Programming Model

It Eliminates Single Points of Failure

As nodes don’t share any resources, applications can continue functioning even if one or more nodes fail. Yes, application performance drops with each node that fails, but the application can continue to function, as nodes don’t share any resources between themselves.

Its Simpler to Scale

As each node can serve requests to the application without having to deal with resource contention, then as many additional nodes as are required can be added, as and when necessary, to scale the application to provide sufficient capacity.

It Makes Upgrades Simpler

As with the previous point, individual nodes can be upgraded, or replaced, without shutting down the entire application. No node is aware of any other, so it doesn’t matter if one is removed, upgraded, or improved.

It’s a Drastically Simpler Programming Model

PHP developers don’t, often, have to think about memory too much. In most PHP applications, memory is used within a single, often very short-lived, request. Objects are initialized, used, and dropped. Consequently, developers, generally, don’t have to deal with the implications of objects that persist for a long time; such as with worker processes, or across multiple requests.

Is it A Practical Model?

Like many questions, the answer is: “it depends”, and is based on many factors. The prime among them is: “How is your application is designed and/or developed?” That design (or legacy development) may necessitate not strictly adhering to a shared-nothing architecture, but rather a combination of one or more of the others.

If it helps, cast your mind back to the earlier versions of PHP. In those days, PHP was, virtually, wedded at the proverbial hip with MySQL. Countless articles and tutorials detailed PHP apps storing all their data in a MySQL database.

Then, over the years as applications became larger and more complex, a greater emphasis was given to sessions and caching as well. Sessions were often stored on a shared filesystem or in a database and application objects, and its data were often cached in a high-performance, in-memory data store, such as Redis or Memcached. And let’s not forget the use of queueing servers, such as RabbitMQ, Beanstalkd, ActiveMQ, and Redis.

As a result of these, and other, changes, it’s very easy for PHP applications to more likely be a blend of shared-memory, shared-disk, or shared-everything.

However, if care’s given, each node can still, more or less, operate in a shared-nothing manner.

Is Shared-Nothing The Right Approach?

Before you seek out alternatives, it’s worth asking if you the benefits of not using PHP with Shared Nothing are worth it

As we’ve seen so far, shared-nothing offers a significant number of benefits. Additionally, PHP’s shared nothing design provides a significant performance advantage, and larger frameworks are built around this approach.

That said, there are individual use-cases that work better with sharing memory across requests, such as Websockets. What’s more, if you build an application based around shared-nothing, it’s not a trivial endeavor to move away from it. You can’t just flip the proverbial switch at a time of your choosing. So, I’d strongly encourage to you carefully consider if not using it truly is what you want to do.

Running PHP Without Shared Nothing

If you’re keen to build applications in PHP which don’t fully implement a shared-nothing architecture, then you may be interested in knowing that there are some frameworks available to help out. These are:

  • Roadrunner: a high-performance PHP application server, load-balancer, and process manager written in Golang.
  • Swoole: Enables PHP developers to write high-performance, scalable, concurrent TCP, UDP, Unix socket, HTTP, Websocket services in PHP programming language without too much knowledge about non-blocking I/O programming and low-level Linux kernel.
  • ReactPHP: a low-level library for event-driven programming in PHP. At its core is an event loop, on top of which it provides low-level utilities, such as Streams abstraction, async DNS resolver, network client/server, HTTP client/server and interaction with processes.

In Conclusion

In this article, we’ve taken a (brief) look at four of the most common application architectures, but given particular emphasis to one: shared-nothing. We’ve learned about its key benefits and their impact on application performance. We also got an overview of three of PHP’s most common shared-nothing frameworks, those being Roadrunner, Swoole, and ReactPHP.

I hope that this article’s helped give you a clearer picture of the architectural models available, and why PHP’s default is such a powerful one.

Reliable Integration with Third-Party APIs in PHP

BONUS: We have discussed this topic with an expert in the PHP community in our podcast:

It’s a fact of modern software development that aspects of our applications interact with third-party APIs. This could be for any number of reasons, with some common ones being payment processing, telecommunications, logging, and data analysis.

So, since our applications rely upon third-party APIs so much, we need to ensure that we integrate with them as effectively — and defensively — as we can. Otherwise, when the APIs break, so too will our application — and its reputation with our users.

To help avoid a loss of reputation, I’m going to cover five ways in which you can integrate with third-party APIs and ensure a minimum of downtime and stress.

Implement API Monitoring

Cloudflare status pageCloudflare status page

Let’s start with monitoring. Even though third-party APIs aren’t part of your infrastructure, you still have to know whether they’re working or not. This is especially important when you consider that some of the most notable internet platforms have had outages in recent years.

Here are some of the most notable:

  • Facebook has had two outages in 2019 so far, one in March and another in April. These outages, as you would expect, had a knock-on effect on Facebook’s most well-known services, WhatsApp, and Instagram.
  • Google, Amazon, and Reddit experienced outages on Jun 24, this year. These impacted a significant number of other services and businesses which rely upon Google and Amazons extensive service offerings. Did you do any meaningful work that day?
  • Twitter had an outage in April 2018.

To monitor effectively, you need to integrate monitoring services into your infrastructure and make them a standard part of your development processes. Like any aspect of software development, there are many sites and vendors on the market that provide monitoring services.

A number of the more well-known ones are:

Alternatively, several third-party APIs provide a status endpoint, such as the one below from Cloudflare, which are easy enough to integrate against. They may offer SDKs which make the process relatively trivial. Alternatively, you could roll a homegrown solution, such as the one-liner below, which checks if Cloudflare is currently online, using Selenium with Python.

from selenium import webdriver

executable_path = "path/to/geckodriver" domain = "https://www.cloudflarestatus.com/" xpath = "//div/span[starts-with(@class,'status')]"

options = webdriver.FirefoxOptions() options.headless = True driver = webdriver.Firefox(options=options, executable_path=executable_path) driver.get(domain)

element = driver.find_element_by_xpath(xpath) print(element.text) driver.quit() 

Respect the APIs Rate Limits

Most APIs — especially when using guest accounts or introductory free tiers — limit the number of requests, by using rate limits. Rate limits restrict clients’ ability to make more than a fixed number of requests to an API within a given time frame.

The time frame can range from requests per minute to requests per day. When rate limits are reached, subsequent requests are either temporarily blocked or throttled, until the time frame expires. One example is [Git Hub’s API], which rate limits requests to 5,000 requests per hour.

There are many reasons why APIs implement rate-limiting. One of the most common is to avoid a malicious script hogging all of an APIs resources.

There are several ways in which APIs communicate this information. Using Git Hub’s API as our working example; the API sends three response headers, these are:

Header Description
X-RateLimit-Limit The maximum number of requests you’re permitted to make per hour.
X-RateLimit-Remaining The number of requests remaining in the current rate limit window.
X-RateLimit-Reset The time at which the current rate limit window resets in UTC epoch seconds.

Here’s an example, taken from their API documentation:

HTTP/1.1 200 OK Date: Mon, 01 Jul 2013 17:27:06 GMT Status: 200 OK X-RateLimit-Limit: 60 X-RateLimit-Remaining: 56 X-RateLimit-Reset: 1372700873 

From this information, you can see that:

  • Requests are rate limited to a maximum of 60 requests per hour.
  • 56 requests remain within the current time frame.
  • The rate limit window resets on Mon Jul 1 19:47:53 CEST 2013 (date -d @1372700873).

Ensure that this information is used in your application’s integration code, to respond appropriately when the rate limit draws near and when it is exceeded.

Applications should also check if servers are overwhelmed by checking a response’s HTTP status code. If an HTTP 429 (Too Many Requests) is returned, you know that the server’s under high load and can’t currently respond.

For an amusing introduction to rate limiting, check out What is API Rate Limiting All About?.

Implement Retries

There are several ways in which code can handle rate limits (and API request failures). The application could choose to ignore exceptions thrown when rate limits are reached, but doing so isn’t altogether helpful. The application could instead be coded to back off when approaching the rate limit.

Alternatively, a third approach is to implement retries. In short, retries attempt to complete a failed API request a set number of times within a set interval.

If one of the requests completes successfully, then the job is removed. If not, then the failed request is logged for later analysis. Retries could likely be handled by a combination of a queueing server and a background process.

If rate limits are regularly being exceeded, it may be time to either revisit your application’s architecture or to consider increasing your rate plan. That said, use monitoring to stay abreast of request volume and rate limit overruns.

Always Have Tests

It should go without saying, but tests are essential for code that integrates against third-party APIs, as well as everything else. I was all too glad that I had them in place some years ago when I was integrating against a third-party API.

While the vendor’s documentation was, for the most part, quite good, the stability of the API wasn’t! As a result, requests to the vendor’s API would intermittently fail. By having tests in place, however, along with the code under version control, we could verify that our code:

  1. Hadn’t changed
  2. Still integrated correctly, based on the latest available version of the vendor’s API documentation.

Knowing this was particularly important. This was because the vendor’s first resort was often to say that there must be some mistake or change in our code because the relevant section of their API had not changed. Having tests showed me that our code was unchanged and gave me the confidence to pursue the fact that a recent release on their end was responsible for the outage.

Read the Documentation

Stripe documentation home pageStripe documentation home page

A well-written API has well written and feature-complete documentation, containing the details of, but not limited to:

  • Client errors
  • Every endpoint
  • Example requests
  • How to authenticate
  • Pagination
  • Request parameters (which document every field, its type, and a short description)
  • Response headers
  • Response objects (which document every field, its type, as well as a short description).

In addition to this, the API documentation is laid out in a well-structured, intuitive, and easy-to-read format. An excellent example of this is Stripe’s API documentation. I’ve used it on several occasions and have found it to be a great example.

Assuming that the API documentation is updated with each API release, you can refer to it with confidence. If you’re looking to integrate with a third-party API and its API is either poorly documented — or worse, not documented — I strongly suggest choosing an alternative vendor.

In Conclusion

Those are five ways that help ensure reliable communication when integrating against third-party APIs. I hope they help make the process simpler, more reliable, and more efficient.

PHP Timeouts, Retries and Limits

BONUS: We have discussed this topic with an expert in the PHP community in our podcast:

We have blogged about timeouts, retries and limits before in these three blog posts:

We discussed all these topics with Bastian Hoffmann from our partner SysEleven in this podcast episode.

Four Key Considerations When Running PHP Applications On Multiple Servers

BONUS: We have discussed this topic with an expert in the PHP community in our podcast:

Building and deploying PHP applications on one server is a, relatively, straightforward process. However, what about deploying a PHP application across multiple servers? In this article, I’m going to discuss four key considerations to bear in mind when deploying PHP applications when doing so.

Load Balancing

Load balancing is where requests are distributed uniformly across servers in a server pool. Load balancers receive user requests and determine which server in a server pool to forward the request for final processing. They can either be hardware- (e.g., F5 Big-IP, and Cisco ACE) or software-based (e.g., HAProxy, Traefik, and Nginx).

Simple load balancer diagramSimple load balancer diagram

Using them increases network efficiency, and application reliability and capacity, by adding servers on a planned basis or to meet short-term demand. In applications that use them, users never know that the same server isn’t handling their requests every time. All they know is that their requests are handled.

Load balancers typically use one of six methods for determining the server to pass a given request. These are:

  • Round Robin: Requests are distributed evenly across all servers in the pool.
  • Least Connections: Requests are sent to the server with the least number of currently active requests.
  • IP Hash: Requests are routed based on the client’s IP Address.
  • Generic Hash: Requests are routed based on a user-defined key.
  • Least Time: Requests are sent to the server with the lowest latency.
  • Random: Requests are distributed randomly across all servers in the pool.

While the benefits are many, migrating a load balanced-architecture requires a number of factors to be considered, which include:

  • Does each server in the cluster have the same physical capacity?
  • How Are Nodes Upgraded?
  • What Happens if a Node Is Unreachable or Fails?
  • What Kind of Monitoring Is Required?

These, and other questions, need answering before choosing and implementing the correct load balancer. That said, if you’re going to setup load balancing yourself, I strongly encourage you to use NGINX, as it’s likely the most used option. You can find out how to get started with NGINX’s load balancing documentation.

Sessions

Now that we’ve considered load balancing, the next logical consideration is: how are sessions handled? Sessions allow applications to get around HTTP’s stateless nature and preserve information across multiple requests (e.g., login status and shopping cart items).

By default, PHP stores sessions on the filesystem of the server which handles the user’s request. For example, if User A makes a request to Server B, then User A’s session is created and stored on Server B.

However, when requests are shared across multiple servers, this configuration likely results in broken functionality. For example:

  • Users may be part-way through a shopping cart and find that their cart is unexpectedly empty
  • Users may be randomly redirected to the login form
  • Users may be part-way through a survey only to see that all their answers have been lost

There are two options to prevent this:

  • Centrally-stored sessions; and
  • Sticky sessions

Centrally Stored Sessions

Sessions can be centrally stored by using a caching server (e.g., Redis or Memcached), a database (e.g., MySQL or PostgreSQL), or a shared filesystem (e.g., NFS or [Glusterfs]). Of these options, the best is a caching server. This is for two reasons:

  1. They’re a key-value, in-memory storage solution, which gives them greater responsiveness than an SQL database.
  2. As sessions are always written when a request ends, SQL databases must write to the database on every request. This requirement can easily can lead to table locking and slow writes.

When storing sessions centrally, you need to be careful that the session store doesn’t become a single point of failure. This can be avoided by setting up the store in a clustered configuration. That way, if one server in the cluster goes down, it’s not the end of the world, as another can be added to replace it.

Sticky Sessions

An alternative to session caching is Session Stickiness (or Session Persistence). This is where user requests are directed to the same server for the lifetime of their session. It may sound like an excellent idea at first, but there several potential drawbacks, including:

  • Will cold and hot spots develop within the cluster?
  • What happens when a server isn’t available, is over-burdened, or has to be upgraded?

For these reasons, and others, I don’t recommend this approach.

Shared Files

How will shared files be updated? To make things just that much trickier, there are, effectively, two types of shared files:

  1. User-provided files and one-off items, such as PDF invoices.
  2. Code files and templates.

These need to be handled in different ways. Let’s start with code files and templates.

These types of shared files need to be deployed to all servers when a new release or patch is made available. Failing to do this will see any number of unexpected functionality breaks.

The question is: what’s the best approach to deploy them? You can’t take down and update nodes individually. Why? What happens if users are directed to a server with new code on one request and directed to a server with the old code on a subsequent request? Answer: broken functionality.

One solution is to change the load balancer method to stop directing requests to nodes during updates. After nodes are updated, they’re allowed to accept new session requests.

This could then be repeated until all nodes within the cluster have been upgraded or patched. It’s workable, but it’s also, potentially, quite complicated and time-consuming.

Now let’s look at user provided and one-off files. These types of files, include images, such as a profile image PDF invoices, and company/organizational reports. These types of files need a shared filesystem, whether local or remote (such as S3).

Some potential solutions are a clustered filesystem such as Glusterfs, or a SaaS solution, such as Amazon S3, or Google Cloud Storage.

A shared (clustered) filesystem simplifies deployment processes, as new releases and patches, only need to update files in one location. The deployment process likely doesn’t need to handle file replication to each node within the filesystem cluster, as the service should provide that.

However, like centrally stored sessions, the filesystem has the potential to become a single point of failure, if it goes down or becomes inaccessible. So, this needs to be considered and planned for as well.

Implementing one of these solutions allow files to be centrally located, where each node can directly access it, as and when required. Many of PHP’s major frameworks (including Laravel, Symfony, and Zend Expressive) natively support this approach. Alternatively, packages such as Flysystem can help you implement this functionality in your application.

Job Automation

It’s quite common in modern applications — especially in PHP — to use Cron to automate regular tasks. These can be for any number of reasons, including file cleanup, cleanup abandoned shopping carts, email processing, and user account maintenance.

However, if the application is composed of multiple nodes, there are several questions to consider. For example:

  • Which server does Cron run on?
  • Does the Cron service run on a separate server from the web application?
  • If one server is dedicated to running Cron tasks:
    • What happens when it goes down, say because of a hardware failure?
    • What happens when it’s taken down for maintenance?
    • What happens when it’s not accessible?

You could roll your own solution, or you could use one of several existing solutions, such as Dkron or Apache Mesos and Airbnb Chronos. Each of these has its pros and cons.

If you roll your own solution, it may be a lot of work in addition to your existing application. It may lead you to experience the same multi-server considerations that we’re currently discussing. Alternatively, if you use one of the above solutions, you will need to plan out the implementation and maintenance of that server and how to best integrate it with your application.

All of these are viable approaches. It’s just important to consider this in advance.

In Conclusion

Those are four key considerations to keep in mind when transitioning from a single to a multi-server setup. There are others in addition to these four.

However, these are four of the most important. I hope that they provide a sound foundation for helping you to understand the potential changes and pitfalls involved.

5 Ways to Increase PHP Performance

BONUS: We have discussed this topic with an expert in the PHP community in our podcast:

Out of the proverbial, box, PHP provides a decent performance. However, there are several things that we, as PHP developers and systems administrators, can do to increase its performance even further; sometimes with almost no effort.

In this post, I’m going to step through five of those ways. By the time you’re finished reading, you should see at least a notable increase in the performance of your PHP application. Let’s begin.

Use PHP 8 (or at least 7)

One of the best ways to improve PHP’s performance is to run the latest version (PHP 8). Or if you are struggling with old legacy code, at least a version of PHP 7. They offer a significant speed improvement over any previous PHP 5 version. Now, results will always vary, as no two applications are ever the same. However, based on reports by a range of different developers and notable hosting companies, the performance improvements are significant.

Here’s a small sample:

If that’s not enough for you, check your benchmarks and see what they say.

Additionally, PHP 7.0.0 was released on December 3rd, 2015, which is already almost a decade ago.

What’s more, the final release of PHP 5, 5.6, went EOL (End of Life) on Dec 31, 2018. So, if you haven’t already, it’s about time you made the transition to PHP 7. Make the upgrade and, as Rasmus said at phpCE 2018, go green(er) by reducing your hosting costs.

Uninstall Xdebug

The next thing to check is that Xdebug is not installed on your production servers. Sure, Xdebug is one of the most sophisticated and comprehensive profilers and debuggers for PHP, but it should never be enabled (even installed) on a production server.

While the performance benchmarks vary (they always do), one report on Stack Overflow showed a 50% performance boost by completely removing Xdebug. Though for some modes in Xdebug 3, the overhead is lower. What’s more, it’s important to note that even though Xdebug was installed on the server — it wasn’t even enabled!

Xdebug performance benchmarkXdebug performance benchmark

To check if it’s installed, run php -m | grep -i xdebug or check your hosting provider’s administration panel. And if you’d like to know more about how Xdebug works, check out the Xdebug documentation.

Use Composer Optimize Autoloader

While we’re all likely very familiar with using Composer to handle package management for us, if we don’t consider optimizing the configuration it generates, our applications aren’t going to perform as well as possible.

Here’s a quote from the composer documentation which explains why:

due to the way PSR-4 and PSR-0 autoloading rules are set up, it Composer needs to check the filesystem before resolving a class name conclusively. This slows things down quite a bit, but it is convenient in development environments because when you add a new class, it can immediately be discovered/used without having to rebuild the autoloader configuration.

To improve performance, Composer offers three optimization levels; these are:

  • Class map generation
  • Authoritative class maps
  • APCu cache

1: Class map generation

This strategy converts PSR-4/PSR-0 rules into classmap rules. This strategy is quicker because the classmap can instantly return the full path to known files and avoids a filesystem stat operation.

To enable this optimization level, run the following command:

composer dump-autoload --optimize 

2/A: Authoritative class maps

In addition to automatically enabling Level 1, when using this level, if a class is not found in the generated classmap, the autoloader will not attempt to look on the filesystem according to PSR-4 rules. To enable this optimization level, run the following command:

composer dump-autoload --classmap-authoritative 

2/B: APCu cache

This level adds an APCu cache as a fallback for the class map. However, it does not generate the classmap. Given that, level one would need to be enabled manually. To enable this optimization level, run the following command:

composer dump-autoload --apcu 

There are Trade-Offs

While each of these levels can improve application performance, they each have trade-offs which need to be understood before they’re used. Make sure you consult the Composer documentation before using them.

Use OPcache

Since PHP is an interpreted, not a compiled, language, the PHP runtime needs to convert source code to executable byte code, before it can be executed. And as PHP is a shared-nothing architecture, this process needs to happen on each request.

However, with Opcode cache (OPcache), this step only needs to happen once for each file, as the generated Opcodes can be cached in shared memory and referenced there instead.

So, as you can imagine, OPcache is one of the least intensive ways to improve the performance of a PHP application, because no code needs to change. Some reports show up to a 70% speed improvement.

There have been several Opcode caches for PHP over the years. OPCache (formerly Zend Cache) has been bundled with PHP since version 5.5 — and is enabled by default in PHP 7.

To know more about it, check out the OPcache documentation. To know more about performance tweaking OPcache, check out Tideway’s post on tuning it.

Use Preloading

If you’ve not heard of PHP 7.4’s new preloading feature, yet, it’s very cool! In a nutshell, the feature takes OPcache functionality further than it has ever gone before.

To quickly recap, the first time that a PHP source file is encountered, it has to be parsed, then compiled into machine-dependent bytecode, before the Zend Engine can execute it. OPcache significantly reduces the overhead of this process, as after the first time that source code is parsed and compiled down the bytecodes are then stored in an Opcode cache in shared memory.

The next time a request for that file is encountered, PHP checks to see if the Opcode cache has bytecodes for the file. If it does, they’re returned and used. If not (or if the source file has changed since the bytecodes were compiled), the source file is parsed, compiled, and cached. This gives a notable performance boost to PHP.

Now, let’s look at how preloading works. To quote the implementing RFC:

On server startup – before any application code is run – we may load a certain set of PHP files into memory – and make their contents “permanently available” to all subsequent requests that will be served by that server. All the functions and classes defined in these files will be available to requests out of the box, exactly like internal entities. Preloading ensures that:

  • All functions and most classes, defined in these files will be permanently loaded into PHP’s function and class tables and become permanently available in the context of any future request.
  • PHP resolves class dependencies and links with parent, interfaces, and traits (something that doesn’t happen with Opcode caching).
  • PHP removes unnecessary includes and performs other optimizations.

By having the code for an entire application, including its framework (such as Zend Expressive, Symfony, and Laravel) preloaded into memory, code that only changed on server restart, most applications will perform significantly better.

That said, preloading has a few, potential, drawbacks that you should know about:

  • A server restart is required when source files change.
  • Preloading won’t work in shared hosting environments.
  • Preloading won’t work when there are multiple versions of the same application.

Preloading isn’t magical. Both your code and deployment processes will have to be refactored to take advantage of it. For example, someone will have to develop a custom loader script to determine which files to load on server startup, and that script will have to be run at server startup. Regardless, it’s a significant improvement, one worth investing in!

Use a Profiler

Now for an option that will take a little more work than any of the previous five. Often we jump in and attempt to guess where the performance bottlenecks in our applications are, using intuition and educated guesses.

While these can work, they’re not the most efficient approaches. Instead, we can use code profilers to analyze our code and show where the bottlenecks are. Specifically, they help answer questions such as the following:

  • How many times was each method called?
  • What was the maximum execution time of each method?
  • What was the average execution time of each method?
  • How many times was a file included?
  • What path did a request take through an application (from the first to the last code file)?

By drilling down into a profiler’s results, you can often be pleasantly surprised, though more likely shocked, to find that your application is executing code paths and classes that you never expected.

Based on the information in the profiler report, you and your team can then begin to understand better what your application is doing and make informed refactorings to change it, as and where required.

If you’re just getting started with profiling, there are several options available for PHP. The most commonly used are:

In Conclusion

And those are five ways to improve the quality of your PHP applications notably. Any one of them on their own will deliver you a notable performance improvement. However, when used together, you should expect to see a significant performance, not to mention quality, improvement.

August 2023: Updated to reflect current version of PHP (8) and other references.

New Feature: Multiple Production Environments in Pro License

All Tideways applications with a Pro license can now create additional production Environments in addition to the one that is available by default.

Configuring a second or more environments as “production” effects the retention and the traces/minute collected for these environments. By default non-production environments have just 1 day of retention and 1 trace/minute.

Using multiple production environments is often useful if your application is run on multiple clusters for geographical reasons, for example one in Europe, one in the US and one in Asia.

It can also help to temporarily increase the retention and traces/minute for staging environments or environments built for intensive load-testing.

To configure an environment as production go to your “Application Settings” and “Server & Environments” screen.

A new column “Production” indicates which environment has production behavior, which by default is always only the environment by the same name. Click on the Edit link next to a “No” to get to the settings screen and click the checkbox and assign a traces/minute limit to this environment.

The traces/minute limit assigned is substracted from existing production environments. All of them together have 100 traces / minute in the Pro plan.

What Is Garbage Collection in PHP And How Do You Make The Most Of It?

BONUS: We have discussed this topic with an expert in the PHP community in our podcast:

Thanks to PHP being an interpreted language and the fact that it has a garbage collector, PHP developers don’t often have to think about memory management. Unlike developers in compiled languages, such as C/C++, we don’t have to give that much thought to memory allocation and deallocation.

However, it’s helpful to have a broad understanding of how garbage collection works in PHP, along with how you can interact with it so that you can create high performing applications.

In this article, we’ll cover two things:

  • The basics of how garbage collection works in PHP
  • About some of the functions available for interacting with it

By the end of the article, you will be better able to understand how garbage collection affects your application.

How Does Garbage Collection Work In PHP?

PHP’s garbage collection works in three ways:

  • Variables Fall Out Of Scope
  • Reference Counting
  • Garbage Collection

Variable Falls Out Of Scope

If a variable falls out of scope and is not used anywhere else, then it is automatically garbage collected. However, this process can be invoked manually by using unset(). In the example below:

  • $foo will be automatically garbage collected as soon as display_var() finishes executing; and
  • $user will be garbage collected because it was removed via unset().
<?php

function display_var()
{
     $foo = "bar";
     echo $foo; 
}

$user = "Matthew";
unset($user); 

Reference Counting

Similar to other languages, such as Python, Perl, and Tcl (version 8), PHP uses reference counting to help determine when variables are eligible to be garbage collected.

Reference counting is where PHP internally keeps track of how many symbols point to a given variable. When the number of symbols pointing to a variable drops to zero, then the variable is a candidate for being garbage collected at the end of the current request.

In the example below, $A is initialized to “value“, and then $B is initialized pointing to $A.

<?php

$A = "value";
$B = $A; 

When $A is first initialized, it has one reference, the current scope. When $B is initialized to point to $A, then there are two references to $A. If at some point, $B is removed, then $A is then eligible to be garbage collected.

To see how many references are stored for $A, in the code above, you can call xdebug_debug_zval(), assuming that you have Xdebug installed. Using the example above, you will see the following output:

a: (refcount=1, is_ref=0)='value' 

Garbage Collection

If a variable is part of a cyclic reference, e.g., where $A points to $B and $B back to $A, then the variable can only be cleaned up by PHP’s garbage collector. The garbage collector is triggered whenever 10,000 possible cyclic objects or arrays are currently in memory, and one of them falls out of scope.

The collector is enabled by default in every request. In general, this is a good thing. However, as it’s a process that runs, it requires cycles and computational resources, which can’t be devoted to your application. So, if your application is highly time-sensitive, it may be necessary to disable it, if only briefly.

It can be disabled in two ways:

  1. By calling gc_disable
  2. By setting zend.enable_gc boolean to false.

Additionally, if you call the function gc_collect_cycles, then garbage collection is triggered even if you don’t have 10,000 of them in memory yet.

Garbage Collection Has Improved in PHP 7.3

Garbage collection has improved notably in the 7.3 release of PHP, after the merge of a PR by Dmitry Stogov and Nikita Popov. The changes, as the benchmarks below attest, show a marked improvement in the performance of PHP’s garbage collector — especially for an application with a large number of objects.

// Very, very, very many objects 
GC       |    OLD |   NEW disabled |  1.32s | 1.50s enabled  | 12.75s | 2.32s

// Very many objects 
GC       |    OLD |   NEW disabled |  0.87s | 0.87s enabled  |  1.48s | 0.94s

// Less many objects 
GC       |    OLD |   NEW disabled |  1.65s | 1.62s enabled  |  1.75s | 1.62s 

You can see that the garbage collector’s execution time drops notably, in all but when an application only has a small number of objects; which is understandable.

Garbage Collection Statistics

Now we have a basic understanding of what garbage collection is, how it’s implemented in PHP, and when it’s triggered. However, we need more information to be able to use it effectively. Specifically, when it is triggered and how efficient each garbage collection run is.

As of PHP 7.3, PHP provides basic garbage collection information in user land PHP, via gc_status. The function returns:

  • The number of garbage collection runs
  • The number of objects collected
  • The current garbage collection threshold
  • The number of garbage collection roots

For greater detail, however, Xdebug is required. It supports writing far more comprehensive garbage collection statistics in a human-readable, tabular format to a configurable file and directory. Below, you can see what the file would look like:

Garbage Collection Report version: 1 creator: xdebug 2.6.0 (PHP 7.2.0)

Collected | Efficiency% | Duration | Memory Before | Memory After | Reduction% | Function 
----------+-------------+----------+---------------+--------------+------------+---------     
    10000 |    100.00 % |  0.00 ms |       5539880 |       579880 |    79.53 % | bar     
    10000 |    100.00 % |  0.00 ms |       5540040 |       580040 |    79.53 % | Garbage::produce      
     4001 |     40.01 % |  0.00 ms |       2563048 |       578968 |    77.41 % | gc_collect_cycles 

Here is what each table column means:

ColumnDescription
CollectedThe number of items that the garbage collector cleaned up.
Efficiency%The garbage collection efficiency percentage.
DurationThe time taken for garbage collection to complete.
Memory BeforeThe amount of memory available before garbage collection.
Memory AfterThe amount of memory available after garbage collection.
Reduction%The percentage of memory saved by the garbage collection run.
FunctionThe name of the function which garbage collection was run on.
Class (not shown in this example)The name of the class which garbage collection was run on.

Available Settings

The extension adds three new Xdebug settings; these are:

  • xdebug.gc_stats_enable: This enables the collection of garbage collection statistics, which is disabled by default. The file name and directory where the statistics are written are configurable by xdebug.gc_stats_output_name and xdebug.gc_stats_output_dir respectively.
  • xdebug.gc_stats_output_dir: This sets the directory where the statistics file is written to. Note, this setting can not be set with ini_set().
  • xdebug.gc_stats_output_name: This sets the name of the file that the statistics are written to.

In Conclusion

While not something that PHP developers have to give much consideration to, as a routine matter of course, Garbage collection is still something that can be essential to know, if we are to ensure that our applications perform as optimally as possible.

If you want to know more about it, make sure you refer to the PHP manual and the other links in the further reading section below.

Further Reading

Improving Magento 2 Performance

This summer we worked together with David on the performance of a Magento 2 project and found some general improvements that the whole Magento community will benefit from.

Plugins querying the current Magento Version

Many plugins support multiple minor versions of Magento (2.0, 2.1, 2.2 or 2.3) and may need to check the version of core, to invoke slightly different behavior that changed over the different versions.

As a plugin developer, this is possible by calling ProductMetadata::getVersion() inside your code or plugin. However, this method is not just a “getter” like its name suggests, but it actually performs a quite expensive operation.

See in this Tideways callgraph, that the call takes 174ms for the request of our customers shop:

The reason is here that the method actually uses the Composer API to query the version of Magento, instead of returning a fixed constant. The Composer API used loads all package versions, this can include packages that get their version set by Git branch, which causes Composer to do a shell call to git to retrieve the version.

Again naming strikes as a hard problem of software engineering here. Especially for public APIs of frameworks that are used by different developers than the authors, it is important to set expectations about the performance by naming methods well. If the name would have been extractVersionFromComposer(), then plugin developers would maybe have realized the negative performance implications much earlier.

Now, thanks to David’s Pull Request to Magento, the return value of this method will be cached indefinitely across requests since version 2.3.4, fixing the bottleneck after 4 years in a stable Magento releases.

If you still run lower minor versions of Magento, you can fix that problem by overwriting getVersion() to return a harcoded value, for example 2.2.2.

Implicitly Resetting Magento Compile Step

What we realized looking at the callgraphs was that a lot of code was using development-environment factories, even though the container was built with the command setup:di:compile and should be in production environment.

This was caused by the command module:enable -all executed after the compile step and resetting the cache partially. In hindsight it makes sense that the compile step generates the config cache for the current configuration, and when you enable modules then the compile step needs to be re-executed.

As for the specific shop we worked on, response times dropped as much as 700ms from 1 second to 300ms by caching the call to getVersion() and fixing the DI cache.

Black Friday Performance of Magento, Oxid, Shopware shops 2016/2017

With all the customers running Tideways on their Magento, Oxid or Shopware shops I was interested in how in the aggregated average, those shops usually perform on Black Friday compared to the 8 weeks before and the weeks after leading up to Christmas.

A lot of e-commerce shops have either large Black Friday, Cyber Monday or week long christmas campaigns, which can increase the traffic to the shops significantly. Both with and without caching a lot of this traffic will directly slow down PHP application servers because of the additional requests that compete for the same amount of Server, CPU & Memory resources.

To quantify this effect across all our customers we looked at the aggregated, anonymized historical data of all our customers with either a Magento, Oxid or Shopware shop for the years 2016 and 2017. We only looked at shops that had at least 100.000 requests on Black Friday and at least 14 days of data reported before and after that date. The sample size of this study is 168.

What change in requests can you expect on Black Friday?

On average across all 168 applications we saw an increase in the number of PHP backend requests (behind any potential caches) of 66% on Black Friday. The maximum increase was 874%.

30 of the 168 shops did not run specific Black Friday campaigns as their requests were equal or lower than each day before. Shops without Black Friday campaigns seem to have a dip in requests and then recover immediately in the time afterwards. Another interpretation that we cannot see from our data is that their shops were not available 100% on Black Friday due to too much traffic.

In the days after Black Friday the increase in PHP requests compared to October and November still amounted to 20% on average across all 168 shops with the highest increase being 287%.

How does increased BlackFriday traffic affect performance?

From the Tideways perspective the changes in requests are not the most interesting part that we can study from our historical data, but we are interested in subsequent performance changes.

Of our 168 applications we found that 49 were optimized by their operators in the time before Black Friday or received additional computing resources (we can’t see that from our data), leading to a better performance on Black Friday and afterwards.

If we look at all the shops that haven’t optimized their performance or scaled computing resources before Black Friday sufficiently, then we can see that their 95% percentile performance increases by around 29% on average. The shops that got hit hardest were bogged down by 95% percentile increases of 500% and more, sometimes leading to response times way above 10 seconds.

Even if your shop uses caching for most of your frontend, getting hit by these performance increases means that the basket and checkout process is much slower than usually and can lead to aborted carts and customer that leave your shop because of slowness.

The relationship of slow performance with increased page abandonment has been studied by Google, Amazon and many other major e-commerce players. They leave no doubt that it is important to get in front of your shop performance when planning small and large Black Friday, Cyber Monday and Christmas campaigns.

And if you know that your shop can’t handle a large increase in traffic, then maybe optimize your campaigns to be smaller and more spread across christmas season.

Ready for Black Friday with insights from Tideways Profiler

If you run a Magento, Spryker, Oxid, Shopware or any other PHP based webshop, then Tideways can help you find out about the endpoints with the highest impact on performance, bottlenecks, slow SQL queries and help you find optimizations to get ready for Black Friday and Christmas season.

This includes the personalized Black Friday preparation report that we will sent you in the aftermath of Christmas 2018 and again next year in August.

The difficulty of Memory Profiling in PHP

Did you ever have a memory leak in your PHP program and couldn’t locate the exact source in your code? From my experience with memory profiling in PHP, this is caused by the PHP engine and how it manages memory.

PHP uses a custom memory manager on top of the native memory management in C for multiple reasons:

  1. Better performance by allocating larger blocks of consecutive memory
  2. Ability to easily implement shared nothing, freeing all memory at the end of the request

So if you create a new string of size 200 KB (or any other kind of variable) in a function of your program, three things could happen in the PHP engine:

  1. The current and peak memory of the script are (nearly) the same, then the PHP Memory Manager needs to go to the kernel to allocate more memory and in turn the peak memory usage increases by (roughly) 200 KB.
  2. The current memory usage of the script is much lower than the peak memory that was allocated due to previouvly executed functions. PHP can re-use the memory and doesn’t need to talk to the kernel.
  3. The garbage collector could trigger and make the function look like it actually decreased memory usage.

This makes it difficult to find out about memory problems by looking at the changes of memory_get_usage() and memory_get_peak_usage() between function calls, because it highly depends on how your program is structured to understand where memory is allocated and freed. Sometimes it is obvious to see in a memory profiler but often it is not.

New Allocation Memory Hooks in PHP 7

This is why I was excited to see that PHP 7 easily allows to hook into the Memory Manager and count the number of allocations and memory. We added this feature to our XHProf fork last week.

Counting allocations is how most Ruby Memory Profilers work, so I hoped that their approach would yield better results.

Turns out, it does not yield good results.

Looking at two Composer runs in comparison between v1.3.3 and v1.4, which included a huge memory optimization , I realized that while peak memory is only 260 MB (1.3.3) and 120 MB (1.4), measuring the total amount of allocations both versions need 733 MB (1.3.3) and 859 MB (1.4) respectively.

So while Composer 1.4 allocates more memory in total, it does so in a way that only needs 40% of peak memory at the same time, which leads to a performance improvement. Allocating memory that the kernel already provided to the PHP script is not nearly as big a problem.

That means looking at total allocated memory can be quite misleading, because it is possible that its freed immediately and doesn’t affect the peak memory level.

The same is true for looking only at memory_get_usage(), because again for the most memory hungry function it is much larger on Composer 1.4 (322 MB) than on 1.3.3 (149 MB) even though peak memory usage is halfed.

The problem with the memory_get_usage() profiling approach is that we have no way of correlating if a function creates memory and its kept around permanently until the script ends, or if its (almost) immediately freed.

You just have to hope that looking at increases in memory_get_peak_usage() for each function points you to the memory leak.

Correlating allocation and free with php-memprof

If memory_get_peak_usage() is of no help there is another approach.

The alloc/free correlation problems can be worked around by using php-memprof extension, which allows to find out how much memory a function has allocated that is still in use at the end of the script.

It does this by keeping a large internal structure of every single memory allocation that happend during the execution of a function. I haven’t tested the CPU and memory overhead, but I assume this intensive approach prohibits its permanent use in production.

Improved Time Explorer

We have just rolled out an improvement to the Time Explorer, the low precision (15 minutes) chart that is rendered in the Dashboard and Application view with performance data for the complete retention period of your plan.

It is now shown directly below the the head navigation now and stays there when navigating through to snapshots or transaction details screen. Additionally it now includes the service and environment switchers that have been part of the main application graph before.

With this change the navigation of monitoring data is much more smooth and you don’t loose the context anymore by a complete re-render when selecting a timeframe.

On plan with just 1 day retention no time explorer is rendered and you can still navigate the time using the four period lengths on the right, top side of the new navigation box.

Testing a new approach to Memory Profiling in PHP with XHProf

Memory profiling in PHP has traditionally been hard. Most memory profilers compare the memory or peak memory before and after a function call to find out how much memory usage increased or decreased. This can be achieved by calling the equivalent of memory_get_usage() and memory_get_peak_usage() functions from within the profiling extension.

But this approach has one major blind spot, when a function allocates a lot of memory but also frees it again.

With PHP 7 there is a new hook into the Zend memory allocator that allows us to count the number of memory allocations, memory frees and the size of all memory allocations. We originally saw a similar approach to this in Danack’s MemTrigger extension and adopted it to fit the XHProf datastructure.

We have added experimental (!) support for this new approach to memory profiling into our open-source XHProf extension and are looking for feedback from you, if this collected memory data provides a more useful way of finding memory problems.

To use this feature with one of the existing XHProf UIs, you can call the Profiler with:

<?php

tideways_xhprof_enable(     TIDEWAYS_XHPROF_FLAGS_MEMORY_PMU |     TIDEWAYS_XHPROF_FLAGS_MEMORY_ALLOC |     TIDEWAYS_XHPROF_FLAGS_MEMORY_ALLOC_AS_MU ); 

The second flag will enable the allocation based profiler and store the data in the “mu” key that is already processed by all the existing UIs. This way no additional changes are necessary to your stack to get first feedback.

If you use the TIDEWAYS_XHPROF_FLAGS_MEMORY_ALLOC instead, then three additional keys are part of the XHProf payload for each parent+child function pair:

  • mem.na The sum of the number of all allocations in this function.
  • mem.nf The sum of the number of all frees in this function.
  • mem.aa The amount of allocated memory.

With our CLI tool for analying XHProf data (sorry, no binary releases yet) we can then render this data much like Latency or Memory to get a result similar to this example:

FUNCTION COUNT NUM ALLOC ALLOC AMOUNT NUM FREES
ComposerAutoloadincludeFile@1 105 130952 43371.22 KB 137090
array_change_key_case 184 22635 3291.38 KB 0
ComposerAutoloadincludeFile 114 18888 23357.67 KB 19252
ComposerAutoloadincludeFile@3 14 11328 4831.90 KB 12025
each 2686 9052 901.87 KB 0
Smarty::fetch@1 26 9033 5560.42 KB 9032
PDO::query 101 8963 2786.97 KB 1107
ComposerAutoloadincludeFile@2 19 8186 4940.82 KB 8799
ComposerAutoloadincludeFile@5 3 5557 1724.64 KB 5935
explode 624 4643 367.98 KB 55
is_readable 427 4393 146.58 KB 4408
Smarty::_smarty_include@1 5 3920 2578.56 KB 3927
OxidEsalesEshopCommunityCoreModelBaseModel::_getFieldLongName 1722 3468 152.72 KB 2876
OxidEsalesEshopCommunityCoreModelBaseModel::_setFieldData 1397 3271 183.32 KB 3207
smarty_core_load_plugins 41 3054 3904.06 KB 3055
preg_match 580 2954 234.51 KB 1227
OxidEsalesEshopCommunityCoreField::__construct 1440 2942 486.25 KB 56
file_exists 374 2637 140.97 KB 2621
main() 1 2446 1611.23 KB 2569
OxidEsalesEshopCommunityCoreStrMb::preg_replace 658 1970 91.58 KB 2108
trim 2431 1936 65.45 KB 0
OxidEsalesEshopCommunityApplicationModelCategory::_setFieldData 641 1924 90.27 KB 1968
strtolower 3056 1802 76.48 KB 0
Smarty::_smarty_include 5 1715 969.62 KB 2253
OxidEsalesEshopCommunityCoreUtils::getLangCache 1 1710 351.24 KB 1712
func_get_args 713 1431 223.44 KB 0

Please write your feedback to the Issue Tracker of the XHProf extension or directly to [email protected]

How does the PHP Realpath Cache work and how to configure it?

The realpath cache in PHP is often overlooked and its exact workings are a bit of a mystery to many developers, fueled by a lot of explanations on the web that are just plain wrong.

How exactly is the realpath cache working and at which level of PHP? There has been some buzz around realpath cache in the last weeks, so it is a hot topic to look into.

First, The PHP 7.2 changelogs contain a small note about the realpath cache size, raising the question what about the previous default in pre 7.2 deployments and if you should look into changing it:

realpath_cache_size: Set to 4096k by default

The previous default was just 16k, a 256x increase is significant. This change was also backported to PHP 7.0 and PHP 7.1 patch releases in January.

Second, the blog post “Is it all Opcache’s fault?” on the facile.it Engineering blog identifies the realpath cache as a potential problem during symlink deployments. Knowing about this issue is important when building your own deploy strategy.

Time to dive into the realpath cache, how it works and how to configure it once and for all.

If you are fluent in C you find all the answers in Zend/zend_virtual_cwd.h and Zend/zend_virtual_cwd.c files in php-src, in this blog post I am trying to explain this in plain (technical) English.

First myth buster, the realpath cache is not actually used just by the PHP realpath function. It triggers in many of the filesystem functions when using the file:// stream wrapper. The most important ones being fopen, file_get_contents, is_file, is_dir, require, require_once, include and include_once.

Whenever these functions are called, PHP makes a lookup in the realpath cache for an entry that could look like this when retrieved from realpath_cache_get():

'/var/www/wordpress/wp-includes/class-walker-category.php' =>    array (size=4)       'key' => int 6963295217931825180       'is_dir' => boolean false       'realpath' => string '/var/www/wordpress/wp-includes/class-walker-category.php' (length=56)       'expires' => int 1507388147 

You can see this cache helps with finding out the realpath, directory or file status of a file even when the accessed file is not a symlink (meaning filename and realpath are the same). It makes sense to always store entries, even if the file is not a symlink, because we can avoid a file I/O call this way.

Configuration of Realpath Cache

So how can you configure the realpath cache? Two options are available:

  1. How long is an entry stored in the cache? (realpath_cache_ttl)
  2. How many entries can be stored in the cache using a maximum number of bytes, not a maximum number of entries value. (realpath_cache_size)

The time to live defaults to 120 seconds. any change to a symlink might be invisible to a PHP process for this amount of time. A cache hit does not extend the time to live, so it doesn’t make sense to set the realpath_cache_ttl to just a few seconds even on servers under constant load. I think the default is OK and doesn’t need change.

The cache size in bytes can be confusing to understand, because the realpath cache is stored on the process level not in shared memory like Opcache.

A simple calculation explains this: Are you using PHP-FPM or Apache/mod_php with a worker size of 100? Then the realpath cache of 4096K can lead to a total cache memory size of 100 * 4 MB. The memory used by the realpath cache is not pre-allocated, that means the memory requirement is not automatically adding 4 MB for every PHP process, it will just use the memory it needs up to the maximum size.

Be careful: Because the cache does not use the PHP memory managers API, you will not see the realpath cache memory being used in memory_get_peak_usage();. You can see the current size of the realpath cache for the process currently executing with the function realpath_cache_size();.

Also, all realpath cache realted functions only work on the current process: realpath_cache_get(), realpath_cache_size() or even clearstatcache() when used with the realpath flag.

How many items can the cache hold? It depends on the length of the path begin cached. If the realpath is different than the path, then it stores two filepaths strings, otherwise just one.

For our previous wordpress entry example, we can calculate metadata size (56 bytes) plus the string length of 56 bytes = 112 bytes, which would allow the cache to hold around 37.000 items with a cache size of 4096K, but only about a hundred entries for a cache size of 16KB.

If we would use symlinks (as certain deployment strategies do, more on that in the facile.it post), then the cache can hold half the number of items, around 18500 (assuming path length is roughly the same as realpath length).

So 4096K is usually more then enough, even for applications using frameworks with a lot of files, like Symfony, Zend Framework, Laravel, Magento and so on. 16K however is much too small.

Rule of Thumb: Always have a realpath cache that can hold entries for all your files in memory. If you use symlink deployment, then make it double or triple the amount of files.

Next week I will write about realpath cache pitfalls, if you want to be in the loop you can subscribe to our newsletter on PHP performance right below.

Environments are back: Monitor and Trace Staging, QA and Dev Servers

We think monitoring and tracing should be performed across the whole application lifecycle and that it should be easy to compare changes between production, staging/QA and production environments.

But for technical reasons we had to remove explicit support for multi-environment monitoring and tracing almost 2 years ago, when our backend and worker jobs couldn’t handle the data from our original prototype data model.

Since back then we recommended to create additional applications for each environment, which was a “hack” that made comparisons between the different environments impossible and required us to introduce additional concepts such as “Staging Applications” when we introduced our new application based pricing model in March 2017.

This week we re-introduced Environments as a feature into Tideways. Every application with a “Basic” or “Standard” license can now monitor and collect data separately from the production servers into individual environments. You can have an unlimited amount of different environments and a limited amount of them active at the same time (based on the license). Using settings in the UI or a REST API you can control the active environments and integrate them into your development or continuous integration workflow easily.

Switching between environments in the UI is then as simple as using the Service/Environment switcher in the application’s main screen, see an example of that for our own Tideways Profiler application in this screenshot:

Using the “Compare Trace” functionality you can now select traces between different environments. The Trace List screen has a new filter for the environment to make the selection simple.

Over the next weeks we will add more functionality to compare environments with each other for performance degradations both manually from the UI and automatically via notifications and hooks.

See the documentation on how to get started with Environments. We recommend everyone using staging applications today, to migrate to environments.

Improved Dashboard with Preview Chart and Recent Performance Metrics

We have improved the dashboard in Tideways to include preview charts with the last 24 hours of performance data and the performance of the most recent 15 minute interval. This provides you with a much better overview of all your applications and by selecting any time range in the preview chart you can directly jump to a time-range that interests you.

New Dashboard with Preview Charts for demo organizationNew Dashboard with Preview Charts for demo organization

In this screenshot you can see the dashboard for our current set of five demo applications.

This feature is immediately available for all applications with Lite, Basic and Standard licenses (not staging applications) and for all old request-based plans of sizes “Small”, “Medium” and “Large” (not for the “Extra Small (XS)” plans).

Using php-fpm as a simple built-in async queue

There are many tasks that a web-request should not perform directly so the user doesn’t have to wait many seconds for a response. Examples include sending emails, uploading images to a CDN, resizing images or making expensive call to external services and many more depending on your use-case.

The usual advice you find on the internet is to setup a queue such as RabbitMQ, Redis, Kafka, Gearman or Beanstalkd. But this means another service that you need to install, setup, maintain and monitor. With some of the queue systems operating them includes a steep learning phase that requires time and money for additional hardware.

But maybe you just need a poor mans version of an asynchronous queue without all the overhead? Then why not just use PHP-FPM itself?

Maybe you are hosting on a platform that either runs only web-requests or makes it difficult to start and communicate with workers?

Granted, this is more of an experimental approach, but I think it is a perfectly valid way to use PHP-FPM sockets.

PHP-FPM already acts as a queue for Nginx/Apache FastCGI clients. While your web-request is running you can just send another FastCGI request to the same PHP-FPM socket asynchronously and non-blocking. This request is immediately executed in another php-fpm process in parallel and you could wait for it to complete or just fire and forget.

The good news: There is already a PHP library that support you with this task, so that you can skip learning about the FastCGI protocol, how to do asynchronous streaming in PHP and get going immediately with just a few lines of code.

The author Holger Woltersdorf has additional blog posts that combine his library with Redis or RabbitMQ, my experimental solution skips these middleman and talks directly to PHP-FPM without requiring another queue.

composer require hollodotme/fast-cgi-client:^1.0 

Then in a web-request just the client to send an async request:

<?php
// /var/www/index.php
use hollodotmeFastCGIClient;
use hollodotmeFastCGISocketConnectionsUnixDomainSocket;
use hollodotmeFastCGIRequestsPostRequest;

$connection = new UnixDomainSocket(
    '/var/run/php/php7.0-fpm.sock', 5000, 5000
);
$client = new Client($connection);
$content = http_build_query([
    'task' => 'SendMail',
    'payload' => json_encode('...')
]);

$request = new PostRequest('/var/www/worker.php', $content);
$client->sendAsyncRequest($request);

In the worker.php script we are calling, we can unpack the payload and delegate it to a handler:

<?php
// /var/www/worker.php
namespace MyProjectCommands;

class SendMailCommand {
    public function handle(array $payload) {
        // send mail here
    }
}

$class = sprintf('MyProjectCommands%sCommand', $_POST['task']);
$command = new $class(json_decode($_POST['payload'], true));
$worker->handle($command); 

Voila! We have built ourselves a simple asynchronous queue.

The code is intentionally simple, you can add a layer of abstraction around this if you want to improve the code, add error handling and more.

There are obvious downsides to this approach that I don’t want to sweep under the rug:

  • Most important, this is an experimental approach, since this is not the primary way of using php-fpm.
  • Your long(er) running worker jobs can block legimate web-requests from execution and take away resources from your webserver unless you increase simoultaneous requests per pool, setup another FPM pool for this or talk to a remote PHP-FPM.
  • Jobs are not persistent and there is no retry for failed tasks or built-in metrics for monitoring. You can however build all this into your setup yourself. For monitoring, Tideways works out of the box with this approach and can monitor response, error-rates and collect traces for you.

For simple asynchronous tasks such as sending a registration or password forgotten mail, sending requests to third party analytics services or uploading images to a CDN this approach is a nice workaround before starting with more complex queuing setups. If you are willing to live with the downsides then you already have a “perfect” queue with php-fpm.

How to optimize the PHP garbage collector usage to improve memory and performance?

The behaviour of PHP’s Garbage Collection (GC) can be a small mystery and you might wonder how it works and if you can optimize its usage for your application.

Is your script running out of memory and you are looking for ways to reduce it? PHP’s GC is a way to to reduce the memory of your script. However, arbitrarily littering your code with gc_enable(), gc_disable() and gc_collect_cycles() calls everywhere does not automatically help and obviously reduces the expressiveness of your code.

Do you know if the garbage collector is slowing down your requests or speeding them up? When does it get triggered automatically? How much memory does each run clean up?

Maybe you fear that your application is suffering from a similar inefficient garbage collection usage than Composer did three years ago, where selectively disabling the collector improved their performance by up to 90%.

Sadly, answers to these questions are not available to you from the PHP engine, unless you want to recompile with obscure debug flags. Yeah, no thank you!

Before I show you a tool to access all the necessary information, you should first understand the available optimization potential and trade-offs with PHPs GC. Because sometimes you need to enable the garbage collector and sometimes its better to disable it.

The following summary is as bare bones as it can get to understand PHP memory. Please see Anthony Ferrara’s post on “What about garbage” if you want to dive deep into this topic.

How does PHP cleanup memory?

  1. If a variable falls out of scope and is not used in any other place of the currently executed code anymore, then it is garbage collected automatically. You can force this early by using unset() to end variables scope early.
  2. If a variable is part of a cyclic reference, where A points to B and B back to A, then the variable can only be cleaned up by PHPs cycle garbage collector. It is triggered whenever 10000 possible cyclic objects or arrays are currently in memory and one of them falls out of scope. The collector is enabled by default in every request, but it can be toggled with the functions gc_enable() and gc_disable().
  3. If you call the function gc_collect_cycles(), then collection of cyclic references is triggered explicitly even if you don’t have 10000 of them in memory yet.

The optimal performance strategy is to enable the garbage collector when the GC can clean up as many possible (high efficiency, many cleanups) from the potential 10000 cyclic references and to disable it when it finds out that most of them are still used (low efficiency, few cleanups).

But how then can you find out if you need to enable or disable the garbage collection or not? As I mentioned PHP does not actually provide statistics about cleanup mechanisms 2 and 3.

The garbage_stats PHP extension

This is where our small PHP extension garbage_stats comes to the rescue. It is based on the garbage collection hook that I co-proposed with Adam Harvey for PHP 7 and up. Most of the code is extracted from our tideways extension and enhanced by a simple CLI mode to print statistics without requiring changes to your code.

Lets run garbage_stats with a version of Composer that does not disable garbage collection to simulate the previous bottleneck:

$ php -dgc_stats.enable=1 -dgc_stats.show_report=1 bin/composer update Found 157 garbage collection runs in current script.

Collected | Efficency% | Duration | Reduction% | Function ----------|------------|----------|------------|---------       796 |     7.96 % |  2.59 ms |     0.63 % | [..]::loadProviderListings         0 |     0.00 % |  0.91 ms |    -0.00 % | [..]::loadProviderListings         0 |     0.00 % | 17.19 ms |    -0.01 % | [..]::parseConstraints         0 |     0.00 % | 19.95 ms |    -0.03 % | ArrayLoader::load         0 |     0.00 % | 22.36 ms |    -0.02 % | Pool::computeWhatProvides         0 |     0.00 % | 30.40 ms |    -0.01 % | ArrayLoader::parseLinks         0 |     0.00 % | 29.05 ms |    -0.00 % | Rule2Literals::equals         0 |     0.00 % | 29.00 ms |    -0.01 % | [..]::createRule2Literals         0 |     0.00 % | 32.90 ms |    -0.09 % | RuleWatchNode::__construct         0 |     0.00 % | 35.08 ms |    -0.09 % | Solver::solve         0 |     0.00 % | 44.10 ms |    -0.09 % | makeAssertionRuleDecisions         0 |     0.00 % | 53.28 ms |    -0.01 % | Solver::runSat         0 |     0.00 % | 24.21 ms |    -0.00 % | Transaction::findUpdates       183 |     1.83 % | 31.34 ms |     0.01 % | Installer::doInstall         0 |     0.00 % | 24.10 ms |    -0.00 % | Installer::doInstall 

IEKS. The table only shows a selection of the 157 garbage collection runs, but except two of them they all collect 0 cyclic references when running and don’t reduce the memory at all while still running for 10ms and more. ICEBERG AHEAD! This is the use-case for calling gc_disable() during the whole execution of your long running script.

Contrast this with a simple test script that can efficiently clean up cyclic references with the this output:

Found 7 garbage collection runs in current script.

Collected | Efficency% | Duration | Reduction% | Function ----------|------------|----------|------------|---------         0 |     0.00 % |  0.00 ms |    -0.14 % | gc_collect_cycles     10000 |   100.00 % |  4.23 ms |    89.43 % | foo     10000 |   100.00 % |  3.32 ms |    89.40 % | foo     10000 |   100.00 % |  2.48 ms |    89.37 % | foo     10000 |   100.00 % |  5.01 ms |    89.33 % | Test::foo      9000 |    90.00 % |  2.50 ms |    79.74 % | Test::foo     10000 |   100.00 % |  3.15 ms |    81.36 % | Test::foo 

But what if your long running script does not fall into the fully efficient (~100%) or inefficient (~0%) section? The solution is simple and requires thorough work:

  1. With the garbage_stats extension, find out which section of your code is triggering the garbage collection inefficiently. In web requests you can use the function gc_stats() to get information about individual garbage collection runs.
  2. Call gc_disable() before a non GC-efficient code section is executed.
  3. Call gc_enable() after a non GC-efficient code section is finished and then gc_collect_cycles() to clean up.

Happy efficient garbage collection!

P.S.: Tideways is collecting garbage collection details across your application in production If you want to find out how garbage collection affects your user performance, sign up for a 30 days trial.

Improved Timeline Profiler with Stacktraces, Zoom and more

We just shipped a huge update to the Timeline Profiler in Tideways that improves the level of detail and usability of this core profiling feature. This is the first major step of several more updates to come in the next month.

First, we have added a clear parent-child relationship between timespans in the timeline, so that a child span is now rendered below its parent in almost all cases.

In addition we now show the list of parents of a selected span in the details panel:

Span in Tideways Timeline Profiler with a list of all its parentsSpan in Tideways Timeline Profiler with a list of all its parents

That is not the only information that we have added, with the release of PHP extension version 4.1.3 and daemon 1.5.8 we are now collecting the last 10 stack-trace frames of a span that takes longer than 50 milliseconds.

Stacktrace for a slow span in Tideways Timeline ProfilerStacktrace for a slow span in Tideways Timeline Profiler

You can configure this threshold to be lower if your application is very fast but we recommend it not be lower than 5-10% of your usual response time. We are still experimenting a little with the exact limits, but daemon and backend post-filter the amount of stacktraces depending on your plan.

The third improvement we shipped is a zoom behaviour for the timeline itself. When your trace has many small SQL or HTTP queries, potentially on various different datasources, then it helps to select just a portion of the timeline and look at the exact timings of each span more closely.

Using zoom is intuitive and uses a drag and drop behaviour to select the timeline.

Zoom into a Tideways Timeline ProfilerZoom into a Tideways Timeline Profiler

Introducing SQL and HTTP duration trace filters and advanced query language

Finding a specific trace you are interested in just got much easier with the addition of three new filtering features.

  1. You can now filter traces by duration or amount of executed SQL and HTTP queries in a trace:

    This allows you to find traces with SQL or HTTP behaviour that you are interested in.

  2. You can now view a list of the last 100 traces that you have looked at, or even filter it by additional criteria. You can find this option in the “User:” filter under the name “Last 100 viewed by me”.

  3. We have introduced a query language that allows you to filter by any field using a more complex set of operators and with the help of AND/OR conditions, to find exactly the traces you are looking for.

    You can find more details about the query language in the documentation.

In combination the response time, bottlenecks, events and service filters that we added last month, you now have much more filters at your disposal to find and fix problems in your application.

We are looking foward to your feedback and if you have ideas for new filters we are happy to hear from you, as we want to keep adding more filters in the future.