Push current changes

This commit is contained in:
Alain Schlesser 2021-10-25 09:10:18 +02:00
parent f248ed3625
commit 552276d06e
No known key found for this signature in database
GPG key ID: C2188A5B63449075
17 changed files with 1106 additions and 195 deletions

View file

@ -23,20 +23,6 @@ Feature: Profile a specific hook
| smilies_init() | 2 | 0 |
| feed_links() | 8 | 0 |
@less-than-php-7 @require-wp-4.0
Scenario: Profile an intermediate stage hook
Given a WP install
When I run `wp profile hook wp_head:before --fields=callback,cache_hits,cache_misses`
Then STDOUT should be a table containing rows:
| callback | cache_hits | cache_misses |
| locate_template() | 0 | 0 |
| load_template() | 0 | 0 |
And STDOUT should not contain:
"""
WP_CLI\Profile\Profiler->wp_tick_profile_begin()
"""
@require-wp-4.0
Scenario: Profile a hook before the template is loaded
Given a WP install
@ -129,43 +115,3 @@ Feature: Profile a specific hook
"""
Warning: Called 1
"""
@less-than-php-7 @require-wp-4.0
Scenario: Profile the mu_plugins:before hook
Given a WP install
And a wp-content/mu-plugins/awesome-file.php file:
"""
<?php
function awesome_func() {
// does nothing
}
awesome_func();
"""
When I run `wp profile hook muplugins_loaded:before --fields=callback`
Then STDOUT should contain:
"""
wp-content/mu-plugins/awesome-file.php
"""
@less-than-php-7 @require-wp-4.0
Scenario: Profile the :after hooks
Given a WP install
When I run `wp profile hook wp_loaded:after`
Then STDOUT should contain:
"""
do_action()
"""
When I run `wp profile hook wp:after`
Then STDOUT should contain:
"""
do_action_ref_array()
"""
When I run `wp profile hook wp_footer:after`
Then STDOUT should contain:
"""
do_action()
"""

65
src/Collector.php Normal file
View file

@ -0,0 +1,65 @@
<?php
namespace WP_CLI\Profile;
/**
* The collector is responsible for collect a specific subset of data across the
* requested scope.
*/
interface Collector {
/**
* Register the collector with the WordPress lifecycle.
*
* @param Scope $scope Scope for which to collect the data.
*
* @return void
*/
public function register( Scope $scope );
/**
* Start the collection for a given hook.
*
* @param string $hook Hook to start the collection for.
*
* @return void
*/
public function start_hook( $hook );
/**
* Stop the collection for a given hook.
*
* @param string $hook Hook to stop the collection for.
*
* @return void
*/
public function stop_hook( $hook );
/**
* Start the collection for a given hook and callback.
*
* @param string $hook Hook to start the collection for.
* @param callable $callback Callback to start the collection for.
* @param int $index Index of the callback.
*
* @return void
*/
public function start_callback( $hook, $callback, $index );
/**
* Stop the collection for a given hook and callback.
*
* @param string $hook Hook to start the collection for.
* @param callable $callback Callback to start the collection for.
* @param int $index Index of the callback.
*
* @return void
*/
public function stop_callback( $hook, $callback, $index );
/**
* Report the collected data.
*
* @return Report
*/
public function report();
}

View file

@ -0,0 +1,100 @@
<?php
namespace WP_CLI\Profile\Collector;
use RuntimeException;
use WP_CLI\Profile\Collector;
use WP_CLI\Profile\Report;
use WP_CLI\Profile\Scope;
/**
* Collect database queries for the requested scope.
*/
final class DatabaseQueries implements Collector {
/**
* Internal storage for collected data.
*
* @var array
*/
private $data = [];
/**
* Register the collector with the WordPress lifecycle.
*
* @param Scope $scope Scope for which to collect the data.
*
* @return void
*/
public function register( Scope $scope ) {
switch ( $scope->get_type() ) {
case Scope::TYPE_ALL_HOOKS:
break;
case Scope::TYPE_HOOK:
break;
case Scope::TYPE_STAGE:
break;
default:
throw new RuntimeException(
"Trying to register unsupported scope {$scope->get_type()} in DatabaseQueries collector"
);
}
}
/**
* Start the collection for a given hook.
*
* @param string $hook Hook to start the collection for.
*
* @return void
*/
public function start_hook( $hook ) {
}
/**
* Stop the collection for a given hook.
*
* @param string $hook Hook to stop the collection for.
*
* @return void
*/
public function stop_hook( $hook ) {
}
/**
* Start the collection for a given hook and callback.
*
* @param string $hook Hook to start the collection for.
* @param callable $callback Callback to start the collection for.
* @param int $index Index of the callback.
*
* @return void
*/
public function start_callback( $hook, $callback, $index ) {
}
/**
* Stop the collection for a given hook and callback.
*
* @param string $hook Hook to start the collection for.
* @param callable $callback Callback to start the collection for.
* @param int $index Index of the callback.
*
* @return void
*/
public function stop_callback( $hook, $callback, $index ) {
}
/**
* Report the collected data.
*
* @return Report
*/
public function report() {
return new Report\DatabaseQueries( $this->data );
}
}

