Static Cache Wrangler Performance Profiler

The Static Cache Wrangler Performance Profiler is an optional Must-Use (MU) plugin that provides real-time performance metrics for developers running WordPress with the Static Cache Wrangler plugin.

It adds deep instrumentation hooks that measure execution time, memory usage, file I/O, and asynchronous asset processing—without affecting normal site operation.

When installed, it exposes a complete suite of WP-CLI commands for analyzing generation efficiency and system resource behavior across your static builds.

PHP
<?php
/**
 * Plugin Name: Static Cache Wrangler - Performance Profiler
 * Plugin URI: https://moderncli.dev/code/static-cache-wrangler/
 * Description: WP-CLI performance profiling companion for Static Cache Wrangler — monitors memory usage, execution time, and resource consumption.
 * Version: 1.0.0
 * Author: Derick Schaefer
 * Author URI: https://moderncli.dev/author/
 * Text Domain: stcw-profiler
 * Requires at least: 5.0
 * Requires PHP: 7.4
 * License: GPL v2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 *
 * This is a Must-Use (MU) plugin designed for CLI-only operation.
 */

if (!defined('ABSPATH')) exit;

// Plugin constants
define('STCW_PROFILER_VERSION', '1.0.0');
define('STCW_PROFILER_FILE', __FILE__);
define('STCW_PROFILER_DIR', plugin_dir_path(__FILE__));

/**
 * Main profiler class
 */
class STCW_Performance_Profiler {

    private $current_profile = [];
    private $current_async_batch = [];
    private static $instance = null;

    public static function get_instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        if (!$this->is_stcw_active()) {
            return;
        }

        $this->register_hooks();

