The image extension just got a lot leaner. We profiled it with JFR and found some juicy stuff — redundant Tika MIME detection, exception-driven control flow, double writes, and a race condition in member function dispatch.
Requires Lucee 6.2.5+
Catch-up: what happened since 3.0.0.9
A few releases went out without announcements, so here’s the summary:
3.0.1.0 — Lucee 6 + 7 compat
- Added javax servlet support with dual TLD entries for Lucee 6 compatibility (LDEV-6120)
- Note: the initial javax implementation was broken — use 3.0.1.1+
3.0.1.1 — jakarta/javax fix
- LDEV-6157 —
<cfimage action="writeToBrowser">threwNoSuchMethodErroron Lucee 6 becausetouchDestination()bytecode was bound to jakarta servlet API - CI now builds once, tests against Lucee 6.2 + 7.0 (stable + snapshot), Java 11 + 21
3.0.1.2 — CVE fix
- LDEV-5129 — removed unused bundled jars including commons-io (CVE-2024-47554)
3.0.1.3 — performance + bug fixes
Three tickets, nine commits:
-
LDEV-6243 — Fixed a race condition where
img.getWidth()and other member functions would intermittently fail withCan't cast Object type [Struct] to a value of type [image]under concurrent requests. ~1% failure rate, now zero. BIF equivalents (imageGetWidth()) were never affected. -
LDEV-6244 — The extension was calling
GetApplicationSettings()via reflection on every image operation to read coder config. This triggeredDummyWSHandlerclass load exceptions internally — 128,000 exceptions in a 130-second benchmark! Replaced withpc.getApplicationContext().getCustom("image"), same approach the S3 extension uses. -
LDEV-6245 — Five performance fixes:
- Removed a redundant static coder cache that was bypassed by
if (true ||(since 2022) - Fixed a missing
returninAImageIOInterface.write()that caused every file write through TwelveMonkeys to encode and write the image twice - Stopped throwing and catching 96,000
IOExceptionsfor normal format matching control flow - Apache Tika MIME detection was running 3-4 times per
imageRead(once per coder). Now runs once and passes the result down
- Removed a redundant static coder cache that was bypassed by
Benchmarks
5,000 iterations, inspect never, Lucee 7.1.0.82-SNAPSHOT, Java 21:
| Test | 3.0.1.2 | 3.0.1.3 | Improvement |
|---|---|---|---|
| imageRead (PNG) | 872/s | 1,230/s | +41% |
| imageWrite (PNG→JPG) | 272/s | 453/s | +67% |
| imageInfo | 801/s | 1,093/s | +36% |
| imageWrite (PNG) | 306/s | 333/s | +9% |
| imageResize | 250/s | 270/s | +8% |
The read-heavy operations improved the most because that’s where the overhead was — Tika detection, exception handling, and the GetApplicationSettings BIF call. Resize and write are more modest because they’re dominated by actual pixel work.
Exception count: 354,332 → 2,309 (99.3% reduction)
Less exceptions = less GC pressure = more consistent throughput. GC counts dropped across the board (e.g. imageRead: 37 → 21 GCs per 5k ops).
JFR profile — before and after
Before: Tika MIME detection was nearly as expensive as the actual image resize (~14k CPU samples). GetApplicationSettings and DummyWSHandler exception handling dominated the profile.
After: The top of the profile is now actual image work — TwelveMonkeys resize and PNG decode/encode. As it should be.