mirror of
https://github.com/Minestom/Minestom.git
synced 2025-01-13 19:51:27 +01:00
Dispatcher testing (#570)
This commit is contained in:
parent
b79054f8e8
commit
da69526f49
@ -7,7 +7,6 @@ import net.minestom.server.instance.InstanceManager;
|
|||||||
import net.minestom.server.monitoring.TickMonitor;
|
import net.minestom.server.monitoring.TickMonitor;
|
||||||
import net.minestom.server.network.ConnectionManager;
|
import net.minestom.server.network.ConnectionManager;
|
||||||
import net.minestom.server.network.socket.Worker;
|
import net.minestom.server.network.socket.Worker;
|
||||||
import net.minestom.server.thread.DispatchUpdate;
|
|
||||||
import net.minestom.server.thread.MinestomThread;
|
import net.minestom.server.thread.MinestomThread;
|
||||||
import net.minestom.server.thread.ThreadDispatcher;
|
import net.minestom.server.thread.ThreadDispatcher;
|
||||||
import net.minestom.server.timer.SchedulerManager;
|
import net.minestom.server.timer.SchedulerManager;
|
||||||
@ -31,7 +30,7 @@ public final class UpdateManager {
|
|||||||
private volatile boolean stopRequested;
|
private volatile boolean stopRequested;
|
||||||
|
|
||||||
// TODO make configurable
|
// TODO make configurable
|
||||||
private final ThreadDispatcher threadDispatcher = ThreadDispatcher.singleThread();
|
private final ThreadDispatcher<Chunk> threadDispatcher = ThreadDispatcher.singleThread();
|
||||||
|
|
||||||
private final Queue<LongConsumer> tickStartCallbacks = new ConcurrentLinkedQueue<>();
|
private final Queue<LongConsumer> tickStartCallbacks = new ConcurrentLinkedQueue<>();
|
||||||
private final Queue<LongConsumer> tickEndCallbacks = new ConcurrentLinkedQueue<>();
|
private final Queue<LongConsumer> tickEndCallbacks = new ConcurrentLinkedQueue<>();
|
||||||
@ -55,7 +54,7 @@ public final class UpdateManager {
|
|||||||
*
|
*
|
||||||
* @return the current thread provider
|
* @return the current thread provider
|
||||||
*/
|
*/
|
||||||
public @NotNull ThreadDispatcher getThreadProvider() {
|
public @NotNull ThreadDispatcher<Chunk> getThreadProvider() {
|
||||||
return threadDispatcher;
|
return threadDispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +88,7 @@ public final class UpdateManager {
|
|||||||
* @param chunk the loaded chunk
|
* @param chunk the loaded chunk
|
||||||
*/
|
*/
|
||||||
public void signalChunkLoad(@NotNull Chunk chunk) {
|
public void signalChunkLoad(@NotNull Chunk chunk) {
|
||||||
this.threadDispatcher.signalUpdate(new DispatchUpdate.ChunkLoad(chunk));
|
this.threadDispatcher.createPartition(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -100,7 +99,7 @@ public final class UpdateManager {
|
|||||||
* @param chunk the unloaded chunk
|
* @param chunk the unloaded chunk
|
||||||
*/
|
*/
|
||||||
public void signalChunkUnload(@NotNull Chunk chunk) {
|
public void signalChunkUnload(@NotNull Chunk chunk) {
|
||||||
this.threadDispatcher.signalUpdate(new DispatchUpdate.ChunkUnload(chunk));
|
this.threadDispatcher.deletePartition(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -237,8 +236,7 @@ public final class UpdateManager {
|
|||||||
this.threadDispatcher.updateAndAwait(tickStart);
|
this.threadDispatcher.updateAndAwait(tickStart);
|
||||||
|
|
||||||
// Clear removed entities & update threads
|
// Clear removed entities & update threads
|
||||||
final long tickTime = System.currentTimeMillis() - tickStart;
|
this.threadDispatcher.refreshThreads();
|
||||||
this.threadDispatcher.refreshThreads(tickTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doTickCallback(Queue<LongConsumer> callbacks, long value) {
|
private void doTickCallback(Queue<LongConsumer> callbacks, long value) {
|
||||||
|
@ -27,7 +27,9 @@ public interface Acquirable<T> {
|
|||||||
final Thread currentThread = Thread.currentThread();
|
final Thread currentThread = Thread.currentThread();
|
||||||
if (currentThread instanceof TickThread) {
|
if (currentThread instanceof TickThread) {
|
||||||
return ((TickThread) currentThread).entries().stream()
|
return ((TickThread) currentThread).entries().stream()
|
||||||
.flatMap(chunkEntry -> chunkEntry.entities().stream());
|
.flatMap(partitionEntry -> partitionEntry.elements().stream())
|
||||||
|
.filter(tickable -> tickable instanceof Entity)
|
||||||
|
.map(tickable -> (Entity) tickable);
|
||||||
}
|
}
|
||||||
return Stream.empty();
|
return Stream.empty();
|
||||||
}
|
}
|
||||||
@ -149,19 +151,19 @@ public interface Acquirable<T> {
|
|||||||
@NotNull Handler getHandler();
|
@NotNull Handler getHandler();
|
||||||
|
|
||||||
final class Handler {
|
final class Handler {
|
||||||
private volatile ThreadDispatcher.ChunkEntry chunkEntry;
|
private volatile ThreadDispatcher.Partition partition;
|
||||||
|
|
||||||
public ThreadDispatcher.ChunkEntry getChunkEntry() {
|
public ThreadDispatcher.Partition getChunkEntry() {
|
||||||
return chunkEntry;
|
return partition;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
public void refreshChunkEntry(@NotNull ThreadDispatcher.ChunkEntry chunkEntry) {
|
public void refreshChunkEntry(@NotNull ThreadDispatcher.Partition partition) {
|
||||||
this.chunkEntry = chunkEntry;
|
this.partition = partition;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TickThread getTickThread() {
|
public TickThread getTickThread() {
|
||||||
final ThreadDispatcher.ChunkEntry entry = this.chunkEntry;
|
final ThreadDispatcher.Partition entry = this.partition;
|
||||||
return entry != null ? entry.thread() : null;
|
return entry != null ? entry.thread() : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,6 @@ import net.minestom.server.potion.PotionEffect;
|
|||||||
import net.minestom.server.potion.TimedPotion;
|
import net.minestom.server.potion.TimedPotion;
|
||||||
import net.minestom.server.tag.Tag;
|
import net.minestom.server.tag.Tag;
|
||||||
import net.minestom.server.tag.TagHandler;
|
import net.minestom.server.tag.TagHandler;
|
||||||
import net.minestom.server.thread.DispatchUpdate;
|
|
||||||
import net.minestom.server.timer.Schedulable;
|
import net.minestom.server.timer.Schedulable;
|
||||||
import net.minestom.server.timer.Scheduler;
|
import net.minestom.server.timer.Scheduler;
|
||||||
import net.minestom.server.timer.TaskSchedule;
|
import net.minestom.server.timer.TaskSchedule;
|
||||||
@ -785,7 +784,7 @@ public class Entity implements Viewable, Tickable, Schedulable, TagHandler, Perm
|
|||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
protected void refreshCurrentChunk(Chunk currentChunk) {
|
protected void refreshCurrentChunk(Chunk currentChunk) {
|
||||||
this.currentChunk = currentChunk;
|
this.currentChunk = currentChunk;
|
||||||
MinecraftServer.getUpdateManager().getThreadProvider().signalUpdate(new DispatchUpdate.EntityUpdate(this));
|
MinecraftServer.getUpdateManager().getThreadProvider().updateElement(this, currentChunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1424,7 +1423,7 @@ public class Entity implements Viewable, Tickable, Schedulable, TagHandler, Perm
|
|||||||
if (!passengers.isEmpty()) passengers.forEach(this::removePassenger);
|
if (!passengers.isEmpty()) passengers.forEach(this::removePassenger);
|
||||||
final Entity vehicle = this.vehicle;
|
final Entity vehicle = this.vehicle;
|
||||||
if (vehicle != null) vehicle.removePassenger(this);
|
if (vehicle != null) vehicle.removePassenger(this);
|
||||||
MinecraftServer.getUpdateManager().getThreadProvider().signalUpdate(new DispatchUpdate.EntityRemove(this));
|
MinecraftServer.getUpdateManager().getThreadProvider().removeElement(this);
|
||||||
this.removed = true;
|
this.removed = true;
|
||||||
Entity.ENTITY_BY_ID.remove(id);
|
Entity.ENTITY_BY_ID.remove(id);
|
||||||
Entity.ENTITY_BY_UUID.remove(uuid);
|
Entity.ENTITY_BY_UUID.remove(uuid);
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
package net.minestom.server.thread;
|
|
||||||
|
|
||||||
import net.minestom.server.entity.Entity;
|
|
||||||
import net.minestom.server.instance.Chunk;
|
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
@ApiStatus.Internal
|
|
||||||
public sealed interface DispatchUpdate permits
|
|
||||||
DispatchUpdate.ChunkLoad, DispatchUpdate.ChunkUnload,
|
|
||||||
DispatchUpdate.EntityUpdate, DispatchUpdate.EntityRemove {
|
|
||||||
record ChunkLoad(@NotNull Chunk chunk) implements DispatchUpdate {
|
|
||||||
}
|
|
||||||
|
|
||||||
record ChunkUnload(@NotNull Chunk chunk) implements DispatchUpdate {
|
|
||||||
}
|
|
||||||
|
|
||||||
record EntityUpdate(@NotNull Entity entity) implements DispatchUpdate {
|
|
||||||
}
|
|
||||||
|
|
||||||
record EntityRemove(@NotNull Entity entity) implements DispatchUpdate {
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +1,12 @@
|
|||||||
package net.minestom.server.thread;
|
package net.minestom.server.thread;
|
||||||
|
|
||||||
import net.minestom.server.MinecraftServer;
|
import net.minestom.server.Tickable;
|
||||||
import net.minestom.server.entity.Entity;
|
import net.minestom.server.acquirable.Acquirable;
|
||||||
import net.minestom.server.instance.Chunk;
|
|
||||||
import net.minestom.server.utils.MathUtils;
|
|
||||||
import org.jctools.queues.MessagePassingQueue;
|
import org.jctools.queues.MessagePassingQueue;
|
||||||
import org.jctools.queues.MpscUnboundedArrayQueue;
|
import org.jctools.queues.MpscUnboundedArrayQueue;
|
||||||
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Unmodifiable;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.Phaser;
|
import java.util.concurrent.Phaser;
|
||||||
@ -15,21 +15,23 @@ import java.util.concurrent.Phaser;
|
|||||||
* Used to link chunks into multiple groups.
|
* Used to link chunks into multiple groups.
|
||||||
* Then executed into a thread pool.
|
* Then executed into a thread pool.
|
||||||
*/
|
*/
|
||||||
public final class ThreadDispatcher {
|
public final class ThreadDispatcher<P> {
|
||||||
private final ThreadProvider provider;
|
private final ThreadProvider<P> provider;
|
||||||
private final List<TickThread> threads;
|
private final List<TickThread> threads;
|
||||||
|
|
||||||
// Chunk -> ChunkEntry mapping
|
// Partition -> dispatching context
|
||||||
private final Map<Chunk, ChunkEntry> chunkEntryMap = new HashMap<>();
|
// Defines how computation is dispatched to the threads
|
||||||
|
private final Map<P, Partition> partitions = new WeakHashMap<>();
|
||||||
|
// Cache to retrieve the threading context from a tickable element
|
||||||
|
private final Map<Tickable, Partition> elements = new WeakHashMap<>();
|
||||||
// Queue to update chunks linked thread
|
// Queue to update chunks linked thread
|
||||||
private final ArrayDeque<Chunk> chunkUpdateQueue = new ArrayDeque<>();
|
private final ArrayDeque<P> partitionUpdateQueue = new ArrayDeque<>();
|
||||||
|
|
||||||
// Requests consumed at the end of each tick
|
// Requests consumed at the end of each tick
|
||||||
private final MessagePassingQueue<DispatchUpdate> updates = new MpscUnboundedArrayQueue<>(1024);
|
private final MessagePassingQueue<DispatchUpdate<P>> updates = new MpscUnboundedArrayQueue<>(1024);
|
||||||
|
|
||||||
private final Phaser phaser = new Phaser(1);
|
private final Phaser phaser = new Phaser(1);
|
||||||
|
|
||||||
private ThreadDispatcher(ThreadProvider provider, int threadCount) {
|
private ThreadDispatcher(ThreadProvider<P> provider, int threadCount) {
|
||||||
this.provider = provider;
|
this.provider = provider;
|
||||||
TickThread[] threads = new TickThread[threadCount];
|
TickThread[] threads = new TickThread[threadCount];
|
||||||
Arrays.setAll(threads, i -> new TickThread(phaser, i));
|
Arrays.setAll(threads, i -> new TickThread(phaser, i));
|
||||||
@ -37,41 +39,17 @@ public final class ThreadDispatcher {
|
|||||||
this.threads.forEach(Thread::start);
|
this.threads.forEach(Thread::start);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull ThreadDispatcher of(@NotNull ThreadProvider provider, int threadCount) {
|
public static <P> @NotNull ThreadDispatcher<P> of(@NotNull ThreadProvider<P> provider, int threadCount) {
|
||||||
return new ThreadDispatcher(provider, threadCount);
|
return new ThreadDispatcher<>(provider, threadCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull ThreadDispatcher singleThread() {
|
public static <P> @NotNull ThreadDispatcher<P> singleThread() {
|
||||||
return of(ThreadProvider.SINGLE, 1);
|
return of(ThreadProvider.counter(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Unmodifiable
|
||||||
* Represents the maximum percentage of tick time that can be spent refreshing chunks thread.
|
public @NotNull List<@NotNull TickThread> threads() {
|
||||||
* <p>
|
return threads;
|
||||||
* Percentage based on {@link MinecraftServer#TICK_MS}.
|
|
||||||
*
|
|
||||||
* @return the refresh percentage
|
|
||||||
*/
|
|
||||||
public float getRefreshPercentage() {
|
|
||||||
return 0.3f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimum time used to refresh chunks and entities thread.
|
|
||||||
*
|
|
||||||
* @return the minimum refresh time in milliseconds
|
|
||||||
*/
|
|
||||||
public int getMinimumRefreshTime() {
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum time used to refresh chunks and entities thread.
|
|
||||||
*
|
|
||||||
* @return the maximum refresh time in milliseconds
|
|
||||||
*/
|
|
||||||
public int getMaximumRefreshTime() {
|
|
||||||
return (int) (MinecraftServer.TICK_MS * 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -80,55 +58,79 @@ public final class ThreadDispatcher {
|
|||||||
* @param time the tick time in milliseconds
|
* @param time the tick time in milliseconds
|
||||||
*/
|
*/
|
||||||
public void updateAndAwait(long time) {
|
public void updateAndAwait(long time) {
|
||||||
for (TickThread thread : threads) thread.startTick(time);
|
|
||||||
this.phaser.arriveAndAwaitAdvance();
|
|
||||||
// Update dispatcher
|
// Update dispatcher
|
||||||
this.updates.drain(update -> {
|
this.updates.drain(update -> {
|
||||||
if (update instanceof DispatchUpdate.ChunkLoad chunkUpdate) {
|
if (update instanceof DispatchUpdate.PartitionLoad<P> chunkUpdate) {
|
||||||
processLoadedChunk(chunkUpdate.chunk());
|
processLoadedChunk(chunkUpdate.partition());
|
||||||
} else if (update instanceof DispatchUpdate.ChunkUnload chunkUnload) {
|
} else if (update instanceof DispatchUpdate.PartitionUnload<P> partitionUnload) {
|
||||||
processUnloadedChunk(chunkUnload.chunk());
|
processUnloadedChunk(partitionUnload.partition());
|
||||||
} else if (update instanceof DispatchUpdate.EntityUpdate entityUpdate) {
|
} else if (update instanceof DispatchUpdate.ElementUpdate<P> elementUpdate) {
|
||||||
processUpdatedEntity(entityUpdate.entity());
|
processUpdatedElement(elementUpdate.tickable(), elementUpdate.partition());
|
||||||
} else if (update instanceof DispatchUpdate.EntityRemove entityRemove) {
|
} else if (update instanceof DispatchUpdate.ElementRemove elementRemove) {
|
||||||
processRemovedEntity(entityRemove.entity());
|
processRemovedEntity(elementRemove.tickable());
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException("Unknown update type: " + update.getClass().getSimpleName());
|
throw new IllegalStateException("Unknown update type: " + update.getClass().getSimpleName());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Tick all partitions
|
||||||
|
for (TickThread thread : threads) thread.startTick(time);
|
||||||
|
this.phaser.arriveAndAwaitAdvance();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called at the end of each tick to clear removed entities,
|
* Called at the end of each tick to clear removed entities,
|
||||||
* refresh the chunk linked to an entity, and chunk threads based on {@link ThreadProvider#findThread(Chunk)}.
|
* refresh the chunk linked to an entity, and chunk threads based on {@link ThreadProvider#findThread(Object)}.
|
||||||
*
|
*
|
||||||
* @param tickTime the duration of the tick in ms,
|
* @param nanoTimeout max time in nanoseconds to update partitions
|
||||||
* used to ensure that the refresh does not take more time than the tick itself
|
|
||||||
*/
|
*/
|
||||||
public void refreshThreads(long tickTime) {
|
public void refreshThreads(long nanoTimeout) {
|
||||||
final ThreadProvider.RefreshType refreshType = provider.getChunkRefreshType();
|
switch (provider.refreshType()) {
|
||||||
if (refreshType == ThreadProvider.RefreshType.NEVER)
|
case NEVER -> {
|
||||||
return;
|
// Do nothing
|
||||||
|
}
|
||||||
final int timeOffset = MathUtils.clamp((int) ((double) tickTime * getRefreshPercentage()),
|
case ALWAYS -> {
|
||||||
getMinimumRefreshTime(), getMaximumRefreshTime());
|
final long currentTime = System.nanoTime();
|
||||||
final long endTime = System.currentTimeMillis() + timeOffset;
|
int counter = partitionUpdateQueue.size();
|
||||||
final int size = chunkUpdateQueue.size();
|
while (true) {
|
||||||
int counter = 0;
|
final P partition = partitionUpdateQueue.pollFirst();
|
||||||
while (true) {
|
if (partition == null) break;
|
||||||
final Chunk chunk = chunkUpdateQueue.pollFirst();
|
// Update chunk's thread
|
||||||
if (chunk == null) break;
|
Partition partitionEntry = partitions.get(partition);
|
||||||
// Update chunk's thread
|
assert partitionEntry != null;
|
||||||
ChunkEntry chunkEntry = chunkEntryMap.get(chunk);
|
final TickThread previous = partitionEntry.thread;
|
||||||
if (chunkEntry != null) chunkEntry.thread = retrieveThread(chunk);
|
final TickThread next = retrieveThread(partition);
|
||||||
this.chunkUpdateQueue.addLast(chunk);
|
if (next != previous) {
|
||||||
if (++counter > size || System.currentTimeMillis() >= endTime)
|
partitionEntry.thread = next;
|
||||||
break;
|
previous.entries().remove(partitionEntry);
|
||||||
|
next.entries().add(partitionEntry);
|
||||||
|
}
|
||||||
|
this.partitionUpdateQueue.addLast(partition);
|
||||||
|
if (--counter <= 0 || System.nanoTime() - currentTime >= nanoTimeout) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void signalUpdate(@NotNull DispatchUpdate update) {
|
public void refreshThreads() {
|
||||||
this.updates.relaxedOffer(update);
|
refreshThreads(Long.MAX_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createPartition(P partition) {
|
||||||
|
signalUpdate(new DispatchUpdate.PartitionLoad<>(partition));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deletePartition(P partition) {
|
||||||
|
signalUpdate(new DispatchUpdate.PartitionUnload<>(partition));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateElement(Tickable tickable, P partition) {
|
||||||
|
signalUpdate(new DispatchUpdate.ElementUpdate<>(tickable, partition));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeElement(Tickable tickable) {
|
||||||
|
signalUpdate(new DispatchUpdate.ElementRemove<>(tickable));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -140,86 +142,94 @@ public final class ThreadDispatcher {
|
|||||||
this.threads.forEach(TickThread::shutdown);
|
this.threads.forEach(TickThread::shutdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
private TickThread retrieveThread(Chunk chunk) {
|
private TickThread retrieveThread(P partition) {
|
||||||
final int threadId = Math.abs(provider.findThread(chunk)) % threads.size();
|
final int threadId = provider.findThread(partition);
|
||||||
return threads.get(threadId);
|
final int index = Math.abs(threadId) % threads.size();
|
||||||
|
return threads.get(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processLoadedChunk(Chunk chunk) {
|
private void signalUpdate(@NotNull DispatchUpdate<P> update) {
|
||||||
final TickThread thread = retrieveThread(chunk);
|
this.updates.relaxedOffer(update);
|
||||||
final ChunkEntry chunkEntry = new ChunkEntry(thread, chunk);
|
|
||||||
thread.entries().add(chunkEntry);
|
|
||||||
this.chunkEntryMap.put(chunk, chunkEntry);
|
|
||||||
this.chunkUpdateQueue.add(chunk);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processUnloadedChunk(Chunk chunk) {
|
private void processLoadedChunk(P partition) {
|
||||||
final ChunkEntry chunkEntry = chunkEntryMap.remove(chunk);
|
if (partitions.containsKey(partition)) return;
|
||||||
if (chunkEntry != null) {
|
final TickThread thread = retrieveThread(partition);
|
||||||
TickThread thread = chunkEntry.thread;
|
final Partition partitionEntry = new Partition(thread);
|
||||||
thread.entries().remove(chunkEntry);
|
thread.entries().add(partitionEntry);
|
||||||
}
|
this.partitions.put(partition, partitionEntry);
|
||||||
this.chunkUpdateQueue.remove(chunk);
|
this.partitionUpdateQueue.add(partition);
|
||||||
}
|
if (partition instanceof Tickable tickable) {
|
||||||
|
processUpdatedElement(tickable, partition);
|
||||||
private void processRemovedEntity(Entity entity) {
|
|
||||||
var acquirableEntity = entity.getAcquirable();
|
|
||||||
ChunkEntry chunkEntry = acquirableEntity.getHandler().getChunkEntry();
|
|
||||||
if (chunkEntry != null) {
|
|
||||||
chunkEntry.entities.remove(entity);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processUpdatedEntity(Entity entity) {
|
private void processUnloadedChunk(P partition) {
|
||||||
ChunkEntry chunkEntry;
|
final Partition partitionEntry = partitions.remove(partition);
|
||||||
|
if (partitionEntry != null) {
|
||||||
|
TickThread thread = partitionEntry.thread;
|
||||||
|
thread.entries().remove(partitionEntry);
|
||||||
|
}
|
||||||
|
this.partitionUpdateQueue.remove(partition);
|
||||||
|
}
|
||||||
|
|
||||||
var acquirableEntity = entity.getAcquirable();
|
private void processRemovedEntity(Tickable tickable) {
|
||||||
chunkEntry = acquirableEntity.getHandler().getChunkEntry();
|
Partition partition = elements.get(tickable);
|
||||||
|
if (partition != null) {
|
||||||
|
partition.elements.remove(tickable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processUpdatedElement(Tickable tickable, P partition) {
|
||||||
|
Partition partitionEntry;
|
||||||
|
|
||||||
|
partitionEntry = elements.get(tickable);
|
||||||
// Remove from previous list
|
// Remove from previous list
|
||||||
if (chunkEntry != null) {
|
if (partitionEntry != null) {
|
||||||
chunkEntry.entities.remove(entity);
|
partitionEntry.elements.remove(tickable);
|
||||||
}
|
}
|
||||||
// Add to new list
|
// Add to new list
|
||||||
chunkEntry = chunkEntryMap.get(entity.getChunk());
|
partitionEntry = partitions.get(partition);
|
||||||
if (chunkEntry != null) {
|
if (partitionEntry != null) {
|
||||||
chunkEntry.entities.add(entity);
|
this.elements.put(tickable, partitionEntry);
|
||||||
acquirableEntity.getHandler().refreshChunkEntry(chunkEntry);
|
partitionEntry.elements.add(tickable);
|
||||||
|
if (tickable instanceof Acquirable<?> acquirable) {
|
||||||
|
acquirable.getHandler().refreshChunkEntry(partitionEntry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class ChunkEntry {
|
public static final class Partition {
|
||||||
private volatile TickThread thread;
|
private TickThread thread;
|
||||||
private final Chunk chunk;
|
private final List<Tickable> elements = new ArrayList<>();
|
||||||
private final List<Entity> entities = new ArrayList<>();
|
|
||||||
|
|
||||||
private ChunkEntry(TickThread thread, Chunk chunk) {
|
private Partition(TickThread thread) {
|
||||||
this.thread = thread;
|
this.thread = thread;
|
||||||
this.chunk = chunk;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NotNull TickThread thread() {
|
public @NotNull TickThread thread() {
|
||||||
return thread;
|
return thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NotNull Chunk chunk() {
|
public @NotNull List<Tickable> elements() {
|
||||||
return chunk;
|
return elements;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiStatus.Internal
|
||||||
|
sealed interface DispatchUpdate<P> permits
|
||||||
|
DispatchUpdate.PartitionLoad, DispatchUpdate.PartitionUnload,
|
||||||
|
DispatchUpdate.ElementUpdate, DispatchUpdate.ElementRemove {
|
||||||
|
record PartitionLoad<P>(@NotNull P partition) implements DispatchUpdate<P> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NotNull List<Entity> entities() {
|
record PartitionUnload<P>(@NotNull P partition) implements DispatchUpdate<P> {
|
||||||
return entities;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
record ElementUpdate<P>(@NotNull Tickable tickable, P partition) implements DispatchUpdate<P> {
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
ChunkEntry that = (ChunkEntry) o;
|
|
||||||
return chunk.equals(that.chunk);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
record ElementRemove<P>(@NotNull Tickable tickable) implements DispatchUpdate<P> {
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(chunk);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,6 @@ package net.minestom.server.thread;
|
|||||||
|
|
||||||
import com.github.benmanes.caffeine.cache.Cache;
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
import net.minestom.server.instance.Chunk;
|
|
||||||
import net.minestom.server.instance.Instance;
|
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
@ -11,39 +9,32 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
@ApiStatus.Experimental
|
@ApiStatus.Experimental
|
||||||
public interface ThreadProvider {
|
public interface ThreadProvider<T> {
|
||||||
ThreadProvider PER_CHUNk = new ThreadProvider() {
|
static <T> @NotNull ThreadProvider<T> counter() {
|
||||||
private final AtomicInteger counter = new AtomicInteger();
|
return new ThreadProvider<>() {
|
||||||
|
private final Cache<T, Integer> cache = Caffeine.newBuilder().weakKeys().build();
|
||||||
|
private final AtomicInteger counter = new AtomicInteger();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int findThread(@NotNull Chunk chunk) {
|
public int findThread(@NotNull T partition) {
|
||||||
return counter.getAndIncrement();
|
return cache.get(partition, i -> counter.getAndIncrement());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ThreadProvider PER_INSTANCE = new ThreadProvider() {
|
}
|
||||||
private final Cache<Instance, Integer> cache = Caffeine.newBuilder().weakKeys().build();
|
|
||||||
private final AtomicInteger counter = new AtomicInteger();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int findThread(@NotNull Chunk chunk) {
|
|
||||||
return cache.get(chunk.getInstance(), i -> counter.getAndIncrement());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ThreadProvider SINGLE = chunk -> 0;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a server tick for all chunks based on their linked thread.
|
* Performs a server tick for all chunks based on their linked thread.
|
||||||
*
|
*
|
||||||
* @param chunk the chunk
|
* @param partition the partition
|
||||||
*/
|
*/
|
||||||
int findThread(@NotNull Chunk chunk);
|
int findThread(@NotNull T partition);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines how often chunks thread should be updated.
|
* Defines how often chunks thread should be updated.
|
||||||
*
|
*
|
||||||
* @return the refresh type
|
* @return the refresh type
|
||||||
*/
|
*/
|
||||||
default @NotNull RefreshType getChunkRefreshType() {
|
default @NotNull RefreshType refreshType() {
|
||||||
return RefreshType.NEVER;
|
return RefreshType.NEVER;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,16 +43,16 @@ public interface ThreadProvider {
|
|||||||
*/
|
*/
|
||||||
enum RefreshType {
|
enum RefreshType {
|
||||||
/**
|
/**
|
||||||
* Chunk thread is constant after being defined.
|
* Thread never change after being defined once.
|
||||||
|
* <p>
|
||||||
|
* Means that {@link #findThread(Object)} will only be called once for each partition.
|
||||||
*/
|
*/
|
||||||
NEVER,
|
NEVER,
|
||||||
/**
|
/**
|
||||||
* Chunk thread should be recomputed as often as possible.
|
* Thread is updated as often as possible.
|
||||||
|
* <p>
|
||||||
|
* Means that {@link #findThread(Object)} may be called multiple time for each partition.
|
||||||
*/
|
*/
|
||||||
CONSTANT,
|
ALWAYS
|
||||||
/**
|
|
||||||
* Chunk thread should be recomputed, but not continuously.
|
|
||||||
*/
|
|
||||||
RARELY
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.minestom.server.thread;
|
package net.minestom.server.thread;
|
||||||
|
|
||||||
import net.minestom.server.MinecraftServer;
|
import net.minestom.server.MinecraftServer;
|
||||||
|
import net.minestom.server.Tickable;
|
||||||
import net.minestom.server.entity.Entity;
|
import net.minestom.server.entity.Entity;
|
||||||
import net.minestom.server.instance.Chunk;
|
import net.minestom.server.instance.Chunk;
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
@ -25,7 +26,7 @@ public final class TickThread extends MinestomThread {
|
|||||||
private volatile boolean stop;
|
private volatile boolean stop;
|
||||||
|
|
||||||
private long tickTime;
|
private long tickTime;
|
||||||
private final List<ThreadDispatcher.ChunkEntry> entries = new ArrayList<>();
|
private final List<ThreadDispatcher.Partition> entries = new ArrayList<>();
|
||||||
|
|
||||||
public TickThread(Phaser phaser, int number) {
|
public TickThread(Phaser phaser, int number) {
|
||||||
super(MinecraftServer.THREAD_NAME_TICK + "-" + number);
|
super(MinecraftServer.THREAD_NAME_TICK + "-" + number);
|
||||||
@ -50,26 +51,19 @@ public final class TickThread extends MinestomThread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void tick() {
|
private void tick() {
|
||||||
for (ThreadDispatcher.ChunkEntry entry : entries) {
|
for (ThreadDispatcher.Partition entry : entries) {
|
||||||
final Chunk chunk = entry.chunk();
|
final List<Tickable> elements = entry.elements();
|
||||||
try {
|
if (elements.isEmpty()) continue;
|
||||||
chunk.tick(tickTime);
|
for (Tickable element : elements) {
|
||||||
} catch (Throwable e) {
|
if (lock.hasQueuedThreads()) {
|
||||||
MinecraftServer.getExceptionManager().handleException(e);
|
this.lock.unlock();
|
||||||
}
|
// #acquire() callbacks should be called here
|
||||||
final List<Entity> entities = entry.entities();
|
this.lock.lock();
|
||||||
if (!entities.isEmpty()) {
|
}
|
||||||
for (Entity entity : entities) {
|
try {
|
||||||
if (lock.hasQueuedThreads()) {
|
element.tick(tickTime);
|
||||||
this.lock.unlock();
|
} catch (Throwable e) {
|
||||||
// #acquire() callbacks should be called here
|
MinecraftServer.getExceptionManager().handleException(e);
|
||||||
this.lock.lock();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
entity.tick(tickTime);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
MinecraftServer.getExceptionManager().handleException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,7 +78,7 @@ public final class TickThread extends MinestomThread {
|
|||||||
LockSupport.unpark(this);
|
LockSupport.unpark(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<ThreadDispatcher.ChunkEntry> entries() {
|
public Collection<ThreadDispatcher.Partition> entries() {
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
176
src/test/java/thread/ThreadDispatcherTest.java
Normal file
176
src/test/java/thread/ThreadDispatcherTest.java
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
package thread;
|
||||||
|
|
||||||
|
import net.minestom.server.Tickable;
|
||||||
|
import net.minestom.server.thread.ThreadDispatcher;
|
||||||
|
import net.minestom.server.thread.ThreadProvider;
|
||||||
|
import net.minestom.server.thread.TickThread;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
import java.util.concurrent.Phaser;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class ThreadDispatcherTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void elementTick() {
|
||||||
|
final AtomicInteger counter = new AtomicInteger();
|
||||||
|
ThreadDispatcher<Object> dispatcher = ThreadDispatcher.singleThread();
|
||||||
|
assertEquals(1, dispatcher.threads().size());
|
||||||
|
assertThrows(Exception.class, () -> dispatcher.threads().add(new TickThread(new Phaser(), 1)));
|
||||||
|
|
||||||
|
var partition = new Object();
|
||||||
|
Tickable element = (time) -> counter.incrementAndGet();
|
||||||
|
dispatcher.createPartition(partition);
|
||||||
|
dispatcher.updateElement(element, partition);
|
||||||
|
assertEquals(0, counter.get());
|
||||||
|
|
||||||
|
dispatcher.updateAndAwait(System.currentTimeMillis());
|
||||||
|
dispatcher.updateElement(element, partition); // Should be ignored
|
||||||
|
dispatcher.createPartition(partition); // Ignored too
|
||||||
|
assertEquals(1, counter.get());
|
||||||
|
|
||||||
|
dispatcher.updateAndAwait(System.currentTimeMillis());
|
||||||
|
assertEquals(2, counter.get());
|
||||||
|
|
||||||
|
dispatcher.removeElement(element);
|
||||||
|
dispatcher.updateAndAwait(System.currentTimeMillis());
|
||||||
|
assertEquals(2, counter.get());
|
||||||
|
|
||||||
|
dispatcher.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void partitionTick() {
|
||||||
|
// Partitions implementing Tickable should be ticked same as elements
|
||||||
|
final AtomicInteger counter1 = new AtomicInteger();
|
||||||
|
final AtomicInteger counter2 = new AtomicInteger();
|
||||||
|
ThreadDispatcher<Tickable> dispatcher = ThreadDispatcher.singleThread();
|
||||||
|
assertEquals(1, dispatcher.threads().size());
|
||||||
|
|
||||||
|
Tickable partition = (time) -> counter1.incrementAndGet();
|
||||||
|
Tickable element = (time) -> counter2.incrementAndGet();
|
||||||
|
dispatcher.createPartition(partition);
|
||||||
|
dispatcher.updateElement(element, partition);
|
||||||
|
assertEquals(0, counter1.get());
|
||||||
|
assertEquals(0, counter2.get());
|
||||||
|
|
||||||
|
dispatcher.updateAndAwait(System.currentTimeMillis());
|
||||||
|
assertEquals(1, counter1.get());
|
||||||
|
assertEquals(1, counter2.get());
|
||||||
|
|
||||||
|
dispatcher.updateAndAwait(System.currentTimeMillis());
|
||||||
|
assertEquals(2, counter1.get());
|
||||||
|
assertEquals(2, counter2.get());
|
||||||
|
|
||||||
|
dispatcher.deletePartition(partition);
|
||||||
|
dispatcher.updateAndAwait(System.currentTimeMillis());
|
||||||
|
assertEquals(2, counter1.get());
|
||||||
|
assertEquals(2, counter2.get());
|
||||||
|
|
||||||
|
dispatcher.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void uniqueThread() {
|
||||||
|
// Ensure that partitions are properly dispatched across threads
|
||||||
|
final int threadCount = 10;
|
||||||
|
ThreadDispatcher<Tickable> dispatcher = ThreadDispatcher.of(ThreadProvider.counter(), threadCount);
|
||||||
|
assertEquals(threadCount, dispatcher.threads().size());
|
||||||
|
|
||||||
|
final AtomicInteger counter = new AtomicInteger();
|
||||||
|
Set<Thread> threads = new CopyOnWriteArraySet<>();
|
||||||
|
Set<Tickable> partitions = IntStream.range(0, threadCount)
|
||||||
|
.mapToObj(value -> (Tickable) (time) -> {
|
||||||
|
final Thread thread = Thread.currentThread();
|
||||||
|
assertInstanceOf(TickThread.class, thread);
|
||||||
|
assertEquals(1, ((TickThread) thread).entries().size());
|
||||||
|
assertTrue(threads.add(thread));
|
||||||
|
counter.getAndIncrement();
|
||||||
|
})
|
||||||
|
.collect(Collectors.toUnmodifiableSet());
|
||||||
|
assertEquals(threadCount, partitions.size());
|
||||||
|
|
||||||
|
partitions.forEach(dispatcher::createPartition);
|
||||||
|
assertEquals(0, counter.get());
|
||||||
|
|
||||||
|
dispatcher.updateAndAwait(System.currentTimeMillis());
|
||||||
|
assertEquals(threadCount, counter.get());
|
||||||
|
|
||||||
|
dispatcher.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void threadUpdate() {
|
||||||
|
// Ensure that partitions threads are properly updated every tick
|
||||||
|
// when RefreshType.ALWAYS is used
|
||||||
|
interface Updater extends Tickable {
|
||||||
|
int getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
final int threadCount = 10;
|
||||||
|
ThreadDispatcher<Updater> dispatcher = ThreadDispatcher.of(new ThreadProvider<>() {
|
||||||
|
@Override
|
||||||
|
public int findThread(@NotNull Updater partition) {
|
||||||
|
return partition.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull RefreshType refreshType() {
|
||||||
|
return RefreshType.ALWAYS;
|
||||||
|
}
|
||||||
|
}, threadCount);
|
||||||
|
assertEquals(threadCount, dispatcher.threads().size());
|
||||||
|
|
||||||
|
Map<Updater, Thread> threads = new ConcurrentHashMap<>();
|
||||||
|
Map<Updater, Thread> threads2 = new ConcurrentHashMap<>();
|
||||||
|
Set<Updater> partitions = IntStream.range(0, threadCount)
|
||||||
|
.mapToObj(value -> new Updater() {
|
||||||
|
private int v = value;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getValue() {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick(long time) {
|
||||||
|
final Thread currentThread = Thread.currentThread();
|
||||||
|
assertInstanceOf(TickThread.class, currentThread);
|
||||||
|
if (threads.putIfAbsent(this, currentThread) == null) {
|
||||||
|
this.v = value + 1;
|
||||||
|
} else {
|
||||||
|
assertEquals(value + 1, v);
|
||||||
|
threads2.putIfAbsent(this, currentThread);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).collect(Collectors.toUnmodifiableSet());
|
||||||
|
assertEquals(threadCount, partitions.size());
|
||||||
|
|
||||||
|
partitions.forEach(dispatcher::createPartition);
|
||||||
|
|
||||||
|
dispatcher.updateAndAwait(System.currentTimeMillis());
|
||||||
|
|
||||||
|
dispatcher.refreshThreads();
|
||||||
|
|
||||||
|
dispatcher.updateAndAwait(System.currentTimeMillis());
|
||||||
|
|
||||||
|
assertEquals(threads2.size(), threads.size());
|
||||||
|
assertNotEquals(threads, threads2, "Threads have not been updated at all");
|
||||||
|
for (var entry : threads.entrySet()) {
|
||||||
|
final Thread thread1 = entry.getValue();
|
||||||
|
final Thread thread2 = threads2.get(entry.getKey());
|
||||||
|
assertNotEquals(thread1, thread2);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatcher.shutdown();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user