autoload = $autoload; $this->cron = $cron; } /** * Summarise. * * @param array $session Session row. * @param array $requests Request rows (decoded). */ public function summarize( array $session, array $requests ): array { $owner_desc = array(); $by_url = array(); // url => pass => record. $duplicates = array(); $queries_off = false; foreach ( $requests as $r ) { $by_url[ $r['url'] ][ $r['pass'] ] = $r; if ( isset( $r['data']['meta']['savequeries'] ) && ! $r['data']['meta']['savequeries'] ) { $queries_off = true; // SAVEQUERIES defined as false in wp-config.php: the queries stay empty. } foreach ( (array) ( $r['data']['descriptors'] ?? array() ) as $k => $d ) { $owner_desc[ $k ] = Owner::label( (array) $d ); } if ( ! empty( $r['data']['meta']['is_admin'] ) ) { continue; // Duplicate libraries are a front-end finding: what admin pages load is left out. } foreach ( (array) ( $r['data']['duplicates'] ?? array() ) as $d ) { $duplicates[ $d['library'] . '|' . implode( ',', $d['owners'] ) ] = $d; } } $urls = array(); $owners = array(); foreach ( $by_url as $url => $passes ) { $n = count( $passes ); $totals = array_map( static function ( $r ) { return (float) $r['total_ms']; }, $passes ); $queries = array_map( static function ( $r ) { return (int) $r['query_count']; }, $passes ); $is_admin = false; $overhead = 0.0; $mem = 0; foreach ( $passes as $r ) { $is_admin = ! empty( $r['data']['meta']['is_admin'] ); $overhead += (float) ( $r['data']['meta']['overhead_ms'] ?? 0 ); $mem = max( $mem, (int) $r['peak_mem_kb'] ); } $urls[ $url ] = array( 'url' => $url, 'label' => UrlSet::describe( (string) $url )['label'], 'is_admin' => $is_admin, 'passes' => $n, 'total_ms' => round( array_sum( $totals ) / $n, 2 ), 'min_ms' => round( min( $totals ), 2 ), 'max_ms' => round( max( $totals ), 2 ), 'queries' => (int) round( array_sum( $queries ) / $n ), 'overhead_ms' => round( $overhead / $n, 2 ), 'peak_mem_kb' => $mem, ); // Per owner per URL averages. $acc = array(); foreach ( $passes as $r ) { foreach ( (array) ( $r['data']['owners'] ?? array() ) as $key => $o ) { if ( ! isset( $acc[ $key ] ) ) { $acc[ $key ] = array( 'php' => array(), 'incl' => array(), 'queries' => array(), 'query_ms' => array(), 'bytes' => 0, 'unknown' => 0, 'blocking' => 0, 'blk_b' => 0, 'hooks' => array(), 'slowest' => array(), 'assets' => array(), ); } $acc[ $key ]['php'][] = (float) ( $o['php_ms_self'] ?? 0 ); $acc[ $key ]['incl'][] = (float) ( $o['php_ms_incl'] ?? 0 ); $acc[ $key ]['queries'][] = (int) ( $o['queries']['count'] ?? 0 ); $acc[ $key ]['query_ms'][] = (float) ( $o['queries']['ms'] ?? 0 ); $acc[ $key ]['bytes'] = max( $acc[ $key ]['bytes'], (int) ( $o['asset_bytes'] ?? 0 ) ); $acc[ $key ]['unknown'] = max( $acc[ $key ]['unknown'], (int) ( $o['assets_unknown'] ?? 0 ) ); $acc[ $key ]['blocking'] = max( $acc[ $key ]['blocking'], (int) ( $o['blocking'] ?? 0 ) ); $acc[ $key ]['blk_b'] = max( $acc[ $key ]['blk_b'], self::blocking_bytes( (array) $o ) ); foreach ( (array) ( $o['hooks'] ?? array() ) as $h ) { $acc[ $key ]['hooks'][ $h['hook'] ] = ( $acc[ $key ]['hooks'][ $h['hook'] ] ?? 0 ) + (float) $h['ms'] / $n; } foreach ( (array) ( $o['queries']['slowest'] ?? array() ) as $q ) { $acc[ $key ]['slowest'][] = $q + array( 'url' => $url ); } foreach ( (array) ( $o['assets'] ?? array() ) as $a ) { $acc[ $key ]['assets'][ $a['type'] . ':' . $a['handle'] ] = $a; } } } foreach ( $acc as $key => $a ) { if ( ! isset( $owners[ $key ] ) ) { $owners[ $key ] = array( 'key' => $key, 'per_url' => array(), 'hooks' => array(), 'slowest' => array(), 'assets' => array(), 'php_samples' => array(), 'incl_samples' => array(), ); } $owners[ $key ]['per_url'][ $url ] = array( 'php_ms' => round( array_sum( $a['php'] ) / $n, 2 ), 'php_min' => round( min( $a['php'] ), 2 ), 'php_max' => round( max( $a['php'] ), 2 ), 'php_incl_ms' => round( array_sum( $a['incl'] ) / $n, 2 ), 'php_pct' => $urls[ $url ]['total_ms'] > 0 ? round( ( array_sum( $a['php'] ) / $n ) / $urls[ $url ]['total_ms'] * 100, 1 ) : 0.0, 'queries' => (int) round( array_sum( $a['queries'] ) / $n ), 'query_ms' => round( array_sum( $a['query_ms'] ) / $n, 2 ), 'assets_kb' => round( $a['bytes'] / 1024, 1 ), 'assets_unknown' => $a['unknown'], 'blocking' => $a['blocking'], 'blocking_kb' => round( $a['blk_b'] / 1024, 1 ), ); $owners[ $key ]['php_samples'] = array_merge( $owners[ $key ]['php_samples'], $a['php'] ); $owners[ $key ]['incl_samples'] = array_merge( $owners[ $key ]['incl_samples'], $a['incl'] ); foreach ( $a['hooks'] as $hook => $ms ) { $owners[ $key ]['hooks'][ $hook ] = ( $owners[ $key ]['hooks'][ $hook ] ?? 0 ) + $ms; } $owners[ $key ]['slowest'] = array_merge( $owners[ $key ]['slowest'], $a['slowest'] ); if ( ! $is_admin ) { $owners[ $key ]['assets'] = $owners[ $key ]['assets'] + $a['assets']; // Front-end assets only. } } } $autoload = $this->autoload->analyze(); $cron = $this->cron->analyze(); foreach ( array_keys( $autoload['owners'] ) as $key ) { if ( ! isset( $owners[ $key ] ) && ( isset( $owner_desc[ $key ] ) || isset( $autoload['descriptors'][ $key ] ) ) ) { $owners[ $key ] = array( 'key' => $key, 'per_url' => array(), 'hooks' => array(), 'slowest' => array(), 'assets' => array(), 'php_samples' => array( 0.0 ), 'incl_samples' => array( 0.0 ), ); } } $n_urls = max( 1, count( $urls ) ); $excluded = self::excluded_key( (string) ( $session['exclude_slug'] ?? '' ) ); foreach ( $owners as $key => $o ) { $desc = $owner_desc[ $key ] ?? $autoload['descriptors'][ $key ] ?? array( 'key' => $key, 'type' => 'unknown', 'name' => $key, ); $slug = self::slug( $desc ); $php = $o['php_samples']; $sum_php = 0.0; $sum_q = 0; $sum_qms = 0.0; $kb = 0.0; $unknown = 0; $block = 0; $blk_kb = 0.0; $adm_kb = 0.0; foreach ( $o['per_url'] as $url => $u ) { $sum_php += $u['php_ms']; $sum_q += $u['queries']; $sum_qms += $u['query_ms']; if ( ! empty( $urls[ $url ]['is_admin'] ) ) { $adm_kb = max( $adm_kb, $u['assets_kb'] ); // Reported apart: admin pages are not front-end pages. continue; } $kb = max( $kb, $u['assets_kb'] ); $unknown = max( $unknown, $u['assets_unknown'] ); $block = max( $block, $u['blocking'] ); $blk_kb = max( $blk_kb, $u['blocking_kb'] ); } arsort( $o['hooks'] ); usort( $o['slowest'], static function ( $x, $y ) { return $y['ms'] <=> $x['ms']; } ); $dups = array(); foreach ( $duplicates as $d ) { if ( in_array( $key, (array) $d['owners'], true ) ) { $lib = (string) $d['library']; foreach ( (array) $d['owners'] as $ok ) { if ( $ok !== $key ) { $dups[ $lib ]['others'][ $ok ] = (string) ( $owner_desc[ $ok ]['name'] ?? $ok ); } } $dups[ $lib ]['library'] = $lib; // One entry per library, others merged across URLs. } } $dups = array_values( array_map( static function ( $d ) { return array( 'library' => $d['library'], 'others' => array_values( $d['others'] ), ); }, $dups ) ); $row = array( 'key' => $key, 'name' => (string) $desc['name'], 'type' => (string) $desc['type'], 'slug' => $slug, 'file' => (string) ( $desc['file'] ?? '' ), 'installed' => ! empty( $desc['installed'] ), 'excluded' => $key === $excluded, 'php_ms' => round( $sum_php / $n_urls, 2 ), 'php_min' => round( min( $php ), 2 ), 'php_max' => round( max( $php ), 2 ), 'php_incl_ms' => round( array_sum( $o['incl_samples'] ) / max( 1, count( $o['incl_samples'] ) ), 2 ), 'queries' => (int) round( $sum_q / $n_urls ), 'query_ms' => round( $sum_qms / $n_urls, 2 ), 'assets_kb' => $kb, 'assets_unknown' => $unknown, 'admin_assets_kb' => $adm_kb, 'blocking' => $block, 'blocking_kb' => $blk_kb, 'autoload_kb' => round( (float) ( $autoload['owners'][ $key ]['bytes'] ?? 0 ) / 1024, 1 ), 'autoload_count' => (int) ( $autoload['owners'][ $key ]['count'] ?? 0 ), 'autoload_rows' => (array) ( $autoload['owners'][ $key ]['rows'] ?? array() ), 'cron_per_day' => (float) ( $cron['owners'][ $key ]['per_day'] ?? 0 ), 'cron_events' => (array) ( $cron['owners'][ $key ]['events'] ?? array() ), 'cron_interval_min' => (float) ( $cron['owners'][ $key ]['min_interval_min'] ?? 0 ), 'per_url' => $o['per_url'], 'top_hooks' => array_slice( array_map( static function ( $hook, $ms ) use ( $n_urls ) { return array( 'hook' => $hook, 'ms' => round( $ms / $n_urls, 2 ), ); }, array_keys( $o['hooks'] ), $o['hooks'] ), 0, 15 ), 'slowest_queries' => array_slice( $o['slowest'], 0, 10 ), 'assets' => array_values( $o['assets'] ), 'duplicates' => $dups, ); $row['verdicts'] = Verdicts::for_owner( $row, $urls, $slug ); $owners[ $key ] = $row; } $owners = Scoring::apply( $owners ); $front = array(); $admin = array(); $q_all = 0; foreach ( $urls as $u ) { if ( $u['is_admin'] ) { $admin[] = $u['total_ms']; } else { $front[] = $u['total_ms']; } $q_all += $u['queries']; } return array( 'owners' => $owners, 'urls' => $urls, 'totals' => array( 'frontend_ms' => $front ? round( array_sum( $front ) / count( $front ), 2 ) : 0.0, 'admin_ms' => $admin ? round( array_sum( $admin ) / count( $admin ), 2 ) : 0.0, 'queries' => $q_all, 'requests' => count( $requests ), ), 'duplicates' => array_values( $duplicates ), 'autoload' => array( 'total_kb' => round( $autoload['total_bytes'] / 1024, 1 ), 'unknown' => $autoload['unknown'], ), 'cron' => array( 'total_per_day' => $cron['total_per_day'], 'orphans' => $cron['orphans'], ), 'exclude' => (string) ( $session['exclude_slug'] ?? '' ), 'object_cache' => (bool) wp_using_ext_object_cache(), 'queries_off' => $queries_off, 'generated_at' => current_time( 'mysql', true ), 'version' => BLOATWRIGHT_VERSION, ); } /** * Size of an owner's render-blocking assets in one request record: the recorded total, or (for * records written before it was stored) the sum over its blocking assets. * * @param array $o Owner entry of a request record. */ public static function blocking_bytes( array $o ): int { if ( isset( $o['blocking_bytes'] ) ) { return (int) $o['blocking_bytes']; } $bytes = 0; foreach ( (array) ( $o['assets'] ?? array() ) as $a ) { if ( ! empty( $a['blocking'] ) ) { $bytes += max( 0, (int) ( $a['bytes'] ?? 0 ) ) + (int) ( $a['inline'] ?? 0 ); } } return $bytes; } /** * Owner key of the plugin file a "try without" session filtered out ('' when none). * * @param string $file Plugin file, e.g. "example-slider/example-slider.php". */ public static function excluded_key( string $file ): string { if ( '' === $file ) { return ''; } $parts = explode( '/', $file ); return 'plugin:' . ( count( $parts ) > 1 ? $parts[0] : preg_replace( '/\.php$/', '', $parts[0] ) ); } /** * Flag the row of the plugin a "try without" session filtered out, for summaries stored before * rows carried the flag. Rank and weight stay as stored; the screens hide them for this row. * * @param array $owners Owner summaries (key => row). * @param string $file Plugin file left out of the session ('' when none). * @return array */ public static function mark_excluded( array $owners, string $file ): array { $key = self::excluded_key( $file ); if ( '' !== $key && isset( $owners[ $key ] ) && ! isset( $owners[ $key ]['excluded'] ) ) { $owners[ $key ]['excluded'] = true; $owners[ $key ]['verdicts'] = array( Verdicts::excluded() ); } return $owners; } /** * Directory slug from a descriptor. * * @param array $desc Descriptor. */ public static function slug( array $desc ): string { $key = (string) ( $desc['key'] ?? '' ); if ( 0 === strpos( $key, 'plugin:' ) ) { return substr( $key, 7 ); } if ( 0 === strpos( $key, 'theme:' ) ) { return substr( $key, 6 ); } return $key; } }