Cayenne 5.0-M3 against 5.0-SNAPSHOT · Strimillinn's model and data · 15 October 2026

Cayenne 5 under load

Twelve query workloads on Strimillinn's model and its database. Each was run at 1, 4 and 16 threads on both Cayenne versions, with no rendering involved. The concurrency work in SNAPSHOT is real and large, but pays off where Strimillinn rarely goes: a single context shared between threads, so most of the performance we gained came from a plainer reason: SNAPSHOT builds objects from rows with half the garbage.

M3 5.0-M3 release SNAPSHOT 5.0-SNAPSHOT at 78785e4e4

The dictionary page

The heavy dictionary page query (pageJoint) is 1.13× faster at one thread and 1.14× at four. The gain is flat across thread counts and matches the 11% measured on the real page last night, which is the signature of cheaper per-thread work, not of less contention. Allocation per query falls from 295 MB to 137 MB. The page's parallel part (twins and trigram-similar texts on virtual threads, pageFanOut) is no faster at any thread count, because Postgres is the cost there.

Concurrency really starts to show once the database is taken out. Served from the query cache into fresh contexts, the same query goes from 2.0× faster at one thread to 2.9× at sixteen: M3 slows from 68 to 197 ms under load, SNAPSHOT only from 34 to 67 ms. With one context shared by every thread, SNAPSHOT is 8–17× faster at 4 and 16 threads. Strimillinn neither uses the query cache nor shares contexts between threads (every StrimillinnCore.newContext() is fresh), so those gains are available but not yet collected.

Shared context
17.5×

Faults in a shared context at 16 threads

385 µs on M3, 22 µs on SNAPSHOT. M3 serialises threads on the whole context; SNAPSHOT's per-object locks (CAY-3048) let them pass each other. From the cache it's 11.6×, and 7.9× already at 4 threads.

Building objects
2.0–2.9×

Cached rows to objects

The page query from the query cache: 68 → 34 ms at one thread, 197 → 67 ms at sixteen, with 79% less allocation. This is the part of every fetch that SNAPSHOT rewrote, and it scales better too.

Under load
−62%

Garbage collection on the page query

At 16 threads the page query's GC time drops from 14.4 s to 5.4 s over the run (disjoint prefetches: 10.3 s → 2.5 s). Wall time there is flat because 16 threads plus Postgres saturate 14 cores, but a busy server would feel the lighter heap.

Speed

How much faster, by thread count

Each workload at 1, 4 and 16 threads. Right of the line is faster on SNAPSHOT; the scale is logarithmic, so 2× and ½× are the same distance from 1×. Whiskers run from the slowest to the fastest ratio the two 99.9% confidence intervals allow; a whisker crossing 1× means no measurable difference.

1 thread 4 threads 16 threads
Scaling

Throughput as threads are added

Operations per second across all threads. A line that keeps rising scales; a flat line means the threads are waiting on each other or on the database.

Memory

Allocation per operation

Bytes allocated per operation at one thread, measured by JMH's GC profiler. Less allocation means less garbage collection, which is where much of the time goes under load.

M3 SNAPSHOT

Retained heap, measured three times per version: data rows are less than half the size (1,101 → 525 bytes per row, CAY-3050). Objects and their snapshots cost the same on both versions, about 470 bytes per object retained and 603 including the snapshot cache. One M3 object run read 588 B/object twice; SNAPSHOT read 471 every time, so treat that spread as heap measurement noise rather than a difference.

Reading it

What the results say

All results

Every workload, every thread count

WorkloadThreadsM3, µs/opSNAPSHOT, µs/opSpeedupM3 allocSNAPSHOT alloc

Average time per operation as each thread sees it, ± the 99.9% confidence interval. A speedup counts only when the confidence intervals don't overlap; otherwise it's shown in grey.

The workloads

What each one does

Method

How it was measured

Project
~/git/cayenne-bench: JMH benchmarks on Strimillinn's datamap, with the generated model classes of each version (a441cba00 for M3, befbb0bb9 for SNAPSHOT). No WebObjects, no templates. Build with -Pm3 or -Psnapshot, run with ./run.sh full.
Database
The local copy of Strimillinn's database: 2.1 million entries, 280,000 receipts. PostgreSQL 17 on the same machine. Every workload is read-only.
Runtime
As Strimillinn sets it up: same project file, 100,000-row snapshot cache, Hikari pool, minus the commit listeners. 4 GB heap, G1.
JMH
Order
For each workload and thread count, M3 then SNAPSHOT, so slow drift over the night affects both.
Checks
Each trial first checks that the page query returns 17,367 rows as data rows and as objects in that version, and that the id pools are complete. A wrong answer stops the trial.
Machine