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