View file

@ -0,0 +1,193 @@
<?php
namespace WP_CLI\Profile\Collector;
use RuntimeException;
use WP_CLI;
use WP_CLI\Profile\Collector;
use WP_CLI\Profile\Report;
use WP_CLI\Profile\Scope;
/**
* Collect execution time for the requested scope.
*/
final class ExecutionTime implements Collector {
/**
* Internal storage for collected data.
*
* @var array
*/
private $data = [
'hook' => [],
];
/**
* Previous hook that was collected.
*
* @var string
*/
private $previous_hook;
/**
* Register the collector with the WordPress lifecycle.
*
* @param Scope $scope Scope for which to collect the data.
*
* @return void
*/
public function register( Scope $scope ) {
switch ( $scope->get_type() ) {
case Scope::TYPE_ALL_HOOKS:
break;
case Scope::TYPE_HOOK:
break;
case Scope::TYPE_STAGE:
break;
default:
throw new RuntimeException(
"Trying to register unsupported scope {$scope->get_type()} in ExecutionTime collector"
);
}
}
/**
* Start the collection for a given hook.
*
* @param string $hook Hook to start the collection for.
*
* @return void
*/
public function start_hook( $hook ) {
$this->record_hook_metric(
$hook,
'start_time',
microtime( true )
);
}
/**
* Stop the collection for a given hook.
*
* @param string $hook Hook to stop the collection for.
*
* @return void
*/
public function stop_hook( $hook ) {
$this->record_hook_metric(
$hook,
'end_time',
microtime( true )
);
}
/**
* Start the collection for a given hook and callback.
*
* @param string $hook Hook to start the collection for.
* @param callable $callback Callback to start the collection for.
* @param int $index Index of the callback.
*
* @return void
*/
public function start_callback( $hook, $callback, $index ) {
$this->record_callback_metric(
$hook,
$callback,
'start_time',
microtime( true )
);
}
/**
* Stop the collection for a given hook and callback.
*
* @param string $hook Hook to start the collection for.
* @param callable $callback Callback to start the collection for.
* @param int $index Index of the callback.
*
* @return void
*/
public function stop_callback( $hook, $callback, $index ) {
$this->record_callback_metric(
$hook,
$callback,
'end_time',
microtime( true )
);
}
/**
* Report the collected data.
*
* @return Report
*/
public function report() {
var_dump( $this->data );
return new Report\ExecutionTime( $this->data );
}
/**
* Record a metric for a hook.
*
* @param string $hook Hook to collect the metric fo.
* @param string $metric Metric to collect.
* @param mixed $value Value to collect.
*/
private function record_hook_metric( $hook, $metric, $value ) {
$this->add_array_keys_as_needed( $this->data, [ 'hooks', $hook ] );
$this->data['hooks'][ $hook ][ $metric ] = $value;
}
/**
* Record a metric for a callback.
*
* @param string $hook Hook to collect the metric fo.
* @param callable $callback Callback to collect the metric for.
* @param string $metric Metric to collect.
* @param mixed $value Value to collect.
*/
private function record_callback_metric( $hook, $callback, $metric, $value ) {
if ( is_callable( $callback ) ) {
$callback = $this->get_callback_hash( $callback );
}
if ( is_array( $callback ) ) {
$callback = $this->get_array_hash( $callback );
}
$this->add_array_keys_as_needed(
$this->data,
[ 'hooks', $hook, 'callbacks', $callback ]
);
$this->data['hooks'][ $hook ]['callbacks'][ $callback ][ $metric ] = $value;
}
/**
* Add missing array keys as needed for several levels in one go.
*
* @param array $array Array to set the missing keys for.
* @param array<string> $keys Array of keys to add if missing.
*/
private function add_array_keys_as_needed( &$array, $keys ) {
while ( count( $keys ) > 0) {
$key = array_shift( $keys );
if ( ! isset( $array[ $key ]) || ! is_array( $array[ $key ] ) ) {
$array[ $key ] = [];
}
$array =& $array[ $key ];
}
}
private function get_callback_hash( callable $callback ) {
return 5;
}
private function get_array_hash( array $callback ) {
var_dump( array_keys( $callback ) );
}
}

View file

