Feature Focus: “Metrics Revolutions”, Episode 3

By Jérôme Vieilledent, on Jul 16, 2020

Blog post series index:

The Metrics, Episode 1
Metrics Reloaded, Episode 2
Metrics Revolutions, Episode 3 (you are here)
Metrics Resurrections, Episode 4

In the previous episodes we saw how metrics can be your best friend, and how you can leverage them to write regression tests.

Almost 600 metrics are provided as built-in, but this is often not enough. Indeed, we cannot create metrics pertaining to your code on your behalf:

  • only you have the knowledge of your codebase, its purpose and architecture;
  • only you know the “business” objectives of your app, and can set a performance budget accordingly.

Note: your code is anyhow not shared with Blackfire.

The good news is that you can create custom metrics, based on your own business logic, and use them in your tests. This constitutes one of the most powerful features of Blackfire, available in all supported languages.

Defining Custom Metrics

There are many reasons you may want to define custom metrics. The first one was teased in the previous episode: What if there is no metrics for what I want to test?

Let’s consider the following PHP code:

namespace App\Utils;

class Greetings implements GreetingsInterface
{
    public function phrase(string $greeting, string $extra): string
    {
        return $this->complicatedGreetingsAlgorithm($greeting, $extra);
    }

    private function complicatedGreetingsAlgorithm(string $greeting, string $extra)
    {
        usleep(500000);
        
        return sprintf('%s %s', $greeting, $extra);
    }
}

Let’s assume that, thanks to Blackfire, we discovered that calling Greetings::phrase() has a significant performance impact. We couldn’t find any existing metrics to rely on, so we decided to define a custom metric.

The whole point of a metric is to gather cost information from one or several method calls. The cost information actually reflects the dimensions available in Blackfire:

  • Wall Time;
  • I/O time;
  • CPU time;
  • Memory / Peak memory;
  • Network In / Network Out.

In addition to these dimensions, any metric can also gather the number of times it has been called, adding a count dimension. By default, cost and count are combined for all metrics.

Our metric, defined in the .blackfire.yaml file at the root of our codebase, could look like the following:

metrics:
    greetings:
        matching_calls:
            php:
                - callee: "=App\\Utils\\Greetings::phrase"

I know, I could rely on complicatedGreetingsAlgorithm() function, but it is private. That would not be compliant to the Open-Closed Principle of SOLID!

The good thing of such metrics is that you can group several callees under the same metric, and even filter out by caller, e.g.:

metrics:
    greetings:
        matching_calls:
            php:
                - callee: "=App\\Utils\\Greetings::phrase"
                - callee: "=App\\Utils\\Salutation::hello"
                  caller: "=App\\Controller\\SomeController::index"

Using the YAML code above, Salutation::hello() call will be part of the greetings metric, but only if it is called by SomeController::index()

Metrics For the Timeline View

Another good reason to define custom metrics is to emphasize part of our code in the Timeline View. To do so, we need to add timeline: true to your metric, and a descriptive label to make it more understandable:

metrics:
    greetings:
        label: Greetings phrases
        timeline: true
        matching_calls:
            php:
                - callee: "=App\\Utils\\Greetings::phrase"
Blackfire metrics for the Timeline View

Flexibility of the Selector

Did you notice that our Greetings class implemented an interface? It would be nice if any class implementing the same interface could be caught within the same metric, wouldn’t it? Well, the metrics system actually provides a collection of selectors, including one matching several instances of a class/interface.

The selector is the character used in front of (or surrounding) the function.

metrics:
    greetings:
        matching_calls:
            php:
                - callee: "|App\\Utils\\GreetingsInterface::phrase"

In the example above, all calls to phrase() function, from any implementors of GreetingsInterface will be caught in the greetings metric. This way we follow the Liskov Substitution Principle 😉.

Capturing Argument Values

Another powerful feature offered by custom metrics is the ability to capture argument values. This can be critical as it makes it possible to differentiate function calls by argument. By capturing arguments, different nodes can be created for the same function in the call-graph, when the passed arguments are different.

A good built-in example is the “database aware” function calls, which are discriminated by SQL queries. In this case, SQL queries are actually arguments of a PDOStatement::execute() call, for example.

Let’s consider our greetings metric:

metrics:
    greetings:
        matching_calls:
            php:
                - callee:
                        selector: "|App\\Utils\\GreetingsInterface::phrase"
                        argument:
                            1: "*"

Using this snippet, all calls to Greetings::phrase() will be discriminated by the first argument, regardless the value.

Going Further with Argument Capturing

It is also possible to go even further with argument capturing as you can:

  • Capture multiple arguments;
  • Use selectors to filter out arguments;
  • If a captured argument is a hash map, you can use its keys (very useful for config values in WordPress or Drupal for example)

Conclusion

Reading this series of articles, we unveiled the great power of metrics, and the level of control they provide, not only for your tests, but also for a better analysis and understanding of your applications.

With metrics, you get the ability to fine-tune how Blackfire renders a profile, and, as a consequence, more visibility on the behavior of you applications.

Writing metrics and assertions can be done with our Premium and Enterprise subscriptions.

Play with the demo or subscribe now!

Happy Profiling!

Jérôme Vieilledent

As a Developer Advocate, Jérôme is all about spreading the love! His technical/development background enable him to talk as a peer to peers with our developer users. You’ll read his tips and advices on performance management with Blackfire. And he’ll support you as a day-to-day user.