Last active
October 15, 2025 18:12
-
-
Save prettyboymp/6999924cb865f060d5b374efe5ec9e33 to your computer and use it in GitHub Desktop.
Test script to analyze performance metrics for WooCommerce BATCH requests to products.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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