@ -0,0 +1,100 @@
<?php
namespace WP_CLI\Profile\Collector;
use RuntimeException;
use WP_CLI\Profile\Collector;
use WP_CLI\Profile\Report;
use WP_CLI\Profile\Scope;
/**
* Collect HTTP requests for the requested scope.
*/
final class HttpRequests implements Collector {
/**
* Internal storage for collected data.
*
* @var array
*/
private $data = [];
/**
* Register the collector with the WordPress lifecycle.
*
* @param Scope $scope Scope for which to collect the data.
*
* @return void
*/
public function register( Scope $scope ) {
switch ( $scope->get_type() ) {
case Scope::TYPE_ALL_HOOKS:
break;
case Scope::TYPE_HOOK:
break;
case Scope::TYPE_STAGE:
break;
default:
throw new RuntimeException(
"Trying to register unsupported scope {$scope->get_type()} in HttpRequests collector"
);
}
}
/**
* Start the collection for a given hook.
*
* @param string $hook Hook to start the collection for.
*
* @return void
*/
public function start_hook( $hook ) {
}
/**
* Stop the collection for a given hook.
*
* @param string $hook Hook to stop the collection for.
*
* @return void
*/
public function stop_hook( $hook ) {
}
/**
* Start the collection for a given hook and callback.
*
* @param string $hook Hook to start the collection for.
* @param callable $callback Callback to start the collection for.
* @param int $index Index of the callback.
*
* @return void
*/
public function start_callback( $hook, $callback, $index ) {
}
/**
* Stop the collection for a given hook and callback.
*
* @param string $hook Hook to start the collection for.
* @param callable $callback Callback to start the collection for.
* @param int $index Index of the callback.
*
* @return void
*/
public function stop_callback( $hook, $callback, $index ) {
}
/**
* Report the collected data.
*
* @return Report
*/
public function report() {
return new Report\HttpRequests( $this->data );
}
}

View file