        if (defined('WP_CLI') && WP_CLI) {
            WP_CLI::add_command('stcw profiler', 'STCW_Profiler_CLI');
        }
    }

    private function is_stcw_active() {
        return class_exists('STCW_Core');
    }

    private function register_hooks() {
        // Page generation profiling
        add_action('wp', [$this, 'start_page_profiling'], 0);
        add_action('wp', [$this, 'monitor_output_buffer_start'], 0);
        add_action('shutdown', [$this, 'end_page_profiling'], 999);

        // File save operations
        add_action('stcw_before_file_save', [$this, 'start_file_operation'], 10, 1);
        add_action('stcw_after_file_save', [$this, 'end_file_operation'], 10, 2);

        // Asset profiling (synchronous)
        add_filter('stcw_before_asset_download', [$this, 'start_asset_profiling'], 10, 1);
        add_filter('stcw_after_asset_download', [$this, 'end_asset_profiling'], 10, 2);

        // Async batch profiling
        add_action('stcw_before_asset_batch', [$this, 'start_async_batch']);
        add_action('stcw_after_asset_batch', [$this, 'end_async_batch'], 10, 2);
    }

    /*-------------------------------------
     * PAGE GENERATION PROFILING
     *-------------------------------------*/

    public function start_page_profiling() {
        if (is_admin() || !method_exists('STCW_Core', 'is_enabled') || !STCW_Core::is_enabled()) {
            return;
        }

        $request_uri = isset($_SERVER['REQUEST_URI'])
            ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI']))
            : '/';

        $this->current_profile = [
            'url'                => $request_uri,
            'start_time'         => microtime(true),
            'start_memory'       => memory_get_usage(),
            'start_peak_memory'  => memory_get_peak_usage(),
            'assets'             => [],
            'file_operations'    => [],
        ];
    }

    public function monitor_output_buffer_start() {
        if (empty($this->current_profile)) {
            return;
        }

        $this->current_profile['output_buffer_start_time']   = microtime(true);
        $this->current_profile['output_buffer_start_memory'] = memory_get_usage();
    }

    public function end_page_profiling() {
        if (empty($this->current_profile)) {
            return;
        }

        $end_time        = microtime(true);
        $end_memory      = memory_get_usage();
        $end_peak_memory = memory_get_peak_usage();

        $profile = [
            'url'                   => $this->current_profile['url'],
            'timestamp'             => current_time('mysql'),
            'generation_time'       => round(($end_time - $this->current_profile['start_time']) * 1000, 2),
            'memory_used'           => $this->format_bytes($end_memory - $this->current_profile['start_memory']),
            'memory_used_bytes'     => $end_memory - $this->current_profile['start_memory'],
            'peak_memory'           => $this->format_bytes($end_peak_memory),
            'peak_memory_bytes'     => $end_peak_memory,
            'peak_memory_increase'  => $this->format_bytes($end_peak_memory - $this->current_profile['start_peak_memory']),
            'assets_processed'      => count($this->current_profile['assets']),
            'file_operations'       => count($this->current_profile['file_operations']),
        ];

        // Capture total memory usage for entire PHP process
        $profile['total_memory_at_end'] = $this->format_bytes(memory_get_usage(true));

        if (isset($this->current_profile['output_buffer_start_time'])) {
            $ob_time                     = ($end_time - $this->current_profile['output_buffer_start_time']) * 1000;
            $profile['output_buffer_time']    = round($ob_time, 2);
            $profile['output_buffer_memory']  = $this->format_bytes(
                $end_memory - $this->current_profile['output_buffer_start_memory']
            );
        }

        if (!empty($this->current_profile['assets'])) {
            $profile['asset_details'] = $this->current_profile['assets'];
        }

        if (!empty($this->current_profile['file_operations'])) {
            $profile['file_operation_details'] = $this->current_profile['file_operations'];
        }

        $this->save_profile($profile);
        $this->current_profile = [];
    }

    /*-------------------------------------
     * FILE OPERATIONS
     *-------------------------------------*/

    public function start_file_operation($file_path) {
        if (empty($this->current_profile)) {
            return;
        }

        $op_key = md5($file_path);
        $this->current_profile['file_operations'][$op_key] = [
            'file'         => $file_path,
            'start_time'   => microtime(true),
            'start_memory' => memory_get_usage(),
        ];
    }

    public function end_file_operation($result, $file_path) {
        if (empty($this->current_profile)) {
            return;
        }

        $op_key = md5($file_path);
        if (!isset($this->current_profile['file_operations'][$op_key])) {
            return;
        }

        $start = $this->current_profile['file_operations'][$op_key];
        $this->current_profile['file_operations'][$op_key] = array_merge($start, [
            'end_time'    => microtime(true),
            'duration_ms' => round((microtime(true) - $start['start_time']) * 1000, 2),
            'memory_used' => $this->format_bytes(memory_get_usage() - $start['start_memory']),
            'success'     => $result,
        ]);
    }

    /*-------------------------------------
     * ASSET PROFILING (SYNC)
     *-------------------------------------*/

    public function start_asset_profiling($url) {
        if (empty($this->current_profile)) {
            return $url;
        }

        $asset_key = md5($url);
        $this->current_profile['assets'][$asset_key] = [
            'url'          => $url,
            'start_time'   => microtime(true),
            'start_memory' => memory_get_usage(),
        ];

        return $url;
    }

    public function end_asset_profiling($result, $url) {
        if (empty($this->current_profile)) {
            return $result;
        }

        $asset_key = md5($url);
        if (!isset($this->current_profile['assets'][$asset_key])) {
            return $result;
        }

        $start = $this->current_profile['assets'][$asset_key];
        $this->current_profile['assets'][$asset_key] = array_merge($start, [
            'end_time'    => microtime(true),
            'end_memory'  => memory_get_usage(),
            'duration_ms' => round((microtime(true) - $start['start_time']) * 1000, 2),
            'memory_used' => $this->format_bytes(memory_get_usage() - $start['start_memory']),
            'success'     => $result !== false,
            'file_size'   => $result !== false && file_exists($result) ? filesize($result) : 0,
        ]);

        return $result;
    }

    /*-------------------------------------
     * ASYNC BATCH PROFILING
     *-------------------------------------*/

    public function start_async_batch() {
        $this->current_async_batch = [
            'start_time'   => microtime(true),
            'start_memory' => memory_get_usage(),
        ];
    }

    public function end_async_batch($processed, $failed) {
        if (empty($this->current_async_batch)) {
            return;
        }

        $end_time  = microtime(true);
        $duration  = round(($end_time - $this->current_async_batch['start_time']) * 1000, 2);
        $mem_used  = memory_get_usage() - $this->current_async_batch['start_memory'];

        $profile = [
            'type'        => 'async_asset_batch',
            'timestamp'   => current_time('mysql'),
            'duration_ms' => $duration,
            'memory_used' => $this->format_bytes($mem_used),
            'processed'   => $processed,
            'failed'      => $failed,
        ];

        $this->save_profile($profile);
        $this->current_async_batch = [];
    }

    /*-------------------------------------
     * UTILITIES
     *-------------------------------------*/

    private function save_profile($profile) {
        $log = get_option('stcw_profiler_log', []);
        $log[] = $profile;

        $max_entries = apply_filters('stcw_profiler_max_entries', 100);
        $log = array_slice($log, -$max_entries);

        update_option('stcw_profiler_log', $log, false);
    }

    private function format_bytes($bytes, $precision = 2) {
        $units = ['B', 'KB', 'MB', 'GB'];
        $bytes = max($bytes, 0);
        $pow   = floor(($bytes ? log($bytes) : 0) / log(1024));
        $pow   = min($pow, count($units) - 1);
        $bytes /= pow(1024, $pow);
        return round($bytes, $precision) . ' ' . $units[$pow];
    }

    public static function get_logs() {
        return get_option('stcw_profiler_log', []);
    }

    public static function clear_logs() {
        delete_option('stcw_profiler_log');
    }

    public static function get_statistics() {
        $logs = self::get_logs();

        if (empty($logs)) {
            return [
                'total_profiles' => 0,
                'message'        => 'No profiling data available yet.',
            ];
        }

        $stats = [
            'total_profiles'        => count($logs),
            'avg_generation_time'   => 0,
            'max_generation_time'   => 0,
            'min_generation_time'   => PHP_INT_MAX,
            'avg_memory_used'       => 0,
            'max_memory_used'       => 0,
            'total_assets'          => 0,
            'total_file_operations' => 0,
        ];

        $total_time   = 0;
        $total_memory = 0;

        foreach ($logs as $log) {
            if (isset($log['type']) && $log['type'] === 'async_asset_batch') {
                continue;
            }

            $time    = $log['generation_time'];
            $memory  = $log['memory_used_bytes'];

            $total_time   += $time;
            $total_memory += $memory;

            $stats['max_generation_time'] = max($stats['max_generation_time'], $time);
            $stats['min_generation_time'] = min($stats['min_generation_time'], $time);
            $stats['max_memory_used']     = max($stats['max_memory_used'], $memory);
            $stats['total_assets']       += $log['assets_processed'];
            $stats['total_file_operations'] += $log['file_operations'];
        }

        $count = max(1, count(array_filter($logs, fn($l) => empty($l['type']))));
        $stats['avg_generation_time']     = round($total_time / $count, 2);
        $stats['avg_memory_used']         = round($total_memory / $count);
        $stats['avg_memory_used_formatted'] = self::format_bytes_static($stats['avg_memory_used']);
        $stats['max_memory_used_formatted'] = self::format_bytes_static($stats['max_memory_used']);

        // Async batch aggregation
        $async_batches = array_filter($logs, fn($l) => isset($l['type']) && $l['type'] === 'async_asset_batch');
        $stats['async_batches'] = count($async_batches);
        if ($async_batches) {
            $stats['avg_async_duration'] = round(array_sum(array_column($async_batches, 'duration_ms')) / count($async_batches), 2);
            $stats['total_async_assets'] = array_sum(array_column($async_batches, 'processed'));
        }

        return $stats;
    }

    private static function format_bytes_static($bytes, $precision = 2) {
        $units = ['B', 'KB', 'MB', 'GB'];
        $bytes = max($bytes, 0);
        $pow   = floor(($bytes ? log($bytes) : 0) / log(1024));
        $pow   = min($pow, count($units) - 1);
        $bytes /= pow(1024, $pow);
        return round($bytes, $precision) . ' ' . $units[$pow];
    }
}

