#2021 - Use smarter persistence context for findEach/iterate/stream queries - improve performance of un-tuned findEach/iterate/stream queries that invoke lazy loading

This commit is contained in:
rob bygrave
2020-06-17 21:50:11 +12:00
parent 0e33d58bb2
commit 9b70900267
7 changed files with 307 additions and 36 deletions
@@ -61,6 +61,22 @@ public interface PersistenceContext {
*/
int size(Class<?> rootType);
/**
* Return a copy of the Persistence context to use for large query iteration.
*/
PersistenceContext forIterate();
/**
* Return a new Persistence context during iteration of large query result.
*/
PersistenceContext forIterateReset();
/**
* Return true if the persistence context has grown and hit the 'reset limit'
* during large query iteration.
*/
boolean resetLimit();
/**
* Wrapper on a bean to also indicate if a bean has been deleted.
* <p>
@@ -316,7 +316,6 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
*/
@Override
public JsonReadOptions createJsonReadOptions() {
persistenceContext = getPersistenceContext(query, transaction);
if (query.getPersistenceContext() == null) {
query.setPersistenceContext(persistenceContext);
@@ -327,7 +326,6 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
loadContext = new DLoadContext(this, secondaryQueries);
jsonRead.setLoadContext(loadContext);
}
return jsonRead;
}
@@ -335,8 +333,8 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
* For iterate queries reset the persistenceContext and loadContext.
*/
public void flushPersistenceContextOnIterate() {
if (!iterateSingleContext) {
persistenceContext = new DefaultPersistenceContext();
if (!iterateSingleContext && persistenceContext.resetLimit()) {
persistenceContext = persistenceContext.forIterateReset();
loadContext.resetPersistenceContext(persistenceContext);
if (jsonRead != null) {
jsonRead.setPersistenceContext(persistenceContext);
@@ -350,7 +348,6 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
* transaction scoped.
*/
private PersistenceContext getPersistenceContext(SpiQuery<?> query, SpiTransaction t) {
// check if there is already a persistence context set which is the case
// when lazy loading or query joins are executed
PersistenceContext ctx = query.getPersistenceContext();
@@ -358,7 +355,14 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
// determine the scope (from the query and then server)
PersistenceContextScope scope = ebeanServer.getPersistenceContextScope(query);
return (scope == PersistenceContextScope.QUERY || t == null) ? new DefaultPersistenceContext() : t.getPersistenceContext();
if (scope == PersistenceContextScope.QUERY || t == null) {
return new DefaultPersistenceContext();
}
if (Type.ITERATE == query.getType()) {
return t.getPersistenceContext().forIterate();
} else {
return t.getPersistenceContext();
}
}
/**
@@ -590,7 +594,6 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
* Merge in prior L2 bean cache hits with the query result.
*/
public void mergeCacheHits(BeanCollection<T> result) {
if (cacheBeans != null && !cacheBeans.isEmpty()) {
if (query.getType() == Type.MAP) {
mergeCacheHitsToMap(result);
@@ -175,7 +175,6 @@ class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContext {
@Override
public void loadBean(EntityBeanIntercept ebi) {
// A synchronized (this) is effectively held by EntityBeanIntercept.loadBean()
if (context.desc.lazyLoadMany(ebi)) {
// lazy load property was a Many
return;
@@ -183,7 +182,6 @@ class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContext {
if (context.hitCache) {
Set<EntityBeanIntercept> hits = context.desc.cacheBeanLoadAll(list, persistenceContext, ebi.getLazyLoadPropertyIndex(), ebi.getLazyLoadProperty());
list.removeAll(hits);
if (list.isEmpty() || hits.contains(ebi)) {
// successfully hit the L2 cache so don't invoke DB lazy loading
@@ -193,6 +191,7 @@ class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContext {
LoadBeanRequest req = new LoadBeanRequest(this, ebi, context.hitCache);
context.desc.getEbeanServer().loadBean(req);
list.clear();
}
}
@@ -608,12 +608,10 @@ public class CQuery<T> implements DbReadContext, CancelableQuery, SpiProfileTran
}
QueryIterator<T> readIterate(int bufferSize, OrmQueryRequest<T> request) {
if (bufferSize > 0) {
return new CQueryIteratorWithBuffer<>(this, request, bufferSize);
} else {
if (bufferSize < 2) {
return new CQueryIteratorSimple<>(this, request);
} else {
return new CQueryIteratorWithBuffer<>(this, request, bufferSize);
}
}
@@ -34,18 +34,62 @@ public final class DefaultPersistenceContext implements PersistenceContext {
private final Monitor monitor = new Monitor();
private int putCount;
/**
* Create a new PersistenceContext.
*/
public DefaultPersistenceContext() {
}
/**
* Create as a shallow copy with initial or types that have not been added to.
*/
private DefaultPersistenceContext(DefaultPersistenceContext parent, boolean initial) {
for (Map.Entry<Class<?>, ClassContext> entry : parent.typeCache.entrySet()) {
typeCache.put(entry.getKey(), entry.getValue().copy(initial));
}
}
/**
* Return the initial shallow copy with each ClassContext noting it's initialSize (to detect additions).
*/
@Override
public PersistenceContext forIterate() {
return new DefaultPersistenceContext(this, true);
}
/**
* Return a shallow copy including each ClassContext that has had no additions (still at initialSize).
*/
@Override
public PersistenceContext forIterateReset() {
return new DefaultPersistenceContext(this, false);
}
public boolean resetLimit() {
synchronized (monitor) {
if (putCount < 100) {
return false;
}
putCount = 0;
for (ClassContext value : typeCache.values()) {
if (value.resetLimit()) {
return true;
}
}
// checking after another 100 puts
return false;
}
}
/**
* Set an object into the PersistenceContext.
*/
@Override
public void put(Class<?> rootType, Object id, Object bean) {
synchronized (monitor) {
putCount++;
getClassContext(rootType).put(id, bean);
}
}
@@ -53,6 +97,7 @@ public final class DefaultPersistenceContext implements PersistenceContext {
@Override
public Object putIfAbsent(Class<?> rootType, Object id, Object bean) {
synchronized (monitor) {
putCount++;
return getClassContext(rootType).putIfAbsent(id, bean);
}
}
@@ -133,7 +178,6 @@ public final class DefaultPersistenceContext implements PersistenceContext {
}
private ClassContext getClassContext(Class<?> rootType) {
return typeCache.computeIfAbsent(rootType, k -> new ClassContext());
}
@@ -143,9 +187,45 @@ public final class DefaultPersistenceContext implements PersistenceContext {
private Set<Object> deleteSet;
private int initialSize;
private ClassContext() {
}
/**
* Create as a shallow copy.
*/
private ClassContext(ClassContext parent, boolean initial) {
if (initial || parent.isInitialSize()) {
this.map.putAll(parent.map);
this.initialSize = map.size();
if (parent.deleteSet != null) {
this.deleteSet = new HashSet<>(parent.deleteSet);
}
}
}
/**
* True if the map has not changed from it's initial size (no additions).
*/
private boolean isInitialSize() {
return map.size() == initialSize;
}
/**
* Return a shallow copy if initial copy or it has not grown (still at initialSize).
*/
private ClassContext copy(boolean initial) {
return new ClassContext(this, initial);
}
/**
* Return true if grown above the reset limit size.
*/
private boolean resetLimit() {
return map.size() > initialSize + 1000;
}
@Override
public String toString() {
return "size:" + map.size();
@@ -164,7 +244,6 @@ public final class DefaultPersistenceContext implements PersistenceContext {
}
private Object putIfAbsent(Object id, Object bean) {
Object existingValue = map.get(id);
if (existingValue != null) {
// it is not absent