PHP 8: inline surveillance

Over the past two decades, the Zend Engine virtual machine at the heart of PHP has undergone some improvements. A significant performance jump took place with the release of PHP 7, which saw significant performance gains for WordPress and other traditional web applications. In version PHP 8, in turn, a JIT compiler appeared, which significantly accelerated the execution of resource-intensive algorithms.

However, the primary observation trap used by tracers, profilers, and debuggers to observe the behavior of PHP function calls did not evolve in parallel with the Zend Engine improvements and increasingly slowed down the observed PHP applications. For example, Datadog PHP Tracer just couldn’t evolve with PHP 8 innovations without changing the watchdog trap.

The PHP 8 release includes changes that bring modern surveillance to the PHP runtime. Our team, together with the PHP engine community, has developed and released a new Observer API… Before the release of this API, we, like other PHP developers, faced many problems.

To better understand how watch hooks change and restrict the development process, let’s look at the watch scopes limitations in releases prior to PHP 8.

Observation situation in versions prior to PHP 8

VM execution trap

The PHP virtual machine (VM) is responsible for supporting PHP execution. The VM has a pointer named zend_execute_exwhich can be overridden by extensions. If any extension uses this trap, all calls to functions or methods defined in PHP are made only through it.

While this trap was one of the most popular function call traps before PHP 8, it had several disadvantages:

  • The call stack in PHP was almost unlimited. However, if any extension intercepts zend_execute_ex, all PHP function calls will end up on the built-in C stack, which is limited by the parameter value ulimit -s… As a result, a stack overflow and an abnormal termination of the PHP process can occur.

  • If the extension intercepts zend_execute_ex, calls to all user-defined functions (PHP) are intercepted, not just those that the extension should watch for. This leads to increased processing overhead, especially if the PHP script calls many functions.

  • The compiler generates optimized function call opcodes distinguishing between user-defined function (PHP) calls (DO_UCALL) and calls to internal functions (DO_ICALL), but if the extension intercepts zend_execute_ex, the compiler cannot do this.

  • The trap requires extensions to redirect it to neighboring extensions that might also need it. This can lead to a noisy neighbor problem, and if the trap is misdirected, it can lead to unexpected behavior and even crash.

  • The trap is incompatible with the new JIT compiler in PHP 8.

Considering all the side effects of interception zend_execute_ex, extension developers have to look for other engine pitfalls to implement surveillance.

Special opcode handlers

PHP extensions can provide custom handlers for existing opcodes. Special opcode handlers associated with function calls allow you to implement observation without “exploding the stack” in the zend_execute_ex trap.

However, special opcode handlers, like a virtual machine execution trap, have a number of disadvantages:

  • Special opcode handlers must call adjacent extension opcode handlers for the same hook. This is likely due to a lack of documentation or that two extensions that use a hook for the same opcode are rare, but historically many extensions have not redirected opcode handlers to adjacent extensions.

  • Special opcode handlers can change the state of a VM at runtime. For example, extensions can prevent VMs from calling the standard opcode handler. (This is how the Xdebug scream function.) If an extension must skip an opcode handler, it is not possible to reliably forward the trap to a neighboring extension. As a result, ambiguity arises: two extensions provide a special handler for the same opcode, but one of the extensions changes the state of the VM.

  • Due to the way the generators are implemented, they cannot be fully covered by special opcode handlers.

  • The trap is incompatible with the new JIT compiler in PHP 8.

Due to the difficulty of targeting opcode with special handlers, some profiling extensions use the Zend Extension approach.

The Zend Extension Trap

PHP extensions can be registered as a special extension called Zend Extension… This special extension can access parts of the engine that are not available to regular extensions, including access to the start and end handlers of function calls.

The main disadvantage of this method is that the compiler has to generate two additional opcodes for each function call: one for the call start handler (EXT_FCALL_BEGIN) and another for the end of call handler (EXT_FCALL_END). These additional opcodes greatly degrade performance and are not suitable for a production tracer.

Initial considerations

Due to the shortcomings of the aforementioned engine hooks, some developers suggest other ways to intercept function calls. For example, was created and confirmed tracer conceptwhich inserts nodes into an abstract syntax tree (AST) at compile time. These nodes call monitoring functions. Our team has created concept tracer based on this idea of ​​inserting nodes into AST. However, we do not know of any ready-to-use tracers that use this approach at this time, probably because inserting watch hooks before and after each function call increases the overhead just as much as with Zend Extension.

Due to the low efficiency of the engine trap, which leads to a drop in performance and deterioration of the behavior and stability of PHP, as well as due to the imminent release of a new JIT compiler, there is a serious risk that the observation traps will stop working altogether in PHP 8. This uncertainty forced us to contact the community PHP and outline your preliminary considerations on tracing traps for PHP in October 2019… In turn, this led to a discussion of embedding telemetry traps directly into the Zend Engine, which would lead to tangible improvements over existing surveillance tools. This is how we came up with the idea of ​​creating a new Observer API in PHP 8.

