Feature Focus: “Metrics Revolutions”, Episode 3
Custom metrics are one of the most powerful features of Blackfire, giving you the ability to fine-tune how your profiles are rendered.
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"
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!