This document explains what the wp-admin-flow benchmark does, which URL patterns it requests, what data it collects, and how to write your own tests in this package.
The test script is:
tests/examples/wp-admin-flow.test.js
It simulates a real WordPress/WooCommerce admin session with one or more full iterations:
- Login page + login action
- Navigate to admin dashboard and WC analytics
- Browse and interact with orders/coupons/customers/products
- Create disposable benchmark artifacts (order + product draft + coupon + customer user)
- Add order note
- Cleanup created artifacts
- Logout
Primary use case:
- Compare PHP runtime behavior across different CPU hardware using:
- response header
X-PHP-Runtime-Ms(or configured equivalent) - CSV output from k6 (
php_runtime_mssamples and step tags)
- response header
To populate php_runtime_ms, the target site must emit a response header with PHP execution time in milliseconds.
- Gives a server-side timing signal that is less sensitive to network jitter than client-side
http_req_duration. - Enables per-request stack-variant comparisons across runs (
sampler,stack,testid).
Use your PHP auto-prepend script:
<?php
$__start = hrtime(true);
register_shutdown_function(function () use ($__start) {
$ms = (hrtime(true) - $__start) / 1e6;
if (!headers_sent()) {
header('X-PHP-Runtime-Ms: ' . number_format($ms, 3, '.', ''));
}
});Set this in PHP config for the benchmark environment:
auto_prepend_file=/absolute/path/to/your/prepend-runtime.phpThen restart PHP-FPM/Apache as needed.
curl -I https://your-site.example/wp-admin/Expected header:
X-PHP-Runtime-Ms: <number>
If this header is missing, k6 still runs, but php_runtime_ms will not be populated and php_runtime_header_missing will increase.
Sampler names appear in CSV extra_tags and are the primary grouping key for php_runtime_ms.
| Step | Sampler | When it runs |
|---|---|---|
| A1–A3 | Login page, login POST, dashboard | Always (auth bundle) |
| R1 | WC Admin Home | Always (analytics bundle) |
| O1–O2 | View orders, filter by status | Always (orders bundle) |
| O3 | Search orders | WP_ORDER_SEARCH_QUERY set |
| O4 | Create order form / save / edit | WP_ADMIN_ORDER_CREATE=true (default) |
| O5 | Edit/update fixture order | WP_ADMIN_ORDER_ID set and WP_ADMIN_ORDER_SAVE=true |
| O7 | Add order note (AJAX) | After order create; may lack php_runtime_ms if header absent on AJAX |
| C1 | View coupons | Always (coupons bundle) |
| C2 | Search coupons | WP_COUPON_SEARCH_QUERY set |
| C3–C4 | Create / edit / update coupon | WP_ADMIN_COUPON_CREATE=true (default) |
| U1 | View customers | Always (customers bundle) |
| U2 | Search customers | WP_CUSTOMER_SEARCH_QUERY set |
| U3–U4 | Create / edit / update customer | WP_ADMIN_CUSTOMER_CREATE=true (default) |
| U5 | Customer orders tab | After customer create |
| P1 | View products | Always (catalog bundle) |
| P3 | Search products | WP_PRODUCT_SEARCH_QUERY set |
| P4–P5 | Edit / update fixture product | WP_ADMIN_PRODUCT_ID set; save requires WP_ADMIN_PRODUCT_SAVE=true |
| P6 | Add product form / save draft | WP_ADMIN_PRODUCT_CREATE=true (default) |
| X1–X4 | Order / product / coupon / customer cleanup | WP_ADMIN_CLEANUP=true (default) |
| A4 | Logout | Always (auth bundle) |
There is no P2, P7, or P8 in the current script.
- One VU runs iterations sequentially (
shared-iterationsexecutor). humanSleep()inserts a fixed 2 second pause after most admin steps. This is hard-coded in dev mode;WP_ADMIN_SLEEP_MIN/WP_ADMIN_SLEEP_MAXare not used.WP_ADMIN_ITERATION_SLEEP_SEC(default2) adds pause between full flow repetitions only.- Some sub-flows have no step sleep between requests (e.g. O4 form → save → edit inside
createBenchmarkOrder()).
- Uses
WP_ADMIN_SLEEP_MIN/WP_ADMIN_SLEEP_MAX(random range per step; compose defaults0/0, code fallback5–10if env vars are absent).
- Do not infer request latency from gaps in the
timestampcolumn (1-second buckets + pacing). - Use
php_runtime_msfor server-side PHP time andhttp_req_durationfor end-to-end HTTP time.
The exact requests depend on enabled bundles and fixture variables. Core request patterns include:
GET /<WP_LOGIN_PATH>POST /<WP_LOGIN_PATH>(login payload:log,pwd,wp-submit,redirect_to,testcookie)- Optional hide-login fields when configured:
WP_LOGIN_EXTRA_PARAM(defaultaio_special_field) +WP_LOGIN_EXTRA_FIELD(token/value)
GET /wp-admin/GET <logout href from admin page>
GET /wp-admin/admin.php?page=wc-admin
GET /wp-admin/edit.php?post_type=shop_orderGET /wp-admin/edit.php?post_type=shop_order&post_status=<status>GET /wp-admin/edit.php?...&post_type=shop_order...(search, optional)GET /wp-admin/post-new.php?post_type=shop_orderPOST /wp-admin/post.php(create order)GET /wp-admin/post.php?post=<id>&action=editPOST /wp-admin/admin-ajax.php(woocommerce_add_order_note)- Optional fixture save path:
GET /wp-admin/post.php?post=<fixture_id>&action=editPOST /wp-admin/post.php
GET /wp-admin/edit.php?post_type=shop_couponGET /wp-admin/edit.php?...&post_type=shop_coupon...(search, optional)GET /wp-admin/post-new.php?post_type=shop_couponPOST /wp-admin/post.php(create disposable coupon)GET /wp-admin/post.php?post=<id>&action=editPOST /wp-admin/post.php(update disposable coupon)
GET /wp-admin/admin.php?page=wc-admin&path=%2FcustomersGET /wp-admin/users.php?s=<query>(optional)GET /wp-admin/user-new.phpPOST /wp-admin/user-new.php(create disposable customer user)GET /wp-admin/user-edit.php?user_id=<id>POST /wp-admin/user-edit.php(update disposable customer)GET /wp-admin/admin.php?page=wc-admin&path=/customers/<id>
GET /wp-admin/edit.php?post_type=product[...]GET /wp-admin/edit.php?...&post_type=product...(search, optional)GET /wp-admin/post-new.php?post_type=productPOST /wp-admin/post.php(save product draft)- optional fixture product edit via
/wp-admin/post.php - optional fixture product save only when
WP_ADMIN_PRODUCT_SAVE=true
- Customer cleanup:
/wp-admin/users.php(list + delete action for created benchmark user)
- Coupon cleanup:
/wp-admin/post.php?...action=trash.../wp-admin/edit.php?post_type=shop_coupon&post_status=trash
- Order cleanup paths (trash/delete via discovered admin links):
/wp-admin/post.php?...action=trash.../wp-admin/edit.php?post_type=shop_order&post_status=trash/wp-admin/admin.php?page=wc-orders&status=trash(fallback)
- Product cleanup:
/wp-admin/post.php?...action=trash...
Cleanup matrix (state-safe default):
| Entity | Created in run | Mutated in run | Cleanup action |
|---|---|---|---|
| Order | Yes | Yes | Trash + permanent delete |
| Product draft | Yes | Yes | Trash |
| Coupon | Yes | Yes | Trash + permanent delete |
| Customer user | Yes | Yes | Delete user |
The run writes CSV samples including built-in k6 metrics and custom metrics.
http_req_duration,http_req_failed,http_reqs, etc.- Purpose: transport-level request health and latency overview.
php_runtime_ms(Trend)- Parsed from configured response header (
WP_ADMIN_PHP_RUNTIME_HEADER_NAME)
- Parsed from configured response header (
php_runtime_header_missing(Counter)- Header not found for a sampled request
php_runtime_header_invalid(Counter)- Header present but not parseable numeric milliseconds
Purpose:
- Estimate server-side PHP time independent of network jitter
- Compare per-step runtime across stack variants (
STACK_LABEL) and run IDs (TEST_ID)
testid: run identifierstack: optional stack-variant label (STACK_LABEL, legacy fallback fromCPU_LABEL)sampler: step/action name (e.g.O1 View Orders,P6 Save Product Draft)scenario: k6 scenario name (dev_admin_flowin dev mode,admin_flowin load mode; some built-in metric rows may still referenceadmin_flow)step:main(and sometimesassets)
Purpose:
- Build request-level comparison matrices in spreadsheets:
- rows:
sampler - columns:
stackand/ortestid - values: mean/p95/max of
php_runtime_ms
- rows:
- Uses realistic admin actions instead of synthetic ping endpoints
- Keeps cleanup enabled by default to avoid leaving test data behind
- Default state-safe mode mutates only disposable benchmark entities, never existing records
- Supports multi-iteration runs with fixed pause between full iterations
- Dev mode (
K6_DEV_MODE=true) uses one VU, sequential steps, and a fixed 2s pause between most admin actions for reproducible CPU comparison
If you add new tests under tests/examples, follow these rules so results stay comparable and reproducible.
- Reuse shared helpers from:
tests/lib/options.jstests/lib/wp-admin.jstests/lib/wp-admin-cleanup.js(if mutating data)
- Avoid hidden dependencies on parent project files.
- Use
visitAdminPage(...)where possible. - If direct
http.*calls are needed, include tags:samplerscenariostep
This is required for clean CSV grouping.
For CPU comparison runs:
K6_DEV_MODE=trueWP_ADMIN_RUN_ITERATIONS=<N>(e.g.50for published comparisons)WP_ADMIN_ITERATION_SLEEP_SEC=2(pause between full iterations)WP_ADMIN_MAX_DURATION=<cap>if auto-estimate is too low (e.g.180mfor 50 iterations)- Ensure
X-PHP-Runtime-Ms(or configured header) is present on admin responses
Note: in dev mode, per-step pacing is a fixed 2s (humanSleep()); WP_ADMIN_SLEEP_MIN / WP_ADMIN_SLEEP_MAX apply only when K6_DEV_MODE=false.
Avoid random behavior that adds noise (keep load mode off for CPU studies).
- If your test creates or edits entities, implement cleanup steps.
- Record created IDs and remove them at the end of each iteration.
- Use short stable prefixes for steps (A/O/C/U/P/X/R style is fine).
- Example:
O8 View Refunds,P9 Publish Product. - Stable names make historical CSV analysis much easier.
- Keep
WP_ADMIN_PHP_RUNTIME_HEADER_ENABLED=true. - Watch
php_runtime_header_missingandphp_runtime_header_invalid. - If they rise, fix instrumentation before trusting comparisons.
- Put smoke tests in
tests/smokewhen adding new parsing or tagging helpers. - Validate locally before sharing benchmark changes.
- Run one test per stack variant with unique
TEST_IDandSTACK_LABEL. - Merge/filter CSV rows where
metric_name == php_runtime_ms. - Pivot by
samplervsstack. - Compare
mean,p95, andmax. - Check header counters to verify data quality.