Skip to content
3 changes: 3 additions & 0 deletions features/profile.feature
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Feature: Basic profile usage
See 'wp help profile <command>' for more information on a specific command.
"""

# Skip when object cache is used because sqlite-object-cache fails during manual wp core install
# due to missing wp_options table during cron scheduling.
@skip-object-cache
Scenario: Error when SAVEQUERIES is defined to false
Given an empty directory
And WP files
Expand Down
12 changes: 12 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
parameters:
level: 9
paths:
- src
- profile-command.php
scanDirectories:
- vendor/wp-cli/wp-cli/php
scanFiles:
- vendor/php-stubs/wordpress-stubs/wordpress-stubs.php
- tests/phpstan/scan-files.php

treatPhpDocTypesAsCertain: false
67 changes: 51 additions & 16 deletions src/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,20 @@ class Command {
*
* @skipglobalargcheck
* @when before_wp_load
*
* @param array{0?: string} $args Positional arguments.
* @param array{all?: bool, spotlight?: bool, url?: string, fields?: string, format: string, order: string, orderby?: string} $assoc_args Associative arguments.
* @return void
*/
public function stage( $args, $assoc_args ) {
global $wpdb;

$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 );
$order_val = Utils\get_flag_value( $assoc_args, 'order', 'ASC' );
$order = is_string( $order_val ) ? $order_val : 'ASC';
$orderby_val = Utils\get_flag_value( $assoc_args, 'orderby', null );
$orderby = ( is_string( $orderby_val ) || is_null( $orderby_val ) ) ? $orderby_val : null;

$valid_stages = array( 'bootstrap', 'main_query', 'template' );
if ( $focus && ( true !== $focus && ! in_array( $focus, $valid_stages, true ) ) ) {
Expand Down Expand Up @@ -181,6 +187,7 @@ public function stage( $args, $assoc_args ) {
$fields = array_merge( $base, $metrics );
$formatter = new Formatter( $assoc_args, $fields );
$loggers = $profiler->get_loggers();
/** @var array<string, bool|string> $assoc_args */
if ( Utils\get_flag_value( $assoc_args, 'spotlight' ) ) {
$loggers = self::shine_spotlight( $loggers, $metrics );
}
Expand Down Expand Up @@ -257,13 +264,19 @@ public function stage( $args, $assoc_args ) {
*
* @skipglobalargcheck
* @when before_wp_load
*
* @param array{0?: string} $args Positional arguments.
* @param array{all?: bool, spotlight?: bool, url?: string, fields?: string, format: string, order: string, orderby?: string} $assoc_args
* @return void
*/
public function 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 );
$order_val = Utils\get_flag_value( $assoc_args, 'order', 'ASC' );
$order = is_string( $order_val ) ? $order_val : 'ASC';
$orderby_val = Utils\get_flag_value( $assoc_args, 'orderby', null );
$orderby = ( is_string( $orderby_val ) || is_null( $orderby_val ) ) ? $orderby_val : null;

$profiler = new Profiler( 'hook', $focus );
$profiler->run();
Expand Down Expand Up @@ -293,11 +306,14 @@ public function hook( $args, $assoc_args ) {
$fields = array_merge( $base, $metrics );
$formatter = new Formatter( $assoc_args, $fields );
$loggers = $profiler->get_loggers();
/** @var array<string, bool|string> $assoc_args */
if ( Utils\get_flag_value( $assoc_args, 'spotlight' ) ) {
$loggers = self::shine_spotlight( $loggers, $metrics );
}
$search = Utils\get_flag_value( $assoc_args, 'search', false );
if ( false !== $search && '' !== $search ) {
/** @var array<string, bool|string> $assoc_args */
$search_val = Utils\get_flag_value( $assoc_args, 'search', '' );
$search = is_string( $search_val ) ? $search_val : '';
if ( '' !== $search ) {
if ( ! $focus ) {
WP_CLI::error( '--search requires --all or a specific hook.' );
}
Expand Down Expand Up @@ -357,13 +373,19 @@ public function hook( $args, $assoc_args ) {
* | 0.1009s | 100% | 1 |
* +---------+-------------+---------------+
*
* @param array{0: string} $args Positional arguments.
* @param array{hook?: bool|string, fields: string, format: string, order: string, orderby?: string} $assoc_args Associative arguments.
* @return void
*
* @subcommand eval
*/
public function eval_( $args, $assoc_args ) {
$statement = $args[0];

$order = Utils\get_flag_value( $assoc_args, 'order', 'ASC' );
$orderby = Utils\get_flag_value( $assoc_args, 'orderby', null );
$order_val = Utils\get_flag_value( $assoc_args, 'order', 'ASC' );
$order = is_string( $order_val ) ? $order_val : 'ASC';
$orderby_val = Utils\get_flag_value( $assoc_args, 'orderby', null );
$orderby = ( is_string( $orderby_val ) || is_null( $orderby_val ) ) ? $orderby_val : null;

self::profile_eval_ish(
$assoc_args,
Expand Down Expand Up @@ -426,14 +448,20 @@ function () use ( $statement ) {
* | 0.1009s | 100% | 1 |
* +---------+-------------+---------------+
*
* @param array{0: string} $args Positional arguments.
* @param array{hook?: string|bool, fields?: string, format: string, order: string, orderby?: string} $assoc_args Associative arguments.
* @return void
*
* @subcommand eval-file
*/
public function eval_file( $args, $assoc_args ) {

$file = $args[0];

$order = Utils\get_flag_value( $assoc_args, 'order', 'ASC' );
$orderby = Utils\get_flag_value( $assoc_args, 'orderby', null );
$order_val = Utils\get_flag_value( $assoc_args, 'order', 'ASC' );
$order = is_string( $order_val ) ? $order_val : 'ASC';
$orderby_val = Utils\get_flag_value( $assoc_args, 'orderby', null );
$orderby = ( is_string( $orderby_val ) || is_null( $orderby_val ) ) ? $orderby_val : null;

if ( ! file_exists( $file ) ) {
WP_CLI::error( "'$file' does not exist." );
Expand All @@ -451,6 +479,12 @@ function () use ( $file ) {

/**
* Profile an eval or eval-file statement.
*
* @param array{hook?: string|bool} $assoc_args
* @param callable $profile_callback
* @param string $order
* @param string|null $orderby
* @return void
*/
private static function profile_eval_ish( $assoc_args, $profile_callback, $order = 'ASC', $orderby = null ) {
$hook = Utils\get_flag_value( $assoc_args, 'hook' );
Expand Down Expand Up @@ -500,6 +534,7 @@ private static function profile_eval_ish( $assoc_args, $profile_callback, $order
* Include a file without exposing it to current scope
*
* @param string $file
* @return void
*/
private static function include_file( $file ) {
include $file;
Expand All @@ -508,9 +543,9 @@ private static function include_file( $file ) {
/**
* Filter loggers with zero-ish values.
*
* @param array $loggers
* @param array $metrics
* @return array
* @param array<\WP_CLI\Profile\Logger> $loggers
* @param array<string> $metrics
* @return array<\WP_CLI\Profile\Logger>
*/
private static function shine_spotlight( $loggers, $metrics ) {

Expand Down Expand Up @@ -550,9 +585,9 @@ private static function shine_spotlight( $loggers, $metrics ) {
/**
* Filter loggers to only those whose callback name matches a pattern.
*
* @param array $loggers
* @param string $pattern
* @return array
* @param array<\WP_CLI\Profile\Logger> $loggers
* @param string $pattern
* @return array<\WP_CLI\Profile\Logger>
*/
private static function filter_by_callback( $loggers, $pattern ) {
return array_filter(
Expand Down
89 changes: 70 additions & 19 deletions src/Formatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,32 @@

class Formatter {

/**
* @var \WP_CLI\Formatter
*/
private $formatter;

/**
* @var array<string, mixed>
*/
private $args;

/**
* @var int|null
*/
private $total_cell_index;

/**
* Formatter constructor.
*
* @param array<mixed> $assoc_args
* @param array<string>|null $fields
* @param string|false $prefix
*/
public function __construct( &$assoc_args, $fields = null, $prefix = false ) {
if ( null === $fields ) {
$fields = [];
}
$format_args = array(
'format' => 'table',
'fields' => $fields,
Expand All @@ -24,10 +43,19 @@ public function __construct( &$assoc_args, $fields = null, $prefix = false ) {
}

if ( ! is_array( $format_args['fields'] ) ) {
$format_args['fields'] = explode( ',', $format_args['fields'] );
$fields_val = $format_args['fields'];
$fields_str = is_scalar( $fields_val ) ? (string) $fields_val : '';
$format_args['fields'] = explode( ',', $fields_str );
}

$format_args['fields'] = array_filter( array_map( 'trim', $format_args['fields'] ) );
$format_args['fields'] = array_filter(
array_map(
function ( $val ) {
return trim( is_scalar( $val ) ? (string) $val : '' );
},
$format_args['fields']
)
);

if ( isset( $assoc_args['fields'] ) ) {
if ( empty( $format_args['fields'] ) ) {
Expand All @@ -39,9 +67,9 @@ public function __construct( &$assoc_args, $fields = null, $prefix = false ) {
}
}

if ( 'time' !== $fields[0] ) {
if ( ! empty( $fields ) && 'time' !== $fields[0] ) {
$index = array_search( $fields[0], $format_args['fields'], true );
$this->total_cell_index = ( false !== $index ) ? $index : null;
$this->total_cell_index = ( false !== $index ) ? (int) $index : null;
}

$this->args = $format_args;
Expand All @@ -51,11 +79,17 @@ public function __construct( &$assoc_args, $fields = null, $prefix = false ) {
/**
* Display multiple items according to the output arguments.
*
* @param array $items
* @param array<\WP_CLI\Profile\Logger> $items
* @param bool $include_total
* @param string $order
* @param string|null $orderby
* @return void
*/
public function display_items( $items, $include_total, $order, $orderby ) {
if ( 'table' === $this->args['format'] && empty( $this->args['field'] ) ) {
$this->show_table( $order, $orderby, $items, $this->args['fields'], $include_total );
/** @var array<string> $fields */
$fields = $this->args['fields'];
$this->show_table( $order, $orderby, $items, $fields, $include_total );
} else {
$this->formatter->display_items( $items );
}
Expand All @@ -64,15 +98,17 @@ public function display_items( $items, $include_total, $order, $orderby ) {
/**
* Function to compare floats.
*
* @param double $a Floating number.
* @param double $b Floating number.
* @param float $a Floating number.
* @param float $b Floating number.
* @return int
*/
private function compare_float( $a, $b ) {
$a = number_format( $a, 4 );
$b = number_format( $b, 4 );
if ( 0 === $a - $b ) {
$a = round( $a, 4 );
$b = round( $b, 4 );
$diff = $a - $b;
if ( 0.0 === $diff ) {
return 0;
} elseif ( $a - $b < 0 ) {
} elseif ( $diff < 0 ) {
return -1;
} else {
return 1;
Expand All @@ -82,8 +118,12 @@ private function compare_float( $a, $b ) {
/**
* Show items in a \cli\Table.
*
* @param array $items
* @param array $fields
* @param string $order
* @param string|null $orderby
* @param array<\WP_CLI\Profile\Logger> $items
* @param array<string> $fields
* @param bool $include_total
* @return void
*/
private function show_table( $order, $orderby, $items, $fields, $include_total ) {
$table = new \cli\Table();
Expand All @@ -109,7 +149,7 @@ function ( $a, $b ) use ( $order, $orderby ) {
list( $first, $second ) = $orderby_array;

if ( is_numeric( $first->$orderby ) && is_numeric( $second->$orderby ) ) {
return $this->compare_float( $first->$orderby, $second->$orderby );
return $this->compare_float( (float) $first->$orderby, (float) $second->$orderby );
}

return strcmp( $first->$orderby, $second->$orderby );
Expand Down Expand Up @@ -139,13 +179,17 @@ function ( $a, $b ) use ( $order, $orderby ) {
}
if ( stripos( $fields[ $i ], '_ratio' ) ) {
if ( ! is_null( $value ) ) {
assert( is_array( $totals[ $i ] ) );
$totals[ $i ][] = $value;
}
} else {
$totals[ $i ] += $value;
$current_total = is_numeric( $totals[ $i ] ) ? $totals[ $i ] : 0;
$add_value = is_numeric( $value ) ? $value : 0;
$totals[ $i ] = $current_total + $add_value;
}
if ( stripos( $fields[ $i ], '_time' ) || 'time' === $fields[ $i ] ) {
$values[ $i ] = round( $value, 4 ) . 's';
$value_num = is_numeric( $value ) ? (float) $value : 0.0;
$values[ $i ] = round( $value_num, 4 ) . 's';
}
}
$table->addRow( $values );
Expand All @@ -156,11 +200,18 @@ function ( $a, $b ) use ( $order, $orderby ) {
continue;
}
if ( stripos( $fields[ $i ], '_time' ) || 'time' === $fields[ $i ] ) {
$totals[ $i ] = round( $value, 4 ) . 's';
assert( is_numeric( $value ) );
$totals[ $i ] = round( (float) $value, 4 ) . 's';
}
if ( is_array( $value ) ) {
if ( ! empty( $value ) ) {
$totals[ $i ] = round( ( array_sum( array_map( 'floatval', $value ) ) / count( $value ) ), 2 ) . '%';
$float_values = array_map(
function ( $val ) {
return floatval( is_scalar( $val ) ? $val : 0 );
},
$value
);
$totals[ $i ] = round( ( array_sum( $float_values ) / count( $value ) ), 2 ) . '%';
} else {
$totals[ $i ] = null;
}
Expand Down
Loading