Hibernate Extension 5.6.15.12-SNAPSHOT

Performance Fixes for Large Batch ORM Workloads

Investigating an OOM report from a user processing 3.8M entities nightly, we profiled the extension against the Ortus fork and found 5.6.15.11 was 32% slower with 33% more memory usage — despite identical per-entity retained heap.

The root cause was transient allocation churn during Hibernate flush dirty-checking, not a memory leak.

What changed in 5.6.15.12-SNAPSHOT

  • String.replace() instead of Util.replace() in HibernateCaster.toHibernateType() — the old code allocated a new StringBuilder + String on every call, even when no replacement was needed (which was almost always). At scale this created ~360 million unnecessary objects per 50k entity run.

  • SQL type caching in CFCGetter — Hibernate’s property accessor was resolving the SQL type from a string name on every property read (toLowerCase + string matching + if/else chain). The type never changes for a given property, so we resolve it once in the constructor and cache it.

LDEV-6253

Results (50k entity insert, no session clear)

5.6.15.11 5.6.15.12 Ortus 6.5.4
Time 138 sec 55 sec 102 sec
Rate 361 ent/sec 903 ent/sec 487 ent/sec

5.6.15.12 is now 46% faster than Ortus on the same workload. Heap dumps confirm identical per-entity memory between both extensions (155 KB difference on 459 MB).

Results (50k entity insert, with session clear — the recommended pattern)

5.6.15.11 5.6.15.12
Time 5.3 sec 3.5 sec
Rate 9,489 ent/sec 14,204 ent/sec
Heap growth 309 MB 244 MB

With proper flush+clear batching (the realistic production pattern), .12 is 33% faster with 21% less heap. This is the LDEV-6252 Lucee core optimisation making a direct impact on the per-property-read cost across 500k property reads per run.

Batch ORM best practice: flush + clear

If you’re processing large numbers of entities (bulk imports, nightly jobs, migrations), always call ormClearSession() after each ormFlush(). Without it, every entity you’ve ever saved stays attached to the Hibernate session, and each subsequent flush dirty-checks all of them — not just the new batch. This makes total work O(N²) and eventually leads to OOM.

for ( var i = 1; i <= totalRecords; i++ ) {
    var entity = entityNew( "MyEntity" );
    // ... set properties ...
    entitySave( entity );

    if ( i mod 50 == 0 ) {
        ormFlush();
        ormClearSession();  // detach processed entities, free memory
    }
}
ormFlush();  // flush any remainder

With clear, 50k entities inserts in 3.5 seconds (14,204 ent/sec). Without clear, the same 50k takes 55 seconds due to the accumulated dirty-checking overhead.

The LDEV-6252 core optimisations specifically improved the clear path — 33% faster and 21% less heap compared to the previous snapshot — because with small batches, the per-property-read overhead is a larger proportion of total work. 500k property reads per run (1000 flushes × 50 entities × 10 properties) add up when each one was doing three unnecessary string comparisons and a double map lookup.

Lucee 7.1.0.86-SNAPSHOT

In addition to the extension improvements, based on the findings, I also made some additional performance improvements for Lucee 7.1.

Lucee 6.2.5.48+ is supported by the Lucee Hibernate 5.6.15 extension, it’s just that the newer Lucee versions bring additional fixes, features and performance improvements, upgrades are worth it!

The profiling also identified two optimisations in Lucee core’s CFC property access path — LDEV-6252. These benefit any CFC-heavy workload, not just ORM:

  • Single-lookup getOrDefault in ConcurrentHashMapNullSupport — the old implementation did two ConcurrentHashMap lookups (get + containsKey) when a value was null. The fix uses an absent sentinel to resolve it in a single atomic call, also eliminating a TOCTOU race condition.

  • Map-first reorder in ComponentScopeShadow.get() — every property read was checking equalsIgnoreCase for SUPER, THIS, and STATIC before hitting the map. Normal property access (99%+ of calls) never matches these. The map is now checked first, with special keys as a fallback.

On the ORM clear-path benchmark (the realistic batch pattern with ormClearSession() after each flush), this gives a 33% speed improvement and 21% less heap growth.

LDEV-6252

Documentation

1 Like