/**
 * WP-CLI commands for profiler
 */
class STCW_Profiler_CLI {

    /**
     * Calculate average total PHP memory usage
     */
    private function get_average_total_memory() {
        $logs = STCW_Performance_Profiler::get_logs();
        $totals = [];

        foreach ($logs as $log) {
            if (isset($log['total_memory_at_end'])) {
                $value = floatval(preg_replace('/[^0-9.]/', '', $log['total_memory_at_end']));
                $totals[] = $value;
            }
        }

        if (empty($totals)) {
            return 'n/a';
        }

        $avg = array_sum($totals) / count($totals);
        return round($avg, 2) . ' MB';
    }

    /**
     * Show profiler statistics
     *
     * ## EXAMPLES
     *     wp stcw profiler stats
     *
     * @when after_wp_load
     */
    public function stats() {
        $stats = STCW_Performance_Profiler::get_statistics();

        if ($stats['total_profiles'] === 0) {
            WP_CLI::warning('No profiling data available yet.');
            return;
        }

        WP_CLI::log(WP_CLI::colorize('%GStatic Cache Wrangler - Performance Statistics%n'));
        WP_CLI::log(str_repeat('=', 60));
        WP_CLI::log('');
        WP_CLI::log('Total Profiles:          ' . number_format($stats['total_profiles']));
        WP_CLI::log('Total File Operations:   ' . number_format($stats['total_file_operations']));
        WP_CLI::log('');
        WP_CLI::log(WP_CLI::colorize('%YGeneration Time%n'));
        WP_CLI::log('  Average:  ' . $stats['avg_generation_time'] . ' ms');
        WP_CLI::log('  Maximum:  ' . $stats['max_generation_time'] . ' ms');
        WP_CLI::log('  Minimum:  ' . $stats['min_generation_time'] . ' ms');
        WP_CLI::log('');
        WP_CLI::log(WP_CLI::colorize('%YMemory Usage%n'));
        WP_CLI::log('  Average:  ' . $stats['avg_memory_used_formatted']);
        WP_CLI::log('  Maximum:  ' . $stats['max_memory_used_formatted']);
        WP_CLI::log('  Typical Total PHP Memory (end of request): ~' . $this->get_average_total_memory() . ' (including WordPress core)');

        if (!empty($stats['async_batches'])) {
            WP_CLI::log('');
            WP_CLI::log(WP_CLI::colorize('%YAsync Asset Batches%n'));
            WP_CLI::log('  Total Batches:  ' . $stats['async_batches']);
            WP_CLI::log('  Average Time:   ' . $stats['avg_async_duration'] . ' ms');
            WP_CLI::log('  Total Assets:   ' . $stats['total_async_assets']);
        }
    }