@ -240,6 +240,67 @@ class Command {
$formatter->display_items( $loggers, true, $order, $orderby );
}
/**
* Profile key metrics for WordPress hooks (actions and filters).
*
* In order to profile callbacks on a specific hook, the action or filter
* will need to execute during the course of the request.
*
* ## OPTIONS
*
* [<hook>]
* : Drill into key metrics of callbacks on a specific WordPress hook.
*
* [--all]
* : Profile callbacks for all WordPress hooks.
*
* [--spotlight]
* : Filter out logs with zero-ish values from the set.
*
* [--url=<url>]
* : Execute a request against a specified URL. Defaults to the home URL.
*
* [--fields=<fields>]
* : Display one or more fields.
*
* [--format=<format>]
* : Render output in a particular format.
* ---
* default: table
* options:
* - table
* - json
* - yaml
* - csv
* ---
*
* [--order=<order>]
* : Ascending or Descending order.
* ---
* default: ASC
* options:
* - ASC
* - DESC
* ---
*
* [--orderby=<orderby>]
* : Order by fields.
*
* @when before_wp_load
* @subcommand new-hook
*/
public function new_hook( $args, $assoc_args ) {
$focus = Utils\get_flag_value( $assoc_args, 'all', isset( $args[0] ) ? $args[0] : null );
$order = Utils\get_flag_value( $assoc_args, 'order', 'ASC' );
$orderby = Utils\get_flag_value( $assoc_args, 'orderby', null );
$profiler = new NewProfiler( new Scope\AllHooks() );
$profiler->profile();
$profiler->report();
}
/**
* Profile arbitrary code execution.
*

260
src/NewProfiler.php Normal file
View file

@ -0,0 +1,260 @@
<?php
namespace WP_CLI\Profile;
use WP_CLI;
use WP_Hook;
final class NewProfiler {
const DEFAULT_COLLECTORS = [
Collector\ExecutionTime::class,
Collector\DatabaseQueries::class,
Collector\HttpRequests::class,
];
/**
* Array of instantiated collectors.
*
* @var array<Collector>
*/
private $collectors = [];
/**
* Scope to profile.
*
* @var Scope
*/
private $scope;
/**
* Current depth of the hook.
*
* @var int
*/
private $hook_depth = 0;
/**
* Instantiate a profiler instance.
*
* @param Scope $scope Scope to profile.
*/
public function __construct( Scope $scope ) {
$this->scope = $scope;
}
/**
* Execute a profile run.
*/
public function profile() {
$this->collectors = $this->register_collectors();
WP_CLI::add_wp_hook( 'all', [ $this, 'wp_hook_begin' ] );
$this->run_wordpress();
}
/**
* Profiling verbosity at the beginning of every action and filter.
*/
public function wp_hook_begin() {
$hook = current_filter();
if ( $this->scope->includes_hook( $hook ) ) {
$this->start_hook_collectors( $hook );
}
if ( 0 === $this->hook_depth
&& ! is_null( $this->previous_filter_callbacks ) ) {
$this->set_hook_callbacks( $this->previous_filter, $this->previous_filter_callbacks );
$this->previous_filter_callbacks = null;
}
if ( 0 === $this->hook_depth
&& $this->scope->includes_hook( $hook ) ) {
$this->wrap_hook_callbacks( $hook );
}
$this->hook_depth++;
WP_CLI::add_wp_hook( $hook, [ $this, 'wp_hook_end' ], PHP_INT_MAX );
}
/**
* Profiling verbosity at the end of every action and filter.
*/
public function wp_hook_end( $filter_value = null ) {
$hook = current_filter();
$this->stop_hook_collectors( $hook );
$this->hook_depth--;
return $filter_value;
}
/**
* Get an aggregated report of all collectors.
*
* @return Report Aggregated report of all collectors.
*/
public function report() {
$reports = [];
foreach ( $this->collectors as $collector ) {
$reports[] = $collector->report();
}
return new Report\Aggregated( $reports );
}
/**
* Register a set of collectors based on their classes.
*
* @return array<Collector> Array of collectors that were registered.
*/
private function register_collectors() {
$collectors = [];
foreach ( self::DEFAULT_COLLECTORS as $collector_class ) {
$collector = $this->instantiate_collector_class( $collector_class );
$collector->register( $this->scope );
$collectors[] = $collector;
}
return $collectors;
}
/**
* Instantiate a collector class.
*
* @param string $collector_class Collector class to instantiate.
*
* @return Collector Instantiated collector.
*/
private function instantiate_collector_class( $collector_class ) {
return new $collector_class();
}
/**
* Runs through the entirety of the WP bootstrap process.
*/
private function run_wordpress() {
// WordPress already ran once.
if ( function_exists( 'add_filter' ) ) {
return;
}
WP_CLI::get_runner()->load_wordpress();
wp();
define( 'WP_USE_THEMES', true );
// Template is normally loaded in global scope, so we need to replicate
foreach ( $GLOBALS as $key => $value ) {
global ${$key}; // phpcs:ignore
// PHPCompatibility.PHP.ForbiddenGlobalVariableVariable.NonBareVariableFound -- Syntax is updated to compatible with php 5 and 7.
}
ob_start();
require_once ABSPATH . WPINC . '/template-loader.php';
ob_get_clean();
}
/**
* Wrap hook callbacks with a timer.
*/
private function wrap_hook_callbacks( $hook ) {
$callbacks = $this->get_hook_callbacks( $hook );
if ( false === $callbacks ) {
return;
}
foreach ( $callbacks as $priority => $priority_callbacks ) {
foreach ( $priority_callbacks as $index => $callback ) {
$callbacks[ $priority ][ $index ] = [
'function' => function ( ...$args ) use ( $hook, $callback, $index ) {
var_dump( $index );
$this->start_callback_collectors( $hook, $callback, $index );
$value = $callback['function']( ...$args );
$this->stop_callback_collectors( $hook, $callback, $index );
return $value;
},
'accepted_args' => $callback['accepted_args'],
];
}
}
$this->set_hook_callbacks( $hook, $callbacks );
}
private function start_hook_collectors( $hook ) {
foreach ( $this->collectors as $collector ) {
$collector->start_hook( $hook );
}
}
private function stop_hook_collectors( $hook ) {
foreach ( $this->collectors as $collector ) {
$collector->stop_hook( $hook );
}
}
private function start_callback_collectors( $hook, $callback, $index ) {
foreach ( $this->collectors as $collector ) {
$collector->start_callback( $hook, $callback, $index );
}
}
private function stop_callback_collectors( $hook, $callback, $index ) {
foreach ( $this->collectors as $collector ) {
$collector->stop_callback( $hook, $callback, $index );
}
}
/**
* Get the callbacks for a given hook.
*
* @param string $hook Hook to get the callbacks for.
* @return array|false Array of callbacks, or false if the hook is unknown.
*/
private function get_hook_callbacks( $hook ) {
global $wp_filter;
if ( ! isset( $wp_filter[ $hook ] ) ) {
return false;
}
$callbacks = $wp_filter[ $hook ] instanceof WP_Hook
? $wp_filter[ $hook ]->callbacks
: $wp_filter[ $hook ];
if ( is_array( $callbacks ) ) {
return $callbacks;
}
return false;
}
/**
* Set the callbacks for a given hook.
*
* @param string $hook Hook to set the callbacks for.
* @param mixed $callbacks Callbacks to set for the hook.
*/
private function set_hook_callbacks( $hook, $callbacks ) {
global $wp_filter;
if ( ! isset( $wp_filter[ $hook ] ) && class_exists( 'WP_Hook' ) ) {
$wp_filter[ $hook ] = new WP_Hook();
}
if ( $wp_filter[ $hook ] instanceof WP_Hook ) {
$wp_filter[ $hook ]->callbacks = $callbacks;
} else {
$wp_filter[ $hook ] = $callbacks;
}
}
}

View file

