*Proof of concept to enable intermediate hook profiling on PHP7

This commit is contained in:
Derrick Hammer 2020-04-25 14:59:56 -04:00 committed by Alain Schlesser
parent 343063dcc8
commit ce8eb8987e
No known key found for this signature in database
GPG key ID: 832A67716C7B07AD
3 changed files with 179 additions and 9 deletions

12
command.php Normal file
View file

@ -0,0 +1,12 @@
<?php
if ( class_exists( 'WP_CLI' ) ) {
require_once dirname( __FILE__ ) . '/inc/class-command.php';
require_once dirname( __FILE__ ) . '/inc/class-formatter.php';
require_once dirname( __FILE__ ) . '/inc/class-logger.php';
require_once dirname( __FILE__ ) . '/inc/class-profiler.php';
if ( version_compare( PHP_VERSION, '7.0.0' ) >= 0 ) {
require_once dirname( __FILE__ ) . '/inc/class-filestreamwrapper.php';
}
WP_CLI::add_command( 'profile', 'runcommand\Profile\Command' );
}

View file

@ -0,0 +1,150 @@
<?php
namespace runcommand\Profile;
/**
* Stream Wrapper Class to create a temporary file with ticks enabled
* Props to https://github.com/hakre for the original P.O.C.
*
* Class FileStreamWrapper
*
* @package runcommand\Profile
* @author Derrick Hammer
*/
class FileStreamWrapper {
/**
* @var string
*/
const PROTOCOL = 'file';
/**
* @var string
*/
const PHP_TICK = "\ndeclare(ticks=1);\n";
/**
* @var resource
*/
public $context;
/**
* @var resource
*/
private $handle;
/**
* @var string
*/
private $file;
public function stream_open( $path, $mode, $options, &$opened_path ) {
if ( isset( $this->handle ) ) {
throw new \UnexpectedValueException( 'Handle congruency' );
}
$use_include_path = true;
$context = $this->context;
if ( null === $context ) {
$context = stream_context_get_default();
}
self::restore();
$data = @file_get_contents( $path );
if ( false !== $data && preg_match( '~^(<\?php\s*)$~m', $data ) ) {
$result = preg_replace(
'~^(<\?php\s*)$~m',
'\\0' . self::PHP_TICK,
$data,
1
);
$pathinfo = pathinfo( $path );
$this->file = $pathinfo['dirname'] . DIRECTORY_SEPARATOR . $pathinfo['filename'] . '_profile.' . $pathinfo['extension'];
file_put_contents( $this->file, $result );
$handle = @fopen( $this->file, $mode, $use_include_path, $context );
} else {
$handle = @fopen( $path, $mode, $use_include_path, $context );
}
self::init();
if ( false === $handle ) {
return false;
}
$meta = stream_get_meta_data( $handle );
if ( ! isset( $meta['uri'] ) ) {
throw new \UnexpectedValueException( 'Uri not in meta data' );
}
$opened_path = $meta['uri'];
$this->handle = $handle;
if ( $this->file ) {
register_shutdown_function( [ $this, 'cleanup' ] );
}
return true;
}
public static function restore() {
$result = stream_wrapper_restore( self::PROTOCOL );
if ( false === $result ) {
throw new \UnexpectedValueException( 'Failed to restore' );
}
}
public static function init() {
$result = stream_wrapper_unregister( self::PROTOCOL );
if ( false === $result ) {
throw new \UnexpectedValueException( 'Failed to unregister' );
}
stream_wrapper_register( self::PROTOCOL, '\runcommand\Profile\FileStreamWrapper', 0 );
}
/**
* @return array
*/
public function stream_stat() {
self::restore();
$array = @fstat( $this->handle );
self::init();
return $array;
}
/**
* @param $count
*
* @return string
*/
public function stream_read( $count ) {
self::restore();
$result = fread( $this->handle, $count );
self::init();
return $result;
}
public function stream_eof() {
self::restore();
$result = @feof( $this->handle );
self::init();
return $result;
}
public function stream_set_option( $option, $arg1, $arg2 ) {
return true;
}
public function url_stat( $path, $flags ) {
self::restore();
$array = @stat( $path );
self::init();
return $array;
}
public function cleanup() {
@unlink( $this->file );
}
}

View file

@ -47,6 +47,7 @@ class Profiler {
private $tick_query_offset = null;
private $tick_cache_hit_offset = null;
private $tick_cache_miss_offset = null;
private $is_php7 = false;
public function __construct( $type, $focus ) {
$this->type = $type;
@ -99,6 +100,9 @@ class Profiler {
}
}
);
$this->is_php7 = version_compare( PHP_VERSION, '7.0.0' ) >= 0;
if ( 'hook' === $this->type
&& ':before' === substr( $this->focus, -7, 7 ) ) {
$stage_hooks = array();
@ -131,10 +135,6 @@ class Profiler {
*/
public function wp_tick_profile_begin( $value = null ) {
if ( version_compare( PHP_VERSION, '7.0.0' ) >= 0 ) {
WP_CLI::error( 'Profiling intermediate hooks is broken in PHP 7, see https://bugs.php.net/bug.php?id=72966' );
}
// Disable opcode optimizers. These "optimize" calls out of the stack
// and hide calls from the tick handler and backtraces.
// Copied from P3 Profiler
@ -157,8 +157,13 @@ class Profiler {
// WordPress.PHP.NoSilencedErrors.Discouraged -- ini_set can be disabled on server.
}
register_tick_function( array( $this, 'handle_function_tick' ) );
declare( ticks = 1 );
declare( ticks=1 );
if ( $this->is_php7 ) {
register_tick_function( array( $this, 'handle_function_tick' ) );
FileStreamWrapper::init();
}
return $value;
}
@ -167,6 +172,7 @@ class Profiler {
*/
public function wp_tick_profile_end( $value = null ) {
unregister_tick_function( array( $this, 'handle_function_tick' ) );
FileStreamWrapper::restore();
$this->tick_callback = null;
return $value;
}
@ -340,8 +346,9 @@ class Profiler {
$location = '';
$callback = '';
if ( in_array( strtolower( $frame['function'] ), array( 'include', 'require', 'include_once', 'require_once' ), true ) ) {
$callback = $frame['function'] . " '" . $frame['args'][0] . "'";
if ( in_array( strtolower( $frame['function'] ), [ 'include', 'require', 'include_once', 'require_once' ] ) ) {
$ext = pathinfo( $frame['args'][0] , PATHINFO_EXTENSION);
$callback = $frame['function'] . " '" . str_replace( "_profile.{$ext}" , ".{$ext}", $frame['args'][0]) . "'";
} elseif ( isset( $frame['object'] ) && method_exists( $frame['object'], $frame['function'] ) ) {
$callback = get_class( $frame['object'] ) . '->' . $frame['function'] . '()';
} elseif ( isset( $frame['class'] ) && method_exists( $frame['class'], $frame['function'] ) ) {
@ -358,7 +365,8 @@ class Profiler {
}
if ( isset( $frame['file'] ) ) {
$location = $frame['file'];
$ext = pathinfo( $frame['file'], PATHINFO_EXTENSION );
$location = str_replace( "_profile.{$ext}", ".{$ext}", $frame['args'][0] );
if ( isset( $frame['line'] ) ) {
$location .= ':' . $frame['line'];
}