From 096668a06c4470456ca141f243acb34fb4d6409c Mon Sep 17 00:00:00 2001
From: Leon van Zantvoort
Date: Thu, 16 Jul 2026 17:15:45 +0200
Subject: [PATCH] perf(core): cut per-row overhead on the result-mapping hot
path
Reduce work done while materializing result sets:
- QueryImpl: resolve the JDBC column reader once per column per query
(cached per target type via ClassValue) instead of re-running the
target-type switch for every column of every row. Reference-returning
getters skip the now-redundant wasNull() probe.
- WeakInterner: key entities by concrete type then primary key in a
two-level map, avoiding a Ref allocation per interned entity while
keeping the same identity semantics.
- RecordMapper: reuse the compiled plan's cached parameterTypes length
instead of re-walking the record structure on every query, and
short-circuit the dirty-tracking lookup for non-transactional reads.
---
.../java/st/orm/core/spi/WeakInterner.java | 65 ++++---
.../st/orm/core/template/impl/QueryImpl.java | 183 +++++++++++-------
.../orm/core/template/impl/RecordMapper.java | 34 ++--
3 files changed, 177 insertions(+), 105 deletions(-)
diff --git a/storm-core/src/main/java/st/orm/core/spi/WeakInterner.java b/storm-core/src/main/java/st/orm/core/spi/WeakInterner.java
index 40a217c39..f839b07d4 100644
--- a/storm-core/src/main/java/st/orm/core/spi/WeakInterner.java
+++ b/storm-core/src/main/java/st/orm/core/spi/WeakInterner.java
@@ -24,22 +24,21 @@
import java.util.Map;
import java.util.WeakHashMap;
import st.orm.Entity;
-import st.orm.Ref;
/**
* A weak interner that ensures canonical instances of objects while holding them weakly to permit garbage collection.
*
* This class uses a dual-path interning strategy optimized for different object types:
*
- * Entities : Uses primary key-based lookup via {@link Ref} for efficient equality checks. Entities are
- * stored in a separate map with {@link ReferenceQueue}-based cleanup to ensure stale entries are removed when
- * entities are garbage collected.
+ * Entities : Keyed by concrete type and primary key for efficient lookup. Entities are stored with
+ * {@link ReferenceQueue}-based cleanup to ensure stale entries are removed when entities are garbage
+ * collected.
* Non-entities : Uses object equality-based lookup via {@link WeakHashMap}, which provides automatic
* cleanup when objects are no longer strongly referenced.
*
*
* The primary key-based lookup for entities avoids potentially expensive deep equality checks on complex entity
- * objects, while maintaining correct identity semantics (same primary key = same canonical instance).
+ * objects, while maintaining correct identity semantics (same type and primary key = same canonical instance).
*
* This class is not thread-safe. A new instance is expected to be created for each result set processing call,
* ensuring that interning is scoped to a single query execution.
@@ -52,8 +51,11 @@ public final class WeakInterner {
/** Queue for tracking garbage-collected entities to enable lazy cleanup of {@link #entityMap}. */
private ReferenceQueue> queue;
- /** Map for entities, using {@link Ref} (primary key) for efficient lookup. Keys are held strongly. */
- private Map[, RefWeakReference> entityMap;
+ /**
+ * Map for entities, keyed by concrete type then primary key. The two-level structure lets the hot path look up
+ * and store entities using the primary key value already in hand.
+ */
+ private Map], Map> entityMap;
/**
* Creates a new weak interner.
@@ -68,8 +70,8 @@ public WeakInterner() {
* Interns the given object, ensuring that only one canonical instance exists. If an equivalent object is already
* present, returns the existing instance. Otherwise, adds the new object to the interner and returns it.
*
- * For {@link Entity} instances, lookup is performed using the entity's primary key (via {@link Ref}) for
- * efficiency. For all other objects, lookup is based on object equality.
+ * For {@link Entity} instances, lookup is performed using the entity's type and primary key for efficiency.
+ * For all other objects, lookup is based on object equality.
*
* @param object the object to intern.
* @param the type of the object.
@@ -101,8 +103,11 @@ public > E get(@Nonnull Class entityType, @Nonnull Object
return null;
}
drainQueue();
- Ref> ref = Ref.of(entityType, pk);
- WeakReference> existing = entityMap.get(ref);
+ Map byPk = entityMap.get(entityType);
+ if (byPk == null) {
+ return null;
+ }
+ PkWeakReference existing = byPk.get(pk);
if (existing != null) {
Entity> result = existing.get();
if (result != null) {
@@ -114,7 +119,7 @@ public > E get(@Nonnull Class entityType, @Nonnull Object
}
/**
- * Interns an entity using its primary key (via {@link Ref}) for efficient lookup.
+ * Interns an entity using its type and primary key for efficient lookup.
*
* This avoids expensive deep equality checks on complex entity objects. The entity is stored with a weak
* reference, and cleanup is handled via {@link #drainQueue()} when entities are garbage collected.
@@ -130,8 +135,10 @@ private > E internEntity(@Nonnull E entity) {
} else {
drainQueue();
}
- Ref> ref = Ref.of(entity);
- WeakReference> existing = entityMap.get(ref);
+ Class> type = entity.getClass();
+ Object pk = entity.id();
+ Map byPk = entityMap.computeIfAbsent(type, k -> new HashMap<>());
+ PkWeakReference existing = byPk.get(pk);
if (existing != null) {
var result = existing.get();
if (result != null) {
@@ -139,7 +146,7 @@ private > E internEntity(@Nonnull E entity) {
return (E) result;
}
}
- entityMap.put(ref, new RefWeakReference(ref, entity, queue));
+ byPk.put(pk, new PkWeakReference(type, pk, entity, queue));
return entity;
}
@@ -177,29 +184,37 @@ private T internObject(@Nonnull T object) {
/**
* Removes stale entries from {@link #entityMap} by polling the reference queue.
*
- * When an entity is garbage collected, its {@link RefWeakReference} is enqueued. This method polls the queue
+ *
When an entity is garbage collected, its {@link PkWeakReference} is enqueued. This method polls the queue
* and removes the corresponding entries from the map. Uses a two-argument remove to ensure only the exact
* weak reference is removed, preventing removal of a newer entry with the same key.
*/
private void drainQueue() {
- RefWeakReference weakReference;
- while ((weakReference = (RefWeakReference) queue.poll()) != null) {
- entityMap.remove(weakReference.ref, weakReference);
+ PkWeakReference weakReference;
+ while ((weakReference = (PkWeakReference) queue.poll()) != null) {
+ Map byPk = entityMap.get(weakReference.type);
+ if (byPk != null) {
+ // Two-argument remove ensures only the exact stale reference is removed, never a newer entry that
+ // reused the same primary key.
+ byPk.remove(weakReference.pk, weakReference);
+ }
}
}
/**
- * A weak reference to an entity that retains the associated {@link Ref} for map cleanup.
+ * A weak reference to an entity that retains its type and primary key for map cleanup.
*
* When the entity is garbage collected, this reference is enqueued in the {@link ReferenceQueue}, allowing
- * {@link #drainQueue()} to remove the corresponding entry from {@link #entityMap} using the stored ref.
+ * {@link #drainQueue()} to remove the corresponding entry from {@link #entityMap} using the stored type and
+ * primary key.
*/
- private static final class RefWeakReference extends WeakReference> {
- final Ref> ref;
+ private static final class PkWeakReference extends WeakReference> {
+ final Class> type;
+ final Object pk;
- RefWeakReference(Ref> ref, Entity> referent, ReferenceQueue super Entity>> q) {
+ PkWeakReference(Class> type, Object pk, Entity> referent, ReferenceQueue super Entity>> q) {
super(referent, q);
- this.ref = ref;
+ this.type = type;
+ this.pk = pk;
}
}
}
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/QueryImpl.java b/storm-core/src/main/java/st/orm/core/template/impl/QueryImpl.java
index 9f977ec3d..a6cfb4451 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/QueryImpl.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/QueryImpl.java
@@ -625,6 +625,12 @@ protected Spliterator rowSpliterator(@Nonnull ResultSet resultSet, int co
}
var columnSkipper = mapper.columnSkipper();
var calendarSupplier = lazy(() -> Calendar.getInstance(TimeZone.getTimeZone(ZoneOffset.UTC)));
+ // Resolve the per-column reader once per query rather than re-evaluating the target-type dispatch for every
+ // column of every row. The column types are fixed for the lifetime of the result set.
+ ColumnReader[] readers = new ColumnReader[columnCount];
+ for (int i = 0; i < columnCount; i++) {
+ readers[i] = columnReaderFor(types[i]);
+ }
return new Spliterators.AbstractSpliterator<>(Long.MAX_VALUE, Spliterator.ORDERED) {
@Override
public boolean tryAdvance(@Nonnull Consumer super T> action) {
@@ -636,10 +642,10 @@ public boolean tryAdvance(@Nonnull Consumer super T> action) {
if (columnSkipper != null) {
// Skip decoding non-key columns of entities that are already cached.
columnSkipper.readRow(args, index ->
- readColumnValue(resultSet, index + 1, types[index], calendarSupplier));
+ readers[index].read(resultSet, index + 1, calendarSupplier));
} else {
for (int i = 0; i < columnCount; i++) {
- args[i] = readColumnValue(resultSet, i + 1, types[i], calendarSupplier);
+ args[i] = readers[i].read(resultSet, i + 1, calendarSupplier);
}
}
action.accept(mapper.newInstance(args));
@@ -653,75 +659,116 @@ public boolean tryAdvance(@Nonnull Consumer super T> action) {
};
}
- private static Object readColumnValue(
- @Nonnull ResultSet rs,
- int columnIndex,
- @Nonnull Class> targetType,
- @Nonnull Supplier calendarSupplier
- ) throws SQLException {
- Object value = switch (targetType) {
- // Primitives & basics.
- case Class> c when c == Short.TYPE || c == Short.class -> rs.getShort(columnIndex);
- case Class> c when c == Integer.TYPE || c == Integer.class -> rs.getInt(columnIndex);
- case Class> c when c == Long.TYPE || c == Long.class -> rs.getLong(columnIndex);
- case Class> c when c == Float.TYPE || c == Float.class -> rs.getFloat(columnIndex);
- case Class> c when c == Double.TYPE || c == Double.class -> rs.getDouble(columnIndex);
- case Class> c when c == Byte.TYPE || c == Byte.class -> rs.getByte(columnIndex);
- case Class> c when c == Boolean.TYPE || c == Boolean.class -> rs.getBoolean(columnIndex);
- case Class> c when c == String.class -> rs.getString(columnIndex);
- case Class> c when c == BigDecimal.class -> rs.getBigDecimal(columnIndex);
- case Class> c when c == ByteBuffer.class -> {
- byte[] bytes = rs.getBytes(columnIndex);
- yield bytes != null ? ByteBuffer.wrap(bytes).asReadOnlyBuffer() : null;
- }
- case Class> c when c == UUID.class -> {
- Object obj = rs.getObject(columnIndex);
- yield obj instanceof UUID u ? u : obj != null ? UUID.fromString(obj.toString()) : null;
- }
- case Class> c when c.isEnum() -> rs.getString(columnIndex); // Enum handled by mapper.
- case Class> c when c == java.util.Date.class -> {
- Timestamp ts = rs.getTimestamp(columnIndex, calendarSupplier.get());
- yield ts != null ? new java.util.Date(ts.getTime()) : null;
- }
- case Class> c when c == Calendar.class -> {
- Timestamp ts = rs.getTimestamp(columnIndex, calendarSupplier.get());
- if (ts == null) yield null;
- Calendar out = (Calendar) calendarSupplier.get().clone();
+ /**
+ * Reads a single column value from a result set, decoding it to the resolved target type. One reader is resolved
+ * per column (see {@link #columnReaderFor}) and reused for every row, so the target-type dispatch is not repeated
+ * for every column of every row.
+ */
+ @FunctionalInterface
+ private interface ColumnReader {
+ Object read(@Nonnull ResultSet rs, int columnIndex, @Nonnull Supplier calendarSupplier)
+ throws SQLException;
+ }
+
+ /**
+ * Cache of resolved column readers, keyed by target type. Readers are stateless (the calendar is supplied per
+ * call), so a single instance per type is shared across every query for the lifetime of the VM. The value is
+ * computed at most once per type and read on a fast, lock-free path.
+ */
+ private static final ClassValue COLUMN_READERS = new ClassValue<>() {
+ @Override
+ protected ColumnReader computeValue(@Nonnull Class> type) {
+ return buildColumnReader(type);
+ }
+ };
+
+ /**
+ * Resolves the column reader for the given target type, computing it at most once per type for the lifetime of
+ * the VM.
+ */
+ private static ColumnReader columnReaderFor(@Nonnull Class> targetType) {
+ return COLUMN_READERS.get(targetType);
+ }
+
+ /**
+ * Builds the column reader for the given target type. Primitive-returning JDBC getters honour
+ * {@link ResultSet#wasNull()} so that a SQL NULL maps to {@code null} rather than the getter's zero value;
+ * reference-returning getters already yield {@code null} for SQL NULL, so no {@code wasNull} probe is needed.
+ */
+ private static ColumnReader buildColumnReader(@Nonnull Class> targetType) {
+ return switch (targetType) {
+ // Ordered most-common-first. Resolution is paid once per column per query, so this mainly trims the
+ // setup path, but keeping the frequent types (identifiers, names, flags) at the front is free.
+ case Class> c when c == String.class -> (rs, i, cal) -> rs.getString(i);
+ case Class> c when c == Integer.TYPE || c == Integer.class ->
+ (rs, i, cal) -> { int v = rs.getInt(i); return rs.wasNull() ? null : v; };
+ case Class> c when c == Long.TYPE || c == Long.class ->
+ (rs, i, cal) -> { long v = rs.getLong(i); return rs.wasNull() ? null : v; };
+ case Class> c when c == Boolean.TYPE || c == Boolean.class ->
+ (rs, i, cal) -> { boolean v = rs.getBoolean(i); return rs.wasNull() ? null : v; };
+ case Class> c when c == Double.TYPE || c == Double.class ->
+ (rs, i, cal) -> { double v = rs.getDouble(i); return rs.wasNull() ? null : v; };
+ // Remaining numeric primitives. wasNull distinguishes a genuine zero from a SQL NULL.
+ case Class> c when c == Short.TYPE || c == Short.class ->
+ (rs, i, cal) -> { short v = rs.getShort(i); return rs.wasNull() ? null : v; };
+ case Class> c when c == Float.TYPE || c == Float.class ->
+ (rs, i, cal) -> { float v = rs.getFloat(i); return rs.wasNull() ? null : v; };
+ case Class> c when c == Byte.TYPE || c == Byte.class ->
+ (rs, i, cal) -> { byte v = rs.getByte(i); return rs.wasNull() ? null : v; };
+ case Class> c when c == BigDecimal.class -> (rs, i, cal) -> rs.getBigDecimal(i);
+ case Class> c when c == ByteBuffer.class -> (rs, i, cal) -> {
+ byte[] bytes = rs.getBytes(i);
+ return bytes != null ? ByteBuffer.wrap(bytes).asReadOnlyBuffer() : null;
+ };
+ case Class> c when c == UUID.class -> (rs, i, cal) -> {
+ Object obj = rs.getObject(i);
+ return obj instanceof UUID u ? u : obj != null ? UUID.fromString(obj.toString()) : null;
+ };
+ case Class> c when c.isEnum() -> (rs, i, cal) -> rs.getString(i); // Enum handled by mapper.
+ case Class> c when c == java.util.Date.class -> (rs, i, cal) -> {
+ Timestamp ts = rs.getTimestamp(i, cal.get());
+ return ts != null ? new java.util.Date(ts.getTime()) : null;
+ };
+ case Class> c when c == Calendar.class -> (rs, i, cal) -> {
+ Timestamp ts = rs.getTimestamp(i, cal.get());
+ if (ts == null) return null;
+ Calendar out = (Calendar) cal.get().clone();
out.setTimeInMillis(ts.getTime());
- yield out;
- }
- case Class> c when c == Timestamp.class -> rs.getTimestamp(columnIndex, calendarSupplier.get());
- case Class> c when c == java.sql.Date.class -> rs.getDate(columnIndex);
- case Class> c when c == Time.class -> rs.getTime(columnIndex);
+ return out;
+ };
+ case Class> c when c == Timestamp.class -> (rs, i, cal) -> rs.getTimestamp(i, cal.get());
+ case Class> c when c == java.sql.Date.class -> (rs, i, cal) -> rs.getDate(i);
+ case Class> c when c == Time.class -> (rs, i, cal) -> rs.getTime(i);
// java.time using vendor-safe approach.
- case Class> c when c == LocalDateTime.class -> {
- Timestamp ts = rs.getTimestamp(columnIndex);
- yield ts != null ? ts.toLocalDateTime() : null;
- }
- case Class> c when c == LocalDate.class -> {
- java.sql.Date d = rs.getDate(columnIndex);
- yield d != null ? d.toLocalDate() : null;
- }
- case Class> c when c == LocalTime.class -> {
- Time t = rs.getTime(columnIndex);
- yield t != null ? t.toLocalTime() : null;
- }
- case Class> c when c == Instant.class -> {
- Timestamp ts = rs.getTimestamp(columnIndex, calendarSupplier.get());
- yield ts != null ? ts.toInstant() : null;
- }
- case Class> c when c == OffsetDateTime.class -> {
- Timestamp ts = rs.getTimestamp(columnIndex, calendarSupplier.get());
- yield ts != null ? OffsetDateTime.ofInstant(ts.toInstant(), ZoneOffset.UTC) : null;
- }
- case Class> c when c == ZonedDateTime.class -> {
- Timestamp ts = rs.getTimestamp(columnIndex, calendarSupplier.get());
- yield ts != null ? ZonedDateTime.ofInstant(ts.toInstant(), ZoneOffset.UTC) : null;
- }
- default -> rs.getObject(columnIndex);
+ case Class> c when c == LocalDateTime.class -> (rs, i, cal) -> {
+ Timestamp ts = rs.getTimestamp(i);
+ return ts != null ? ts.toLocalDateTime() : null;
+ };
+ case Class> c when c == LocalDate.class -> (rs, i, cal) -> {
+ java.sql.Date d = rs.getDate(i);
+ return d != null ? d.toLocalDate() : null;
+ };
+ case Class> c when c == LocalTime.class -> (rs, i, cal) -> {
+ Time t = rs.getTime(i);
+ return t != null ? t.toLocalTime() : null;
+ };
+ case Class> c when c == Instant.class -> (rs, i, cal) -> {
+ Timestamp ts = rs.getTimestamp(i, cal.get());
+ return ts != null ? ts.toInstant() : null;
+ };
+ case Class> c when c == OffsetDateTime.class -> (rs, i, cal) -> {
+ Timestamp ts = rs.getTimestamp(i, cal.get());
+ return ts != null ? OffsetDateTime.ofInstant(ts.toInstant(), ZoneOffset.UTC) : null;
+ };
+ case Class> c when c == ZonedDateTime.class -> (rs, i, cal) -> {
+ Timestamp ts = rs.getTimestamp(i, cal.get());
+ return ts != null ? ZonedDateTime.ofInstant(ts.toInstant(), ZoneOffset.UTC) : null;
+ };
+ default -> (rs, i, cal) -> {
+ Object value = rs.getObject(i);
+ return rs.wasNull() ? null : value;
+ };
};
- if (rs.wasNull()) return null;
- return value;
}
@Override
diff --git a/storm-core/src/main/java/st/orm/core/template/impl/RecordMapper.java b/storm-core/src/main/java/st/orm/core/template/impl/RecordMapper.java
index bd2527a05..0ced231c7 100644
--- a/storm-core/src/main/java/st/orm/core/template/impl/RecordMapper.java
+++ b/storm-core/src/main/java/st/orm/core/template/impl/RecordMapper.java
@@ -137,7 +137,9 @@ static Optional> getFactory(int columnCount,
@Nonnull RecordType type,
@Nonnull RefFactory refFactory,
@Nullable TransactionContext transactionContext) throws SqlTemplateException {
- if (getParameterCount(type) == columnCount) {
+ // The compiled plan already holds the flat column count (its parameterTypes length), cached per type.
+ // Reuse it instead of re-walking the record structure on every query, as getParameterCount would.
+ if (compiledFor(type, refFactory).parameterTypes().length == columnCount) {
return Optional.of(wrapConstructor(type, refFactory, transactionContext));
}
return empty();
@@ -446,6 +448,17 @@ private static ObjectMapper wrapConstructor(@Nonnull RecordType type,
return wrapConstructor(type, refFactory, transactionContext, new WeakInterner());
}
+ /**
+ * Resolves the transaction-scoped entity cache for the top-level entity type. Only called when the type is an
+ * entity within a transaction and caching is required.
+ */
+ @SuppressWarnings("unchecked")
+ private static EntityCache, ?> resolveEntityCache(@Nonnull TransactionContext transactionContext,
+ @Nonnull RecordType type) {
+ return (EntityCache, ?>) transactionContext.entityCache(
+ (Class extends Entity>>) type.type(), CacheRetention.fromConfig(StormConfig.defaults()));
+ }
+
private static ObjectMapper wrapConstructor(@Nonnull RecordType type,
@Nonnull RefFactory refFactory,
@Nullable TransactionContext transactionContext,
@@ -454,18 +467,15 @@ private static ObjectMapper wrapConstructor(@Nonnull RecordType type,
boolean isEntity = Entity.class.isAssignableFrom(type.type());
// Determine cache read/write policy.
// Cache read: return cached instances (identity preservation) - only at REPEATABLE_READ+
- // Cache write: store for dirty tracking OR for identity preservation
+ // Cache write: store for dirty tracking OR for identity preservation.
+ // Only entities in a transaction can be cached; the leading conditions short-circuit the dirty-tracking
+ // lookup so it is never computed on a non-transactional read.
boolean cacheReadEnabled = transactionContext != null && transactionContext.isRepeatableRead();
- boolean dirtyTrackingEnabled = getUpdateMode(type, StormConfig.defaults()) != OFF;
- boolean cacheWriteEnabled = cacheReadEnabled || dirtyTrackingEnabled;
- EntityCache, ?> entityCache;
- if (transactionContext != null && isEntity && cacheWriteEnabled) {
- //noinspection unchecked
- entityCache = (EntityCache, ?>) transactionContext.entityCache(
- (Class extends Entity>>) type.type(), CacheRetention.fromConfig(StormConfig.defaults()));
- } else {
- entityCache = null;
- }
+ EntityCache, ?> entityCache =
+ transactionContext != null && isEntity
+ && (cacheReadEnabled || getUpdateMode(type, StormConfig.defaults()) != OFF)
+ ? resolveEntityCache(transactionContext, type)
+ : null;
PkInfo pkInfo = compiled.pkInfo();
ColumnSkipper columnSkipper = createColumnSkipper(type, compiled, cacheReadEnabled, entityCache,
interner, transactionContext);