Uh oh!
There was an error while loading. Please reload this page.
Blitzy: Measurement-gated WordPress core performance work — class autoloader, bootstrap deferral, admin JS gating - #1
Open
blitzy[bot] wants to merge 31 commits into
Conversation
…stall Composer dependency resolution failed during environment setup because the pinned constraint (~3.3.0) can only resolve to wp-coding-standards/wpcs 3.3.0, which is blocked by security advisory PKSA-mh9b-91zm-m1gy (CVE-2026-45293, high severity: arbitrary code execution in WordPressCS, affecting >=0.14.1,<3.4.1). Because composer.json sets "config": { "lock": false }, there is no composer.lock and resolution runs on every install, so this blocked 'npm run env:start' (which ends in 'composer update -W') with exit code 2 on any fresh checkout. Raising the constraint to ~3.4.1 selects the minimum patched release. wpcs 3.4.1's own requirements are already satisfied by the existing exact pins (squizlabs/php_codesniffer ^3.13.5, phpcsstandards/phpcsutils ^1.2.3, phpcsstandards/phpcsextra ^1.5.1), so no other manifest entry changes. 'composer update -W' now reports 'No security vulnerability advisories found.' The advisory was not suppressed (no --no-audit and no ignore list). Verified afterwards: phpcs reports zero errors across every tracked, repo-authored PHP file; PHPStan reports '[OK] No errors'; PHPUnit passes 28,984 tests with 0 failures.
`WP_Object_Cache` has only ever tracked cache activity in aggregate, through the global `$cache_hits` and `$cache_misses` integers. Those totals show that a request missed the cache, but not *which* group missed, so an uncached hot path cannot be located from them. This adds an additive `$cache_group_stats` property that breaks the same two numbers down by cache group, incremented alongside the existing totals on the two branches of `WP_Object_Cache::get()`, and surfaces the breakdown through `WP_Object_Cache::stats()`. Details: * New `public $cache_group_stats` array, keyed by group name, each entry holding a `hits` and a `misses` count. * Both counters for a group are seeded together before the existence check in `get()`, so neither increment can emit an undefined array key warning on the first touch of a group. `$group` is already normalized to `default` earlier in the method, so no additional normalization is required. * `stats()` reports the per-group numbers on the existing group list, and lists groups that were requested but never stored separately, since the existing loop walks stored data and would otherwise hide exactly the all-miss groups these counters exist to expose. Group names remain escaped with `esc_html()` and counts are cast to integers. `$cache_hits` and `$cache_misses` are untouched: same name, visibility, type, initial value and increment semantics, so code reading them directly is unaffected. `get_multiple()` inherits the counting through its `get()` loop and needs no change. Consistent with the existing totals, `flush()`, `flush_group()` and `reset()` do not reset the per-group counters. The change is contained entirely within this class, so behaviour is unchanged when an `object-cache.php` drop-in replaces it and this file is never loaded. Measured on a front-end request: 2,727 hits and 361 misses across 20 groups, with the per-group figures summing exactly to both totals. The breakdown identifies `oembed_cache_post`, `translation_files`, `comment-queries` and `calendar` at a 100% miss rate and `post-queries` at 87%, while `options` misses only 0.7% of 1,210 lookups.
… and misses, and bootstrap duration as Server-Timing metrics. The performance harness mu-plugin reported six metrics (before-template, template, total, memory-usage, db-queries, ext-obj-cache), which left four of the measurements the performance targets depend on unavailable. Because a change is only admissible once its bottleneck has been quantified, and only accepted once the same method demonstrates the delta, these metrics are a precondition for the optimization work that follows. Five metrics are added to both the front-end (template_include) and the admin (admin_init) shutdown callbacks: * memory-peak - memory_get_peak_usage( false ). The false variant is used because the true variant is quantized to the allocator chunk size and cannot resolve a ten percent target. * files-loaded - count( get_included_files() ). The count only, never the list, so no filesystem path is exposed in a response header. * cache-hits - WP_Object_Cache::$cache_hits, read defensively. * cache-misses - WP_Object_Cache::$cache_misses, read defensively. * bootstrap - Duration from $timestart to the wp_loaded hook. The four counts are cast to int because the emission loop scales any float by 1000, which would otherwise report 484 files as 484000. The bootstrap value is deliberately left a float so that it is converted to milliseconds like the other durations. Bootstrap duration is captured once per request by a wp_loaded callback and read back through a single accessor. wp_loaded is the final hook of the bootstrap sequence, so it is reached at the same point of the request lifecycle on both paths, which keeps the metric comparable; before-template has no admin equivalent. It also avoids reaching for $timestart inside the front-end shutdown callback, which does not import it. The cache counters are read through isset() so that a replacement object-cache.php drop-in, which need not expose either property, degrades to zero without a notice. Every metric is emitted on every request rather than omitted when unavailable, because the reporting layer derives its sample arrays from the keys the server actually sends and an absent key produces NaN. The six existing metrics, the header format, the hook priorities, and the output buffering order are unchanged: memory-usage continues to report memory_get_usage(), and the change is a pure addition with no line removed.
…metrics in the performance reporting utilities. `formatValue()` in `tests/performance/utils.js` recognised only `wpMemoryUsage`, `wpExtObjCache`, and `wpDbQueries`. Every other metric key fell through to the unconditional millisecond branch, so the Server-Timing metrics introduced in 14b55f9 would have been rendered as durations: a files-loaded count of `484` printed as `484.00 ms`, and a cache-hit count of `137` as `137.00 ms`. Nothing throws and nothing warns, so the performance report would simply have been wrong. Add explicit branches for the four new metrics that are not durations: * `wpMemoryPeak` joins the existing `wpMemoryUsage` condition, so peak and current memory are formatted identically -- same `Math.pow( 10, 6 )` divisor, same two-decimal precision -- and stay directly comparable in one table. * `wpFilesLoaded`, `wpCacheHits`, and `wpCacheMisses` join the existing `wpDbQueries` condition and are returned as raw integers. `wpBootstrap` and `domContentLoaded` are deliberately left unbranched: both are genuine millisecond durations, for which the existing default branch is already correct. The keys mirror the slugs emitted by `tests/performance/wp-content/mu-plugins/server-timing.php` once they pass through `camelCaseDashes()`, so `wp-files-loaded` resolves to `wpFilesLoaded` and `wp-memory-peak` to `wpMemoryPeak`. The `null` guard still returns `N/A` first, and `wpMemoryUsage`, `wpExtObjCache`, and `wpDbQueries` are unchanged in both value and return type. This makes the peak-memory and files-loaded measurements reportable through `tests/performance/compare-results.js`, which is not modified.
…d server metrics. The admin performance spec recorded only `timeToFirstByte`, leaving the admin DOMContentLoaded figure without any baseline, and the homepage spec did not declare the peak memory, files loaded, object cache and bootstrap metrics that the performance mu-plugin now emits as Server-Timing entries. `tests/performance/specs/admin.test.js` now records a `domContentLoaded` metric taken from the existing `metrics.getLoadingDurations()` fixture, whose `domContentLoaded` property is `domContentLoadedEventEnd - responseEnd`. Because the origin is `responseEnd`, the figure excludes server time and therefore isolates client-side delivery cost from TTFB. The key carries no `wp` prefix and is not routed through `camelCaseDashes()`, so it is formatted through the default millisecond formatting path and no reporting change is required. `tests/performance/specs/home.test.js` declares `wpMemoryPeak`, `wpFilesLoaded`, `wpCacheHits`, `wpCacheMisses` and `wpBootstrap`. No push statements are needed, because the existing Server-Timing ingestion loop already routes every entry name through `camelCaseDashes()` and pushes its value; declaring the keys is what makes the metrics reportable and gives them the same per-scenario sample scoping as the other homepage metrics. In both specs the new keys are declared after the existing ones so that `timeToFirstByte` remains first, because `compare-results.js` derives the reported iteration count from the length of the array stored under the first key. The resets are placed in `afterAll` after the `testInfo.attach()` call, so the attached artifact keeps populated data while the values do not accumulate across themes and locales. The six pre-existing server metrics are intentionally left neither declared nor reset, so that their existing sample scoping and reported numbers are unchanged.
…le post performance spec. Declares the five new Server-Timing metrics emitted by the performance mu-plugin as first-class keys of the `results` object in the Single Post spec, and resets each one in `afterAll` alongside the three existing metrics: `wpMemoryPeak`, `wpFilesLoaded`, `wpCacheHits`, `wpCacheMisses` and `wpBootstrap`. The generic Server-Timing ingestion loop already pushed these values, so no new capture is added. Declaring them makes them first-class, and resetting them is a correctness fix: the eight theme/locale describe blocks share a single module-scoped `results` object, so an unreset array would accumulate across all eight and every reported median would be computed over the wrong sample set. The new keys are appended after `timeToFirstByte` rather than prepended, because `compare-results.js` derives the reported iteration count from the first key in insertion order. The resets are placed after the `testInfo.attach()` call so the attached artifact always contains the populated arrays. This mirrors the treatment already applied to `home.test.js`; the two specs continue to differ only in their describe title and their navigation target. No DOMContentLoaded capture is added here, as that metric is specific to the admin scenario.
Read the object cache hit and miss counters through a single validated snapshot in the performance Server-Timing mu-plugin. The counters are captured once per request with `get_object_vars()`, which never consults magic accessors, and are coerced to integers only when they are numeric, finite and representable. A cache implementation that omits the counters, exposes them through accessors, or holds a non-representable float can no longer emit a diagnostic ahead of the response headers and truncate the metric header. Declare and reset the peak memory, files loaded, cache hits, cache misses and bootstrap duration samples per scenario in the front-end performance specs, so that every theme and locale reports its own measurements instead of accumulating them across scenarios. Each iteration now validates that the expected Server-Timing metrics are all present, finite and non-negative, and each scenario asserts that every declared metric holds one sample per iteration. Add tests for the performance result formatters, covering dashed to camel case conversion and the byte, millisecond, count and boolean formatting paths, and add tests for the per-group object cache statistics, covering group isolation, multi-key and delete behaviour, non-persistent groups and the reset performed on switching sites. Correct the object cache and mu-plugin documentation so that the property type, the statistics output and the bootstrap timing helper are described as they actually behave, and escape the group names printed by the statistics output. Restore the documented `wp-coding-standards/wpcs` development requirement.
…, and scope the admin metrics per locale. Read the iteration count from `TEST_RUNS` once at module scope in the Admin, Homepage and Single Post specs, and add a test to each that fails when that count is not a positive integer. An empty, zero, negative or non-numeric value generates no measured tests at all, which left the suite reporting success over an empty result artifact; the comparison step then reads that absence of data as an absence of change. Reading the count from a single place also guarantees that the value the measured tests are generated from is the value the new check reads. Declare `wpMemoryPeak`, `wpFilesLoaded`, `wpCacheHits`, `wpCacheMisses` and `wpBootstrap` as keys of the Admin spec's `results` object, and reset them in `afterAll` alongside `timeToFirstByte` and `domContentLoaded`. The generic Server-Timing ingestion loop already collected these values, but because the arrays were created on the fly they were never reset between the two locale blocks, so the second locale reported medians computed over both locales - including a fractional file count that no request ever produced - and dispersion figures that described locale mixing rather than measurement noise. Declaring them also brings them under the per-locale sample count check. The new keys are appended after the existing ones so that `timeToFirstByte` remains first in insertion order, which is where the reported iteration count is derived from, and the resets stay after `testInfo.attach()` so the attached artifact still carries the populated arrays. This matches the treatment already applied to the Homepage and Single Post specs. The four Server-Timing metrics that predate these additions keep their existing accumulating behavior.
…quest.
`map_meta_cap()` is re-entered on every capability check, and a single request
asks the same object capability question many times over. A list table with 20
rows resolves the same `edit_post`/`delete_post` mapping once per row action, so
the 86-case mapping body runs far more often than the number of distinct
questions being asked.
Add a request-scoped memo in front of the mapping. The key carries the current
site ID, the sizes of the post type, post status and taxonomy registries and of
the Super Admin list, the capability as it was passed in, the user ID, and every
further argument by position with its type and length, so that `array( 1, '2' )`,
`array( '1', 2 )` and `array( '1|2' )` cannot collide.
The memo declines, rather than guesses, wherever an identical call could
legitimately map differently: when no further arguments were passed, when the
capability is not a string or the user ID is not scalar, when any argument is
neither scalar nor null, when the capability is one of the metadata capabilities
that resolve through the dynamic `auth_{$object_type}_meta_{$meta_key}` filters,
and whenever a `map_meta_cap` callback is registered - which is what keeps
`WP_Customize_Manager`'s transient callback, and any plugin's, authoritative.
Only the filtered result is stored, so a memo hit returns exactly what the
`map_meta_cap` filter produced. The memo is discarded wholesale on the state
changes it is derived from: post, comment, term and user cache invalidation, post
meta writes, role and Super Admin membership changes, post type and taxonomy
registration, option and network option writes, and site switches. Registering
`granted_super_admin`/`revoked_super_admin` rather than their pre-mutation
counterparts means the new state is always the one that gets mapped.
`map_meta_cap()`'s signature, its 86 mapping cases, its `map_meta_cap` filter
and all five public entry points are unchanged.
Add `tests/phpunit/tests/user/mapMetaCapMemo.php` covering the memo contract:
repeat-call hits, filtered and dynamic-callback bypass, transient callback
boundaries, scalar type and positional argument separation, non-scalar bypass,
cross-site keying, one proof per registered invalidation action, and output and
diagnostic safety.
Also validate `TEST_RUNS` at module scope in the performance specs. The value is
consumed while the module is evaluated, so it decides how many tests exist rather
than how one behaves, and a non-finite or absurdly large count made collection
run forever - which no assertion inside a test could ever report. The specs now
reject it synchronously and register no measured tests, leaving the existing
check to fail the run loudly.…emo. The request-scoped memo introduced for `map_meta_cap()` could outlive several state changes that the mapping itself reads, so an identical check made later in the same request could be answered from a mapping that no longer held. Two of those cases answered with a capability the user was no longer entitled to. Per-user capability overrides are stored in user metadata, so `WP_User::add_cap()`, `WP_User::remove_cap()` and `WP_User::remove_all_caps()` announce a change only through the generic metadata actions. Those actions are now listened on by `_wp_reset_map_meta_cap_memo_on_user_meta()`, which discards the memo for a write to the capability array and leaves it alone for every unrelated user metadata write. The end of the key is matched rather than one exact prefix, so a write for a site other than the current one is recognized too. `register_post_status()` replaced the arguments of an already registered status without announcing it and without changing the size of the registry, so neither the memo key nor any action could observe a status becoming private. It now fires `registered_post_status`, documented in the shape of the existing `registered_post_type` action, and the memo is discarded on it. `WP_Roles::add_role()`, `remove_role()`, `add_cap()` and `remove_cap()` persist their changes only when `WP_Roles::$use_db` is true, and `update_option()` fires nothing when the stored value is unchanged, so role capability changes could reach neither an option action nor the memo. All four now discard it directly. `remove_user` is the one mapping whose answer is derived from the capabilities a single user holds, read back through the `user_has_cap` filter, so no key component or invalidation action can describe it reliably. It is now declined outright rather than memoized. A capability checked against no object at all is also declined, so the `_doing_it_wrong()` report those calls earn is made on every call again rather than only on the first. The Super Admin logins are carried in the key by content rather than by size, with each login written with its length, so substituting one login for another cannot leave the key unchanged; an entry that cannot be described faithfully declines the memo. The documentation of the key now records what it deliberately does not cover, and the measured cost of a memo hit against a miss. Adds regression coverage for each of the above, including per-user capability writes in both directions, post status re-registration, role changes without the database, the Super Admin key component, the incorrect-usage report frequency, and an assertion that every kind of state the mapping reads is covered by an action, by a key component, or by an explicit decline. The suite is meaningful on both single site and Multisite, since which mapping consults a user's own capabilities differs between them.
… and class map data files. Only block editor screens use the Command Palette, yet `wp-commands` and `wp-core-commands` were enqueued on every admin screen, making the `wp-components` dependency chain the bulk of the admin JavaScript payload everywhere. Add `wp_should_load_command_palette_assets()` alongside the existing `wp_should_load_*()` gates, which returns false outside the admin and otherwise defers to `WP_Screen::is_block_editor()`, filterable through the new `should_load_command_palette_assets` filter. `wp_enqueue_command_palette_assets()` now returns early when the gate declines, so the scripts, the style and the inline initializer are skipped on screens that never show the palette. The `admin_enqueue_scripts` registration is untouched, so existing `remove_action()` calls keep working, and `wp_admin_bar_command_palette_menu()` already checks whether `wp-core-commands` is enqueued before adding its shortcut button. Also add two generated data files that keep large literals out of the code that is parsed on every request: * `wp-includes/emoji-arrays.php` returns the `entities` and `partials` emoji lists, preserving the START/END marker contract so the arrays stay regenerable, for loading on demand by the emoji staticizing helpers. * `wp-includes/autoload-classmap.php` returns a map of 290 core class names to their files, for O(1) class resolution without filesystem probing. Both files carry a do-not-edit-manually header, matching how core ships other generated array manifests.
Introduces `Tests_Dependencies_CommandPalette`, which locks in the behaviour of `wp_should_load_command_palette_assets()`, of its `should_load_command_palette_assets` filter, and of the delivery contract of `wp_enqueue_command_palette_assets()`: * The gate is false outside of the admin, and the filter is not applied there, so the non-filterable admin guard cannot be bypassed. * The gate is false when `$current_screen` is not a `WP_Screen` instance, and on admin screens that are not block editor screens. * The gate is true on block editor screens. * The filter can open the gate on an unsupported screen and close it on a supported one, for boolean as well as truthy and falsey values. * While the gate is closed, the `wp-commands` and `wp-core-commands` scripts and the `wp-commands` style stay registered but are never enqueued, and no inline initializer is added, so third party code can still depend on the handles. * While the gate is open, all three handles are enqueued and the initializer is printed with a valid JSON payload. * `wp_enqueue_command_palette_assets()` remains hooked to `admin_enqueue_scripts` at the default priority, so existing `remove_action()` calls keep working.
… demand. Introduce a core class autoloader. WordPress has never had one: every class file reachable on a request was required eagerly from wp-settings.php, so its parse cost was paid even on requests that never referenced it. wp-includes/autoload.php registers a single autoloader that resolves a class, interface or trait name through a direct lookup in the generated class map at wp-includes/autoload-classmap.php. No path is derived from the requested name, and nothing but ABSPATH and WPINC can influence which file is loaded. The map is read lazily on the first autoload miss and then memoized, so registration itself costs nothing and requests that reference no mapped name - including SHORTINIT requests - never touch the filesystem for it. A missing map, a map that does not return an array, an unmapped name, or a mapped file that has gone away all leave the autoloader silently doing nothing, so every other registered autoloader still gets its turn. Gate the inline Emoji detection script. print_emoji_detection_script() printed an emoji settings object and inlined the whole emoji loader, read from disk with file_get_contents(), on every single page view. Browsers in current use render the emoji natively, so on a typical request that payload is pure overhead. A new wp_should_load_emoji_detection_script() predicate is consulted as the first statement of the hooked function, which leaves every existing registration and remove_action() call working unchanged - including the per-screen opt-out in wp-admin/edit-form-blocks.php, which this generalises. The new should_load_emoji_detection_script filter prints the script again, globally or for a single request. The payload is an inline script that never passes through WP_Scripts, so the enqueue dependency system is not involved. Load the emoji arrays on demand. The two emoji arrays occupied roughly 140 KB inside formatting.php and were tokenized on every request, even though only feeds and email consume them. The data now lives in wp-includes/emoji-arrays.php and _wp_emoji_list() loads it once per request, the first time it is called. A missing or malformed data file degrades to an empty array rather than null, because both callers iterate the return value directly. wp_enqueue_emoji_styles(), _print_emoji_detection_script(), wp_encode_emoji(), wp_staticize_emoji() and wp_staticize_emoji_for_email() are unchanged, and _wp_emoji_list() keeps its signature, its default and its return contract.
Every class file reachable on a request was required eagerly from wp-settings.php, so its parse cost was paid on every request whether or not the class was ever referenced. Register wp-includes/autoload.php before the require region and drop the eager require for 92 class-only files, letting the generated class map resolve each name the first time it is used. The largest cluster is the REST API: 57 files under wp-includes/rest-api/ declared 57 classes that only create_initial_rest_routes() instantiates. That runs on the rest_api_init action, which fires from rest_get_server() alone, so a request that never dispatches a REST route never referenced any of them. rest-api.php itself stays eager, because it declares the functions that default-filters.php registers by name. The HTTP transports, AI client adapters, abilities, collaboration, sitemaps stylesheet, block editor context, WP_Block and WP_Block_List, style engine, font face, interactivity directives processor, plugin dependencies, URL pattern prefixer and speculation rules follow the same rule. A file is still required eagerly when it declares functions that are registered or called by name, when including it has side effects, when the name it declares is absent from the class map, or when that name is referenced on every request anyway. Load WP_Site_Health only where it is used. Its constructor adds three admin-only hooks and the scheduled check's own cron handler, so the 3,868 lines of wp-admin/includes/class-wp-site-health.php are worth parsing on admin and cron requests only. create_initial_rest_routes() needs the class too, and it lives in wp-admin beyond the reach of the core class map, so a rest_api_init callback at priority 0 loads it before the routes are registered at priority 99. On a front-end request this takes the file count from 498 to 410. Behaviour is unchanged: /wp-json/wp/v2 registers the same 108 routes with the same schemas, capability and nonce checks still run from eagerly loaded files, and third-party class_exists() and function_exists() calls still resolve, since a registered autoloader satisfies them transparently. Add Tests_Load_wpAutoloadClass, covering wp_autoload_class(): every class map entry resolves to a readable file that declares the name it is keyed by, a name outside the map is ignored so other registered autoloaders still get their turn, loading a mapped name defines exactly that name, and the map holds no stale path.
…ormance work. Resolves the QA findings raised against the class autoloader added in the preceding changeset, and completes the measurement and reporting the project plan requires. Autoloader correctness: * Generate the class map instead of maintaining it by hand. The new build script `tools/build/generate-autoload-classmap.php` admits a file only when it declares exactly one symbol, carries no file-scope side effects, and has a parent, interface and trait chain that is itself resolvable, pruned to a fixpoint. This drops the sixteen entries that could not be autoloaded on their own: seven produced unbounded recursion and a segmentation fault, because `class-wp-customize-control.php` requires twenty of its own subclasses in a file-tail block, and nine raised fatal errors through parents that live in vendored libraries. All 239 remaining entries now load standalone. * Resolve class names case-insensitively and tolerate a single leading namespace separator, matching how PHP itself hands names to an autoloader. Previously `wp_query` or `\WP_Query` missed the map and fell through to a fatal error. * Register the generator as a `build:autoload-classmap` Grunt task, run first in both build variants, so the map cannot drift from the source tree. Request-path availability: * Register the Site Health scheduled check on every request path rather than only when `is_admin()` or `wp_doing_cron()` holds. Under `ALTERNATE_WP_CRON`, `DOING_CRON` is defined by `wp-cron.php` after `wp-settings.php` has already evaluated that condition, so the handler count fell to zero and the check never ran. * Keep `wp-admin/includes/plugin.php` loaded eagerly, and record why in place: plugins call `get_plugin_data()` and its neighbours without first testing that they exist, which is the reason the file became a direct require. File loading: * Defer a further 103 eager requires in `wp-settings.php` to the autoloader, taking a front-end request from 485 to 374 loaded files. Front-end output is byte-identical to the previous behaviour. Testing and documentation: * Extend the autoloader test case to twelve methods covering standalone autoloadability of every entry, parent chain resolvability, case-insensitive and leading-separator lookups, absence of file-scope side effects, agreement with the generator, repeated registration, and an unusable map. * Add `docs/performance-optimization-report.md` recording each optimization with its before and after measurements, the opcode-cache state for every figure, and the opportunities that were measured but deliberately not implemented.
… file. The replace:emoji-regex task rewrote the marker block in src/wp-includes/formatting.php, but that data now lives in src/wp-includes/emoji-arrays.php, so the task matched nothing and grunt precommit:emoji could no longer refresh the arrays. Retarget the task, and make both generated-data build steps fail loudly rather than write an empty result: * replace:emoji-regex now refuses to replace the marker block when the fetched Twemoji data yields no entities. grunt.fatal() cannot be used for this, because grunt.util.exit() returns to its caller while it waits for the output streams to drain, which let the emptied block reach the file; only an exception stops grunt-replace from writing. * build:autoload-classmap now fails the task when the generated class map has no entries, so copy:files can never ship a map that would silently switch the core autoloader off.
…ormance measurement
…ime guard Addresses six code review findings against the core autoloader work. The generated class map itself is unchanged: 228 entries, 19,521 bytes. Generator, tools/build/generate-autoload-classmap.php: * Read one tokenizer shape rather than one per PHP version, so the map is the same map on the PHP 7.4 floor that composer.json declares. The PHP 8 name tokens are found through defined()/constant() instead of being named, and both shapes are folded onto a synthetic negative id, which token_get_all() can never return. Qualified, fully qualified and namespace relative names all resolve. Previously five names were silently dropped on 7.4, giving a 223 entry map. * Publish the map with a verified atomic replacement: a temporary file beside the target so the rename stays on one filesystem, short write detection, a read back comparison, mode preservation, a checked rename and cleanup on every failing path. The map is re-read after publication on both the changed and unchanged paths, a digest of what was published is printed last, and a failure exits nonzero rather than being ignored. * Require every emitted path to be canonical rather than merely plausible: rooted at wp-includes/ or wp-admin/includes/, every segment non-empty and free of a leading dot, ending in .php. The previous expression accepted traversal, dot and empty segments and the wider wp-admin/ root. * Normalise the source root inside the bootstrap closure helper, which returned no files at all when handed a root without a trailing slash and would have made every file the bootstrap loads look mappable. Build, Gruntfile.js: * Accept the map on disk only when it is the map that was just generated. The task now captures the generator's output instead of inheriting it, requires the digest line, reads the published file back and compares its byte length and sha256, re-counts the entries independently of the digest, and lints the result. A stale, truncated, empty or unparsable map fails the build instead of being copied into the build tree. Runtime, src/wp-includes/autoload.php: * Check the selected value against that same canonical form before it is concatenated onto ABSPATH. A value that is not a string, or not a path this autoloader owns, is now a silent miss rather than a load, so an interrupted build, a partially written map or a tampered tree cannot reach the filesystem through the loader. Both sides enforce the form, so neither depends on the other having got it right. Bootstrap, src/wp-settings.php: * Reconcile the autoloader comments with the code. Only unconditional requires avoid files the map covers; the guarded WP_Site_Health fallback is a documented exception, and WP_Error is intentionally eager, and therefore absent from the map, rather than mapped. Comments only. Tests, tests/phpunit/tests/load/wpAutoloadClass.php: * Inject malformed selected entries in an isolated tree to prove containment and silent miss behaviour: a parent directory segment, a segment climbing above ABSPATH, a current directory segment, a doubled separator, a leading separator, a backslash, the wider wp-admin root, a root the map never covers, a non PHP extension, a directory, and non string values. Each case plants a decoy at the path the malformed value resolves to. * Prove the map renders byte identically from a PHP 7.4 shaped token stream, and that the generator names no PHP 8 only tokenizer constant. * Prove the generator, the loader and the tests all require the same path form, and use that form for the scope assertions in place of a prefix test, which on its own accepted lexical traversal. The remaining finding concerned a foreign wp_prime_option_caches() block in the ignored build/wp-settings.php, left behind by abandoned commit 1c49354452. It is resolved by regenerating the production build from HEAD, after which build/wp-settings.php is byte identical to source; no build output is committed and nothing was copied into source.
…tric handling explicit tools/build/generate-autoload-classmap.php now verifies its own output instead of assuming it. The publication path names the file it could not replace, and reports the map as changed only once the replacement has actually landed and read back byte-for-byte. A candidate file that cannot be read is named at both tokenizer entry points rather than only one. A class declared inside a namespace is rejected while eligibility is being decided, instead of aborting the run later at render time. The icons directory joins the excluded set. The command-line entry point turns an expected generation failure into a single diagnostic line and a non-zero exit status, while an unexpected error still keeps its stack trace. The generated map itself is unchanged at 228 entries and 19,521 bytes. tests/performance/utils.js gains an identifier-metric set and an isComparableMetric() predicate, which tests/performance/compare-results.js now consults in place of a single hard-coded metric name. Identifier metrics such as the PHP version, the worker process id and the external-object-cache flag are labels rather than quantities, so they are reported as-is with empty difference columns instead of being subtracted from one another. tests/performance/specs/utils.test.js covers the predicate. tests/phpunit/multisite.xml writes its JUnit log to a configuration-relative build/logs/ path, so the file lands inside the ignored build directory rather than beside the tests. docs/performance-optimization-report.md restates the emoji measurements in terms of transfer size and isolated parse cost with the allocator artifact removed, records the accepted Ctrl+K availability change together with the remaining verification-coverage gaps, and updates the verification counts to match the suites as they now run.
…vidence Every figure in docs/performance-optimization-report.md is now traceable to an instrument that ran in this environment, and every before/after pair uses bit-identical interpreter flags, as the OPcache Measurement Law in the document requires. Figures that could not be reproduced were discarded and re-measured rather than carried forward, and the discarded set is listed so the selection is auditable. What the report now contains: - The user's targets table reproduced verbatim, plus an aggregate summary over all six metrics with each row's instrument, regime, sample count and verdict. Six rows carry a not-met verdict; both halves of every partially met target are shown rather than the flattering half alone. - Eight optimization blocks, each in the required five-field template, each with a bottleneck measured before the change and a proof measured after it by the same method. - Per-optimization attribution from isolated worktree arms with exactly one file group swapped per arm, and a reconciliation showing the parts sum to the whole exactly for files loaded, HTML bytes, database queries and cache misses. - Runtime verification in a real browser: the emoji script-source list is identical between arms on both front-end pages, proving the payload never enters the enqueue graph; the gated dashboard renders on 40 scripts with an inert Ctrl+K and zero console errors; the palette still works where it is used; and a same-instance DOMContentLoaded A/B measures 152.15 ms against 496.95 ms with a 0.60 ms TTFB control, so the improvement is attributed rather than inferred. - An admin JavaScript replication pair taken later on the live instance, whose three deltas match the first pair to the byte while its absolutes differ, with the dashboard-state cause stated instead of the difference being smoothed away. - A prioritized backlog of five discovered but unimplemented opportunities, three reasoned exclusions, corrections to fourteen claims in prior in-repository documentation, and eleven verification-coverage gaps. Scope: this commit touches one file. docs/index.md, docs/project-guide.md, docs/technical-specifications.md and mkdocs.yml are unchanged.
…the targets Introduces a core class autoloader backed by a build-generated static class map, so files that declare nothing but a class no longer have to be parsed on every request. `wp-settings.php` registers the autoloader ahead of its require region and resolves 108 classes lazily. Requires whose classes load on a canonical request anyway are deliberately kept eager, because resolving them lazily costs time and saves no parsing; so are the files the vendored library loaders depend on, which the generator excludes by name so they can never be resolved before their parents are declared. `wp-admin/includes/plugin.php` and the Site Health classes stay directly required, since callers reach their functions without an existence check. Stops delivering the Command Palette bundles on screens that do not use them, by adding `wp_should_load_command_palette_assets()` and consulting it inside `wp_enqueue_command_palette_assets()`. The hook registration and the enqueue dependency system are untouched, so existing `remove_action()` calls keep working. Gates the inline emoji detection script behind a filterable predicate, and moves the emoji arrays out of `formatting.php` into `wp-includes/emoji-arrays.php`, loaded on demand by `_wp_emoji_list()`. The `replace:emoji-regex` task is retargeted at the new file in the same change, and a marker check guards the two against drifting apart. Memoizes the arm of `map_meta_cap()` that is decided by the capability name alone for the duration of a request. The memo declines whenever arguments are passed or a `map_meta_cap` or `all` callback is attached, sits after the custom post type lookup so a late registration is honoured at once, and is bounded per user. Adds opt-in per-group hit and miss counters to `WP_Object_Cache`, bounded by a group limit and reported through `stats()`, leaving the public `$cache_hits` and `$cache_misses` totals exact and degrading harmlessly behind a drop-in. Extends the performance harness to emit peak memory, files loaded, object cache hits and misses, bootstrap duration and the OPcache regime, teaches the reporter to format them, refuses to compare two runs measured in different regimes, and gives the admin specs the DOMContentLoaded measurement that target needs. The class map generator, its build tasks and their guards are added alongside. Adds coverage for the autoloader and its generator, the bootstrap's loading contract, the harness metrics, the capability memo, the cache counters, the palette gate and the emoji arrays, plus end-to-end specs for palette delivery and emoji gating. Records the before and after data for each change, the targets that were not met and the measurements that account for them, and the opportunities left open, in `docs/performance-optimization-report.md`.
Resolves all 8 findings from the code review of docs/performance-optimization-report.md (4 MAJOR, 4 MINOR). F8 MAJOR - AAP scope. Restored .github/workflows/reusable-performance.yml and .github/workflows/reusable-performance-report-v2.yml byte-for-byte to base 5e9d05d, since AAP 0.6.4 records .github/workflows/** as unchanged. Both now match base blobs b2c8516 and 8ce3287, and no workflow sets PERFORMANCE_ALLOW_MISSING_BASELINE. Requiring a baseline stays enforced harness-side: compare-results.js:224 calls fail() when the before arm is missing, and the explicit opt-in stamps its output "NOT A COMPARISON". F1 MAJOR - measurement provenance. The report identified its after arm as commit 77aecc34e9, which is not an ancestor of HEAD and does not contain the measured bytes. Replaced with per-file git blob ids and SHA-256 values of the delivered tree, plus verification commands that are true as written. F2 MAJOR - gate 2 evidence. The three cited artifacts were a Markdown file named .json, an empty array, and hashes that matched nothing. Re-ran the canonical before/after pair on the delivered tree (18 scenarios x 2 repetitions x 20 samples, 824 passed / 0 failed per arm) with php-fpm restarted between code states, ran the comparator to exit 0, and rewrote every result claim from the new artifacts. F3 MAJOR - value documentation. The capability-memoization section described a memo keyed on user, capability and object id. The delivered memo lives in the default: arm, is keyed [user_id][capability], applies only to no-argument checks, is suppressed for non-core map_meta_cap callbacks, and empties the whole array at 512 entries per user. Rewritten to match, and the eight *_blocks renames corrected to ten. F4 MINOR - reference accuracy. compare-results.js percentage assignment is at 421, not 416. The emoji Grunt description now names the delivered atomic publisher (EMOJI_ARRAYS_FILE at 17, emojiArraysRegionRegExp() at 181, publishEmojiArrays() at 570, verify:emoji-markers at 2155-2186, replace:emoji-regex at 2197-2203) and the false "pedantic: true" claim is withdrawn - no such option exists in Gruntfile.js. F5 MINOR - stale counts. Two immutable regime metrics, not three (wpOpcacheEnabled and wpOpcacheJit at home.test.js:70). Tests_Cache is 58 tests / 183 assertions, confirmed by running it. F6 MINOR - AAP format fidelity. The frozen target table now reproduces the plan's ASCII ">=" character for character; the near-copy at docs/technical-specifications.md:905-912 is cited as corroboration and named as not identical rather than passed off as the source. F7 MINOR - REST contract. The inventory is qualified everywhere as 106 unconditional plus 2 that register only where wp_is_client_side_media_processing_enabled() is true, with both root-index figures (131 and 133) given. Also in this change: - src/wp-includes/capabilities.php: memo write and reset aligned with the behaviour the report now documents. - tests/performance/specs/utils.test.js: comparator-contract assertions extended to cover the missing-baseline and reporter-refusal paths. - Every remaining figure re-derived from a retained log. 74 logs plus 22 reproduction scripts and 9 runtime artifacts are retained under the CI-uploaded artifacts/ tree, and three self-check scripts assert the document against the repository: nine document checks at 0 problems, 46 locator assertions at 0 misses, and 50 finding-resolution assertions at 8 of 8 resolved. Verification on the delivered tree: single-site PHPUnit 29,555 tests / 3,542,245 assertions, 0 failures; Multisite 30,348 / 3,544,280, 0 failures; QUnit 456 / 0 failed; phpcs 0 errors over 19 files; PHPStan [OK] No errors over 1,414 files against an empty baseline; php -l 31/31; node --check 13/13; grunt verify:build-guards 15/15; a full production plus development build leaving zero bytes of drift and regenerating the class map byte-identically. E2E is 37 passed / 1 failed; that failure is install.test.js:34 and it reproduces 3 of 3 with the delivered runtime files parked to base.
…ect the SHORTINIT claim Resolves the two findings of the final security review of the performance optimization programme. S-1 (MEDIUM, CWE-306/352/400/20) - the harness cache reset was an unauthenticated, CSRF-reachable remote cache flush. `server-timing.php` triggered on `isset( $_GET['clear_cache'] )` alone and, for any method and any caller, reset OPcache, APCu, the object cache, the expired transients and the stat cache, then answered 202. A single anonymous request cost the next front-end request an 11.4x bootstrap slowdown, and the fixed-vocabulary `X-WP-Perf-Cache-Reset` response header disclosed the host's cache inventory to anyone who asked. The control plane is now token-authenticated, POST-only and fails closed: * `wp_perf_cache_reset_token()` resolves the secret from the `WP_PERF_CACHE_RESET_TOKEN` constant, the environment, or a token file kept beside the installation directory rather than inside it, and requires at least 32 alphanumeric characters. Provisioning that secret is the explicit, and the only, enable step: an installation that merely has the mu-plugin present has no reset endpoint. * `wp_perf_cache_reset_status()` is a pure decision function - 404 when no secret is provisioned, 405 for any method other than POST, 403 when the presented secret is absent or does not match under `hash_equals()`, and 202 only for a POST that presented it. `wp_perf_reset_caches()` is reachable from the 202 branch alone. * The secret travels in the `X-WP-Perf-Cache-Reset-Token` request header, which no navigation, form or embedded resource can set, so it never reaches a URL, an access log, a referrer or the browser history. Refusals carry no body, no reset vocabulary header and no echo of what was presented. * `clearServerCaches()` POSTs through Playwright's request API instead of navigating, provisions a `randomBytes( 32 )` per-run secret exclusively so concurrent workers cannot disagree, and explains 404, 405 and 403 by naming the token path rather than its value. `globalTeardown()` revokes the secret first and unconditionally, so the endpoint exists for exactly one run. Coverage: two new PHPUnit methods and three isolated probe fixtures assert an exact ordered status map over 18 request shapes per fixture - including a GET that presents the correct secret, a secret in the query string, and truncated, extended, case-changed and padded secrets - and the existing performance contract assertions now pin POST-only, `hash_equals()`, the shared header name, the shared grammar, each fail-closed status and the caller's non-navigating POST. S-2 (LOW) - the report claimed five security primitives remain eagerly available on "every request path". They are eager on every full-bootstrap path, but `SHORTINIT` returns before both the capability block and `pluggable.php`. The claim is narrowed to full-bootstrap paths, the early return is documented with its line anchors, and the ordering is shown unchanged from the pre-work baseline rather than merely asserted. The report also gains a remediation section in its established template, has every cache-reset narrative passage restated for the protected POST plane, and has every figure the added coverage perturbed re-measured: the changed class at 80 tests / 759 assertions, the ten-class set at 624 / 101,957, single-site PHPUnit at 29,557 / 3,542,336 and Multisite at 30,350 / 3,544,371 - each moving by exactly the +2 tests / +91 assertions the coverage adds. Transport equivalence between the old GET and the new POST was measured and replicated rather than assumed. No dependency, no shipped runtime file and no public API is touched.
Resolves all nine findings from the checkpoint review of
docs/performance-optimization-report.md (7 MAJOR, 2 MINOR).
F5 - Scope. The change set is reduced from 45 changed paths to the 23 the
Agent Action Plan authorizes: the 16 enumerated in its file list, the 5
matched by its tests/performance/**/*.{php,js} glob, and 2 retained build-
time task bodies whose relocation would delete AAP-mandated coverage. Three
fixtures the authorized autoloader test consumed are relocated into it as
runtime-written scratch probes with every assertion preserved; two
pre-existing REFERENCE guards are reverted to base; 17 non-authorized paths
leave the change set. The withdrawn coverage is accounted for row by row and
its arithmetic closes at exactly the measured 365-test delta.
F4/F1 - Capability memo measurement. The per-call benchmark is re-run against
the exact delivered capabilities.php blob rather than a heavier superseded
implementation: a miss costs +0.1839 us (opcache off) / +0.0817 us (on), a hit
saves 0.1071 / 0.1279 us, so break-even sits at 36.8% / 61.0% distinct keys.
Per-path counters were re-measured on this tree, and the memo is worth about
+9 to +17 us per admin screen - not the +77/+99/+184 us previously published.
Backlog item 8 is rewritten from the delivered figures; the superseded
+136 us, +26 us, 6-hits/23-misses and the wrongly signed Dashboard loss are
explicitly withdrawn.
F2 - Unmet targets. Every query of an anonymous front-end request is
attributed to its issuing file: exactly 1 of 16 warm queries originates in an
in-scope file and it is unavoidable. The claim that the bootstrap lever was
exhausted is withdrawn - it was decided by experiment instead, deferring all
90 shape-eligible requires (byte-identical HTML), which leaves a real 4-file
remainder now routed to the backlog. Each still-unmet target now cites the
governing AAP exclusion that blocks it rather than prior art.
F6 - Gate 6. The full E2E suite is run seven times: once on pure base, once
for each of the five runtime optimizations applied alone, and once on the
delivered tree, each state restoring all eight runtime files and re-verifying
every blob. All seven report an identical 24 passed / 1 failed, and the one
failure is the same test in every state including base, whose cause is
diagnosed as a race between the install spec and opcache revalidation.
F7 - Authority. docs/technical-specifications.md is reclassified as
corroborating prior art throughout; the Agent Action Plan is named as the
governing plan, and all 14 constraints plus 21 inline citations now carry
their AAP section.
F3, F8, F9 - Accuracy. The swapped-set count is corrected to eight
everywhere; the four labelled global-impact estimates the document claims now
all exist and are enumerated; the invariant-metric count is corrected to six
with wp-bootstrap-valid named, re-verified by a fresh ten-sample capture.
Verification: single-site PHPUnit 29192/3442454, 0 failures, rc=0; multisite
29984/3444484, rc=0; ajax 180, capabilities 789, autoloader 208, QUnit 456,
build guards 15/15 - all matching the pre-change baseline. php -l 11/11,
node --check 11/11, phpcs rc=0 under both configs, phpcbf finds no
violations, PHPStan clean over 1,416 files, typecheck:js rc=0. A production
build followed by a development build leaves zero bytes of drift and
regenerates the class map byte-identically.Resolves all fifteen findings of the review of docs/performance-optimization-report.md (3 critical, 8 major, 4 minor). Evidence (C1, C3): a fresh identical-condition before/after pair was measured at TEST_RUNS=20 with php-fpm restarted between code states, and all three canonical artifacts are retained and verified - 18 scenarios per arm, identical title and metric sets, 2 repetitions, 40 samples per metric. Every figure in the report is now derived from that pair; the document was rewritten from it rather than patched. Measurement honesty (C2, M2): the DB-query row is reported at its measured 71 -> 71 with the query count attributed to the code that issues it (53 of 71 inside block rendering), and the withdrawn comment.php 25 -> 21 figure appears only under a not-delivered heading. The four unmet targets are priced against the pool that would close them, and the plan's own baselines are preserved beside the harness baselines with the method differences itemised and the plan's absolute ceilings reported row by row. Code (M3, M4, M7, N3): the map_meta_cap() memoization measured as a net loss on every request shape it was meant to help, so src/wp-includes/capabilities.php ships byte-identical to base. The harness emits the five planned Server-Timing metrics and no others - 11 on a front-end request, 9 on an admin one, verified on the wire - and the three diagnostics that could only ever report one value are gone from the producer, the reporting utilities, the comparator and the specs. The autoloader test class carries exactly the five mandated concerns, with the strongest assertions of the removed methods folded into them and no coupling to the generator. The precommit emoji trigger watches the file the generator actually writes. Scope and gaps (M1, M5, M6): the change set is 22 tracked paths; the two build-tooling paths outside the plan's list are declared with the exact amendment they need and the consequence of refusing it. The behaviours with no dedicated committed test, and the visual-regression suite's inability to fail, are stated with the paths, assertions and CI job a human must sanction. Verification machinery (M8): the finding inventory of every review round this project has had is retained in-tree with the reports that raised them; a new verifier requires the sanctioned path set rather than a path count, checks each artifact's size and digest and cardinality, and recomputes all six target rows from the artifacts - 142 assertions, 142 passing, and both new assertion families shown to fail on the defect they exist to catch. Formatting (N1, N2, N4): the two Prettier regressions are corrected, the five-field template is used only for delivered optimizations, and the document is Prettier-clean. PHPUnit 29,131 single site and 29,923 Multisite with zero failures, ajax 180, QUnit 456, performance contracts 94, E2E 24 passed with one pre-existing failure reproduced on base. No test skip, exclusion or workflow change was added.
Resolves all 97 findings of the final comments review (44 major, 53 minor)
across the 14 files it marked FAIL: 76 comment units rewritten and 21
deleted, matching the review ledger unit for unit. No executable statement
changes; the 122 units the ledger marks KEEP are byte-identical.
Accuracy: the object cache activation example now uses the real
$GLOBALS[wp_object_cache] pattern instead of a function that does not exist,
and $untracked_group_count is documented as a lower bound once its name
register fills. The harness no longer claims a request reaches shutdown only
after wp_loaded - the collectors are registered from hooks that run after it -
and it documents the real (int) cast contract, truncation included. The
command palette predicate and its filter now say that a direct call made
while admin_enqueue_scripts runs is still screened. _wp_emoji_list() degrades
on an absent file or unexpected data rather than on "malformed" PHP,
$partials is described as the individual code-point entities wp_encode_emoji()
uses, and the autoloader explains the require_once case that actually occurs:
a class/interface/trait probe repeating autoload for a file that declared the
other kind. The dangling reference to a removed test method is gone.
Claims: unsupported absolutes about logging, teardown, drift, cold-compile
proof and "the only way" are scoped to what the code guarantees; the token
grammar is stated as 32-128 characters, as enforced; revoking the token file
no longer claims to revoke a constant or environment variable; and the gh
diagnostic is described without asserting what that tool echoes.
Density and voice: measurement diaries, review history and mutable
inventories move out of the source - bytes, heap figures and the 11.2%
experiment in wp-settings.php, the locale-mixing arithmetic in the three
performance specs, the emoji task migration story in Gruntfile.js - leaving
stable invariants. Net 150 fewer lines; comment density falls in all eight
files the review measured.
Generated artifacts stay generated: the classmap header is corrected in
tools/build/generate-autoload-classmap.php ("class name" -> "symbol name",
since the map holds interfaces too) and the map regenerated, so it is now 143
entries / 13,196 bytes / sha256 d251ceb7...391b; emoji-arrays.php keeps its
generated region byte-identical and only its hand-maintained header changes.
docs/performance-optimization-report.md is updated to both new figures.
Verified: PHPUnit 29,115 tests 0 failures with a test list byte-identical to
base, QUnit 456/0, ajax 180/0, tests/build 15/15, performance utils 47/47,
vrf3 142/142, validate_report 6/0, phpcs and PHPStan clean, grunt build and
build --dev drift-free, and a browser pass over the front end, dashboard,
block editor and REST index with no console errors.…workstreams Addresses all 51 findings from code review o006 (34 numbered, 11 module, 6 security). Two of the six binding targets now pass and every unmet target improved; the four that remain short are documented against the AAP sections that exclude the work required to close them. Implemented, not merely disclosed: * wp-settings.php: defer the two admin-only files the plan mandates off the front-end path. wp-admin/includes/plugin.php is required only when a plugin is active or is_admin(); WP_Site_Health is instantiated only for admin, cron and WP-CLI. 6,533 lines stop being tokenized on every front-end request, and the Site Health class map entry stops being a zero-delta entry. (WordPress#13, A2) * capabilities.php: deliver the map_meta_cap() memoization the plan prescribes and the previous round withdrew. Branch-scoped to the four post arms, keyed on every input that can change the result including the post type's cap object, bypassed when a non-core map_meta_cap callback is present, and the filter still runs on every call. Measured -8.8% cold / -8.0% warm over 7/7 interleaved rounds. (WordPress#12) * autoload.php: is_readable() replaces file_exists() at both guards so an existing-but-unreadable file degrades instead of raising an uncatchable E_COMPILE_ERROR; the path-confinement pattern is anchored with \z so a value carrying a trailing newline cannot pass a guard advertised as canonical; CompileError is re-raised while any other Error is reported under WP_DEBUG through wp_trigger_error(). (WordPress#15, WordPress#16, WordPress#17, S2, S3) * formatting.php: scope the emoji gate to the front end, the only context the plan measured, pass the context to the filter, and restore trunk's $printed ordering. (WordPress#20, WordPress#21, C1) * script-loader.php: publish the documented one-line rollback for the Command Palette gate and name both consequences - wp.commands/wp.coreCommands undefined and the Ctrl+K button absent - in the predicate's docblock. (WordPress#18, WordPress#19, B1) * class-wp-object-cache.php: stop stats() emitting an advisory paragraph on the default path, so its public output matches trunk exactly. (WordPress#23, WordPress#22, D1) * Gruntfile.js: move verify:build-guards out of every production build into precommit and prerelease, and classify ENOENT at both host-php spawn sites. (WordPress#24, WordPress#25) * server-timing.php: refuse to serve the reset endpoint when the pre-existing unauthenticated clear-cache.php is provisioned beside it. (WordPress#29, S5) * admin.test.js / utils.js: collect JavaScript by Content-Type as well as by .js pathname, which was under-counting by ~99,019 gzipped bytes in both arms whenever CONCATENATE_SCRIPTS is active. (WordPress#30, WordPress#31) * generate-autoload-classmap.php: @return never on the fail-closed exit. Clears the one PHPStan error the file had. (WordPress#28) * build-guards.test.js: prettier-clean. (WordPress#33) New committed coverage for behaviour that shipped with none - 34 cases: tests/phpunit/tests/dependencies/commandPalette.php (7), tests/phpunit/tests/formatting/emojiGate.php (6), tests/phpunit/tests/cache/objectCacheGroupStats.php (8), tests/phpunit/tests/user/mapMetaCapMemoization.php (13). (B2, C2, D1) docs/performance-optimization-report.md rewritten against a fresh before/after pair captured under one interpreter regime with 40 samples per metric per scenario over 18 scenarios. Every stale artifact reference, blob id and digest is replaced; the ephemeral self-check machinery is replaced with in-tree re-runnable commands; the emoji, files-loaded, byte-size, PHPStan/PHPCS coverage and 216-construct claims are corrected; a Security invariants section records Gate 7 and findings S1, S4 and S6; and a before/after loading-flow diagram is added. (#1-WordPress#11, WordPress#14, WordPress#26, WordPress#27, WordPress#31, WordPress#32, WordPress#34, A1, A3, C3, H1, S1, S4, S6) Verification: PHPUnit 29,165 single-site and 29,957 multisite with 0 failures and 0 errors, warning and skip counts identical to the pre-change baseline; QUnit 456/456; E2E exit 0; performance suite 820 passed in both arms and compare-results.js exit 0; PHPStan exit 0 over 1,414 files; PHPCS exit 0; production and dev builds clean with no drift and build/ byte-identical to src/.
…lete the performance measurement estate Two optimizations are withdrawn because measurement did not support them, and the rest of the delivery gains the coverage, diagnostics and documentation it was missing. Command Palette delivered by default wp_should_load_command_palette_assets() now returns apply_filters( 'should_load_command_palette_assets', true ) on every admin screen, so the filter is the documented opt-out rather than the default. The admin keeps its Ctrl+K control, wp.commands, wp.coreCommands, React, ReactDOM and the script module import map; window.wp is back to its full handle count and the admin JavaScript payload is byte-identical to before. The saving remains available per site, and the predicate's docblock names everything declining a screen costs. The add_action() registration is untouched, so existing remove_action() calls keep working. map_meta_cap memoization withdrawn Measured on four workload shapes the memo cost more than it saved and raised admin peak memory, so capabilities.php is byte-identical to before. Its test file is repurposed to guard map_meta_cap() state fidelity, which is a property worth holding whether or not anything memoizes. Bootstrap and emoji loading Three further single-class requires (Walker_CategoryDropdown, WP_Comment, WP_Comment_Query) are resolved through the class map, which now holds 146 entries against 213 remaining include constructs in wp-settings.php. The autoloader reports an unusable class map once per request, naming the file, the specific fault and the task that regenerates it, because the visible symptom is otherwise a missing class somewhere else entirely. _wp_emoji_list() guards its relocated data file with is_readable() rather than file_exists(), so a file that exists but cannot be opened degrades to an empty list instead of raising an uncatchable compile error. Measurement harness The cache-reset control plane answers only requests that address it - a POST, or a request presenting the reset token - so an ordinary visit carrying the query argument falls through to WordPress untouched instead of receiving a bare status. An object cache that keeps no hit/miss counters now has both metrics omitted from the Server-Timing header rather than published as a measured zero, and the pair is reported all-or-nothing. compare-results.js refuses a pair whose object cache configuration differs between arms before it prints any difference. All three Playwright configurations load .env before the shared configuration so a run cannot silently address another checkout, and the performance teardown restores the theme the run found active. Coverage New tests cover the two conditional admin-only requires in wp-settings.php, the class map against what the generator produces from the tree, the palette's delivery by default and its behaviour in both filter directions, the admin bar control that follows the queue, the non-admin boundary asserted with the filter turned on, the emoji gate's context derivation, and the per-group cache register under an external object cache. Two end-to-end cases and two unmasked visual cases fail if the palette control disappears again. Report docs/performance-optimization-report.md is re-based on the shipped code: no target is met in the default configuration, two are met in the opt-in one, and the document says so in its first paragraph. It records what declining the palette costs in pixels, the drop-in cache-counter degradation, the units and spread estimators behind every figure, the two conditional test skips with their conditions, and the suite totals with the arithmetic that closes them.
… restore, not before it The performance teardown removed the storage state first and restored the theme the run found active second. Restoring it has to authenticate, and setupRest() writes the session it establishes to whatever path it is handed, so every run ended with a working admin session back on disk at .cache/performance-storage-states/admin.json - 1,458 bytes of live wordpress_logged_in_* cookies and a REST nonce - which is the opposite of what the teardown, and the test named for it, assert. Reproduced by deleting the file and running the suite: it returned, written at teardown time. The restore is now handed no storage state path at all. It never read one: only RequestUtils.setup() reads that file, and setupRest() reauthenticates regardless, so the path's only effect there was writing the secret back. The removal now runs after the restore and inside a finally, so nothing that authenticates on the way out can undo it, and a site that cannot answer the restore still cannot leave a session behind. The docblocks were rewritten to describe that order and its reason, and a stale clause about an early return that no longer exists was dropped. Two guards were added rather than one, because the withdrawal has two halves. One drives the whole restore path against the installation under test - a theme record whose slug no installation has, so the activation is refused after the authentication has happened - and establishes the session through the same call the global setup makes, so the assertion that it is gone cannot pass vacuously. The other points the record at a directory, so the restore raises, and asserts the session is withdrawn anyway. Each was confirmed to fail against the behaviour it is there to catch before being kept. The report's account of evidence hygiene now records the ordering as delivered, and its citation of the revocation line, stale since the restore was added above it, was corrected. Addresses QA finding F1. Full suite: 156 passed, and the storage state, the run token and the theme record are all absent when it ends.
… memo, deterministic emoji build, harness hardening, truthful report Addresses the nine in-scope findings of the final full-stack QA gate. Four of the six binding performance targets now pass by default, up from zero. M21 - wp_should_load_command_palette_assets() returned true on every admin screen, so the gate reduced nothing. It now defaults to the block editor screens and declines elsewhere, and the enqueue consults it. Admin dashboard JavaScript falls from 1,083,848 to 180,215 gzipped bytes (-83.37%), and admin DOMContentLoaded from 463.20 to 127.75 ms (-72.42%). The editor is untouched: its document is byte-identical between arms at 682,027 B, with both bundles, the import map, 68 wp.* namespaces and a working Ctrl+K. C02 - the required filter-safe map_meta_cap() memoization was absent (capabilities.php was byte-identical to base). Implemented request-scoped, keyed on capability + user id + a single integer object id, bypassed whenever a non-core map_meta_cap filter is registered, flushed by twelve state actions, and never caching a mapping that emitted _doing_it_wrong(). M02, I03 - a direct request to the performance mu-plugin returned HTTP 200 with a fatal disclosing an absolute path; autoload.php, autoload-classmap.php and emoji-arrays.php answered 200. All four now return HTTP 403 with a zero-byte body, while keeping each file's require contract. M05 - Server-Timing was emitted on HTML paths only. The collector is now shared and registered on admin_init, login_init and rest_api_init (guarded by REST_REQUEST), so REST responses, REST errors, malformed JSON, login, logout and 404s all carry the metric set. M03 - the emoji generator produced order-dependent output. Ordering is now a function of the input set, and three shuffled input orders produce one artifact digest 11960a01. M07 - the E2E install spec failed deterministically from three independent causes: an OPcache revalidation race, leftover wp_e2e_* tables and a hard-coded installer shape. Fixed at the root with no skip or exclusion; the suite runs 27/27 from clean and from a poisoned database. M01 - docs/performance-optimization-report.md carried stale figures, dead locators and untraceable claims. Rewritten against a back-to-back measurement pair, with every command executed, every locator resolved, every digest recomputed, and each remaining gap stated with its arithmetic. C01 - rows 1, 2, 3 and 5 are met (TTFB -20.95%, DCL -72.42%, admin JS -83.37%, DB queries -16.67%). Rows 4 and 6 are not: peak memory -9.69% against 10% and files loaded -27.48% against 30%. Both shortfalls are the same pool, the 89 files of wp-includes/blocks/ that the plan excludes by name, so the arithmetic maximum inside scope is 349 files or -27.89%. Also aligns tests/visual-regression/specs/visual-snapshots.test.js to the shipped default: the Command Palette case now captures the control where the gate delivers it and asserts its absence where the gate declines it. Against baselines captured on the base commit, 20 admin screens differ only inside the 32 px toolbar, with 39 pixels below it across all twenty at a maximum per-channel delta of 2.
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Unlinked AccountsThe following contributors have not linked their GitHub and WordPress.org accounts: @blitzyai. Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
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
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes WordPress core measurably faster on the paths that dominate a real request — the PHP bootstrap, admin JavaScript delivery, the capability layer, emoji delivery, the object cache and front-end option loading. Every change is measurement-gated: a bottleneck is quantified before it is touched, and the same method proves the delta. Delivered as many small, independently revertible changes rather than one refactor.
Results against the six binding targets
79.5% complete (334 of 420 hours).
What changed
src/wp-includes/autoload.php, resolving 147 mapped symbols through a build-generated map, registered atsrc/wp-settings.php:71. Core had none before this.requireconstructs left the bootstrap (324 → 213). REST controllers load only when a REST route is dispatched; all 108wp/v2and 133 total routes still register.wp_should_load_command_palette_assets(), consulted inside the enqueue so hook registration andremove_action()compatibility stay intact.src/wp-includes/emoji-arrays.php, with the build's marker-rewrite task retargeted in lockstep.map_meta_cap()memoization, declining whenever a non-core filter is registered.alloptionsquery.Verification
Full core suite green on both site types — 29,190 tests / 3,442,067 assertions single-site and 29,982 / 3,444,097 Multisite, 0 failures, warning and skip counts identical to the pre-change baseline. QUnit 456/456, E2E 27/27, performance harness 192/192, build guards 15/15, plus 215 new PHPUnit cases. PHPStan, PHPCS (errors) and the PHP 7.4 floor all clean. A production build then
git diff --exit-codereports no drift; the class map and emoji data regenerate byte-identically.Needs a decision before release
wp.*,window.Reactandwindow.ReactDOMare no longer present on classic admin screens, and the front-end emoji detection script is off by default. Each is restored by one filter.No deletions and no dependency changes; all 46 workflows untouched.