From 6fa011a9a6c28ab93b99c57222eb5c23549c8fbb Mon Sep 17 00:00:00 2001 From: Malin Date: Sat, 15 Aug 2026 21:26:58 +0200 Subject: [PATCH] feat: add wordpress-performance-diagnostics skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Paired skills.sh/autoskills.sh catalog scans (2026-08-15, reports in granja/_temp/codex-logs/) independently flagged wordpress/agent-skills' wp-performance module as the best net-new find of the whole scan: a measurement-first, backend-only WP diagnostic workflow (WP-CLI doctor/profile, headless Query Monitor, autoload/object-cache/cron checks) — a strong fit since this fleet's 13+ WordPress sites are all headless/jailed with no browser-first profiling access. Adapted from github.com/wordpress/agent-skills (skills/wp-performance) rather than installed verbatim: swapped the upstream's local/SSH assumptions for the fleet's actual `bastille cmd ... su -m www` remote-execution pattern (reusing wordpress-cli-remote-execution's convention exactly), added a jail/site selector step against the real granja/staging/gringo inventory, narrowed the always-collect baseline to four metrics (TTFB, autoload size, object-cache presence, cron health) with fleet-specific context for each, added redaction guidance for Valkey/DB credentials that diagnostic output can surface, and replaced the upstream's hardcoded "WordPress 7.0+" compatibility claim with per-site version verification. Co-Authored-By: Claude Sonnet 5 --- .../SKILL.md | 253 ++++++++++++++++++ .../references/diagnostic-tools.md | 116 ++++++++ .../references/fix-patterns.md | 133 +++++++++ 3 files changed, 502 insertions(+) create mode 100644 skills/wordpress-performance-diagnostics/SKILL.md create mode 100644 skills/wordpress-performance-diagnostics/references/diagnostic-tools.md create mode 100644 skills/wordpress-performance-diagnostics/references/fix-patterns.md diff --git a/skills/wordpress-performance-diagnostics/SKILL.md b/skills/wordpress-performance-diagnostics/SKILL.md new file mode 100644 index 0000000..16bb160 --- /dev/null +++ b/skills/wordpress-performance-diagnostics/SKILL.md @@ -0,0 +1,253 @@ +--- +name: wordpress-performance-diagnostics +description: Use when investigating why a WordPress site in the fleet is slow (frontend TTFB, wp-admin, a REST route, WP-Cron) or before proposing any performance remediation on one. Backend-only, measurement-first diagnostic workflow for headless/jailed WordPress sites reached via `bastille cmd` — establishes baseline evidence (TTFB, autoloaded-options size, object-cache presence, cron health) before any write/remediation action. +--- + +# WordPress Performance Diagnostics + +Adapted from wordpress/agent-skills' `wp-performance` module +(`github.com/wordpress/agent-skills`, `skills/wp-performance`) — +identified 2026-08-15 via a paired skills.sh/autoskills.sh catalog scan; +both scans independently flagged it as the single best net-new find of +the whole scan, since this fleet's WordPress estate is entirely +headless/jailed with no convenient browser-first profiling access. This +version keeps the upstream's measurement-first structure but replaces its +local/SSH assumptions with the fleet's actual `bastille cmd` +remote-execution pattern, adds a jail/site selector step (13+ sites +across granja/staging/gringo), and narrows the always-collect baseline to +four metrics with fleet-specific context for each. + +## When to use + +- A WordPress site (or several) is reported slow — frontend TTFB, + wp-admin, a REST route, or WP-Cron. +- Before proposing ANY performance remediation on a fleet site — collect + the baseline evidence first, always. +- Comparing before/after a change (theme deploy, plugin update, + object-cache rollout) to confirm it actually helped. + +Every site in this fleet is a jailed, headless WordPress install with no +browser-based profiling access — WP-CLI, `curl`, and log inspection are +the only tools available. Treat this as backend-only by default. + +## Step 0 — read-only-first, always + +This skill's job is to produce evidence, not to remediate. Until the user +has explicitly asked for a fix (and you've already captured a baseline to +measure against), do not: + +- flush any cache (`wp cache flush`, `wp redis flush`/`wp redis enable`) +- install or activate a plugin +- change `wp-config.php` or any debug constant (`WP_DEBUG`, `SAVEQUERIES`) +- restart php-fpm/nginx/valkey + +If a fix is later proposed, re-run the exact same measurements from Step +3 afterward and report the delta — see Verification below. Flushing or +restarting anything *before* you have a baseline destroys the thing you +needed to compare against. + +## Step 1 — identify the jail (site selector) + +The fleet runs 13+ WordPress jails across three hosts. Domain names do +not map to jail names by a fixed rule (jail names are typically the +domain with punctuation stripped, but always confirm, don't guess) — look +it up in the per-host inventory before running anything: + +- `docs/server-granja.md` — granja's "Site jails" table: 13 WordPress + jails, e.g. `menorca.pt` → jail `menorcapt` (`10.20.0.19`), `elrumano.es` + → jail `elrumanoro` (`10.20.0.12`). +- `docs/server-staging.md` — staging's "Site jails" table, e.g. the + `bauxa` jail, which serves Cala Verd restaurant's WordPress site + alongside four other, non-WordPress services in the same jail. +- `docs/server-gringo.md` — gringo's site jails, e.g. `bauxa-app` + (`10.20.0.76`) — the same Cala Verd WordPress site, migrated here + 2026-08-13 but not yet cut over in DNS/NPM; both copies are currently + live, so confirm which one you actually mean before diagnosing. + +If a domain isn't in any of those tables, don't guess a jail name — ask, +or `ssh root@ "bastille list"` and cross-reference `wp option get +siteurl` per jail (the same method used for the 2026-08-13 granja +re-audit) rather than trusting an older or stale note. + +Once you have ``, ``, and the site's on-disk path, confirm the +path rather than assuming a fixed convention — e.g. `bastille cmd +ls /usr/local/www/`. Multi-service jails (like `bauxa-app`, which serves +five separate things behind one jail IP) don't have one obvious WordPress +docroot; guessing wrong here silently profiles the wrong document root. + +## Step 2 — remote WP-CLI wrapper + +Reuse the fleet's established remote-execution pattern exactly — see the +`wordpress-cli-remote-execution` skill for the full gotcha list +(jail-exec output-prefix stripping, `error_reporting(0)` bootstraps +swallowing errors, shell globbing failing silently under some remote +shells). Do not invent a parallel SSH-direct or local-WP-CLI pattern; +every site in this fleet is reached the same way: + +```bash +ssh root@ "bastille cmd sh -c 'cd && su -m www -c \"wp --path=\"'" +``` + +- Run as the jail's web user (`www`) via `su -m www -c`, not as root — + matches the fleet convention and avoids the `wp-content` + ownership-drift problem documented in `wordpress-plugin-conventions` + (files created as root over SSH have already caused a real + WordPress-core-update failure on this fleet — `menorca.ro`, 2026-07-10). +- Always pass `--path=` explicitly, don't rely on `cd` propagating. +- Strip the `bastille cmd` wrapper's own `[]:` prefix line before + parsing any captured output programmatically. + +## Step 3 — the four core baseline metrics + +Collect all four before proposing any fix, even when the symptom seems to +point at one obvious cause — a slow site is frequently more than one +thing at once, and an isolated fix without a baseline can't be verified +afterward. + +**1. TTFB** (time to first byte, measured from outside the jail): + +```bash +curl -s -o /dev/null -w 'ttfb=%{time_starttransfer}s total=%{time_total}s http_code=%{http_code}\n' -H 'Host: ' http:/// +``` + +Hit the jail's own IP directly with the right `Host:` header (per the +`bastille-jail-provisioning` skill's testing gotcha) unless the site's +public vhost is already fully wired — hitting the host's `127.0.0.1` with +a `Host:` header before that vhost exists silently falls through to a +different site's `server_name` and produces a misleadingly "fast" TTFB +for the wrong page entirely. Take at least 3 samples and use the median; +a single run is noise. + +**2. Autoloaded-options size:** + +```bash +ssh root@ "bastille cmd sh -c 'cd && su -m www -c \"wp option list --autoload=on --format=total_bytes --path=\"'" +``` + +No fixed "too big" threshold is asserted here — compare against the +site's own history or a healthy sibling jail rather than trusting one +specific byte count as universally bad. To find the biggest individual +offenders, add `--fields=option_name,size_bytes` and sort the output. + +**3. Object-cache presence:** + +Don't assume every site has a persistent object cache. As of the most +recent audit (`docs/server-granja.md`, `docs/server-cerebro.md`), only +`menorca.pt`/`menorca.ro` on granja have the Redis Object Cache plugin +wired against cerebro's Valkey instance — the other granja jails, and +Cala Verd's MariaDB-only setup, do not. Check, don't assume: + +```bash +... wp eval 'echo wp_using_ext_object_cache() ? "yes" : "no";' --path= +``` + +plus confirm the drop-in file exists: `bastille cmd test -f +/wp-content/object-cache.php && echo present`. If present, `wp +redis status` (Redis Object Cache plugin) reports connection health — +Valkey lives on `cerebro`, with per-site ACL credentials; see the +Redaction section below before pasting any of that output verbatim +anywhere. + +**4. Cron health:** + +```bash +... wp cron event list --path= +... wp cron test --path= +``` + +Look for: events stuck far in the past (missed spawns), duplicate events +on the same hook, and any heavy-looking hook (import/sync/content-agent +jobs — several granja sites are fed by hourly RSS/content-agent crons per +`docs/server-granja.md`) that might be running inline on a page load +instead of via a real WP-Cron spawn. + +## Step 4 — deeper diagnostics (only if the four baseline metrics don't explain it) + +Load [`references/diagnostic-tools.md`](references/diagnostic-tools.md) +for `wp doctor`, `wp profile stage/hook/eval`, and headless Query Monitor +usage via REST response headers — all still read-only, still WP-CLI/curl +based, no browser required. + +Load [`references/fix-patterns.md`](references/fix-patterns.md) once a +dominant bottleneck category is identified (database/queries, autoload, +object cache, cron, remote HTTP calls) — condensed fix-pattern guidance +per category, still gated by the same read-only-first / measure-before- +and-after discipline as everything above. + +## Redaction — before pasting ANY of this into a report + +Diagnostic output on this fleet can surface real secrets. Before quoting +command output verbatim in a report, chat message, or committed doc, +redact: + +- `wp config list` / raw `wp-config.php` contents — `DB_PASSWORD`, auth + keys/salts, `WP_REDIS_USERNAME`/`WP_REDIS_PASSWORD` (Valkey ACL + credentials; per `docs/server-cerebro.md`, per-site passwords live at + `/root/.granja_valkey_creds` on cerebro, the admin password at + `/root/.valkey_admin_pw` — never paste these values themselves either). +- `wp option list` output that happens to include API keys stored as + plugin options (payment-gateway keys, mail-relay credentials, etc.). +- Any Application Password generated while setting up headless Query + Monitor REST auth (see `references/diagnostic-tools.md`). +- Full stack traces / DB error output that can echo a connection string. + +Follow the `` convention used elsewhere in this library (see +the `diagnosing-bugs` skill): replace the secret value in place, keep +everything else so the diagnostic signal survives. When in doubt whether +a value is sensitive, redact it — the cost of over-redacting is a +follow-up question; the cost of under-redacting is a leaked credential in +a transcript or a committed doc. + +## Verification + +- Baseline and after-fix numbers were captured with the *same* method + (Step 3, same site, same `--path`/`--url`, ideally the same time of + day — don't compare a cold-cache run to a warm one). +- `wp cron test` / `wp doctor check` (if installed) are clean or + improved. +- No new PHP errors in the jail's PHP-FPM/WordPress debug log after the + change. +- If the fix required a cache flush, that flush is the *last* step + taken, not a diagnostic shortcut — flushing before measuring erases the + baseline you needed to compare against. + +## Failure modes + +- **"No change" after a fix** — check you're hitting the same jail IP / + domain (`Host:` header) as the baseline, and that PHP's opcache isn't + serving a stale compiled version (clearing it, or restarting php-fpm, + is itself a diagnostic action worth noting in the report, not a routine + step to take silently). +- **Noisy TTFB/profiling numbers** — an hourly content-agent cron job on + granja sites, or a plugin update check, landing mid-sample can skew a + single `curl` run. Take multiple samples. +- **Object-cache check says "no" but you expected "yes"** — confirm which + site this actually is; only two of granja's 13 jails currently have + Redis Object Cache wired up. Check the inventory before treating that + as a bug. + +## Escalation + +- Do not install plugins (including the `wp doctor`/`wp profile` + packages themselves), enable `SAVEQUERIES`/`WP_DEBUG` in a way that + could linger, or flush any shared cache without explicit approval — + cerebro's Valkey DB 0 is genuinely shared across sites, and `wp redis + enable`'s `FLUSHDB` call can transiently clear a *different* site's + cache too (confirmed in `docs/server-cerebro.md`). Treat this as + production-adjacent even on a "staging" host — the `staging` host has + hosted real production traffic before (see `docs/server-staging.md`). +- If system-level profiling (an APM, a PHP profiler extension) is needed + beyond WP-CLI's own `profile` package, that's an infra change — flag it + rather than installing it unilaterally on a jail. + +## Version note + +Do not assume a fleet-wide WordPress version. The upstream `wp-performance` +skill asserts "WordPress 7.0+, PHP 7.4.0+" as its own compatibility +target — treat that as the upstream author's target, not a verified fact +about this fleet. Confirm per-site with `wp core version --path=`. +As one dated data point: granja's 13 WordPress jails were confirmed on +core 7.0.4 during the 2026-08-12 fleet-wide security rollout +(`docs/server-granja.md`) — that's a specific fact about one host at one +point in time, not a floor to assume for staging, gringo, or any future +site. diff --git a/skills/wordpress-performance-diagnostics/references/diagnostic-tools.md b/skills/wordpress-performance-diagnostics/references/diagnostic-tools.md new file mode 100644 index 0000000..6e97d2d --- /dev/null +++ b/skills/wordpress-performance-diagnostics/references/diagnostic-tools.md @@ -0,0 +1,116 @@ +# Deeper diagnostic tools (still read-only, still backend-only) + +Load this file only once the four core baseline metrics (TTFB, autoload +size, object-cache presence, cron health — see `SKILL.md` Step 3) haven't +explained the symptom. Every command here still goes through the fleet's +`bastille cmd ... su -m www -c "wp ..."` wrapper from `SKILL.md` Step 2 — +abbreviated to bare `wp ...` below for readability. + +## `wp doctor` — quick production-readiness checks + +Catches common footguns fast: autoload bloat, `SAVEQUERIES`/`WP_DEBUG` +left on, stale plugin/core versions. + +```bash +wp doctor check +wp doctor list # see available checks +``` + +Install if missing (this is a write action — confirm it's acceptable +before running it, per Step 0): + +```bash +wp package install wp-cli/doctor-command +``` + +Checks especially relevant to performance: + +- `autoload-options-size` — autoloaded-options threshold +- `constant-savequeries-falsy` / `constant-wp-debug-falsy` — flags + perf-costly debug constants left on in what should be production +- cron checks (event count / duplicates) + +Docs: default checks — +https://make.wordpress.org/cli/handbook/doctor-default-checks/ · +customizing checks — +https://make.wordpress.org/cli/handbook/guides/doctor/doctor-customize-config/ + +## `wp profile` — hook/stage-level profiling without a browser + +Install if missing (write action, confirm first): + +```bash +wp package install wp-cli/profile-command +``` + +Recommended sequence: + +1. **Stage overview** — where time goes across bootstrap/main_query/template: + ```bash + wp profile stage --fields=stage,time,cache_ratio [--url=] + ``` +2. **Hook hotspots**: + ```bash + wp profile hook --spotlight [--url=] + wp profile hook init --spotlight [--url=] # drill into one hook + ``` +3. **Targeted eval**: + ```bash + wp profile eval 'do_action("init");' --hook=init + ``` + +Use `--url=` to profile a specific site/route on a multisite-adjacent +setup. `--skip-plugins`/`--skip-themes` can isolate a culprit component, +but changes real behavior while active — note that in the report if you +use it, don't silently profile a degraded version of the site. + +Docs: https://wpcli.dev/docs/profile/stage · +https://wpcli.dev/docs/profile/hook · +https://wpcli.dev/docs/profile/eval + +## Query Monitor, used headlessly + +Query Monitor is normally UI-driven, but it exposes data through +authenticated REST response headers/envelope — no browser required. + +1. Confirm the plugin is active: `bastille cmd test -f + /wp-content/plugins/query-monitor/query-monitor.php`. +2. Authenticate against the REST API — a nonce, or an Application + Password (see Redaction in `SKILL.md`: never paste a generated + Application Password into a report). +3. Request a REST route and inspect response headers (`x-qm-overview-*` + etc.), or request an enveloped response (`?_envelope`) to get a `qm` + property containing DB query details, cache stats, and HTTP API call + details in one payload. + +Configuration constants: https://querymonitor.com/help/configuration-constants/ · +REST-API-specific docs: https://querymonitor.com/wordpress-debugging/rest-api-requests/ · +plugin page: https://wordpress.org/plugins/query-monitor/ + +Guardrails: Query Monitor adds real overhead — don't enable it in +production without approval. If a hosting platform pre-installs it +gated behind a capability, you may need `view_query_monitor` granted to +the authenticating user. + +## Server-Timing headers (if Performance Lab is present) + +```bash +curl -sS -D - -H 'Host: ' http:/// -o /dev/null | grep -i '^server-timing:' +``` + +Requires the Performance Lab plugin (or a module of it) enabled — +https://wordpress.org/plugins/performance-lab/. Don't enable experimental +modules in production without approval; this is a nice-to-have on top of +the four core baseline metrics, not a replacement for them. + +Benchmarking guidance: https://make.wordpress.org/performance/handbook/measuring-performance/benchmarking-server-timing/ + +## General measurement discipline + +- Always capture a baseline before changing anything. +- Keep the test scenario fixed — same URL/route, same logged-in-or-not + state, same underlying data. +- Prefer multiple samples and a median over a single run. + +Measuring-performance handbook: +https://make.wordpress.org/performance/handbook/measuring-performance/ diff --git a/skills/wordpress-performance-diagnostics/references/fix-patterns.md b/skills/wordpress-performance-diagnostics/references/fix-patterns.md new file mode 100644 index 0000000..f80ebd5 --- /dev/null +++ b/skills/wordpress-performance-diagnostics/references/fix-patterns.md @@ -0,0 +1,133 @@ +# Fix patterns by bottleneck category + +Load this file once `references/diagnostic-tools.md` (or the Step 3 +baseline metrics) has pointed at a dominant bottleneck. Pick *one* +primary category to fix at a time — trying to fix everything in one pass +makes the before/after Verification step in `SKILL.md` meaningless, since +you won't know which change caused which delta. + +Every fix below is still gated by `SKILL.md`'s Step 0 (read-only-first) +and Verification (re-measure the same way afterward) sections. + +## Database / query performance + +Use when profiling points at DB time or a high query count. + +- Avoid N+1 query patterns — batch queries, prime caches, avoid per-row + lookups inside a loop. +- Prefer `fields => 'ids'` in `WP_Query`/`get_posts()` when only IDs are + needed. +- Avoid expensive meta queries where possible; consider indexing or a + schema change for genuinely hot lookups. +- Use object caching for repeated reads (see below) rather than + re-querying the same data every request. + +Backend-only tools: Query Monitor via REST (query lists, stack traces, +slow/duplicate flags) — see `diagnostic-tools.md`. `wp db query` for +targeted SQL/`EXPLAIN` — be careful running this in production, it's a +direct DB access path. + +Query Monitor plugin: https://wordpress.org/plugins/query-monitor/ + +## Autoloaded options + +Autoloaded options load on *every* request, so a large autoload payload +hurts every page, not just the page that set the option. + +Quick checks (see `SKILL.md` Step 3 for the always-collect total): + +```bash +wp option list --autoload=on --fields=option_name,size_bytes | sort -n -k 2 | tail +``` + +Fix patterns: + +- Stop autoloading large blobs — store large/rarely-read data with + `autoload=off` instead. +- Move large computed data to transients or the persistent object cache + (if the site has one — check first, see `SKILL.md` Step 3 metric 3) + rather than an autoloaded option. +- Remove stale options left behind by removed plugins/themes — confirm + nothing still reads the option before deleting; a stale-looking option + name isn't proof nothing depends on it. + +Docs: `wp option list` — https://wpcli.dev/docs/option/list · `wp doctor`'s +`autoload-options-size` check — +https://make.wordpress.org/cli/handbook/doctor-default-checks/ + +## Object caching + +Use when profiling shows repeated identical queries or a low cache hit +rate. + +- WordPress's default object cache is per-request memory only — it does + not persist across requests unless a persistent drop-in + (`wp-content/object-cache.php`) is present. On this fleet, that's + currently only true for `menorca.pt`/`menorca.ro` (Redis Object Cache + plugin against cerebro's Valkey) — see `SKILL.md` Step 3 metric 3 + before assuming a site has one. +- `wp cache flush` (or the Redis-plugin-specific `wp redis flush`) can + affect more than the one site you're working on — cerebro's Valkey DB 0 + is genuinely shared across sites by key-prefix, not fully isolated per + site (confirmed in `docs/server-cerebro.md`: `wp redis enable`'s + `FLUSHDB` clears the whole shared DB 0, not just the calling site's + prefix). Never flush without explicit approval. + +Fix patterns: + +- Cache expensive computed results (transients, or the persistent object + cache where one exists) with explicit invalidation — don't rely on + natural expiry alone for data that changes on a known event. +- Avoid unbounded caches — set expirations, or implement real + invalidation hooks tied to the data changing. +- If a site doesn't have a persistent object cache and profiling + genuinely points there, adding one is an infra change (provisioning + Valkey ACL credentials on cerebro, installing the PHP Redis extension + in the jail — package name `php85-pecl-redis`, not `php85-redis`, per + `bastille-jail-provisioning`) — coordinate rather than doing it as a + drive-by inside a performance investigation. + +WP-CLI cache commands: https://wpcli.dev/docs/cache · flush guardrails: +https://wpcli.dev/docs/cache/flush + +## Cron + +Use when cron causes request-time spikes or slowness. + +```bash +wp cron event list +wp cron test # spawning health +wp cron event run --due-now +``` + +Fix patterns: + +- De-duplicate scheduled events and reduce frequency where the task + genuinely doesn't need to run that often. +- Ensure cron callbacks are idempotent and short — a task that's slow + enough to matter for site performance is a candidate for moving off the + request path entirely, not just running it less often. +- Move heavy work (imports, RSS/content-agent jobs — several granja sites + already run these hourly, per `docs/server-granja.md`) fully off the + page-load request path; a badly-timed spawn on a real WP-Cron pseudo-cron + setup can add real latency to whichever request happens to trigger it. + +WP-CLI cron command package: https://github.com/wp-cli/cron-command + +## Remote HTTP API calls + +Use when profiling shows slow external requests (`wp_remote_get()` etc.). + +Fix patterns: + +- Add explicit timeouts and fail-fast behavior — a slow or hanging + third-party API shouldn't be able to make every page load slow. +- Cache responses where the data doesn't need to be live on every request + (transients / persistent object cache where available). +- Batch requests, and avoid calling a remote API on every page load when + a periodic cron-driven refresh would do. +- Move genuinely heavy remote work off the request path entirely (cron or + a queue), same principle as the Cron section above. + +Query Monitor can report HTTP API call timing via the REST envelope +(`qm` property) — see `diagnostic-tools.md`.