⚡ 2026-08-25 settings memoization — bench performance fix
Historical record — 2026-08-25
Preserved as observed. Its figures were re-measured and confirmed on 2026-08-27 by Cluster 4; the canonical memoization is still pending. Current truth: Program Masterplan.
Scope: WRDP bench, 2026-08-25. Measured in-container or by curl -H 'Host: wrdp.loca.zone' http://127.0.0.1:51080…. Citations are against the installed source under wp/wp-content/plugins/quizwizz/.
Method: matched A/B in one sitting. Revert to pristine bytes (sha256-verified against copies taken before any edit), warm with three discarded requests, take the full set, reinstall the memoized files, verify byte-identical, retake the same set. Identical method, warm-up and host conditions in both halves.
Symptom
Most PHP requests cost 1.4–1.7s TTFB; / and /?page_id=2 took 7.6s; /?page_id=1289 took 4.7s.
Root cause
SettingsStore::get()ranSettingsNormalizer::sanitize( merge_defaults( $stored ) ), a full schema deep-merge plus sanitize, on every call with no memoization: 6.51ms per call (1000 calls = 6,513.27ms).UiLanguage::register()hooksgettext,ngettext,gettext_with_context,ngettext_with_context(UiLanguage.php:31-34), routing everyquizwizz-domain string throughchoice()intoSettingsStore::get().- Net: one
__( 'Quiz', 'quizwizz' )cost 13.37ms (1000 calls = 13,373.63ms). - Boot-time label emission therefore dominated
init: 1919.9ms of a 3811.2mswp-load.
Infrastructure was not the constraint, re-measured on the same bench:
- database connect 8.8ms, trivial query 1.58ms
- 103 autoloaded options totalling 42.5KB; 17 scheduled cron events
- container CPU steal 0 jiffies of 20,382,742
- opcache state not established:
opcache_get_status()returnsfalseunder the CLI SAPI used for these probes, so no web-SAPI figure is claimed - decisive evidence: memoization alone moved
initfrom 1919.9ms to 51.0ms with no change to the database, host, option table, opcache or any PHP setting
Fix (bench-local, three files)
includes/Settings/SettingsStore.php—$memokeyed by blog id (:8-9),flush_memo()(:11-13),maybe_flush( $option )(:25-29),register_hooks()(:15-23).get()(:31-46) computes the projection lazily once per request per blog.save_if_revision()flushes (:105) immediately after its threewp_cache_deletecalls (:102-104).includes/UiLanguage.php—$choice_memo(:15-16),flush_choice()(:18-20),maybe_flush_choice( $option )(:22-26);choice()(:52-60) memoized. Filter bodies,load(),mofile()and script-translation logic untouched.includes/Bootstrap/CoreProvider.php—\QuizWizz\Settings\SettingsStore::register_hooks();(:10) directly afterInstaller::register_hooks()(:9).
Trap
The first attempt registered invalidation on added_option_qw_exposure_settings, updated_option_qw_exposure_settings and deleted_option_qw_exposure_settings. WordPress core never fires those. Core fires add_option_{$option} and update_option_{$option} (present tense) plus the generic added_option, updated_option and deleted_option, each with the option name as first argument. The past-tense specific names exist in this codebase only because save_if_revision() emits them manually (SettingsStore.php:119-122).
Consequence, proven in-container: after update_option( 'qw_exposure_settings', … ) the stored iface_lang was de while get() still returned en — a silently stale cache that passed lint, activation and all 46 probe assertions. Corrected wiring uses the generic actions with an option-name guard (SettingsStore.php:19-29, UiLanguage.php:40-43), plus switch_blog for the unguarded flush.
Verification
Micro, in-container:
| measurement | pristine | memoized |
|---|---|---|
__( 'Quiz', 'quizwizz' ) x1000 | 13,373.63ms | 2.67ms |
SettingsStore::get() x1000 | 6,513.27ms | 0.16ms |
Boot phases, same harness in both halves:
| phase | pristine | memoized |
|---|---|---|
plugins_loaded | 286.1ms | 24.5ms |
init | 1919.9ms | 51.0ms |
total wp-load | 3811.2ms | 359.9ms |
2026-08-27 re-verification
A Cluster 3 spot-check on 2026-08-26 reported that the translated-string microbench and
the wp-load headline no longer reproduced, at 3.499985ms/1000 and 539.700ms/4q. Those
were single runs taken while other measurement slices were hitting the same MariaDB
container. Re-measured quiescent, medians of three, nothing else touching the database:
| measurement | published 2026-08-25 | quiescent median 2026-08-27 | verdict |
|---|---|---|---|
__( 'Quiz', 'quizwizz' ) x1000 | 2.67ms | 2.612114ms (2.479 / 2.612 / 2.998) | reproduces |
SettingsStore::get() x1000 | 0.16ms | 0.178099ms (0.161 / 0.178 / 0.223) | reproduces |
total wp-load | 359.9ms | 367.375ms / 4q (351.3 / 367.4 / 432.0) | reproduces |
All three figures stand. The 2026-08-26 spot-check is superseded, not the 2026-08-25 measurements: contended single runs on this bench inflate timings by 40-200%, which is why every figure published here is a quiescent median of three.
Latency, TTFB, best of 3 per URL:
| URL | pristine | memoized | gate |
|---|---|---|---|
/ | 7.645s | 0.362s | < 0.6s |
/wp-login.php | 1.383s | 0.057s | < 0.6s |
/?page_id=2 (craft) | 7.625s | 0.355s | < 1.5s |
/?page_id=1289 (library fixture) | 4.652s | 0.202s | < 1.5s |
/?page_id=865 (Mein QuizWizz) | 1.674s | 0.118s | < 1.0s |
/?post_type=qw_quiz&p=99 (play) | 1.446s | 0.123s | — |
/?post_type=qw_quiz&p=88 (play) | 1.489s | 0.154s | — |
Latency is load-sensitive: a cold single request taken during a concurrent Quartz build (load average 12) measured / at 0.771s. Warmed best-of-3 under that same load: / 0.406s, /?page_id=2 0.382s, both inside gate. Warm with three discarded requests before quoting any figure.
Invalidation, in-container: choice() returned en; after update_option with iface_lang = de both choice() and get()['iface_lang'] returned de; after restoring the original array both returned en and the stored option was byte-identical.
Harness: ./scripts/quizwizz-probe.sh --wave all gave TAP 1..46, 46 assertions, 0 failures, rerun after the A/B cycle. The harness added no lines to logs/debug.log. The only new lines during this work (38528 → 38532) are four Trying to access array offset on false/null in Command line code warnings from the author’s own throwaway diagnostic one-liner, not from QuizWizz or WordPress.
Browser, cache disabled, https://wrdp.loca.zone:
- play page
?post_type=qw_quiz&p=99renders question 1 of 10 with answer choices, navigation TTFB 137ms, zero page errors; QuizWizz strings stayed English under ade_DEsite locale, confirming the memoizedchoice()still honoursiface_lang=en - craft page
?page_id=2loads the builder with Print and Play; selecting a category reports 299 matching questions - Print opens the printable-quiz editor in 517ms with 10 questions, zero page errors
State: reactivated through WP-CLI when the change landed, and confirmed Active after the A/B cycle — the revert/restore touched only file bytes, so no activation hook re-ran. php -l clean in-container for all three files. Live files remain www-data:www-data 0644 at sha256 d0843a7b (SettingsStore), b006eba0 (UiLanguage), 78ee4d52 (CoreProvider).
Release status
releaseEligible: false — bench-local patch installed over the bind mount. The canonical QuizWizz repository needs the same memoization through its locked build pipeline; until then the shipped plugin retains the ~13ms-per-translated-string cost. The release owner applies source changes through that pipeline.