Skip to content

Blitzy: Measurement-gated WordPress core performance work — class autoloader, bootstrap deferral, admin JS gating - #1

Open
blitzy[bot] wants to merge 31 commits into
trunkfrom
blitzy-0ac0cc33-88c3-44dd-8409-367aee62e777
Open

Blitzy: Measurement-gated WordPress core performance work — class autoloader, bootstrap deferral, admin JS gating#1
blitzy[bot] wants to merge 31 commits into
trunkfrom
blitzy-0ac0cc33-88c3-44dd-8409-367aee62e777

Conversation

@blitzy

@blitzyblitzyBot commented Aug 13, 2026

Copy link
Copy Markdown

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

MetricBeforeAfterDeltaTarget
Front-end TTFB425.70 ms336.50 ms−20.95%≥20%
Admin DOMContentLoaded463.20 ms127.75 ms−72.42%≥15%
Admin JS (gzipped)1,083,848 B180,215 B−83.37%≥30%
DB queries / front-end load1815−16.67%≥15%
PHP peak memory8,063,360 B7,282,232 B−9.69%≥10%
PHP files loaded484351−27.48%≥30%

79.5% complete (334 of 420 hours).

What changed

  • A class autoloader for coresrc/wp-includes/autoload.php, resolving 147 mapped symbols through a build-generated map, registered at src/wp-settings.php:71. Core had none before this.
  • 111 eager require constructs left the bootstrap (324 → 213). REST controllers load only when a REST route is dispatched; all 108 wp/v2 and 133 total routes still register.
  • Screen-aware Command Palette gating via wp_should_load_command_palette_assets(), consulted inside the enqueue so hook registration and remove_action() compatibility stay intact.
  • A 143 KB emoji array literal moved out of the tokenizer's path into src/wp-includes/emoji-arrays.php, with the build's marker-rewrite task retargeted in lockstep.
  • Filter-safe, request-scoped map_meta_cap() memoization, declining whenever a non-core filter is registered.
  • Per-group object-cache counters beside the untouched public totals; front-end option priming folded into the existing alloptions query.
  • Instrumentation for peak memory, files loaded, bootstrap duration and cache hit/miss counts — giving the DOMContentLoaded target its first baseline.

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-code reports no drift; the class map and emoji data regenerate byte-identically.

Needs a decision before release

  • The two unmet rows are one gap counted twice. The addressable remainder is two files, so the in-scope maximum is −27.89%. Of the 351 files still loaded, 89 belong to the Gutenberg-synced block tree the plan excludes by name and 154 are function-holding root files no class map can reach. Closing them is a scope decision.
  • Two user-visible defaults need sign-off.wp.*, window.React and window.ReactDOM are no longer present on classic admin screens, and the front-end emoji detection script is off by default. Each is restored by one filter.
  • The admin-appearance boundary is not yet enforced — the visual suite ships no committed baselines.

No deletions and no dependency changes; all 46 workflows untouched.

blitzyai added 30 commits July 31, 2026 04:41
…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.
…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.
@github-actions

Copy link
Copy Markdown

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 props-bot label.

Unlinked Accounts

The 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.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@blitzyai