/*
* Minecraft Forge
* Copyright (c) 2016.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package cubicchunks.server.chunkio.async.forge;
import com.google.common.collect.Maps;
import net.minecraft.server.MinecraftServer;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.PlayerEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import cubicchunks.CubicChunks;
import cubicchunks.server.CubeProviderServer;
import cubicchunks.server.chunkio.CubeIO;
import cubicchunks.world.ICubicWorld;
import cubicchunks.world.IProviderExtras;
import cubicchunks.world.column.Column;
import cubicchunks.world.cube.Cube;
/**
* Brazenly copied from Forge and Sponge and reimplemented to suit our needs: Load cubes and columns outside the main
* thread, then synchronize at the start of the next tick
*/
public class AsyncWorldIOExecutor {
private static final int BASE_THREADS = 1;
private static final int PLAYERS_PER_THREAD = 50;
private static final Map<QueuedCube, AsyncCubeIOProvider> cubeTasks = Maps.newConcurrentMap();
private static final Map<QueuedColumn, AsyncColumnIOProvider> columnTasks = Maps.newConcurrentMap();
private static final AtomicInteger threadCounter = new AtomicInteger();
private static final ThreadPoolExecutor pool = new ThreadPoolExecutor(BASE_THREADS, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
// Sponge start: Use lambda
r -> {
Thread thread = new Thread(r, "Cube I/O Thread #" + threadCounter.incrementAndGet());
thread.setDaemon(true);
return thread;
}
// Sponge end
);
/**
* Load a cube, directly.
*
* @param world The world in which the cube lies
* @param loader The file loader for cubes
* @param cache the cube cache used to load cubes and columns
* @param cubeX X coordinate of the cube to load
* @param cubeY Y coordinate of the cube to load
* @param cubeZ Z coordinate of the cube to load
*
* @return The loaded cube, or null if either not present or the load failed
*/
@Nullable
public static Cube syncCubeLoad(ICubicWorld world, CubeIO loader, CubeProviderServer cache, int cubeX, int cubeY, int cubeZ) {
Column column = cache.loadChunk(cubeX, cubeZ);
QueuedCube key = new QueuedCube(cubeX, cubeY, cubeZ, world);
AsyncCubeIOProvider task = cubeTasks.remove(key); // Remove task because we will call the sync callbacks directly
if (task != null) {
runTask(task);
} else {
task = new AsyncCubeIOProvider(key, loader);
task.setColumn(column);
task.run();
}
task.runSynchronousPart();
return task.get();
}
/**
* Load a column, directly
*
* @param world The world in which the column lies
* @param loader The file loader for columns
* @param x column x position
* @param z column z position
*
* @return The loaded column
*/
public static Column syncColumnLoad(ICubicWorld world, CubeIO loader, int x, int z) {
QueuedColumn key = new QueuedColumn(x, z, world);
AsyncColumnIOProvider task = columnTasks.remove(key); // Remove task because we will call the sync callbacks directly
if (task != null) {
runTask(task);
} else {
task = new AsyncColumnIOProvider(key, loader);
task.run();
}
task.runSynchronousPart();
return task.get();
}
/**
* Runs the async part in current thread or blocks until already running async part is finished
*/
private static void runTask(AsyncIOProvider task) {
if (!pool.remove(task)) // If it wasn't in the pool, and run hasn't isFinished, then wait for the async thread.
{
synchronized (task) // Warn incorrect - task shared via map
{
while (!task.isFinished()) {
try {
task.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Failed to wait for cube/column load", e);
}
}
}
} else {
// If the task was not run yet we still need to load the Cube
task.run();
}
}
//Queue the Cube to be loaded, and call the runnable when isFinished
// Sponge: Runnable -> Consumer<Cube>
/**
* Queue a cube load, running the specified callback when the load has finished. This may cause a two tick delay
* if the column has to be loaded, too! If you need it faster, consider sync loading either column or both
* cube and column.
*
* @param world The world of the cube
* @param loader The file loader for this world
* @param cache The server cube cache
* @param x cube x position
* @param y cube y position
* @param z cube z position
* @param runnable The callback
*/
public static void queueCubeLoad(ICubicWorld world, CubeIO loader, CubeProviderServer cache, int x, int y, int z, Consumer<Cube> runnable) {
QueuedCube key = new QueuedCube(x, y, z, world);
AsyncCubeIOProvider task = cubeTasks.get(key);
if (task == null) {
task = new AsyncCubeIOProvider(key, loader);
task.addCallback(runnable); // Add before calling execute for thread safety
cubeTasks.put(key, task);
pool.execute(task);
} else {
task.addCallback(runnable);
}
Column loadedColumn;
if ((loadedColumn = cache.getLoadedColumn(x, z)) == null) {
cache.asyncGetColumn(x, z, IProviderExtras.Requirement.LIGHT, task::setColumn);
} else {
//it's already there, tell the task to use it
task.setColumn(loadedColumn);
}
}
/**
* Queue a column load, running the specified callback when the load has finished
*
* @param world The world of the column
* @param loader The file loader for this world
* @param x column x position
* @param z column z position
* @param runnable The callback
*/
public static void queueColumnLoad(ICubicWorld world, CubeIO loader, int x, int z, Consumer<Column> runnable) {
QueuedColumn key = new QueuedColumn(x, z, world);
AsyncColumnIOProvider task = columnTasks.get(key);
if (task == null) {
task = new AsyncColumnIOProvider(key, loader);
task.addCallback(runnable); // Add before calling execute for thread safety
columnTasks.put(key, task);
pool.execute(task);
} else {
task.addCallback(runnable);
}
}
/**
* Notify the loader that this cube isn't needed anymore
*
* @param world The world
* @param x cube x position
* @param y cube y position
* @param z cube z position
* @param runnable The runnable that should be dropped
*/
public static void dropQueuedCubeLoad(ICubicWorld world, int x, int y, int z, Consumer<Cube> runnable) {
QueuedCube key = new QueuedCube(x, y, z, world);
AsyncCubeIOProvider task = cubeTasks.get(key);
if (task == null) {
CubicChunks.LOGGER.warn("Attempting to drop cube that wasn't queued in {} @ ({}, {}, {})", world, x, y, z);
return;
}
task.removeCallback(runnable);
// TODO this is not threadsafe
if (!task.hasCallbacks()) {
cubeTasks.remove(key);
pool.remove(task);
}
}
/**
* Notify the loader that this column isn't needed anymore
*
* @param world The world
* @param x column x position
* @param z column z postion
* @param runnable The runnable that should be dropped
*/
public static void dropQueuedColumnLoad(ICubicWorld world, int x, int z, Consumer<Column> runnable) {
QueuedColumn key = new QueuedColumn(x, z, world);
AsyncColumnIOProvider task = columnTasks.get(key);
if (task == null) {
CubicChunks.LOGGER.warn("Attempting to drop column that wasn't queued in {} @ ({}, {})", world, x, z);
return;
}
task.removeCallback(runnable);
if (!task.hasCallbacks()) {
columnTasks.remove(key);
pool.remove(task);
}
//TODO: remove all queued cube tasks for that column
}
/**
* Run a synchronous tick, finishing the loading process for load tasks that are ready
*/
public static void tick() {
Iterator<AsyncCubeIOProvider> cubeItr = cubeTasks.values().iterator();
while (cubeItr.hasNext()) {
AsyncCubeIOProvider task = cubeItr.next();
if (task.isFinished()) {
task.runSynchronousPart();
cubeItr.remove();
}
}
Iterator<AsyncColumnIOProvider> columnIter = columnTasks.values().iterator();
while (columnIter.hasNext()) {
AsyncColumnIOProvider task = columnIter.next();
if (task.isFinished()) {
task.runSynchronousPart();
columnIter.remove();
}
}
}
/**
* Resize async loading pool thread count when players join or leave
*
* @param players New player count
*/
private static void adjustPoolSize(int players) {
pool.setCorePoolSize(Math.max(BASE_THREADS, players/PLAYERS_PER_THREAD));
}
public static void registerListeners() {
MinecraftForge.EVENT_BUS.register(new Object() {
// Resize thread pool based on player count
@SubscribeEvent
public void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent evt) {
MinecraftServer server = evt.player.getServer();
if (server != null) adjustPoolSize(server.getCurrentPlayerCount());
}
@SubscribeEvent
public void onPlayerLoggedOut(PlayerEvent.PlayerLoggedOutEvent evt) {
MinecraftServer server = evt.player.getServer();
if (server != null) adjustPoolSize(server.getCurrentPlayerCount());
}
// Sync completion of loading
@SubscribeEvent
public void onWorldTick(TickEvent.WorldTickEvent evt) {
tick();
}
});
}
}