Repository navigation
perf: memoise pluralize() and singularize() per application - #4184
Conversation
The inflection setup (irregulars, uncountables, the regex rule table) was rebuilt on every call. Results are now served from application.wheels.inflectionCache, keyed by direction and exact text, cleared by set(uncountables/irregulars), a settings fingerprint and reload, bypassed under a tenant override, and bounded to 2,000 entries of up to 100 characters. Refs #4150 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
|
rev1-r1 review @ 06aa8d5 (draft, 4.3): APPROVE on code; merge gated on compat-matrix run 37111637301 and on 4.2.0 shipping.
LOW: the fingerprint (the uncountables list plus the irregulars count) won't notice an irregular word's value changed in place in the settings struct ( |
Wheels Test Results219 271 tests 215 045 ✅ 1h 18m 27s ⏱️ Results for commit 4d49d7b. ♻️ This comment has been updated with latest results. |
…ection Signed-off-by: Peter Amiri <peter@alurium.com> # Conflicts: # vendor/wheels/global/strings.cfm
…f an existing irregular #4186 fixed the BoxLang crash, so the three words go back into the equivalence corpus. A changed plural for an existing irregular isn't fingerprinted (it would double a cache hit), so a spec pins that changing it through set() takes effect, and the docblock and fragment say so. Refs #4150 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
…ection Signed-off-by: Peter Amiri <peter@alurium.com> # Conflicts: # vendor/wheels/global/strings.cfm
…ection Signed-off-by: Peter Amiri <peter@alurium.com> # Conflicts: # vendor/wheels/global/strings.cfm
Final: APPROVE @ 4d49d7b (lifted into 4.2; matrix 37271623294, all 32 legs read)Delta since my approval at 03995a9
Matrix
No open findings. |
Fixes #4150
Summary
pluralize()/singularize()($singularizeOrPluralize) redid all their setup on every call:ReFindNoCasechecks.The result depends only on the text, the direction and the inflection settings, and the framework calls these on a small vocabulary (model, table and route names).
Changes (
vendor/wheels/global/strings.cfm,settings.cfm)$inflect(text, which)is the existing inflection body, moved unchanged. It's the uncached reference the spec compares against.$singularizeOrPluralizekeeps its signature.count = 1still returns the input, and thecount/returnCountformatting is still applied after the lookup, so it's never cached.$cachedInflectionserves$inflect()'s result fromapplication.wheels.inflectionCache.entries, keyed bywhich|text:Compareguards the hit. A case variant (Personvsperson) is computed, never served the other spelling's result.$inflectionCachereturns the cache, or""(no caching) in two cases:request.wheels.tenant.configoverridesuncountablesorirregulars, because$get()honours per-tenant settings, so results can differ per tenant.set(uncountables=…)/set(irregulars=…)drop the cache;set(). Fingerprinting every pair was measured to double the cost of a cache hit (Lucee 7 5 → 8 µs, RustCFML 5.5 → 10.5 µs);application.wheels.The cache is a nested struct of strings in
application.wheels, so invariant 2 (no functions onapplication) isn't involved. It doesn't use Java maps, so it works on RustCFML.Measurements
Per call, median of 3 runs of 2,000 calls after 50 warm-up calls. "Before" is develop's
strings.cfm/settings.cfmswapped into the same worktree. SQLite, Apple M3 Ultra.pluralize("item")before → aftersingularize("products")before → afterTest plan
New
vendor/wheels/tests/specs/global/InflectionCacheSpec.cfc:pluralize/singularizeequal the uncached$inflect()on a miss and on a hit. The words cover regular words, irregulars, uncountables, camelCase, mixed case and edge cases.count/returnCountbehave as before.person/Person/PERSONeach get their own correct result.set(uncountables=…)andset(irregulars=…)take effect immediately;set()of an existing irregular's plural;uncountablesbypasses the cache.The corpus includes
vertex,indexandhalfagain now that #4186 has fixed BoxLang's crash on them.Runs (
directory=runs on SQLite; each cell is pass / fail / error):The full Lucee 7 suite (
tools/test-local.sh) also passes: 6527 passed.After merging develop (#4186) at 03995a9:
globalis 0 fail / 0 error on all six engines, InflectionCacheSpec is 8/8 and InflectionRuleGroupsSpec 3/3 on each, and the full Lucee 7 suite passes (6533). The existingglobal/stringsSpecpasses unchanged.After merging develop for 4.2 (with #4188 and #4191): both PRs changed the singularize rule line in the old
$singularizeOrPluralizebody, which this PR moves into$inflect(). The merge keeps this PR's structure and carries both rule changes into$inflect(); its rule lines are identical to develop's.globalspecs (SQLite) 321 / 0 / 0 on Lucee 7, Lucee 6 and BoxLang, 320 / 0 / 0 on Adobe 2023, Adobe 2025 and RustCFML v0.693.0 (including SingularizeSesRuleSpec and SingularizeDatabasesSpec through the cache); full core suite, Lucee 7 + SQLite: 6739 passed, including theinternalrepo-path specs.🤖 Generated with Claude Code