In collaboration with Iurii Golikov
As a combination of basic edible components is able to form the most delicious gourmet meal, we are going to leverage two different fish from the infinite ocean of programming world to achieve something great: AOP and application instrumentation. By the way, this feature of a system is called emergence — an ability of a system to have properties which its parts do not have on their own. Literally the closest example is the device you read this article with, its parts separately (memory, cpu, display, etc.) can not display an article, but the whole system together can.
Let’s start with Aspect Oriented Programming.
This concept is widely adopted in other languages like Java (see AspectJ), but has not gained popularity it deserves in PHP world yet. In two words, this paradigm aims to extract and incapsulate (hello, OOP) crosscutting functionality. Let’s explore what it is by example:
For createNewUser method, all the important things have borders between lines 13–17. Everything else (auth, logging, etc.) is also important, but not relevant for createNewUser’s mission specifically:
All such things together are called crosscutting functionality. Can you imagine what is going to happen in another methods, classes, modules of this app? Yes, logging will be everywhere, access control will be everywhere, and so on. This functionality crosscuts the codebase indeed, breaking the order we are trying to introduce.
AOP is here to help!
With AOP, crosscutting functionality can be extracted away and planted into a separate namespace to exist individually.
First, create an Aspect. Aspect is how an “extracted” piece of crosscutting concern is called in AOP world. This is our BankingAspect — a class which incapsulates the desired functionality:
Pay attention to the “@before” annotation — it defines a pointcut — concrete moment of the app execution flow when the aspect is called. Here, the aspect is called before every public method of Bank class with any arguments, if the method name matches to /*Money/ regular expression.There are many pointcuts available, including method execution (Before, After and Around) with intuitive DSL on top of it, property access, function calls and more. All the details are here in the official doc.
Here, the aspect just outputs the being called method name and arguments, but not only. In fact, it can change argument list (this is exactly what happens on lines 23–30), modify return value or even skip the method execution at all.
Second, create an utility class ApplicationAspectKernel and register the aspect there:
Third, instantiate the Aspect Kernel at the very beginning of your app, so it can do its job before your classes are loaded via composer:
Here is how our Bank looks like:
And here is the implementation of salary day: money is deducted from company’s bank account and hits the accounts of noble employees, including Developer:
When salary_day.php script is run without Aspect, the output is the following:
Then, we do not touch a single line of the salary distribution algorythm, and just enable the aspect. The result is now influenced by BankingAspect described above:
Now we see the log before every method call. Moreover, and more importantly, Developer got 20€ extra bonus for “unknown” reason, see responsible lines in the aspect code.
That’s it! It works. Through this example app we can notice that AOP knowledge literally pays out at some point. You can find the fully functional example here on Github.
What else can be easily done as a solid, self-contained aspect?
Possibilities are endless. Some examples:
- Caching — saves a return value of any function and caches it somewhere. All subsequent requests will just hit the aspect and get the cached value, so original method will not be even called. Demo implementation can be found here.
- Access Control — tired of managing roles and permissions in every controller again and again? Forgot to add if ($_SERVER[‘REMOTE_ADDR’] == ‘127.0.0.1’) somewhere? You can manage Access Control in a centralized, extensible way — as an aspect.
- Profiling — collect, aggregate and store any runtime information about your code without even touching it! Application metrics like memory, execution time, traffic and CPU, grouped by namespace, class, method or any combination of those — ideal nut to crack with AOP.
- Analytics — more complicated metrics can be collected using AOP as well. Interested what is your real requests / second rates against each third-party paid API you use? Collect and aggregate this information using an aspect, so you come prepared to the discussion about those far too big numbers in your bills. Or maybe you are curious what is so slow about the db sometimes? Put an aspect to collect your timings of your database queries grouped by query, API endpoint or whatever you need. More on that in the next chapter, stay tuned.
Some arguments against using AOP in PHP exist.
As a result of applying AOP, the codebase becomes more readable and maintainable, respecting Don’t Repeat Yourself and Single Responsibility lofty ideals. However, there are some typical complaints about it.
It seems like a “black magic” to me.
Well, aspects are run before original code, after it or even instead of it, and zero changes are required for the original code — it sounds a bit magical for sure. But how can it be black if you know exactly how it works, it is plain PHP, and even debuggable with Xdebug? Therefore, it is rather white, not black for me. And production ready.
Hooking into PHP execution flow on the fly, with a powerful regexp-like DSL inside annotations? It has to be extremely slow under production load!
That sounds like a serious potential problem. Bad performance destroys chances of any tool to be used on production.
Weaving means implanting the aspect’s code inside the original code. In theory, Go!AOP does weaving just once. When the PHP class is being autoloaded, Go!AOP hooks into the composer autoloading process with stream_filter_register function. Therefore, “implanted” version of effected class is saved in Opcache and is served from there for all subsequent requests. All the job is done single time per deploy. That’s how zero production overhead is achivable. Let’s prove that it is true in practice.
Performance test of Go!AOP framework.
There are two scripts involved:
- test_original.php — represents classic banking system, AOP is not used.
- test_modern.php — represents modern banking system, which uses AOP to operate funds.
Functionality-wise, these two scripts are absolutely equal. The only difference between them is that the actual math is done inside Bank classes in the first case, and by an aspect in the second case. There are 100 “aspected” classes created for the test, which is relatively big number.
Keep in mind that debug option of AspectKernel must be set to false during the test run, and to true during development, otherwise the results are incorrect. In debug mode it is just super slow.
Under the above circumstances, Go!AOP introduces ≈6ms overhead of execution time, and ≈ 1.7Mbytes overhead of memory, which sounds good enough:
If we increase the amount of “aspected” classes to sky-high number of 1000 classes (benchmark script on github), then performance also degrades absolutely proportionally:
Each class means 80 method calls in the current test setup. So, our benchmarks demonstrate the following:
- 100 classses = 8000 method calls ≈6ms overhead
- 1000 classes = 80000 method calls ≈60ms overhead
Applying some trivial math, we can conclude that performance overhead of Go!AOP framework is approximately 6ms / 8000 = 750ns per aspect’s method call. The ns means nanoseconds, which is one billionth of second. Digitalocean’s 1Gb 1cpu droplet is used for the benchmark.
Let’s create another benchmark to find out resource consumption of empty method call in PHP:
According to this benchmark, empty method call overhead in PHP, given the same test environment, is around 54ns:
Of course, aspect call is not empty, it does useful things, which makes it reasonably slower than empty method call. Most probably every non-empty method call should be slower than an empty one anyway 🙂
What about typical web application in the wild? We created another benchmark, which is much closer to real life — it has dockerized nginx & php-fpm stack and uses ab testing agent for the run. We are interested in Time per request row of the ab output:
Surprisingly, this new benchmark clearly proves previous results! The overhead is 1435ms–1361ms=74ms, which is practically the same as 60ms we got earlier for plain PHP benchmark script. It seems like we are good to go with AOP in PHP performance-wise!
Please, share in comments if the benchmark results are different on your environment.
We are done with the first ingredient for now. See you in the next article of the series, where we will talk about Application Instrumentation and apply well-known AOP techniques on top!