plugin = $plugin; } /** * Register. */ public function register(): void { \WP_CLI::add_command( 'bloatwright profile', array( $this, 'profile' ), array( 'shortdesc' => 'Run a profiling session or compare two sessions.', 'longdesc' => 'Admin URLs are requested as the WP-CLI user: add the global --user= parameter to profile them. Without a user they are skipped.', 'synopsis' => array( array( 'type' => 'positional', 'name' => 'action', 'optional' => true, 'options' => array( 'run', 'compare', 'list' ), ), array( 'type' => 'positional', 'name' => 'a', 'optional' => true, ), array( 'type' => 'positional', 'name' => 'b', 'optional' => true, ), array( 'type' => 'assoc', 'name' => 'urls', 'optional' => true, ), array( 'type' => 'assoc', 'name' => 'passes', 'optional' => true, ), array( 'type' => 'assoc', 'name' => 'without', 'optional' => true, ), array( 'type' => 'assoc', 'name' => 'format', 'optional' => true, 'default' => 'table', ), ), ) ); \WP_CLI::add_command( 'bloatwright autoload', array( $this, 'autoload' ), array( 'shortdesc' => 'Autoloaded options per plugin.' ) ); \WP_CLI::add_command( 'bloatwright cron', array( $this, 'cron' ), array( 'shortdesc' => 'Scheduled events per plugin.' ) ); } /** * Profile. * * @param array $args Args. * @param array $assoc Assoc. */ public function profile( array $args, array $assoc ): void { $action = (string) ( $args[0] ?? 'run' ); $format = (string) ( $assoc['format'] ?? 'table' ); $repo = $this->plugin->get( SessionRepository::class ); if ( 'list' === $action ) { $rows = array(); foreach ( $repo->all( 20 ) as $s ) { $rows[] = array( 'id' => $s['id'], 'started' => $s['started_at'], 'status' => $s['status'], 'mode' => $s['mode'], 'urls' => count( $s['urls'] ), 'passes' => $s['passes'], 'without' => $s['exclude_slug'], ); } \WP_CLI\Utils\format_items( $format, $rows, array( 'id', 'started', 'status', 'mode', 'urls', 'passes', 'without' ) ); return; } if ( 'compare' === $action ) { $a = $repo->find( (int) ( $args[1] ?? 0 ) ); $b = $repo->find( (int) ( $args[2] ?? 0 ) ); if ( ! $a || ! $b || empty( $a['summary'] ) || empty( $b['summary'] ) ) { \WP_CLI::error( 'Both sessions must exist and be finished.' ); } $diff = Diff::compare( (array) $a['summary'], (array) $b['summary'] ); $rows = array(); foreach ( $diff['owners'] as $o ) { $rows[] = array( 'plugin' => $o['name'], 'php_before' => $o['php_ms']['before'], 'php_after' => $o['php_ms']['after'], 'php_delta' => $o['php_ms']['delta'], 'queries' => $o['queries']['delta'], 'weight' => $o['weight']['delta'], ); } \WP_CLI\Utils\format_items( $format, $rows, array( 'plugin', 'php_before', 'php_after', 'php_delta', 'queries', 'weight' ) ); return; } if ( 'run' !== $action ) { \WP_CLI::error( "Unknown action '{$action}'. Use run, compare or list." ); } $urls = ! empty( $assoc['urls'] ) ? array_map( 'trim', explode( ',', (string) $assoc['urls'] ) ) : array_column( UrlSet::defaults(), 'url' ); $passes = (int) ( $assoc['passes'] ?? Settings::get( 'passes', 3 ) ); $runner = $this->plugin->get( SessionRunner::class ); // Same install step as the admin "Run profile": write the must-use loader when missing or stale. $loader = LoaderInstaller::ensure(); if ( is_wp_error( $loader ) ) { \WP_CLI::error( $loader->get_error_message() ); } $id = $runner->start( $urls, $passes, (string) ( $assoc['without'] ?? '' ), 'CLI' ); if ( is_wp_error( $id ) ) { \WP_CLI::error( $id->get_error_message() ); } if ( get_current_user_id() <= 0 && array_filter( $urls, array( Loopback::class, 'is_admin_url' ) ) ) { \WP_CLI::warning( 'Admin URLs are skipped without a user. Add --user= to profile them as that user.' ); } \WP_CLI::log( sprintf( 'Session #%d: %d URLs × %d passes…', $id, count( $urls ), $passes ) ); $s = $runner->run_all( $id, static function ( $done, $total ) { \WP_CLI::log( sprintf( ' %d/%d', $done, $total ) ); } ); if ( is_wp_error( $s ) ) { \WP_CLI::error( $s->get_error_message() ); } $describe = static function ( $e ) { return $e['url'] . ': ' . $e['error']; }; if ( 'done' !== $s['status'] ) { $errors = array_map( $describe, array_merge( (array) ( $s['progress']['errors'] ?? array() ), (array) ( $s['progress']['skipped'] ?? array() ) ) ); \WP_CLI::error( 'Session failed: ' . ( $errors ? implode( '; ', array_unique( $errors ) ) : 'no requests were recorded.' ) ); } foreach ( array_unique( array_map( $describe, (array) ( $s['progress']['skipped'] ?? array() ) ) ) as $skipped ) { \WP_CLI::warning( $skipped ); } foreach ( (array) ( $s['progress']['missing'] ?? array() ) as $m ) { \WP_CLI::warning( sprintf( 'No measurement arrived for %s (pass %d); it is left out of the results.', $m['url'], $m['pass'] ) ); } if ( 'json' === $format ) { \WP_CLI::line( (string) wp_json_encode( $s['summary'] ) ); return; } $rows = array(); foreach ( (array) $s['summary']['owners'] as $o ) { $rows[] = array( 'rank' => empty( $o['excluded'] ) ? $o['rank'] : '-', 'plugin' => $o['name'], 'weight' => $o['weight'], 'php_ms' => $o['php_ms'], 'queries' => $o['queries'], 'front_kb' => $o['assets_kb'], 'autoload' => $o['autoload_kb'], 'cron_day' => $o['cron_per_day'], 'verdict' => implode( ' ', array_column( (array) $o['verdicts'], 'text' ) ), ); } \WP_CLI\Utils\format_items( $format, $rows, array( 'rank', 'plugin', 'weight', 'php_ms', 'queries', 'front_kb', 'autoload', 'cron_day', 'verdict' ) ); \WP_CLI::success( sprintf( 'Session #%d done: front-end avg %s ms, admin avg %s ms.', $id, $s['summary']['totals']['frontend_ms'], $s['summary']['totals']['admin_ms'] ) ); } /** * Autoload. * * @param array $args Args. * @param array $assoc Assoc. */ public function autoload( array $args, array $assoc ): void { $r = $this->plugin->get( AutoloadAnalyzer::class )->analyze( 5 ); $rows = array(); foreach ( $r['owners'] as $key => $o ) { $rows[] = array( 'owner' => (string) ( $r['descriptors'][ $key ]['name'] ?? $key ), 'kb' => round( $o['bytes'] / 1024, 1 ), 'options' => $o['count'], 'biggest' => (string) ( $o['rows'][0]['name'] ?? '' ), ); } \WP_CLI\Utils\format_items( (string) ( $assoc['format'] ?? 'table' ), $rows, array( 'owner', 'kb', 'options', 'biggest' ) ); \WP_CLI::log( sprintf( 'Total autoloaded: %s KB in %d options.', round( $r['total_bytes'] / 1024, 1 ), $r['total_count'] ) ); } /** * Cron. * * @param array $args Args. * @param array $assoc Assoc. */ public function cron( array $args, array $assoc ): void { $r = $this->plugin->get( CronAnalyzer::class )->analyze(); $rows = array(); foreach ( $r['owners'] as $key => $o ) { $rows[] = array( 'owner' => (string) ( $r['descriptors'][ $key ]['name'] ?? $key ), 'runs_day' => $o['per_day'], 'events' => count( $o['events'] ), 'hooks' => implode( ', ', array_slice( array_column( $o['events'], 'hook' ), 0, 3 ) ), ); } \WP_CLI\Utils\format_items( (string) ( $assoc['format'] ?? 'table' ), $rows, array( 'owner', 'runs_day', 'events', 'hooks' ) ); if ( $r['orphans'] ) { \WP_CLI::warning( count( $r['orphans'] ) . ' scheduled hook(s) have no callback: ' . implode( ', ', array_column( $r['orphans'], 'hook' ) ) ); } } }