Skip to content

Features: Load all feature toggles with one query - #1113

Merged
dkotter merged 1 commit into
WordPress:developfrom
ekamran:shifteq/prime-feature-options
Oct 5, 2026
Merged

dkotter merged 1 commit into
WordPress:developfrom
ekamran:shifteq/prime-feature-options

Conversation

@ekamran

@ekamran ekamran commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

What?

Closes #1112

Loads all feature toggles with one query when the features are initialized, instead of one query for each feature that has never been switched on.

Why?

On a site without a persistent object cache, Loader::initialize_features() runs a separate query on every request for each feature whose wpai_feature_{id}_enabled option has no row yet. A fresh install has 21 features, so that is 21 queries, a little under half of all queries on a default home page. Numbers and details are in #1112.

How?

  • Before the loop, initialize_features() passes all toggle names to wp_prime_option_caches(). Core loads the ones it does not have yet with a single SELECT ... WHERE option_name IN (...) and remembers the missing ones for the rest of the request. The get_option() calls in is_enabled() then do not query.
  • Nothing else changes. is_enabled() still reads the same options through get_option(), with the same filters. The names come from the registry, so features added through wpai_register_features are covered too.
  • When every toggle is already stored and autoloaded, core has nothing to load and no query is added.
  • The call is skipped when a persistent object cache is in use. Those sites do not have the problem, because core keeps the list of missing options in the cache between requests. There the call would only add one cache round trip per request. I measured it with Redis: one extra MGET with 21 keys on every request without the check, none with it.
  • Not included: the two flags wpai_markdown_feeds_flush_rewrite and wpai_key_encryption_resume_migration are still looked up one by one on every request. The Loader does not know the options of single experiments, so adding them needs a small design decision. I can add them here or in a follow-up, whichever you prefer.

Queries per request on a fresh install with nothing switched on (MariaDB 12.3.3, WordPress 7.1.2, no persistent object cache):

Request develop This PR
Home page 47 27
Single post 51 31
REST index (/?rest_route=/) 31 11
Feed 38 18
wp-login.php 30 10
admin-ajax.php 30 10
wp-cron.php 27 7

It is the same 20 fewer queries on SQLite with WordPress 7.1. With three features enabled the home page goes from 44 to 27.

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code and Codex
Used for: Investigation, implementation, tests, measurements, and PR wording. I reviewed the reasoning and test results, and I take responsibility for the contribution.

Testing Instructions

  1. Use a site without a persistent object cache. Activate the plugin and leave some features in the state where they were never switched on.
  2. Save the snippet from Features: every never-enabled feature costs one database query on every request #1112 as wp-content/mu-plugins/count-ai-option-queries.php. It writes one line per request to the PHP error log.
  3. Load any page. On develop the line shows one lookup for each never-enabled feature, plus 2. With this PR it shows 2.
  4. Enable a feature under Settings > AI, check that it works, and switch it off again.

Automated:

  • npm run test:php -- --filter test_feature_toggles_are runs the two new tests. test_feature_toggles_are_loaded_with_a_single_query fails on develop (21 queries, 1 expected) and passes here. The other one checks that nothing is primed when a persistent object cache is in use.
  • The full PHP suite passes on single site and on multisite (1768 tests).
  • composer lint and PHPStan pass.

Changelog Entry

Fixed - Feature toggles are loaded with one database query instead of one per feature on sites without a persistent object cache.

Open WordPress Playground Preview

Loader::initialize_features() asks every feature is_enabled(), which reads
wpai_feature_{id}_enabled with get_option(). The option has no row until the
feature is switched on for the first time, so on sites without a persistent
object cache every never-enabled feature cost one query on every request (21
on a fresh install).

The toggle names are now passed to wp_prime_option_caches() before the loop,
which loads them with a single query. The call is skipped when a persistent
object cache is in use, where core already remembers missing options between
requests.
@ekamran
ekamran requested a review from a team October 5, 2026 20:37
@codecov

codecov Bot commented Oct 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.61%. Comparing base (2ed5642) to head (984347f).

Additional details and impacted files
@@              Coverage Diff              @@
##             develop    #1113      +/-   ##
=============================================
+ Coverage      81.60%   81.61%   +0.01%     
- Complexity      3082     3083       +1     
=============================================
  Files            129      129              
  Lines          12282    12289       +7     
=============================================
+ Hits           10023    10030       +7     
  Misses          2259     2259              
Flag Coverage Δ
unit 81.61% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Oct 5, 2026 •

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.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: ekamran <ekamran@git.wordpress.org>
Co-authored-by: dkotter <dkotter@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@dkotter dkotter added this to the 1.5.0 milestone Oct 5, 2026
@dkotter

dkotter commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

@ekamran

Not included: the two flags wpai_markdown_feeds_flush_rewrite and wpai_key_encryption_resume_migration are still looked up one by one on every request. The Loader does not know the options of single experiments, so adding them needs a small design decision. I can add them here or in a follow-up, whichever you prefer

I'd say handle that in a new PR and I'll merge this one. Thanks!

@dkotter
dkotter merged commit 336b2e4 into WordPress:develop Oct 5, 2026
49 of 68 checks passed
@ekamran
ekamran deleted the shifteq/prime-feature-options branch October 6, 2026 08:52
Sign up for free to 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.

Features: every never-enabled feature costs one database query on every request

2 participants