Improve initial LOD loading speed and add KeyedLockContainer

This commit is contained in:
James Seibel
2025-01-10 07:26:27 -06:00
parent 506b2b0f7b
commit f93b57e935
4 changed files with 157 additions and 77 deletions
@@ -7,6 +7,7 @@ import com.google.common.cache.RemovalNotification;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.KeyedLockContainer;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
@@ -25,9 +26,8 @@ public class DelayedFullDataSourceSaveCache
private final Cache<Long, FullDataSourceV2> dataSourceByPosition;
protected final ReentrantLock[] saveLockArray;
/** Based on the stack overflow post: https://stackoverflow.com/a/45909920 */
protected ReentrantLock getSaveLockForPos(long pos) { return this.saveLockArray[Math.abs(Long.hashCode(pos)) % this.saveLockArray.length]; }
/* don't let two threads load the same position at the same time */
protected final KeyedLockContainer<Long> saveLockContainer = new KeyedLockContainer<>();
private final ISaveDataSourceFunc onSaveTimeoutAsyncFunc;
private final int saveDelayInMs;
@@ -51,15 +51,6 @@ public class DelayedFullDataSourceSaveCache
.removalListener(this::handleDataSourceRemoval)
.<Long, FullDataSourceV2>build();
// the lock array's length is 2x the number of CPU cores so the number of collisions
// should be relatively low without having too many extra locks
int lockCount = Runtime.getRuntime().availableProcessors() * 2;
this.saveLockArray = new ReentrantLock[lockCount];
for (int i = 0; i < lockCount; i++)
{
this.saveLockArray[i] = new ReentrantLock();
}
}
@@ -76,7 +67,7 @@ public class DelayedFullDataSourceSaveCache
{
long inputPos = inputDataSource.getPos();
ReentrantLock lock = this.getSaveLockForPos(inputPos);
ReentrantLock lock = this.saveLockContainer.getLockForPos(inputPos);
try
{
lock.lock();
@@ -20,7 +20,10 @@
package com.seibel.distanthorizons.core.render;
import com.google.common.base.Suppliers;
import com.google.errorprone.annotations.MustBeClosed;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalCause;
import com.google.common.cache.RemovalNotification;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource;
@@ -38,6 +41,7 @@ import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.ColumnRenderBuffer;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.util.KeyedLockContainer;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
@@ -49,6 +53,7 @@ import javax.annotation.WillNotClose;
import java.awt.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
/**
@@ -60,6 +65,41 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
/**
* caching the loaded positions significantly improves initial loading performance
* since the same position doesn't need to be loaded 5 times.
*/
private static final Cache<Long, ColumnRenderSource> CACHED_RENDER_SOURCE_BY_POS
= CacheBuilder.newBuilder()
// availableProcessors() : each process may need to be loading a render source
// +1 : add 1 thread count buffer to reduce the chance of accidentally unloading a render source before it's used
// *5 : each render source needs it's 4 adjacent sides, so a total of 5 render sources are needed per load
.maximumSize((Runtime.getRuntime().availableProcessors() + 1) * 5L)
.removalListener((RemovalNotification<Long, ColumnRenderSource> removalNotification) ->
{
RemovalCause cause = removalNotification.getCause();
if (cause == RemovalCause.EXPIRED
|| cause == RemovalCause.COLLECTED
|| cause == RemovalCause.SIZE)
{
// close the render source after it's been
ColumnRenderSource renderSource = removalNotification.getValue();
if (renderSource != null)
{
renderSource.close();
}
else
{
LOGGER.error("Unable to close null cached render source.");
}
}
})
.<Long, ColumnRenderSource>build();
/** don't let two threads load the same position at the same time */
protected static final KeyedLockContainer<Long> RENDER_LOAD_LOCK_CONTAINER = new KeyedLockContainer<>();
public final long pos;
@@ -182,52 +222,45 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
{
try
{
try (ColumnRenderSource renderSource = this.getRenderSourceForPos(this.pos))
ColumnRenderSource renderSource = this.getRenderSourceForPos(this.pos);
if (renderSource == null)
{
if (renderSource == null)
{
// nothing needs to be rendered
// TODO how doesn't this cause infinite file handler loops?
// to trigger an upload we check if the buffer is null, and we aren't
// setting the render buffer here
return;
}
boolean enableTransparency = Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled;
LodQuadBuilder lodQuadBuilder = new LodQuadBuilder(enableTransparency, this.level.getClientLevelWrapper());
// Getting adjacent data sources without caching
// (IE each position must pull down it's 4 neighbors even if those neighbors
// are used by another position)
// roughly doubles the total time to load vs just loading the center position;
// however, caching the ColumnRenderSource's in memory is extremely
// difficult to do without either memory leaks or explosive memory costs.
// So, we're just going to do it this way.
try (ColumnRenderSource northRenderSource = this.getRenderSourceForPos(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.NORTH));
ColumnRenderSource southRenderSource = this.getRenderSourceForPos(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.SOUTH));
ColumnRenderSource eastRenderSource = this.getRenderSourceForPos(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.EAST));
ColumnRenderSource westRenderSource = this.getRenderSourceForPos(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.WEST)))
{
ColumnRenderSource[] adjacentRenderSections = new ColumnRenderSource[EDhDirection.ADJ_DIRECTIONS.length];
adjacentRenderSections[EDhDirection.NORTH.ordinal() - 2] = northRenderSource;
adjacentRenderSections[EDhDirection.SOUTH.ordinal() - 2] = southRenderSource;
adjacentRenderSections[EDhDirection.EAST.ordinal() - 2] = eastRenderSource;
adjacentRenderSections[EDhDirection.WEST.ordinal() - 2] = westRenderSource;
boolean[] adjIsSameDetailLevel = new boolean[EDhDirection.ADJ_DIRECTIONS.length];
adjIsSameDetailLevel[EDhDirection.NORTH.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.NORTH);
adjIsSameDetailLevel[EDhDirection.SOUTH.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.SOUTH);
adjIsSameDetailLevel[EDhDirection.EAST.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.EAST);
adjIsSameDetailLevel[EDhDirection.WEST.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.WEST);
// the render sources are only needed in this synchronous method,
// then they can be closed
ColumnRenderBufferBuilder.makeLodRenderData(lodQuadBuilder, renderSource, this.level, adjacentRenderSections, adjIsSameDetailLevel);
}
this.uploadToGpuAsync(lodQuadBuilder);
// nothing needs to be rendered
// TODO how doesn't this cause infinite file handler loops?
// to trigger an upload we check if the buffer is null, and we aren't
// setting the render buffer here
return;
}
boolean enableTransparency = Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled;
LodQuadBuilder lodQuadBuilder = new LodQuadBuilder(enableTransparency, this.level.getClientLevelWrapper());
// load adjacent render sources
{
ColumnRenderSource northRenderSource = this.getRenderSourceForPos(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.NORTH));
ColumnRenderSource southRenderSource = this.getRenderSourceForPos(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.SOUTH));
ColumnRenderSource eastRenderSource = this.getRenderSourceForPos(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.EAST));
ColumnRenderSource westRenderSource = this.getRenderSourceForPos(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.WEST));
ColumnRenderSource[] adjacentRenderSections = new ColumnRenderSource[EDhDirection.ADJ_DIRECTIONS.length];
adjacentRenderSections[EDhDirection.NORTH.ordinal() - 2] = northRenderSource;
adjacentRenderSections[EDhDirection.SOUTH.ordinal() - 2] = southRenderSource;
adjacentRenderSections[EDhDirection.EAST.ordinal() - 2] = eastRenderSource;
adjacentRenderSections[EDhDirection.WEST.ordinal() - 2] = westRenderSource;
boolean[] adjIsSameDetailLevel = new boolean[EDhDirection.ADJ_DIRECTIONS.length];
adjIsSameDetailLevel[EDhDirection.NORTH.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.NORTH);
adjIsSameDetailLevel[EDhDirection.SOUTH.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.SOUTH);
adjIsSameDetailLevel[EDhDirection.EAST.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.EAST);
adjIsSameDetailLevel[EDhDirection.WEST.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.WEST);
// the render sources are only needed in this synchronous method,
// then they can be closed
ColumnRenderBufferBuilder.makeLodRenderData(lodQuadBuilder, renderSource, this.level, adjacentRenderSections, adjIsSameDetailLevel);
}
this.uploadToGpuAsync(lodQuadBuilder);
}
catch (Exception e)
{
@@ -235,12 +268,36 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
}
}
@Nullable
@MustBeClosed
private ColumnRenderSource getRenderSourceForPos(long pos)
{
try (FullDataSourceV2 fullDataSource = this.fullDataSourceProvider.get(pos))
ReentrantLock lock = RENDER_LOAD_LOCK_CONTAINER.getLockForPos(pos);
try
{
return FullDataToRenderDataTransformer.transformFullDataToRenderSource(fullDataSource, this.level);
// we don't want multiple threads attempting to load the same position at the same time
lock.lock();
// use the cached data if possible
ColumnRenderSource renderSource = CACHED_RENDER_SOURCE_BY_POS.getIfPresent(pos);
if (renderSource != null)
{
return renderSource;
}
try (FullDataSourceV2 fullDataSource = this.fullDataSourceProvider.get(pos))
{
renderSource = FullDataToRenderDataTransformer.transformFullDataToRenderSource(fullDataSource, this.level);
// only add valid data to the cache (to prevent null pointers)
if (renderSource != null)
{
CACHED_RENDER_SOURCE_BY_POS.put(pos, renderSource);
}
}
return renderSource;
}
finally
{
lock.unlock();
}
}
private boolean isAdjacentPosSameDetailLevel(EDhDirection direction)
@@ -20,14 +20,13 @@
package com.seibel.distanthorizons.core.sql.repo;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.sql.DatabaseUpdater;
import com.seibel.distanthorizons.core.sql.DbConnectionClosedException;
import com.seibel.distanthorizons.core.sql.dto.IBaseDTO;
import com.seibel.distanthorizons.core.util.KeyedLockContainer;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import javax.swing.plaf.nimbus.State;
import java.io.File;
import java.io.IOException;
import java.sql.*;
@@ -61,9 +60,7 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
public final Class<? extends TDTO> dtoClass;
protected final ReentrantLock[] saveLockArray;
/** Based on the stack overflow post: https://stackoverflow.com/a/45909920 */
protected ReentrantLock getSaveLockForKey(TKey key) { return this.saveLockArray[Math.abs(key.hashCode()) % this.saveLockArray.length]; }
protected final KeyedLockContainer<TKey> saveLockContainer = new KeyedLockContainer<>();
@@ -79,16 +76,6 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
this.dtoClass = dtoClass;
// the lock array's length is 2x the number of CPU cores so the number of collisions
// should be relatively low without having too many extra locks
int lockCount = Runtime.getRuntime().availableProcessors() * 2;
this.saveLockArray = new ReentrantLock[lockCount];
for (int i = 0; i < lockCount; i++)
{
this.saveLockArray[i] = new ReentrantLock();
}
try
{
// needed by Forge to load the Java database connection
@@ -206,7 +193,7 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
// a lock is necessary to prevent concurrent modification between
// existsWithKey and insert/update,
// otherwise another thread might cause the insert/update to fail.
ReentrantLock saveLock = this.getSaveLockForKey(dto.getKey());
ReentrantLock saveLock = this.saveLockContainer.getLockForPos(dto.getKey());
try
{
@@ -0,0 +1,45 @@
package com.seibel.distanthorizons.core.util;
import java.util.concurrent.locks.ReentrantLock;
/**
* Can be used to allow an infinite number of keys to
* map to a finite number of locks.
* Useful when loading/modifying positional LOD data and wanting to
* prevent concurrent modifications. <br>
*
* Based on the stack overflow post: https://stackoverflow.com/a/45909920
*/
public class KeyedLockContainer<TKey>
{
protected final ReentrantLock[] lockArray;
//==============//
// constructors //
//==============//
public KeyedLockContainer()
{
// the lock array's length is 2x the number of CPU cores so the number of collisions
// should be relatively low without having too many extra locks
this(Runtime.getRuntime().availableProcessors() * 2);
}
public KeyedLockContainer(int lockCount)
{
this.lockArray = new ReentrantLock[lockCount];
for (int i = 0; i < lockCount; i++)
{
this.lockArray[i] = new ReentrantLock();
}
}
//=========//
// getters //
//=========//
public ReentrantLock getLockForPos(TKey key) { return this.lockArray[Math.abs(key.hashCode()) % this.lockArray.length]; }
}