XHProf is mired in maintainer issues, with multiple forks, and the small code base doesn't provide much value for what we want to do.
PHP 7.1+ provides EG(vm_interrupt), a global variable which, when set to 1, will cause the VM to call the hook zend_interrupt_function fairly promptly. This is now used for timeout handling and pcntl_signal, allowing userspace signal handlers to be called safely, soon after a signal is received.
My proposal is to use this to implement a new profiler for PHP 7.1+, modelled on the LuaSandbox profiler. Like LuaSandbox, it will only support timer_create() with SIGEV_THREAD.
$excimer = new ExcimerProfiler; $excimer->setPeriod( 0.1 /*seconds*/ ); $excimer->setEventType( EXCIMER_CPU ); // The flush callback is automatically called on destruct, or if the specified number of samples were collected $excimer->setFlushCallback( 'flushCallback', 1000 ); $excimer->start(); $excimer->stop(); $log = $excimer->getLog(); function flushCallback( $log ) { // Write file in FlameGraph format with semicolon-separated stacks file_put_contents( 'profile.folded', $log->formatCollapsed(), FILE_APPEND ); } // Using an iterable object for the log avoids the overhead of constructing a PHP array // Because the profiler is request-local, we can use opline/oparray pointers to store logs compactly foreach ( $log as $entry ) { // Storing a timestamp in the log entry allows a couple of interesting applications: // - Percentage of busy versus halted CPU time // - Browser-style time-series flame chart print $entry->getTimestamp() . "\n"; // Trace array format will be similar to Exception::getTrace() foreach ( $entry->getTrace() ) { ... } }
Currently in Xenon, the sampling interval is 600 seconds, which is much larger than the average request time. This could be supported in Excimer by having Excimer stagger the time at which the first event occurs randomly over the period. For example, if the period was 10 seconds, the first event would occur after mt_rand(0, 10000) milliseconds, then the next event would occur exactly 10 seconds after that. So the timer would most often be disarmed at the end of the request without having fired. The flush callback would not be called if the log is empty.
Using a non-static, non-global profiler means that it will be possible to run two profilers at once. We could have one profiler running which records CPU time, and another which records wall clock time, and generate flame graphs for both.
Once we have the relevant infrastructure for timer_create() handling, we could easily provide a generic timer facility to the userspace:
$timer = new ExcimerTimer; $timer->setEventType( EXCIMER_CPU ); $timer->setInterval( 1 ); // one-shot mode $timer->setPeriod( 1 ); // periodic mode $timer->setCallback( $callback ); $timer->start(); $timer->stop(); $timer->getTime(); // timer_gettime() wrapper $timer->getOverrun(); // timer_getoverrun() wrapper
Interestingly, such a facility would probably be able to throw exceptions safely from within the callback, providing a means to abort long-running functions (such as parsing) before the PHP request timeout is hit.