Observer API in PHP 8

Initially, it was important for us that the changes in the field of observation would improve not only the native PHP tracer (Datadog), but also the entire ecosystem of PHP extensions, including a wide range of tracers, profilers and debuggers.

Following our call to the PHP engine community, several enthusiasts have responded to this surveillance initiative. Benjamin Eberlei, Nikita Popov and Dmitry Stogov provided invaluable assistance to our team in implementing the observer API and reviewed a large amount of its code. Joe Watkins, Bob Weinand and several other members of the PHP engine community have also provided valuable input and advice.

Observer API is intended to eliminate all of the above mentioned side effects of function call traps. We solved all the problems one by one.

No more stack limits

Using the observer API, extensions can support a nearly unlimited call stack. During the startup phase of a process (also known as module initialization, or MINIT) an extension can register itself as a function call observer with zend_observer_fcall_register… This means that the Zend Engine should use special observer handlers for call-related opcodes and completely eliminates the artificial stack constraint that occurs when overriding zend_execute_ex.

Performance improvements for unobservable queries. With an observer extension, the compiler can generate observer-specific opcodes such as OBSERVE_DO_UCALL and OBSERVE_DO_ICALL… The problem with this approach is that it complicates the compiler and adds many (namely eight) new opcodes that debug extensions must take into account.

An alternative for the multi-opcode approach is opcode specialization. When specializing an opcode, multiple handlers can be associated with a single opcode. In this case, the compiler itself determines which handler should be run for the special opcode.

One example of an opcode specialization is SPEC(RETVAL) with two variants of the handler for the same opcode, when one handler is started when a value is returned, and the other when it is used. The example below illustrates this concept in action:

$a = return_something();

The compiler generates the opcode DO_UCALL for each of the function calls. But since DO_UCALL has opcode specialization SPEC(RETVAL), then two separate handlers are actually run at runtime.

Browser API Introduces Opcode Specialization SPEC(OBSERVER) for opcodes related to function calls. With an observer extension, all opcodes associated with function calls will trigger special observer handlers (for example, ZEND_RETURN_SPEC_OBSERVER_HANDLER for opcode RETURN). Because handlers are assigned at compile time, this method avoids degradation in performance of handling unobservable queries.

Target functions and methods

In addition, the observer API can only work with functions and methods defined in PHP, which are known as user-defined functions, if necessary. The new design allows each extension to monitor only the functions it needs, rather than all the calls in a row.

The diagram below shows how a watcher extension observes a function foo() in the following PHP script:


# example.php

function foo() {}
function bar() {}

for ($x = 0; $x < 2; $x++) {

The observer API does not observe internal functions (that is, functions from the standard library or from an extension). In general, only some of the internal functions are of interest for extensions. Internal functions that handle I / O are often of interest to tracers, and mechanisms already exist to monitor these functions through special internal function handlers.

Observing with the new JIT compiler

The new JIT compiler in PHP 8 makes it harder to intercept function calls by tracers, profilers, and debuggers without side effects. In the working proposal JIT RFC It says explicitly about the need to create a tracing API that works with the JIT compiler:

“Runtime profiling should work even with JIT code, but it might require the development of an additional tracing API and a corresponding JIT extension to generate traceback calls.”

With the support of JIT author and longtime PHP engine developer Dmitry Stogov, the Observer API is able to fulfill this requirement and provide observation, even if JIT in PHP 8 is enabled

Protection from a “noisy neighbor”

All hook support is now handled by the engine, so observer extensions no longer need to worry about redirecting hooks to neighboring extensions. This allows multiple routers, profilers, and debuggers to run in parallel, avoiding the long-standing noisy neighbor problem that plagued old pitfalls.


Historically, the various function call hooks in the Zend Engine have lagged behind the development of the engine, leading to side effects when observing. The Observer API in PHP 8 not only neutralizes the main side effects of intercepting function calls, but also introduces a more general concept to the engine – observation.

Now the future development of surveillance in PHP will take place under the auspices of ZEND_OBSERVER… This API has already included functions bug tracking, and in the future many other parts of the engine, for example, the stages of compilation, will become open for observation.

If you would like to participate in the further development of observation in PHP, please share your ideas with PHP engine development mailing list… In addition, we invite everyone to contribute to the development of an open source Datadog PHP Tracerwhich has been used in the Observer API since version 0.52.0. And to get serious about developing extensions and opcodes for PHP, send your resume to the APM integration team. Datadog is looking for employees!

The translation of the material was prepared as part of the course “PHP Developer. Professional”… If you are interested in learning more about the course, we invite you to Open Day online, where the teacher will tell in more detail about the training format, program and prospects for graduates.

Similar Posts

Leave a Reply