    /**
     * Show recent profiling logs
     */
    public function logs($args, $assoc_args) {
        $logs = STCW_Performance_Profiler::get_logs();

        if (empty($logs)) {
            WP_CLI::warning('No profiling data available yet.');
            return;
        }

        $count  = isset($assoc_args['count']) ? absint($assoc_args['count']) : 10;
        $format = isset($assoc_args['format']) ? $assoc_args['format'] : 'table';
        $logs   = array_slice($logs, -$count);

        // Separate async and page profiles
        $async = [];
        $pages = [];

        foreach ($logs as $log) {
            if (isset($log['type']) && $log['type'] === 'async_asset_batch') {
                $async[] = [
                    'Type'         => 'Async Batch',
                    'Duration (ms)' => $log['duration_ms'],
                    'Memory'       => $log['memory_used'],
                    'Processed'    => $log['processed'],
                    'Failed'       => $log['failed'],
                    'Timestamp'    => $log['timestamp'],
                ];
            } else {
                $pages[] = [
                    'URL'         => $log['url'] ?? '(N/A)',
                    'Time (ms)'   => $log['generation_time'] ?? '',
                    'Memory'      => $log['memory_used'] ?? '',
                    'Peak Memory' => $log['peak_memory'] ?? '',
                    'File Ops'    => $log['file_operations'] ?? 0,
                    'Timestamp'   => $log['timestamp'],
                ];
            }
        }

        // Page Generation Profiles
        if (!empty($pages)) {
            WP_CLI::log(WP_CLI::colorize('%YPage Generation Profiles%n'));
            WP_CLI::log(str_repeat('-', 60));
            WP_CLI\Utils\format_items($format, $pages, array_keys($pages[0]));
            WP_CLI::log('');
        }

        // Async Asset Batches
        if (!empty($async)) {
            WP_CLI::log(WP_CLI::colorize('%YAsync Asset Batches%n'));
            WP_CLI::log(str_repeat('-', 60));
            WP_CLI\Utils\format_items($format, $async, array_keys($async[0]));
        }
    }

    /**
     * Clear all profiling logs
     *
     * ## EXAMPLES
     *     wp stcw profiler clear
     *
     * @when after_wp_load
     */
    public function clear() {
        STCW_Performance_Profiler::clear_logs();
        WP_CLI::success('Profiling logs cleared.');
    }
}

// Initialize only when profiling is enabled
if (defined('STCW_PROFILING_ENABLED') && STCW_PROFILING_ENABLED) {
    add_action('plugins_loaded', function() {
        STCW_Performance_Profiler::get_instance();
    }, 15);
}
Expand