Reduce the overhead of converting chunk indexes to chunk object, dont tick empty entity list

This commit is contained in:
TheMode 2021-04-09 18:22:29 +02:00
parent 05192de8e1
commit 5ef4d0f9b4
7 changed files with 49 additions and 234 deletions

View File

@ -1,6 +1,7 @@
package net.minestom.server;
import com.google.common.collect.Queues;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.InstanceManager;
import net.minestom.server.network.ConnectionManager;
@ -183,13 +184,12 @@ public final class UpdateManager {
* WARNING: should be automatically done by the {@link Instance} implementation.
*
* @param instance the instance of the chunk
* @param chunkX the chunk X
* @param chunkZ the chunk Z
* @param chunk the loaded chunk
*/
public synchronized void signalChunkLoad(Instance instance, int chunkX, int chunkZ) {
public synchronized void signalChunkLoad(Instance instance, @NotNull Chunk chunk) {
if (this.threadProvider == null)
return;
this.threadProvider.onChunkLoad(instance, chunkX, chunkZ);
this.threadProvider.onChunkLoad(instance, chunk);
}
/**
@ -198,13 +198,12 @@ public final class UpdateManager {
* WARNING: should be automatically done by the {@link Instance} implementation.
*
* @param instance the instance of the chunk
* @param chunkX the chunk X
* @param chunkZ the chunk Z
* @param chunk the unloaded chunk
*/
public synchronized void signalChunkUnload(Instance instance, int chunkX, int chunkZ) {
public synchronized void signalChunkUnload(Instance instance, @NotNull Chunk chunk) {
if (this.threadProvider == null)
return;
this.threadProvider.onChunkUnload(instance, chunkX, chunkZ);
this.threadProvider.onChunkUnload(instance, chunk);
}
/**

View File

@ -485,8 +485,7 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
* @return an unmodifiable {@link Set} containing all the entities in a chunk,
* if {@code chunk} is unloaded, return an empty {@link HashSet}
*/
@NotNull
public Set<Entity> getChunkEntities(Chunk chunk) {
public @NotNull Set<Entity> getChunkEntities(Chunk chunk) {
if (!ChunkUtils.isLoaded(chunk))
return new HashSet<>();

View File

@ -506,7 +506,7 @@ public class InstanceContainer extends Instance {
protected void retrieveChunk(int chunkX, int chunkZ, @Nullable ChunkCallback callback) {
final boolean loaded = chunkLoader.loadChunk(this, chunkX, chunkZ, chunk -> {
cacheChunk(chunk);
UPDATE_MANAGER.signalChunkLoad(this, chunkX, chunkZ);
UPDATE_MANAGER.signalChunkLoad(this, chunk);
// Execute callback and event in the instance thread
scheduleNextTick(instance -> {
callChunkLoadEvent(chunkX, chunkZ);
@ -544,7 +544,7 @@ public class InstanceContainer extends Instance {
OptionalCallback.execute(callback, chunk);
}
UPDATE_MANAGER.signalChunkLoad(this, chunkX, chunkZ);
UPDATE_MANAGER.signalChunkLoad(this, chunk);
callChunkLoadEvent(chunkX, chunkZ);
}
@ -641,7 +641,7 @@ public class InstanceContainer extends Instance {
final Chunk copiedChunk = chunk.copy(chunkX, chunkZ);
copiedInstance.cacheChunk(copiedChunk);
UPDATE_MANAGER.signalChunkLoad(copiedInstance, chunkX, chunkZ);
UPDATE_MANAGER.signalChunkLoad(copiedInstance, copiedChunk);
}
return copiedInstance;
@ -682,7 +682,7 @@ public class InstanceContainer extends Instance {
* Adds a {@link Chunk} to the internal instance map.
* <p>
* WARNING: the chunk will not automatically be sent to players and
* {@link net.minestom.server.UpdateManager#signalChunkLoad(Instance, int, int)} must be called manually.
* {@link net.minestom.server.UpdateManager#signalChunkLoad(Instance, Chunk)} must be called manually.
*
* @param chunk the chunk to cache
*/
@ -823,7 +823,7 @@ public class InstanceContainer extends Instance {
chunk.unload();
UPDATE_MANAGER.signalChunkUnload(this, chunkX, chunkZ);
UPDATE_MANAGER.signalChunkUnload(this, chunk);
}
this.scheduledChunksToRemove.clear();
}

View File

@ -1,173 +0,0 @@
package net.minestom.server.thread;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArraySet;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minestom.server.MinecraftServer;
import net.minestom.server.instance.Instance;
import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
/**
* Separate chunks into group of linked chunks
* <p>
* (1 chunks group = 1 thread execution)
*/
public class PerGroupChunkProvider extends ThreadProvider {
/**
* Chunk -> its chunk group
*/
private final Map<Instance, Long2ObjectMap<LongSet>> instanceChunksGroupMap = new ConcurrentHashMap<>();
/**
* Used to know to which instance is linked a Set of chunks
*/
private final Map<Instance, Map<LongSet, Instance>> instanceInstanceMap = new ConcurrentHashMap<>();
@Override
public void onInstanceCreate(@NotNull Instance instance) {
this.instanceChunksGroupMap.putIfAbsent(instance, new Long2ObjectOpenHashMap<>());
this.instanceInstanceMap.putIfAbsent(instance, new HashMap<>());
}
@Override
public void onInstanceDelete(@NotNull Instance instance) {
this.instanceChunksGroupMap.remove(instance);
this.instanceInstanceMap.remove(instance);
}
@Override
public void onChunkLoad(@NotNull Instance instance, int chunkX, int chunkZ) {
final long loadedChunkIndex = ChunkUtils.getChunkIndex(chunkX, chunkZ);
Long2ObjectMap<LongSet> chunksGroupMap = getChunksGroupMap(instance);
Map<LongSet, Instance> instanceMap = getInstanceMap(instance);
// List of groups which are neighbours
List<LongSet> neighboursGroups = new ArrayList<>();
final long[] chunks = ChunkUtils.getNeighbours(instance, chunkX, chunkZ);
boolean findGroup = false;
for (long chunkIndex : chunks) {
if (chunksGroupMap.containsKey(chunkIndex)) {
final LongSet group = chunksGroupMap.get(chunkIndex);
neighboursGroups.add(group);
chunksGroupMap.remove(chunkIndex);
instanceMap.remove(group);
findGroup = true;
}
}
if (!findGroup) {
// Create group of one chunk
LongSet chunkIndexes = new LongArraySet();
chunkIndexes.add(loadedChunkIndex);
chunksGroupMap.put(loadedChunkIndex, chunkIndexes);
instanceMap.put(chunkIndexes, instance);
return;
}
// The size of the final list, used as the initial capacity
final int size = neighboursGroups.stream().mapToInt(Set::size).sum() + 1;
// Represent the merged group of all the neighbours
LongSet finalGroup = new LongArraySet(size);
// Add the newly loaded chunk to the group
finalGroup.add(loadedChunkIndex);
// Add all the neighbours groups to the final one
for (LongSet chunkCoordinates : neighboursGroups) {
finalGroup.addAll(chunkCoordinates);
}
// Complete maps
for (long index : finalGroup) {
chunksGroupMap.put(index, finalGroup);
}
instanceMap.put(finalGroup, instance);
}
@Override
public void onChunkUnload(@NotNull Instance instance, int chunkX, int chunkZ) {
Long2ObjectMap<LongSet> chunksGroupMap = getChunksGroupMap(instance);
Map<LongSet, Instance> instanceMap = getInstanceMap(instance);
final long chunkIndex = ChunkUtils.getChunkIndex(chunkX, chunkZ);
if (chunksGroupMap.containsKey(chunkIndex)) {
// The unloaded chunk is part of a group, remove it from the group
LongSet chunkCoordinates = chunksGroupMap.get(chunkIndex);
chunkCoordinates.remove(chunkIndex);
chunksGroupMap.remove(chunkIndex);
if (chunkCoordinates.isEmpty()) {
// The chunk group is empty, remove it entirely
instanceMap.entrySet().removeIf(entry -> entry.getKey().isEmpty());
}
}
}
@NotNull
@Override
public List<Future<?>> update(long time) {
List<Future<?>> futures;
int potentialSize = 0;
// Compute the potential array size
{
for (Map<LongSet, Instance> longSetInstanceMap : instanceInstanceMap.values()) {
potentialSize += 1 + longSetInstanceMap.size();
}
futures = new ArrayList<>(potentialSize);
}
instanceInstanceMap.forEach((instance, instanceMap) -> {
// True if the instance ended its tick call
final CountDownLatch countDownLatch = new CountDownLatch(1);
// instance tick
futures.add(pool.submit(() -> {
updateInstance(instance, time);
countDownLatch.countDown();
}));
// Update all the chunks
instanceMap.keySet().forEach(chunksIndexes -> futures.add(pool.submit(() -> {
// Wait for the instance to be updated
// Needed because the instance tick is used to unload waiting chunks
try {
countDownLatch.await();
} catch (InterruptedException e) {
MinecraftServer.getExceptionManager().handleException(e);
}
// Tick all this chunk group
chunksIndexes.forEach((long chunkIndex) -> processChunkTick(instance, chunkIndex, time));
})));
});
return futures;
}
private Long2ObjectMap<LongSet> getChunksGroupMap(Instance instance) {
return instanceChunksGroupMap.computeIfAbsent(instance, inst -> new Long2ObjectOpenHashMap<>());
}
private Map<LongSet, Instance> getInstanceMap(Instance instance) {
return instanceInstanceMap.computeIfAbsent(instance, inst -> new HashMap<>());
}
}

View File

@ -1,15 +1,14 @@
package net.minestom.server.thread;
import it.unimi.dsi.fastutil.longs.LongArraySet;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
/**
@ -17,11 +16,11 @@ import java.util.concurrent.Future;
*/
public class PerInstanceThreadProvider extends ThreadProvider {
private final Map<Instance, LongSet> instanceChunkMap = new HashMap<>();
private final Map<Instance, Set<Chunk>> instanceChunkMap = new ConcurrentHashMap<>();
@Override
public void onInstanceCreate(@NotNull Instance instance) {
this.instanceChunkMap.putIfAbsent(instance, new LongArraySet());
this.instanceChunkMap.putIfAbsent(instance, ConcurrentHashMap.newKeySet());
}
@Override
@ -30,20 +29,16 @@ public class PerInstanceThreadProvider extends ThreadProvider {
}
@Override
public void onChunkLoad(@NotNull Instance instance, int chunkX, int chunkZ) {
public void onChunkLoad(@NotNull Instance instance, @NotNull Chunk chunk) {
// Add the loaded chunk to the instance chunks list
LongSet chunkCoordinates = getChunkCoordinates(instance);
final long index = ChunkUtils.getChunkIndex(chunkX, chunkZ);
chunkCoordinates.add(index);
Set<Chunk> chunks = getChunks(instance);
chunks.add(chunk);
}
@Override
public void onChunkUnload(@NotNull Instance instance, int chunkX, int chunkZ) {
LongSet chunkCoordinates = getChunkCoordinates(instance);
final long index = ChunkUtils.getChunkIndex(chunkX, chunkZ);
// Remove the unloaded chunk from the instance list
chunkCoordinates.remove(index);
public void onChunkUnload(@NotNull Instance instance, @NotNull Chunk chunk) {
Set<Chunk> chunks = getChunks(instance);
chunks.remove(chunk);
}
@NotNull
@ -51,17 +46,19 @@ public class PerInstanceThreadProvider extends ThreadProvider {
public List<Future<?>> update(long time) {
List<Future<?>> futures = new ArrayList<>();
instanceChunkMap.forEach((instance, chunkIndexes) -> futures.add(pool.submit(() -> {
instanceChunkMap.forEach((instance, chunks) -> futures.add(pool.submit(() -> {
// Tick instance
updateInstance(instance, time);
// Tick chunks
chunkIndexes.forEach((long chunkIndex) -> processChunkTick(instance, chunkIndex, time));
for (Chunk chunk : chunks) {
processChunkTick(instance, chunk, time);
}
})));
return futures;
}
private LongSet getChunkCoordinates(Instance instance) {
return instanceChunkMap.computeIfAbsent(instance, inst -> new LongArraySet());
private Set<Chunk> getChunks(Instance instance) {
return instanceChunkMap.computeIfAbsent(instance, inst -> ConcurrentHashMap.newKeySet());
}
}

View File

@ -2,7 +2,6 @@ package net.minestom.server.thread;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
@ -33,12 +32,12 @@ public class SingleThreadProvider extends ThreadProvider {
}
@Override
public void onChunkLoad(@NotNull Instance instance, int chunkX, int chunkZ) {
public void onChunkLoad(@NotNull Instance instance, @NotNull Chunk chunk) {
}
@Override
public void onChunkUnload(@NotNull Instance instance, int chunkX, int chunkZ) {
public void onChunkUnload(@NotNull Instance instance, @NotNull Chunk chunk) {
}
@ -49,8 +48,7 @@ public class SingleThreadProvider extends ThreadProvider {
for (Instance instance : instances) {
updateInstance(instance, time);
for (Chunk chunk : instance.getChunks()) {
final long index = ChunkUtils.getChunkIndex(chunk.getChunkX(), chunk.getChunkZ());
processChunkTick(instance, index, time);
processChunkTick(instance, chunk, time);
}
}
}));

View File

@ -61,19 +61,17 @@ public abstract class ThreadProvider {
* Be aware that this is possible for an instance to load chunks before being registered.
*
* @param instance the instance of the chunk
* @param chunkX the chunk X
* @param chunkZ the chunk Z
* @param chunk the loaded chunk
*/
public abstract void onChunkLoad(@NotNull Instance instance, int chunkX, int chunkZ);
public abstract void onChunkLoad(@NotNull Instance instance, @NotNull Chunk chunk);
/**
* Called when a chunk is unloaded.
*
* @param instance the instance of the chunk
* @param chunkX the chunk X
* @param chunkZ the chunk Z
* @param chunk the unloaded chunk
*/
public abstract void onChunkUnload(@NotNull Instance instance, int chunkX, int chunkZ);
public abstract void onChunkUnload(@NotNull Instance instance, @NotNull Chunk chunk);
/**
* Performs a server tick for all chunks based on their linked thread.
@ -115,20 +113,15 @@ public abstract class ThreadProvider {
/**
* Processes a whole tick for a chunk.
*
* @param instance the instance of the chunk
* @param chunkIndex the index of the chunk {@link ChunkUtils#getChunkIndex(int, int)}
* @param time the time of the update in milliseconds
* @param instance the instance of the chunk
* @param chunk the chunk to update
* @param time the time of the update in milliseconds
*/
protected void processChunkTick(@NotNull Instance instance, long chunkIndex, long time) {
final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
final Chunk chunk = instance.getChunk(chunkX, chunkZ);
protected void processChunkTick(@NotNull Instance instance, @NotNull Chunk chunk, long time) {
if (!ChunkUtils.isLoaded(chunk))
return;
updateChunk(instance, chunk, time);
updateEntities(instance, chunk, time);
}
@ -221,13 +214,15 @@ public abstract class ThreadProvider {
*/
protected void conditionalEntityUpdate(@NotNull Instance instance, @NotNull Chunk chunk, long time,
@Nullable EntityValidator condition) {
final Set<Entity> entities = instance.getChunkEntities(chunk);
if (!instance.getEntities().isEmpty()) {
final Set<Entity> entities = instance.getChunkEntities(chunk);
if (!entities.isEmpty()) {
for (Entity entity : entities) {
if (condition != null && !condition.isValid(entity))
continue;
entity.tick(time);
if (!entities.isEmpty()) {
for (Entity entity : entities) {
if (condition != null && !condition.isValid(entity))
continue;
entity.tick(time);
}
}
}