diff --git a/web/sites/guides/src/content/docs/v4-0-0/upgrading/performance-notes.mdx b/web/sites/guides/src/content/docs/v4-0-0/upgrading/performance-notes.mdx new file mode 100644 index 000000000..fbf4ff614 --- /dev/null +++ b/web/sites/guides/src/content/docs/v4-0-0/upgrading/performance-notes.mdx @@ -0,0 +1,73 @@ +--- +title: Performance Notes for 2.x Upgraders +description: What we measured after a reported 2.x-to-4.x slowdown — the root cause, the fix, and how to get it on your install. +type: explanation +--- + +import { Aside } from '@astrojs/starlight/components'; + +If you ported an app from Wheels 2.x and your test suite or request times got noticeably slower on 4.0.3–4.0.5, this page is for you. A user report of exactly that ([#3213](https://github.com/wheels-dev/wheels/issues/3213)) led to a profiling campaign, a root cause, and a fix. Everything below is a measured number from that work — no projections. + +## The reported regression + +The reporter ran the *same* RocketUnit test suite against both versions: + +| Version | Suite duration | +|---|---| +| Wheels 2.5 | 274 s | +| Wheels 4.0.3 | 1,599 s (~27 min) | + +That's roughly a 6× slowdown on a like-for-like runner (their environment: Lucee 5 on Tomcat). Your mileage will differ by engine and workload, but the mechanism behind it applies to every 4.0.x install before the fix. + +## Root cause: per-instance mixin re-integration + +The cost was not per-test or per-request — it was per **object materialization**. On 4.0.x before the fix, every `model("X").new()` *and every row returned by* `findAll()` / `findOne()` / `findByKey()` re-ran the framework's mixin integration from scratch: + +- a directory listing of `vendor/wheels/model/` +- a `createObject()` **and** a `getMetaData()` call per file in it (18 files) +- a re-resolve of all ~231 mixed-in public methods, each with an override check + +`Controller` and `Mapper` creation carried the identical pattern. A test suite (or any finder-heavy request) materializes huge numbers of objects, so the overhead compounded badly. + +## The fix ([#3236](https://github.com/wheels-dev/wheels/pull/3236)) + +The integration plan is now built **once per application** and replayed cheaply for every subsequent instance: the directory scan, per-file metadata, resolved method references, and the plugin-override set are all cached in application scope (a sibling of the schema cache). The cache is rebuilt on `?reload=true`, so framework and plugin edits are still picked up in development. Semantics are unchanged — the same public methods and `super` aliases are mixed in, in the same order, and a guard spec pins the behavior. + +Measured on Lucee 7 + SQLite: + +| | Before | After | +|---|---|---| +| 2,000 × `model().new()` | 2,772 ms | 1,513 ms (~1.8×) | +| Full framework test suite | 33.3 s | ~22 s | + +The framework suite is only partly instance-creation, so its delta understates the win for an instance-heavy app suite like the one in the original report. + + + +## Where 4.0 stands after the fix + +Independent profiling of the framework in June 2026 (JFR + Apache Bench, Lucee 7 + SQLite) established the wider baseline: + +- **Cold first request ≈ 1.2–1.3 s, and ~85% of it is the Lucee CFML-to-bytecode compiler**, not Wheels bootstrap logic (which measured ≈ 20 ms). Cold-start time is a compile cost you pay once per deploy — see the warm-up notes in the deployment guides. +- **Warm request serving ≈ 0.38 ms in-JVM** (~2,600 req/s single-instance, debug output off). +- **The development debug bar costs ~34% of throughput** (+0.13 ms/request) and inflates response size. It is strictly development-only — production requests never pay it — but keep it in mind when benchmarking in the `development` environment: measure with debug output off, or in `production` mode. + +The practical takeaway for benchmarking your upgraded app: warm the app first (the cold request is dominated by compilation), run in `production`/`testing` mode or with debug off, and compare per-suite or per-request timings before and after applying the fix below. + +## How to get the fix + +The fix merged to `develop` on 2026-06-20 — one day **after** v4.0.5 was tagged, so it is not in any stable release yet. It ships with **v4.0.6**. Until then it is available on the bleeding-edge channel, which tracks `develop`: + +```bash +# install the bleeding-edge CLI (brew/apt/yum/scoop all have a -be channel; +# see the Release Channels guide for your platform's exact package) +wheels upgrade check # preview what would change +wheels upgrade apply # swap your app's vendor/wheels to the BE framework +wheels server start +``` + +`wheels upgrade apply` swaps `vendor/wheels/` in place — do it on a branch or a copy of the app so it's easy to roll back. Once v4.0.6 is out, the stable channel carries the fix and no channel switch is needed. + +→ [Release Channels](/v4-0-0/start-here/release-channels/) covers switching between stable and bleeding-edge (and back) per platform. diff --git a/web/sites/guides/src/sidebars/v4-0-0.json b/web/sites/guides/src/sidebars/v4-0-0.json index 3d317466c..e0d6e5606 100644 --- a/web/sites/guides/src/sidebars/v4-0-0.json +++ b/web/sites/guides/src/sidebars/v4-0-0.json @@ -43,6 +43,7 @@ { "label": "Upgrading from 3.x to 4.0", "link": "/v4-0-0/upgrading/3x-to-4x/" }, { "label": "Upgrading Without the CLI", "link": "/v4-0-0/upgrading/manual-upgrades/" }, { "label": "Upgrading from 2.x to 3.x", "link": "/v4-0-0/upgrading/2x-to-3x/" }, + { "label": "Performance Notes for 2.x Upgraders", "link": "/v4-0-0/upgrading/performance-notes/" }, { "label": "Release Channels", "link": "/v4-0-0/start-here/release-channels/" }, { "label": "Reading the Changelog", "link": "/v4-0-0/upgrading/changelog/" } ]