@ -8,34 +8,34 @@ class Profiler {
private $type;
private $focus;
private $loggers = array();
private $stage_hooks = array(
'bootstrap' => array(
private $loggers = [];
private $stage_hooks = [
'bootstrap' => [
'muplugins_loaded',
'plugins_loaded',
'setup_theme',
'after_setup_theme',
'init',
'wp_loaded',
),
'main_query' => array(
],
'main_query' => [
'parse_request',
'send_headers',
'pre_get_posts',
'the_posts',
'wp',
),
'template' => array(
],
'template' => [
'template_redirect',
'template_include',
'wp_head',
'loop_start',
'loop_end',
'wp_footer',
),
);
],
];
private $current_stage_hooks = array();
private $current_stage_hooks = [];
private $running_hook = null;
private $previous_filter = null;
private $previous_filter_callbacks = null;
@ -74,105 +74,19 @@ class Profiler {
}
/**
* Run the profiler against WordPress
* Run the profiler against WordPress.
*/
public function run() {
WP_CLI::add_wp_hook(
'muplugins_loaded',
function() {
$url = WP_CLI::get_runner()->config['url'];
if ( ! empty( $url ) ) {
WP_CLI::set_url( trailingslashit( $url ) );
} else {
WP_CLI::set_url( home_url( '/' ) );
}
}
);
WP_CLI::add_hook(
'after_wp_config_load',
function() {
if ( defined( 'SAVEQUERIES' ) && ! SAVEQUERIES ) {
WP_CLI::error( "'SAVEQUERIES' is defined as false, and must be true. Please check your wp-config.php" );
}
if ( ! defined( 'SAVEQUERIES' ) ) {
define( 'SAVEQUERIES', true );
}
}
);
if ( 'hook' === $this->type
&& ':before' === substr( $this->focus, -7, 7 ) ) {
$stage_hooks = array();
foreach ( $this->stage_hooks as $hooks ) {
$stage_hooks = array_merge( $stage_hooks, $hooks );
}
$end_hook = substr( $this->focus, 0, -7 );
$key = array_search( $end_hook, $stage_hooks, true );
if ( isset( $stage_hooks[ $key - 1 ] ) ) {
$start_hook = $stage_hooks[ $key - 1 ];
WP_CLI::add_wp_hook( $start_hook, array( $this, 'wp_tick_profile_begin' ), 9999 );
} else {
WP_CLI::add_hook( 'after_wp_config_load', array( $this, 'wp_tick_profile_begin' ) );
}
WP_CLI::add_wp_hook( $end_hook, array( $this, 'wp_tick_profile_end' ), -9999 );
} elseif ( 'hook' === $this->type
&& ':after' === substr( $this->focus, -6, 6 ) ) {
$start_hook = substr( $this->focus, 0, -6 );
WP_CLI::add_wp_hook( $start_hook, array( $this, 'wp_tick_profile_begin' ), 9999 );
} else {
WP_CLI::add_wp_hook( 'all', array( $this, 'wp_hook_begin' ) );
}
WP_CLI::add_wp_hook( 'pre_http_request', array( $this, 'wp_request_begin' ) );
WP_CLI::add_wp_hook( 'http_api_debug', array( $this, 'wp_request_end' ) );
$this->configure_wordpress_context();
$this->enable_savequeries();
WP_CLI::add_wp_hook( 'all', [ $this, 'wp_hook_begin' ] );
WP_CLI::add_wp_hook( 'pre_http_request', [ $this, 'wp_request_begin' ] );
WP_CLI::add_wp_hook( 'http_api_debug', [ $this, 'wp_request_end' ] );
$this->load_wordpress_with_template();
}
/**
* Start profiling function calls on the end of this filter
*/
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
if ( extension_loaded( 'xcache' ) ) {
@ini_set( 'xcache.optimizer', false ); // phpcs:ignore
// WordPress.PHP.NoSilencedErrors.Discouraged -- ini_set can be disabled on server.
} elseif ( extension_loaded( 'apc' ) ) {
@ini_set( 'apc.optimization', 0 ); // phpcs:ignore
// WordPress.PHP.NoSilencedErrors.Discouraged -- ini_set can be disabled on server.
apc_clear_cache();
} elseif ( extension_loaded( 'eaccelerator' ) ) {
@ini_set( 'eaccelerator.optimizer', 0 ); // phpcs:ignore
// WordPress.PHP.NoSilencedErrors.Discouraged -- ini_set can be disabled on server.
if ( function_exists( 'eaccelerator_optimizer' ) ) {
@eaccelerator_optimizer( false ); // phpcs:ignore
// WordPress.PHP.NoSilencedErrors.Discouraged -- disabling eaccelerator on runtime can faild
}
} elseif ( extension_loaded( 'Zend Optimizer+' ) ) {
@ini_set( 'zend_optimizerplus.optimization_level', 0 ); // phpcs:ignore
// WordPress.PHP.NoSilencedErrors.Discouraged -- ini_set can be disabled on server.
}
register_tick_function( array( $this, 'handle_function_tick' ) );
declare( ticks = 1 );
return $value;
}
/**
* Stop profiling function calls at the beginning of this filter
*/
public function wp_tick_profile_end( $value = null ) {
unregister_tick_function( array( $this, 'handle_function_tick' ) );
$this->tick_callback = null;
return $value;
}
/**
* Profiling verbosity at the beginning of every action and filter
* Profiling verbosity at the beginning of every action and filter.
*/
public function wp_hook_begin() {
@ -195,10 +109,10 @@ class Profiler {
}
}
$this->loggers[ $current_filter ] = new Logger(
array(
[
'hook' => $current_filter,
'callback_count' => $callback_count,
)
]
);
$this->loggers[ $current_filter ]->start();
}
@ -217,11 +131,11 @@ class Profiler {
$this->filter_depth++;
WP_CLI::add_wp_hook( $current_filter, array( $this, 'wp_hook_end' ), 9999 );
WP_CLI::add_wp_hook( $current_filter, [ $this, 'wp_hook_end' ], 9999 );
}
/**
* Wrap current filter callbacks with a timer
* Wrap current filter callbacks with a timer.
*/
private function wrap_current_filter_callbacks( $current_filter ) {
@ -234,13 +148,13 @@ class Profiler {
foreach ( $callbacks as $priority => $priority_callbacks ) {
foreach ( $priority_callbacks as $i => $the_ ) {
$callbacks[ $priority ][ $i ] = array(
'function' => function() use ( $the_, $i ) {
$callbacks[ $priority ][ $i ] = [
'function' => function () use ( $the_, $i ) {
if ( ! isset( $this->loggers[ $i ] ) ) {
$this->loggers[ $i ] = new Logger(
array(
[
'callback' => $the_['function'],
)
]
);
}
$this->loggers[ $i ]->start();
@ -249,14 +163,14 @@ class Profiler {
return $value;
},
'accepted_args' => $the_['accepted_args'],
);
];
}
}
self::set_filter_callbacks( $current_filter, $callbacks );
}
/**
* Profiling verbosity at the end of every action and filter
* Profiling verbosity at the end of every action and filter.
*/
public function wp_hook_end( $filter_value = null ) {
@ -276,7 +190,7 @@ class Profiler {
$pseudo_hook = "{$this->current_stage_hooks[$key]}:after";
$this->running_hook = $pseudo_hook;
}
$this->loggers[ $pseudo_hook ] = new Logger( array( 'hook' => $pseudo_hook ) );
$this->loggers[ $pseudo_hook ] = new Logger( [ 'hook' => $pseudo_hook ] );
$this->loggers[ $pseudo_hook ]->start();
}
}
@ -287,7 +201,7 @@ class Profiler {
}
/**
* Handle the tick of a function
* Handle the tick of a function.
*/
public function handle_function_tick() {
global $wpdb, $wp_object_cache;
@ -297,7 +211,7 @@ class Profiler {
$callback_hash = md5( serialize( $this->tick_callback . $this->tick_location ) ); // phpcs:ignore
if ( ! isset( $this->loggers[ $callback_hash ] ) ) {
$this->loggers[ $callback_hash ] = array(
$this->loggers[ $callback_hash ] = [
'callback' => $this->tick_callback,
'location' => $this->tick_location,
'time' => 0,
@ -306,7 +220,7 @@ class Profiler {
'cache_hits' => 0,
'cache_misses' => 0,
'cache_ratio' => null,
);
];
}
$this->loggers[ $callback_hash ]['time'] += $time;
@ -340,7 +254,7 @@ class Profiler {
$location = '';
$callback = '';
if ( in_array( strtolower( $frame['function'] ), array( 'include', 'require', 'include_once', 'require_once' ), true ) ) {
if ( in_array( strtolower( $frame['function'] ), [ 'include', 'require', 'include_once', 'require_once' ], true ) ) {
$callback = $frame['function'] . " '" . $frame['args'][0] . "'";
} elseif ( isset( $frame['object'] ) && method_exists( $frame['object'], $frame['function'] ) ) {
$callback = get_class( $frame['object'] ) . '->' . $frame['function'] . '()';
@ -373,7 +287,7 @@ class Profiler {
}
/**
* Profiling request time for any active Loggers
* Profiling request time for any active Loggers.
*/
public function wp_request_begin( $filter_value = null ) {
foreach ( Logger::$active_loggers as $logger ) {
@ -383,7 +297,7 @@ class Profiler {
}
/**
* Profiling request time for any active Loggers
* Profiling request time for any active Loggers.
*/
public function wp_request_end( $filter_value = null ) {
foreach ( Logger::$active_loggers as $logger ) {
@ -393,7 +307,7 @@ class Profiler {
}
/**
* Runs through the entirety of the WP bootstrap process
* Runs through the entirety of the WP bootstrap process.
*/
private function load_wordpress_with_template() {
@ -403,7 +317,7 @@ class Profiler {
}
if ( 'stage' === $this->type && true === $this->focus ) {
$hooks = array();
$hooks = [];
foreach ( $this->stage_hooks as $stage_hook ) {
$hooks = array_merge( $hooks, $stage_hook );
}
@ -414,7 +328,7 @@ class Profiler {
if ( 'bootstrap' === $this->focus ) {
$this->set_stage_hooks( $this->stage_hooks['bootstrap'] );
} elseif ( ! $this->focus ) {
$logger = new Logger( array( 'stage' => 'bootstrap' ) );
$logger = new Logger( [ 'stage' => 'bootstrap' ] );
$logger->start();
}
}
@ -423,9 +337,6 @@ class Profiler {
$this->loggers[ $this->running_hook ]->stop();
$this->running_hook = null;
}
if ( 'hook' === $this->type && 'wp_loaded:after' === $this->focus ) {
$this->wp_tick_profile_end();
}
if ( 'stage' === $this->type && ! $this->focus ) {
$logger->stop();
$this->loggers[] = $logger;
@ -436,7 +347,7 @@ class Profiler {
if ( 'main_query' === $this->focus ) {
$this->set_stage_hooks( $this->stage_hooks['main_query'] );
} elseif ( ! $this->focus ) {
$logger = new Logger( array( 'stage' => 'main_query' ) );
$logger = new Logger( [ 'stage' => 'main_query' ] );
$logger->start();
}
}
@ -445,9 +356,6 @@ class Profiler {
$this->loggers[ $this->running_hook ]->stop();
$this->running_hook = null;
}
if ( 'hook' === $this->type && 'wp:after' === $this->focus ) {
$this->wp_tick_profile_end();
}
if ( 'stage' === $this->type && ! $this->focus ) {
$logger->stop();
$this->loggers[] = $logger;
@ -466,7 +374,7 @@ class Profiler {
if ( 'template' === $this->focus ) {
$this->set_stage_hooks( $this->stage_hooks['template'] );
} elseif ( ! $this->focus ) {
$logger = new Logger( array( 'stage' => 'template' ) );
$logger = new Logger( [ 'stage' => 'template' ] );
$logger->start();
}
}
@ -477,9 +385,6 @@ class Profiler {
$this->loggers[ $this->running_hook ]->stop();
$this->running_hook = null;
}
if ( 'hook' === $this->type && 'wp_footer:after' === $this->focus ) {
$this->wp_tick_profile_end();
}
if ( 'stage' === $this->type && ! $this->focus ) {
$logger->stop();
$this->loggers[] = $logger;
@ -488,7 +393,7 @@ class Profiler {
}
/**
* Get a human-readable name from a callback
* Get a human-readable name from a callback.
*/
private static function get_name_location_from_callback( $callback ) {
$location = '';
@ -510,11 +415,11 @@ class Profiler {
if ( $reflection ) {
$location = $reflection->getFileName() . ':' . $reflection->getStartLine();
}
return array( $name, $location );
return [ $name, $location ];
}
/**
* Get the short location from the full location
* Get the short location from the full location.
*
* @param string $location
* @return string
@ -536,17 +441,17 @@ class Profiler {
}
/**
* Set the hooks for the current stage
* Set the hooks for the current stage.
*/
private function set_stage_hooks( $hooks ) {
$this->current_stage_hooks = $hooks;
$pseudo_hook = "{$hooks[0]}:before";
$this->loggers[ $pseudo_hook ] = new Logger( array( 'hook' => $pseudo_hook ) );
$this->loggers[ $pseudo_hook ] = new Logger( [ 'hook' => $pseudo_hook ] );
$this->loggers[ $pseudo_hook ]->start();
}
/**
* Get the callbacks for a given filter
* Get the callbacks for a given filter.
*
* @param string
* @return array|false
@ -570,7 +475,7 @@ class Profiler {
}
/**
* Set the callbacks for a given filter
* Set the callbacks for a given filter.
*
* @param string $filter
* @param mixed $callbacks
@ -589,4 +494,32 @@ class Profiler {
}
}
protected function configure_wordpress_context() {
WP_CLI::add_wp_hook(
'muplugins_loaded',
static function () {
$url = WP_CLI::get_runner()->config['url'];
if ( ! empty( $url ) ) {
WP_CLI::set_url( trailingslashit( $url ) );
} else {
WP_CLI::set_url( home_url( '/' ) );
}
}
);
}
protected function enable_savequeries() {
WP_CLI::add_hook(
'after_wp_config_load',
static function () {
if ( defined( 'SAVEQUERIES' ) && ! SAVEQUERIES ) {
WP_CLI::error( "'SAVEQUERIES' is defined as false, and must be true. Please check your wp-config.php" );
}
if ( ! defined( 'SAVEQUERIES' ) ) {
define( 'SAVEQUERIES', true );
}
}
);
}
}

10
src/Report.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace WP_CLI\Profile;
/**
* Report for the collected data.
*/
interface Report {
}

View file

@ -0,0 +1,9 @@
<?php
namespace WP_CLI\Profile\Report;
use WP_CLI\Profile\Report;
final class Aggregated implements Report {
}

View file

@ -0,0 +1,12 @@
<?php
namespace WP_CLI\Profile\Report;
use WP_CLI\Profile\Report;
/**
* Report collected database queries.
*/
final class DatabaseQueries implements Report {
}

View file

@ -0,0 +1,12 @@
<?php
namespace WP_CLI\Profile\Report;
use WP_CLI\Profile\Report;
/**
* Report collected execution time.
*/
final class ExecutionTime implements Report {
}

View file

@ -0,0 +1,12 @@
<?php
namespace WP_CLI\Profile\Report;
use WP_CLI\Profile\Report;
/**
* Report collected HTTP requests.
*/
final class HttpRequests implements Report {
}

29
src/Scope.php Normal file
View file

@ -0,0 +1,29 @@
<?php
namespace WP_CLI\Profile;
/**
* Scope for which to collect data.
*/
interface Scope {
const TYPE_ALL_HOOKS = 'all_hooks';
const TYPE_HOOK = 'hook';
const TYPE_STAGE = 'stage';
/**
* Get the type of the scope.
*
* @return string Type of the scope.
*/
public function get_type();
/**
* Check if the scope includes a given hook.
*
* @param string $hook Hook to check.
*
* @return bool Whether the hook is included in the scope.
*/
public function includes_hook( $hook );
}

31
src/Scope/AllHooks.php Normal file
View file

@ -0,0 +1,31 @@
<?php
namespace WP_CLI\Profile\Scope;
use WP_CLI\Profile\Scope;
/**
* Collect data across all hooks.
*/
final class AllHooks implements Scope {
/**
* Check if the scope includes a given hook.
*
* @param string $hook Hook to check.
*
* @return bool Whether the hook is included in the scope.
*/
public function includes_hook( $hook ) {
return true;
}
/**
* Get the type of the scope.
*
* @return string Type of the scope.
*/
public function get_type() {
return Scope::TYPE_ALL_HOOKS;
}
}

47
src/Scope/Hook.php Normal file
View file

@ -0,0 +1,47 @@
<?php
namespace WP_CLI\Profile\Scope;
use WP_CLI\Profile\Scope;
/**
* Collect data scoped based on a provided hook.
*/
final class Hook implements Scope {
/**
* Hook to scope the data collection by.
*
* @var string
*/
private $hook;
/**
* Instantiate a Hook object.
*
* @param string $hook Hook to scope the data collection by.
*/
public function __construct( $hook ) {
$this->hook = $hook;
}
/**
* Check if the scope includes a given hook.
*
* @param string $hook Hook to check.
*
* @return bool Whether the hook is included in the scope.
*/
public function includes_hook( $hook ) {
return $hook === $this->hook;
}
/**
* Get the type of the scope.
*
* @return string Type of the scope.
*/
public function get_type() {
return Scope::TYPE_HOOK;
}
}

91
src/Scope/Stage.php Normal file
View file

@ -0,0 +1,91 @@
<?php
namespace WP_CLI\Profile\Scope;
use InvalidArgumentException;
use WP_CLI\Profile\Scope;
/**
* Collect data scoped based on a provided stage.
*/
final class Stage implements Scope {
const BOOTSTRAP = 'bootstrap';
const MAIN_QUERY = 'main_query';
const TEMPLATE = 'template';
const STAGE_HOOKS = [
self::BOOTSTRAP => [
'muplugins_loaded',
'plugins_loaded',
'setup_theme',
'after_setup_theme',
'init',
'wp_loaded',
],
self::MAIN_QUERY => [
'parse_request',
'send_headers',
'pre_get_posts',
'the_posts',
'wp',
],
self::TEMPLATE => [
'template_redirect',
'template_include',
'wp_head',
'loop_start',
'loop_end',
'wp_footer',
],
];
/**
* Stage to scope the data collection by.
*
* @var string
*/
private $stage;
/**
* Instantiate a Stage object.
*
* @param string $stage Stage to scope the data collection by.
*/
public function __construct( $stage ) {
if ( ! array_key_exists( $stage, self::STAGE_HOOKS ) ) {
throw new InvalidArgumentException( "Invalid stage {$stage}" );
}
$this->stage = $stage;
}
/**
* Get the stage to scope the collection of data by.
*
* @return string Stage to scope the data collection by.
*/
public function get_stage() {
return $this->stage;
}
/**
* Check if the scope includes a given hook.
*
* @param string $hook Hook to check.
*
* @return bool Whether the hook is included in the scope.
*/
public function includes_hook( $hook ) {
return array_key_exists( $hook, self::STAGE_HOOKS[ $this->stage ] );
}
/**
* Get the type of the scope.
*
* @return string Type of the scope.
*/
public function get_type() {
return Scope::TYPE_STAGE;
}
}