|
<?php |
|
/** |
|
* Benchmark: multisite option/post functions with vs. without switch_to_blog(). |
|
* |
|
* Run via: |
|
* npm run env:cli -- eval-file tests/performance/benchmarks/multisite-options.php \ |
|
* --path=/var/www/src --skip-wordpress=false |
|
* |
|
* Optional environment variables (pass via WP-CLI --exec-file or shell export): |
|
* BENCH_ITERATIONS Number of iterations per function (default 200). |
|
* BENCH_RUNS Warm-up+measure rounds per scenario (default 3). |
|
* BENCH_OUTPUT_FILE Absolute path to write JSON results (default STDOUT only). |
|
* |
|
* Output: JSON to STDOUT. |
|
*/ |
|
|
|
if ( ! defined( 'ABSPATH' ) ) { |
|
die( 'Must be run inside WordPress context via wp eval-file.' ); |
|
} |
|
|
|
if ( ! is_multisite() ) { |
|
fwrite( STDERR, "ERROR: This benchmark requires a multisite installation.\n" ); |
|
fwrite( STDERR, "Set LOCAL_MULTISITE=true in .env and run npm run env:install.\n" ); |
|
exit( 1 ); |
|
} |
|
|
|
// --------------------------------------------------------------------------- |
|
// Config |
|
// --------------------------------------------------------------------------- |
|
$iterations = isset( $_ENV['BENCH_ITERATIONS'] ) ? (int) $_ENV['BENCH_ITERATIONS'] : 200; |
|
$runs = isset( $_ENV['BENCH_RUNS'] ) ? (int) $_ENV['BENCH_RUNS'] : 3; |
|
$output_file = isset( $_ENV['BENCH_OUTPUT_FILE'] ) ? $_ENV['BENCH_OUTPUT_FILE'] : null; |
|
|
|
// --------------------------------------------------------------------------- |
|
// Helpers |
|
// --------------------------------------------------------------------------- |
|
|
|
/** |
|
* Returns elapsed wall-clock seconds using the highest-res timer available. |
|
*/ |
|
function bench_time(): float { |
|
return microtime( true ); |
|
} |
|
|
|
/** |
|
* Flushes as many caches as possible. |
|
*/ |
|
function bench_flush_cache(): void { |
|
wp_cache_flush(); |
|
if ( function_exists( 'opcache_reset' ) ) { |
|
opcache_reset(); |
|
} |
|
} |
|
|
|
/** |
|
* Compute median of a float array. |
|
*/ |
|
function bench_median( array $values ): float { |
|
if ( empty( $values ) ) { |
|
return 0.0; |
|
} |
|
sort( $values ); |
|
$n = count( $values ); |
|
$mid = intdiv( $n, 2 ); |
|
return ( $n % 2 === 0 ) ? ( $values[ $mid - 1 ] + $values[ $mid ] ) / 2 : $values[ $mid ]; |
|
} |
|
|
|
/** |
|
* Counts switch_to_blog() calls made during a callback execution. |
|
* |
|
* @param callable $callback |
|
* @return array{calls: int, elapsed: float} |
|
*/ |
|
function bench_run_and_count( callable $callback ): array { |
|
$switch_count = 0; |
|
$counter = static function () use ( &$switch_count ) { |
|
$switch_count++; |
|
}; |
|
add_action( 'switch_blog', $counter ); |
|
$start = bench_time(); |
|
$callback(); |
|
$elapsed = bench_time() - $start; |
|
remove_action( 'switch_blog', $counter ); |
|
return [ 'calls' => $switch_count, 'elapsed' => $elapsed ]; |
|
} |
|
|
|
// --------------------------------------------------------------------------- |
|
// Setup: create test sites |
|
// --------------------------------------------------------------------------- |
|
|
|
/** @var int[] $test_blog_ids */ |
|
$test_blog_ids = []; |
|
$num_sites = 5; |
|
$run_suffix = time(); // unique per run so repeated executions never collide |
|
|
|
fwrite( STDERR, "Setting up $num_sites test sites...\n" ); |
|
|
|
for ( $i = 1; $i <= $num_sites; $i++ ) { |
|
$blog_id = wpmu_create_blog( |
|
get_current_site()->domain, |
|
"/benchmark-$run_suffix-$i/", |
|
"Benchmark Site $i ($run_suffix)", |
|
get_current_user_id() |
|
); |
|
if ( is_wp_error( $blog_id ) ) { |
|
fwrite( STDERR, "Failed to create test site $i: " . $blog_id->get_error_message() . "\n" ); |
|
exit( 1 ); |
|
} |
|
$test_blog_ids[] = $blog_id; |
|
} |
|
|
|
// Seed each site with a test option and a test post. |
|
$option_name = 'bench_test_option'; |
|
$option_value = 'benchmark_value_' . time(); |
|
|
|
foreach ( $test_blog_ids as $blog_id ) { |
|
update_blog_option( $blog_id, $option_name, $option_value ); |
|
switch_to_blog( $blog_id ); |
|
$post_id = wp_insert_post( [ |
|
'post_title' => "Benchmark post for site $blog_id", |
|
'post_status' => 'publish', |
|
'post_type' => 'post', |
|
'post_content' => 'benchmark', |
|
] ); |
|
restore_current_blog(); |
|
// Store post ID for later retrieval. |
|
update_blog_option( $blog_id, 'bench_test_post_id', $post_id ); |
|
} |
|
|
|
fwrite( STDERR, "Test sites created. Starting benchmarks ($runs runs × $iterations iterations)...\n" ); |
|
|
|
// --------------------------------------------------------------------------- |
|
// Benchmark scenarios |
|
// --------------------------------------------------------------------------- |
|
|
|
/** @var array<string, array{cold: array<string,mixed>, warm: array<string,mixed>}> $results */ |
|
$results = []; |
|
|
|
/** |
|
* Run a single benchmark function across all test sites. |
|
* |
|
* @param string $label Human-readable label. |
|
* @param callable $fn Function under test. Receives ($blog_id, $iteration_index). |
|
* @param int[] $blog_ids Array of blog IDs to cycle through. |
|
* @param int $iter Iterations per run. |
|
* @param int $runs_count Number of measurement runs. |
|
* @param bool $cold Whether to flush caches before each run. |
|
*/ |
|
function bench_measure( string $label, callable $fn, array $blog_ids, int $iter, int $runs_count, bool $cold ): array { |
|
$run_times = []; |
|
$run_switches = []; |
|
|
|
for ( $r = 0; $r < $runs_count; $r++ ) { |
|
if ( $cold ) { |
|
bench_flush_cache(); |
|
} |
|
|
|
$result = bench_run_and_count( static function () use ( $fn, $iter, $blog_ids ) { |
|
$n = count( $blog_ids ); |
|
for ( $i = 0; $i < $iter; $i++ ) { |
|
$blog_id = $blog_ids[ $i % $n ]; |
|
$fn( $blog_id, $i ); |
|
} |
|
} ); |
|
|
|
$run_times[] = $result['elapsed']; |
|
$run_switches[] = $result['calls']; |
|
} |
|
|
|
$total_calls = $iter * $runs_count; // Not used directly but useful for context. |
|
$median_elapsed = bench_median( $run_times ); |
|
$median_ms_per = ( $median_elapsed / $iter ) * 1000; |
|
$total_switches = (int) bench_median( $run_switches ); |
|
|
|
return [ |
|
'label' => $label, |
|
'iterations' => $iter, |
|
'runs' => $runs_count, |
|
'median_elapsed_sec' => round( $median_elapsed, 6 ), |
|
'median_ms_per_call' => round( $median_ms_per, 6 ), |
|
'switch_count' => $total_switches, |
|
'raw_times_sec' => array_map( fn( $t ) => round( $t, 6 ), $run_times ), |
|
'raw_switches' => $run_switches, |
|
]; |
|
} |
|
|
|
// --------------------------------------------------------------------------- |
|
// 1. get_blog_option() |
|
// --------------------------------------------------------------------------- |
|
foreach ( [ 'cold' => true, 'warm' => false ] as $scenario => $cold ) { |
|
$results['get_blog_option'][ $scenario ] = bench_measure( |
|
"get_blog_option ($scenario)", |
|
static function ( int $blog_id ) use ( $option_name ) { |
|
get_blog_option( $blog_id, $option_name ); |
|
}, |
|
$test_blog_ids, |
|
$iterations, |
|
$runs, |
|
$cold |
|
); |
|
fwrite( STDERR, " get_blog_option [$scenario]: done\n" ); |
|
} |
|
|
|
// --------------------------------------------------------------------------- |
|
// 2. update_blog_option() |
|
// --------------------------------------------------------------------------- |
|
foreach ( [ 'cold' => true, 'warm' => false ] as $scenario => $cold ) { |
|
$results['update_blog_option'][ $scenario ] = bench_measure( |
|
"update_blog_option ($scenario)", |
|
static function ( int $blog_id, int $i ) use ( $option_name ) { |
|
update_blog_option( $blog_id, $option_name, 'value_' . $i ); |
|
}, |
|
$test_blog_ids, |
|
$iterations, |
|
$runs, |
|
$cold |
|
); |
|
fwrite( STDERR, " update_blog_option [$scenario]: done\n" ); |
|
} |
|
|
|
// --------------------------------------------------------------------------- |
|
// 3. add_blog_option() + delete_blog_option() (alternating pairs) |
|
// --------------------------------------------------------------------------- |
|
foreach ( [ 'cold' => true, 'warm' => false ] as $scenario => $cold ) { |
|
$temp_option = 'bench_temp_' . uniqid(); |
|
$results['add_delete_blog_option'][ $scenario ] = bench_measure( |
|
"add+delete_blog_option ($scenario)", |
|
static function ( int $blog_id, int $i ) use ( $temp_option ) { |
|
$key = $temp_option . "_$i"; |
|
add_blog_option( $blog_id, $key, 'val' ); |
|
delete_blog_option( $blog_id, $key ); |
|
}, |
|
$test_blog_ids, |
|
$iterations, |
|
$runs, |
|
$cold |
|
); |
|
fwrite( STDERR, " add+delete_blog_option [$scenario]: done\n" ); |
|
} |
|
|
|
// --------------------------------------------------------------------------- |
|
// 4. get_blog_post() |
|
// --------------------------------------------------------------------------- |
|
// Build a map of blog_id => post_id from the seeded data. |
|
$bench_post_ids = []; |
|
foreach ( $test_blog_ids as $blog_id ) { |
|
$bench_post_ids[ $blog_id ] = (int) get_blog_option( $blog_id, 'bench_test_post_id' ); |
|
} |
|
|
|
foreach ( [ 'cold' => true, 'warm' => false ] as $scenario => $cold ) { |
|
$results['get_blog_post'][ $scenario ] = bench_measure( |
|
"get_blog_post ($scenario)", |
|
static function ( int $blog_id ) use ( $bench_post_ids ) { |
|
get_blog_post( $blog_id, $bench_post_ids[ $blog_id ] ); |
|
}, |
|
$test_blog_ids, |
|
$iterations, |
|
$runs, |
|
$cold |
|
); |
|
fwrite( STDERR, " get_blog_post [$scenario]: done\n" ); |
|
} |
|
|
|
// --------------------------------------------------------------------------- |
|
// 5. WP_Site::__get('blogname') |
|
// --------------------------------------------------------------------------- |
|
|
|
foreach ( [ 'cold' => true, 'warm' => false ] as $scenario => $cold ) { |
|
$results['wp_site_blogname'][ $scenario ] = bench_measure( |
|
"WP_Site::blogname ($scenario)", |
|
static function ( int $blog_id ) { |
|
// Access blogname through WP_Site object (triggers get_details() internally). |
|
$site = get_site( $blog_id ); |
|
if ( $site ) { |
|
$_ = $site->blogname; // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition |
|
} |
|
}, |
|
$test_blog_ids, |
|
$iterations, |
|
$runs, |
|
$cold |
|
); |
|
fwrite( STDERR, " WP_Site::blogname [$scenario]: done\n" ); |
|
} |
|
|
|
// --------------------------------------------------------------------------- |
|
// Cleanup |
|
// --------------------------------------------------------------------------- |
|
fwrite( STDERR, "Cleaning up test sites...\n" ); |
|
foreach ( $test_blog_ids as $blog_id ) { |
|
wpmu_delete_blog( $blog_id, true ); |
|
} |
|
|
|
// --------------------------------------------------------------------------- |
|
// Output |
|
// --------------------------------------------------------------------------- |
|
$meta = [ |
|
'php_version' => PHP_VERSION, |
|
'wp_version' => get_bloginfo( 'version' ), |
|
'is_multisite' => is_multisite(), |
|
'iterations' => $iterations, |
|
'runs' => $runs, |
|
'timestamp' => gmdate( 'c' ), |
|
]; |
|
|
|
$output = json_encode( [ 'meta' => $meta, 'results' => $results ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); |
|
|
|
if ( $output_file ) { |
|
file_put_contents( $output_file, $output ); |
|
fwrite( STDERR, "Results written to: $output_file\n" ); |
|
} else { |
|
echo $output . "\n"; |
|
} |
|
|
|
fwrite( STDERR, "Benchmark complete.\n" ); |