Skip to content

Instantly share code, notes, and snippets.

@prettyboymp
Last active October 15, 2025 18:12
Show Gist options
  • Save prettyboymp/6999924cb865f060d5b374efe5ec9e33 to your computer and use it in GitHub Desktop.
Save prettyboymp/6999924cb865f060d5b374efe5ec9e33 to your computer and use it in GitHub Desktop.
Test script to analyze performance metrics for WooCommerce BATCH requests to products.
<?php
/**
* WooCommerce Batch API Test Script with PHP-SPX Profiling Support
*
* This script tests the WooCommerce batch endpoint for products with images
* to help identify performance issues as described in GitHub issue #26029
*
* Usage:
* 1. Configure your WooCommerce credentials below
* 2. Run with php-spx: SPX_ENABLED=1 SPX_KEY=dev php woocommerce_batch_test.php
* 3. Or access via web with SPX UI enabled
*/
// Configuration - Update these with your WooCommerce details
define( 'WC_STORE_URL', 'https://your-site.com' );
// Note: Use a username/password for these - the generated code is using basic auth, not API consumer auth.
define( 'WC_CONSUMER_KEY', 'ck_your_consumer_key_here' );
define( 'WC_CONSUMER_SECRET', 'cs_your_consumer_secret_here' );
// Test configuration
define( 'BATCH_SIZE', 100 ); // Number of products per batch (max 100)
define( 'IMAGES_PER_PRODUCT', 0 ); // Number of images per product
define( 'TEST_ITERATIONS', 5 ); // How many batches to test
class WooCommerceBatchTester {
private $store_url;
private $consumer_key;
private $consumer_secret;
private $auth_header;
// Caches for fetched terms to avoid repeated network calls within a run
private $random_tags = [];
private $random_categories = [];
public function __construct( $store_url, $consumer_key, $consumer_secret ) {
$this->store_url = rtrim( $store_url, '/' );
$this->consumer_key = $consumer_key;
$this->consumer_secret = $consumer_secret;
$this->auth_header = 'Basic ' . base64_encode( $consumer_key . ':' . $consumer_secret );
}
/**
* Fetch all items from a paginated WC endpoint and return as a flat array.
* $endpoint_path should be like '/wp-json/wc/v3/products/tags'
*/
private function getAllPaginated( $endpoint_path, $per_page = 100 ) {
$results = [];
$page = 1;
// Respect WooCommerce API pagination headers if needed; here loop until empty page.
while ( true ) {
$endpoint = $this->store_url . $endpoint_path . '?per_page=' . intval( $per_page ) . '&page=' . intval( $page );
$response = $this->makeRequest( $endpoint, 'GET' );
if ( $response['http_code'] !== 200 ) {
// Break on error; return what’s gathered so far
break;
}
$items = $response['response'];
if ( ! is_array( $items ) || empty( $items ) ) {
break;
}
$results = array_merge( $results, $items );
if ( count( $items ) < $per_page ) {
break;
}
$page ++;
if ( $page > 1000 ) { // safety cap
break;
}
}
return $results;
}
/**
* Fetch a random set of up to 10 existing product tags from the remote site.
* Returns an array of ['id' => int, 'name' => string] compatible with WC product payloads.
*/
private function fetchRandomTags( $max = 10 ) {
if ( ! empty( $this->random_tags ) ) {
return $this->random_tags;
}
$all_tags = $this->getAllPaginated( '/wp-json/wc/v3/products/tags' );
if ( empty( $all_tags ) ) {
// If no tags exist, create a small fallback set of new tags by name (WC will create them).
$fallback = [];
$names = [
'batch-test',
'performance-test',
'test-product',
'demo',
'sample',
'load-test',
'profiling',
'automation',
'api',
'spx'
];
foreach ( $names as $n ) {
$fallback[] = [ 'name' => $n ];
}
// Keep up to $max
$this->random_tags = array_slice( $fallback, 0, $max );
return $this->random_tags;
}
// Randomize and take up to $max, using tag IDs
shuffle( $all_tags );
$picked = array_slice( $all_tags, 0, min( $max, count( $all_tags ) ) );
$this->random_tags = array_map( function ( $tag ) {
// Use IDs so we reference existing terms
return [ 'id' => $tag['id'] ];
}, $picked );
return $this->random_tags;
}
/**
* Fetch a random set of up to 10 existing product categories from the remote site.
* Returns an array of ['id' => int, 'name' => string].
*/
private function fetchRandomCategories( $max = 10 ) {
if ( ! empty( $this->random_categories ) ) {
return $this->random_categories;
}
$all_cats = $this->getAllPaginated( '/wp-json/wc/v3/products/categories' );
if ( empty( $all_cats ) ) {
// If no categories exist, create a single default placeholder that WC may create by name
$this->random_categories = [ [ 'name' => 'Uncategorized' ] ];
return $this->random_categories;
}
// Randomize and take up to $max
shuffle( $all_cats );
$picked = array_slice( $all_cats, 0, min( $max, count( $all_cats ) ) );
$this->random_categories = array_map( function ( $cat ) {
return [ 'id' => $cat['id'] ];
}, $picked );
return $this->random_categories;
}
/**
* Pick a random subset of tags from the pre-fetched list.
* Returns between 1 and 5 tags (or fewer if the pool is small).
*/
private function pickRandomTagsSubset( $pool, $min = 1, $max = 5 ) {
if ( empty( $pool ) ) {
return [];
}
$upper = min( $max, count( $pool ) );
$lower = min( $min, $upper );
$count = rand( $lower, $upper );
// Shuffle a copy and take the first $count
$copy = $pool;
shuffle( $copy );
return array_slice( $copy, 0, $count );
}
/**
* Pick exactly one random category from the pre-fetched list.
*/
private function pickOneRandomCategory( $pool ) {
if ( empty( $pool ) ) {
return [];
}
return [ $pool[ array_rand( $pool ) ] ];
}
/**
* Generate test product data with images, random category, and random tags
*/
public function generateTestProducts( $count = 10, $images_per_product = 3 ) {
$products = [];
// Fetch random sets of 10 tags and 10 categories once per generation
$tag_pool = $this->fetchRandomTags( 10 );
$category_pool = $this->fetchRandomCategories( 10 );
// Sample image URLs for testing (using placeholder images)
$sample_images = [
'http://localhost:8889/wp-content/uploads/2025/01/img-ea.png',
'http://localhost:8889/wp-content/uploads/2025/01/img-ducimus.png',
'http://localhost:8889/wp-content/uploads/2025/01/img-dolorum.png',
'http://localhost:8889/wp-content/uploads/2025/01/img-dolorem.png',
'http://localhost:8889/wp-content/uploads/2025/01/img-vero.png',
];
for ( $i = 1; $i <= $count; $i ++ ) {
$images = [];
// Add images to product
for ( $j = 0; $j < $images_per_product; $j ++ ) {
$images[] = [
'src' => $sample_images[ ( $i + $j ) % count( $sample_images ) ],
'name' => "Test Image " . ( $j + 1 ) . " for Product " . $i,
'alt' => "Alt text for image " . ( $j + 1 ),
'position' => $j
];
}
// Random category (exactly one)
$categories = $this->pickOneRandomCategory( $category_pool );
// Random tags (1 to 5)
$tags = $this->pickRandomTagsSubset( $tag_pool, 1, 5 );
$products[] = [
'name' => 'Batch Test Product ' . $i . ' - ' . date( 'Y-m-d H:i:s' ),
'type' => 'simple',
'regular_price' => number_format( rand( 10, 999 ) + ( rand( 0, 99 ) / 100 ), 2 ),
'description' => 'This is a test product created for batch performance testing. Product number: ' . $i,
'short_description' => 'Test product #' . $i . ' for performance analysis.',
'sku' => 'BATCH-TEST-' . $i . '-' . time(),
'status' => 'publish',
'catalog_visibility' => 'visible',
'stock_status' => 'instock',
'manage_stock' => true,
'stock_quantity' => rand( 10, 100 ),
'images' => $images,
'categories' => $categories, // exactly one random category
'tags' => $tags, // 1–5 random tags
'meta_data' => [
[
'key' => '_test_batch_id',
'value' => 'batch_' . date( 'YmdHis' )
],
[
'key' => '_test_product_number',
'value' => $i
]
]
];
}
return $products;
}
/**
* Execute batch create request
*/
public function batchCreateProducts( $products ) {
$endpoint = $this->store_url . '/wp-json/wc/v3/products/batch';
$data = [
'create' => $products
];
$start_time = microtime( true );
$response = $this->makeRequest( $endpoint, 'POST', $data );
$end_time = microtime( true );
$execution_time = $end_time - $start_time;
return [
'response' => $response,
'execution_time' => $execution_time,
'products_count' => count( $products ),
'endpoint' => $endpoint
];
}
/**
* Execute batch update request
*/
public function batchUpdateProducts( $product_ids ) {
$endpoint = $this->store_url . '/wp-json/wc/v3/products/batch';
// Get the same random pools used during creation for consistency
$tag_pool = $this->fetchRandomTags( 10 );
$category_pool = $this->fetchRandomCategories( 10 );
$updates = [];
foreach ( $product_ids as $id ) {
// Random category (exactly one)
$categories = $this->pickOneRandomCategory( $category_pool );
// Random tags (1 to 5)
$tags = $this->pickRandomTagsSubset( $tag_pool, 1, 5 );
$updates[] = [
'id' => $id,
'regular_price' => number_format( rand( 20, 1999 ) + ( rand( 0, 99 ) / 100 ), 2 ),
'stock_quantity' => rand( 5, 150 ),
'categories' => $categories, // exactly one random category
'tags' => $tags // 1–5 random tags
];
}
$data = [
'update' => $updates
];
$start_time = microtime( true );
$response = $this->makeRequest( $endpoint, 'POST', $data );
$end_time = microtime( true );
$execution_time = $end_time - $start_time;
return [
'response' => $response,
'execution_time' => $execution_time,
'products_count' => count( $product_ids ),
'endpoint' => $endpoint
];
}
/**
* Make HTTP request to WooCommerce API
*/
private function makeRequest( $endpoint, $method = 'GET', $data = null ) {
$curl = curl_init();
$curl_options = [
CURLOPT_URL => $endpoint,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: ' . $this->auth_header,
'Content-Type: application/json',
'User-Agent: WooCommerce-Batch-Tester/1.0'
],
CURLOPT_TIMEOUT => 300, // 5 minutes timeout
CURLOPT_CONNECTTIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => false, // Only for testing
CURLOPT_VERBOSE => false
];
if ( $method === 'POST' && $data ) {
$curl_options[ CURLOPT_POST ] = true;
$curl_options[ CURLOPT_POSTFIELDS ] = json_encode( $data );
}
curl_setopt_array( $curl, $curl_options );
$spxCookies = 'SPX_ENABLED=1;SPX_KEY=dev'; // Use your SPX_KEY value
curl_setopt( $curl, CURLOPT_COOKIE, $spxCookies );
$response = curl_exec( $curl );
$http_code = curl_getinfo( $curl, CURLINFO_HTTP_CODE );
$curl_error = curl_error( $curl );
curl_close( $curl );
if ( $curl_error ) {
throw new Exception( 'cURL Error: ' . $curl_error );
}
$decoded_response = json_decode( $response, true );
return [
'http_code' => $http_code,
'response' => $decoded_response,
'raw_response' => $response
];
}
/**
* Run comprehensive batch tests
*/
public function runBatchTests() {
echo "=== WooCommerce Batch API Performance Test ===\n";
echo "Store URL: " . $this->store_url . "\n";
echo "Batch Size: " . BATCH_SIZE . " products\n";
echo "Images per Product: " . IMAGES_PER_PRODUCT . "\n";
echo "Test Iterations: " . TEST_ITERATIONS . "\n";
echo "Starting tests at: " . date( 'Y-m-d H:i:s' ) . "\n\n";
$total_start_time = microtime( true );
$created_product_ids = [];
// Arrays to track timing data for percentile calculations
$create_times = [];
$update_times = [];
$create_successes = 0;
$update_successes = 0;
try {
// Test batch creation
for ( $iteration = 1; $iteration <= TEST_ITERATIONS; $iteration ++ ) {
echo "--- Iteration $iteration ---\n";
// Generate test products
$products = $this->generateTestProducts( BATCH_SIZE, IMAGES_PER_PRODUCT );
echo "Testing batch CREATE with " . count( $products ) . " products...\n";
$create_result = $this->batchCreateProducts( $products );
$create_time = $create_result['execution_time'];
$create_times[] = $create_time;
echo "Batch CREATE completed in: " . number_format( $create_time, 2 ) . " seconds\n";
echo "HTTP Status: " . $create_result['response']['http_code'] . "\n";
if ( $create_result['response']['http_code'] === 200 ) {
$create_successes ++;
$created_products = $create_result['response']['response']['create'] ?? [];
$successful_creates = array_filter( $created_products, function ( $product ) {
return isset( $product['id'] );
} );
$new_product_ids = array_column( $successful_creates, 'id' );
$created_product_ids = array_merge( $created_product_ids, $new_product_ids );
echo "Successfully created: " . count( $successful_creates ) . " products\n";
if ( ! empty( $new_product_ids ) ) {
echo "Testing batch UPDATE with " . count( $new_product_ids ) . " products...\n";
$update_result = $this->batchUpdateProducts( $new_product_ids );
$update_time = $update_result['execution_time'];
$update_times[] = $update_time;
echo "Batch UPDATE completed in: " . number_format( $update_time, 2 ) . " seconds\n";
echo "HTTP Status: " . $update_result['response']['http_code'] . "\n";
if ( $update_result['response']['http_code'] === 200 ) {
$update_successes ++;
}
}
} else {
echo "CREATE failed with HTTP " . $create_result['response']['http_code'] . "\n";
if ( isset( $create_result['response']['response']['message'] ) ) {
echo "Error: " . $create_result['response']['response']['message'] . "\n";
}
}
echo "\n";
}
$total_end_time = microtime( true );
$total_execution_time = $total_end_time - $total_start_time;
echo "=== Test Summary ===\n";
echo "Total execution time: " . number_format( $total_execution_time, 2 ) . " seconds\n";
echo "Total products created: " . count( $created_product_ids ) . "\n";
echo "Overall success rate: " . number_format( ( $create_successes / TEST_ITERATIONS ) * 100, 1 ) . "%\n\n";
// CREATE Performance Statistics
echo "=== CREATE Request Performance ===\n";
echo "Total CREATE requests: " . count( $create_times ) . "\n";
echo "Successful CREATE requests: " . $create_successes . "\n";
if ( ! empty( $create_times ) ) {
$create_total = array_sum( $create_times );
$create_avg = $create_total / count( $create_times );
$create_p50 = $this->calculatePercentile( $create_times, 50 );
$create_p75 = $this->calculatePercentile( $create_times, 75 );
echo "Total CREATE time: " . number_format( $create_total, 2 ) . " seconds\n";
echo "Average CREATE time: " . number_format( $create_avg, 2 ) . " seconds\n";
echo "CREATE P50 (median): " . number_format( $create_p50, 2 ) . " seconds\n";
echo "CREATE P75: " . number_format( $create_p75, 2 ) . " seconds\n";
echo "CREATE min/max: " . number_format( min( $create_times ), 2 ) . "s / " . number_format( max( $create_times ), 2 ) . "s\n";
}
echo "\n";
// UPDATE Performance Statistics
echo "=== UPDATE Request Performance ===\n";
echo "Total UPDATE requests: " . count( $update_times ) . "\n";
echo "Successful UPDATE requests: " . $update_successes . "\n";
if ( ! empty( $update_times ) ) {
$update_total = array_sum( $update_times );
$update_avg = $update_total / count( $update_times );
$update_p50 = $this->calculatePercentile( $update_times, 50 );
$update_p75 = $this->calculatePercentile( $update_times, 75 );
echo "Total UPDATE time: " . number_format( $update_total, 2 ) . " seconds\n";
echo "Average UPDATE time: " . number_format( $update_avg, 2 ) . " seconds\n";
echo "UPDATE P50 (median): " . number_format( $update_p50, 2 ) . " seconds\n";
echo "UPDATE P75: " . number_format( $update_p75, 2 ) . " seconds\n";
echo "UPDATE min/max: " . number_format( min( $update_times ), 2 ) . "s / " . number_format( max( $update_times ), 2 ) . "s\n";
}
echo "\n";
// Performance insights
if ( ! empty( $create_times ) && ! empty( $update_times ) ) {
echo "\n=== Performance Insights ===\n";
$avg_create = array_sum( $create_times ) / count( $create_times );
$avg_update = array_sum( $update_times ) / count( $update_times );
if ( $avg_create > $avg_update ) {
$ratio = $avg_create / $avg_update;
echo "CREATE operations are " . number_format( $ratio, 1 ) . "x slower than UPDATE operations on average\n";
} else {
$ratio = $avg_update / $avg_create;
echo "UPDATE operations are " . number_format( $ratio, 1 ) . "x slower than CREATE operations on average\n";
}
$products_per_second_create = BATCH_SIZE / $avg_create;
$products_per_second_update = BATCH_SIZE / $avg_update;
echo "Throughput: " . number_format( $products_per_second_create, 1 ) . " products/sec (CREATE), " . number_format( $products_per_second_update, 1 ) . " products/sec (UPDATE)\n";
}
} catch ( Exception $e ) {
echo "Error during test execution: " . $e->getMessage() . "\n";
}
}
/**
* Calculate percentile from array of values
*/
private function calculatePercentile( $data, $percentile ) {
if ( empty( $data ) ) {
return 0;
}
sort( $data );
$n = count( $data );
// Calculate index for percentile
$index = ( $percentile / 100 ) * ( $n - 1 );
// If index is whole number, return that element
if ( $index == intval( $index ) ) {
return $data[ intval( $index ) ];
}
// Otherwise interpolate between two closest values
$lower_index = intval( $index );
$upper_index = $lower_index + 1;
if ( $upper_index >= $n ) {
return $data[ $n - 1 ];
}
// Linear interpolation
$weight = $index - $lower_index;
return $data[ $lower_index ] * ( 1 - $weight ) + $data[ $upper_index ] * $weight;
}
}
// Example usage
if ( php_sapi_name() === 'cli' ) {
// Command line execution
echo "WooCommerce Batch API Tester\n";
echo "============================\n\n";
// Check if credentials are configured
if ( WC_STORE_URL === 'https://your-site.com' ||
WC_CONSUMER_KEY === 'ck_your_consumer_key_here' ||
WC_CONSUMER_SECRET === 'cs_your_consumer_secret_here' ) {
echo "ERROR: Please configure your WooCommerce credentials at the top of this script.\n";
echo "Update WC_STORE_URL, WC_CONSUMER_KEY, and WC_CONSUMER_SECRET\n";
exit( 1 );
}
$tester = new WooCommerceBatchTester( WC_STORE_URL, WC_CONSUMER_KEY, WC_CONSUMER_SECRET );
$tester->runBatchTests();
} else {
// Web execution
echo "<h1>WooCommerce Batch API Tester</h1>";
if ( WC_STORE_URL === 'https://your-site.com' ) {
echo "<p style='color: red;'><strong>Please configure your credentials first!</strong></p>";
} else {
echo "<p>Configure your test parameters and run the batch tests.</p>";
echo "<p><strong>SPX Profiling:</strong> Access with ?SPX_KEY=dev&SPX_UI_URI=/ to enable profiling</p>";
// You can uncomment the following lines to run tests via web interface
$tester = new WooCommerceBatchTester( WC_STORE_URL, WC_CONSUMER_KEY, WC_CONSUMER_SECRET );
echo( "<pre>" );
$tester->runBatchTests();
echo( "</pre>" );
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment