Merge pull request #18 from R0bbyYT/feature/scheduler

Feature/scheduler - Optimization of the scheduler system
This commit is contained in:
TheMode 2020-07-29 06:55:51 +02:00 committed by GitHub
commit 2c58253d95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 419 additions and 198 deletions

View File

@ -10,10 +10,12 @@ import net.minestom.server.instance.block.BlockManager;
import net.minestom.server.instance.block.rule.vanilla.RedstonePlacementRule;
import net.minestom.server.storage.StorageManager;
import net.minestom.server.storage.systems.FileStorageSystem;
import net.minestom.server.timer.TaskRunnable;
import net.minestom.server.utils.time.TimeUnit;
import net.minestom.server.utils.time.UpdateOption;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Main {
@ -34,6 +36,7 @@ public class Main {
commandManager.register(new SimpleCommand());
commandManager.register(new GamemodeCommand());
commandManager.register(new DimensionCommand());
commandManager.register(new ShutdownCommand());
/*RecipeManager recipeManager = MinecraftServer.getRecipeManager();
ShapelessRecipe shapelessRecipe = new ShapelessRecipe("test", "groupname") {
@ -53,12 +56,13 @@ public class Main {
MinecraftServer.getBenchmarkManager().enable(new UpdateOption(10 * 1000, TimeUnit.MILLISECOND));
MinecraftServer.getSchedulerManager().addShutdownTask(new TaskRunnable() {
@Override
public void run() {
System.out.println("Good night");
}
});
MinecraftServer.getSchedulerManager().buildTask(() -> {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss.SSS");
System.out.println(simpleDateFormat.format(new Date()));
}).repeat(1, TimeUnit.SECOND).buildTask();
MinecraftServer.getSchedulerManager().buildShutdownTask(() -> System.out.println("Good night")).buildTask();
PlayerInit.init();

View File

@ -28,7 +28,6 @@ import net.minestom.server.item.ItemStack;
import net.minestom.server.item.Material;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.ping.ResponseDataConsumer;
import net.minestom.server.timer.TaskRunnable;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.Vector;
@ -86,32 +85,29 @@ public class PlayerInit {
ConnectionManager connectionManager = MinecraftServer.getConnectionManager();
BenchmarkManager benchmarkManager = MinecraftServer.getBenchmarkManager();
MinecraftServer.getSchedulerManager().addRepeatingTask(new TaskRunnable() {
@Override
public void run() {
long ramUsage = benchmarkManager.getUsedMemory();
ramUsage /= 1e6; // bytes to MB
MinecraftServer.getSchedulerManager().buildTask(() -> {
long ramUsage = benchmarkManager.getUsedMemory();
ramUsage /= 1e6; // bytes to MB
String benchmarkMessage = "";
for (Map.Entry<String, ThreadResult> resultEntry : benchmarkManager.getResultMap().entrySet()) {
String name = resultEntry.getKey();
ThreadResult result = resultEntry.getValue();
benchmarkMessage += ChatColor.GRAY + name;
benchmarkMessage += ": ";
benchmarkMessage += ChatColor.YELLOW.toString() + MathUtils.round(result.getCpuPercentage(), 2) + "% CPU ";
benchmarkMessage += ChatColor.RED.toString() + MathUtils.round(result.getUserPercentage(), 2) + "% USER ";
benchmarkMessage += ChatColor.PINK.toString() + MathUtils.round(result.getBlockedPercentage(), 2) + "% BLOCKED ";
benchmarkMessage += ChatColor.BRIGHT_GREEN.toString() + MathUtils.round(result.getWaitedPercentage(), 2) + "% WAITED ";
benchmarkMessage += "\n";
}
for (Player player : connectionManager.getOnlinePlayers()) {
ColoredText header = ColoredText.of("RAM USAGE: " + ramUsage + " MB");
ColoredText footer = ColoredText.of(benchmarkMessage);
player.sendHeaderFooter(header, footer);
}
String benchmarkMessage = "";
for (Map.Entry<String, ThreadResult> resultEntry : benchmarkManager.getResultMap().entrySet()) {
String name = resultEntry.getKey();
ThreadResult result = resultEntry.getValue();
benchmarkMessage += ChatColor.GRAY + name;
benchmarkMessage += ": ";
benchmarkMessage += ChatColor.YELLOW.toString() + MathUtils.round(result.getCpuPercentage(), 2) + "% CPU ";
benchmarkMessage += ChatColor.RED.toString() + MathUtils.round(result.getUserPercentage(), 2) + "% USER ";
benchmarkMessage += ChatColor.PINK.toString() + MathUtils.round(result.getBlockedPercentage(), 2) + "% BLOCKED ";
benchmarkMessage += ChatColor.BRIGHT_GREEN.toString() + MathUtils.round(result.getWaitedPercentage(), 2) + "% WAITED ";
benchmarkMessage += "\n";
}
}, new UpdateOption(10, TimeUnit.TICK));
for (Player player : connectionManager.getOnlinePlayers()) {
ColoredText header = ColoredText.of("RAM USAGE: " + ramUsage + " MB");
ColoredText footer = ColoredText.of(benchmarkMessage);
player.sendHeaderFooter(header, footer);
}
}).repeat(10, TimeUnit.TICK).buildTask();
connectionManager.addPacketConsumer((player, packetController, packet) -> {
// Listen to all received packet

View File

@ -0,0 +1,33 @@
package fr.themode.demo.commands;
import net.minestom.server.MinecraftServer;
import net.minestom.server.command.CommandProcessor;
import net.minestom.server.command.CommandSender;
import net.minestom.server.entity.Player;
/**
* A simple shutdown command
*/
public class ShutdownCommand implements CommandProcessor {
@Override
public String getCommandName() {
return "shutdown";
}
@Override
public String[] getAliases() {
return new String[0];
}
@Override
public boolean process(CommandSender sender, String command, String[] args) {
MinecraftServer.stopCleanly();
return true;
}
@Override
public boolean hasAccess(Player player) {
return player.getPermissionLevel() >= 4;
}
}

View File

@ -39,7 +39,6 @@ public final class UpdateManager {
final ConnectionManager connectionManager = MinecraftServer.getConnectionManager();
final EntityManager entityManager = MinecraftServer.getEntityManager();
final InstanceManager instanceManager = MinecraftServer.getInstanceManager();
final SchedulerManager schedulerManager = MinecraftServer.getSchedulerManager();
final long tickDistance = MinecraftServer.TICK_MS * 1000000;
long currentTime;
@ -58,9 +57,6 @@ public final class UpdateManager {
// Waiting players update
entityManager.updateWaitingPlayers();
// Scheduler
schedulerManager.update();
// Keep Alive Handling
final long time = System.currentTimeMillis();
final KeepAlivePacket keepAlivePacket = new KeepAlivePacket(time);

View File

@ -5,7 +5,6 @@ import net.minestom.server.entity.Player;
import net.minestom.server.event.player.PlayerLoginEvent;
import net.minestom.server.network.player.FakePlayerConnection;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.timer.TaskRunnable;
import net.minestom.server.utils.time.TimeUnit;
import net.minestom.server.utils.time.UpdateOption;
@ -43,12 +42,7 @@ public class FakePlayer extends Player {
final FakePlayer fakePlayer = new FakePlayer(uuid, username, option);
fakePlayer.addEventCallback(PlayerLoginEvent.class, event -> {
MinecraftServer.getSchedulerManager().addDelayedTask(new TaskRunnable() {
@Override
public void run() {
scheduledCallback.accept(fakePlayer);
}
}, new UpdateOption(1, TimeUnit.TICK));
MinecraftServer.getSchedulerManager().buildTask(() -> scheduledCallback.accept(fakePlayer)).delay(1, TimeUnit.TICK).buildTask();
});
}
@ -83,12 +77,7 @@ public class FakePlayer extends Player {
super.showPlayer(connection);
if (!option.isInTabList()) {
// Remove from tab-list
MinecraftServer.getSchedulerManager().addDelayedTask(new TaskRunnable() {
@Override
public void run() {
connection.sendPacket(getRemovePlayerToList());
}
}, new UpdateOption(20, TimeUnit.TICK));
MinecraftServer.getSchedulerManager().buildTask(() -> connection.sendPacket(getRemovePlayerToList())).delay(20, TimeUnit.TICK).buildTask();
}
}

View File

@ -21,7 +21,6 @@ import net.minestom.server.network.packet.server.play.UpdateLightPacket;
import net.minestom.server.particle.Particle;
import net.minestom.server.particle.ParticleCreator;
import net.minestom.server.storage.StorageFolder;
import net.minestom.server.timer.TaskRunnable;
import net.minestom.server.utils.BlockPosition;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.chunk.ChunkUtils;
@ -572,18 +571,16 @@ public class InstanceContainer extends Instance {
if (toUpdate == null) {
return;
}
MinecraftServer.getSchedulerManager().addDelayedTask(new TaskRunnable() {
@Override
public void run() {
final CustomBlock currentBlock = instance.getCustomBlock(position);
if (currentBlock == null)
return;
if (currentBlock.getCustomBlockId() != toUpdate.getCustomBlockId()) { // block changed
return;
}
currentBlock.scheduledUpdate(instance, position, getBlockData(position));
MinecraftServer.getSchedulerManager().buildTask(() -> {
final CustomBlock currentBlock = instance.getCustomBlock(position);
if (currentBlock == null)
return;
if (currentBlock.getCustomBlockId() != toUpdate.getCustomBlockId()) { // block changed
return;
}
}, new UpdateOption(time, unit));
currentBlock.scheduledUpdate(instance, position, getBlockData(position));
}).delay(time, unit).buildTask();
}
@Override

View File

@ -2,90 +2,91 @@ package net.minestom.server.timer;
import net.minestom.server.MinecraftServer;
import net.minestom.server.utils.thread.MinestomThread;
import net.minestom.server.utils.time.CooldownUtils;
import net.minestom.server.utils.time.UpdateOption;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* An object which manages all the {@link Task}'s
*/
public class SchedulerManager {
private static final AtomicInteger COUNTER = new AtomicInteger();
private static final AtomicInteger SHUTDOWN_COUNTER = new AtomicInteger();
private static ExecutorService batchesPool = new MinestomThread(MinecraftServer.THREAD_COUNT_SCHEDULER, MinecraftServer.THREAD_NAME_SCHEDULER);
private List<Task> tasks = new CopyOnWriteArrayList<>();
private List<Task> shutdownTasks = new CopyOnWriteArrayList<>();
// A counter for all normal tasks
private final AtomicInteger counter;
// A counter for all shutdown tasks
private final AtomicInteger shutdownCounter;
//A threaded execution
private final ExecutorService batchesPool;
// A single threaded scheduled execution
private final ScheduledExecutorService timerExecutionService;
// A list with all normal registered tasks
private final List<Task> tasks;
// A list with all registered shutdown tasks
private final List<Task> shutdownTasks;
/**
* Add a task with a custom update option and a precise call count
*
* @param runnable the task to execute
* @param updateOption the update option of the task
* @param maxCallCount the number of time this task should be executed
* @return the task id
* Default constructor
*/
public int addTask(TaskRunnable runnable, UpdateOption updateOption, int maxCallCount) {
final int id = COUNTER.incrementAndGet();
runnable.setId(id);
public SchedulerManager() {
this.counter = new AtomicInteger();
this.shutdownCounter = new AtomicInteger();
final Task task = new Task(runnable, updateOption, maxCallCount);
task.refreshLastUpdateTime(System.currentTimeMillis());
this.tasks.add(task);
return id;
this.batchesPool = new MinestomThread(MinecraftServer.THREAD_COUNT_SCHEDULER, MinecraftServer.THREAD_NAME_SCHEDULER);
this.timerExecutionService = Executors.newSingleThreadScheduledExecutor();
this.tasks = new CopyOnWriteArrayList<>();
this.shutdownTasks = new CopyOnWriteArrayList<>();
}
/**
* Add a task which will be repeated without interruption
* Initializes a new {@link TaskBuilder} for creating a task.
*
* @param runnable the task to execute
* @param updateOption the update option of the task
* @return the task id
* @param runnable The task to run when scheduled
* @return the task builder
*/
public int addRepeatingTask(TaskRunnable runnable, UpdateOption updateOption) {
return addTask(runnable, updateOption, 0);
public TaskBuilder buildTask(Runnable runnable) {
return new TaskBuilder(this, runnable);
}
/**
* Add a task which will be executed only once
* Initializes a new {@link TaskBuilder} for creating a shutdown task
*
* @param runnable the task to execute
* @param updateOption the update option of the task
* @return the task id
* @param runnable The shutdown task to run when scheduled
* @return the task builder
*/
public int addDelayedTask(TaskRunnable runnable, UpdateOption updateOption) {
return addTask(runnable, updateOption, 1);
public TaskBuilder buildShutdownTask(Runnable runnable) {
return new TaskBuilder(this, runnable, true);
}
/**
* Adds a task to run when the server shutdowns
* Removes/Forces the end of a task
*
* @param runnable the task to perform
* @return the task id
* @param task The task to remove
*/
public int addShutdownTask(TaskRunnable runnable) {
final int id = SHUTDOWN_COUNTER.incrementAndGet();
runnable.setId(id);
final Task task = new Task(runnable, null, 1);
this.shutdownTasks.add(task);
return id;
public void removeTask(Task task) {
this.tasks.removeIf(toRemove -> toRemove.equals(task));
}
/**
* Shutdown all the tasks and call tasks added from {@link #addShutdownTask(TaskRunnable)}
* Removes/Forces the end of a task
*
* @param task The task to remove
*/
public void removeShutdownTask(Task task) {
this.tasks.removeIf(toRemove -> toRemove.equals(task));
}
/**
* Shutdowns all normal tasks and call the registered shutdown tasks
*/
public void shutdown() {
batchesPool.execute(() -> {
for (Task task : shutdownTasks) {
task.getRunnable().run();
}
});
batchesPool.shutdown();
MinecraftServer.getLOGGER().info("Executing all shutdown tasks..");
for (Task task : this.getShutdownTasks()) {
task.schedule();
}
MinecraftServer.getLOGGER().info("Shutting down the scheduled execution service and batches pool.");
this.timerExecutionService.shutdown();
this.batchesPool.shutdown();
try {
batchesPool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
@ -93,37 +94,56 @@ public class SchedulerManager {
}
/**
* Force the end of a task
* Increments the current counter value.
*
* @param taskId the id of the task to remove
* @return the updated counter value
*/
public void removeTask(int taskId) {
this.tasks.removeIf(task -> task.getId() == taskId);
public int getCounterIdentifier() {
return this.counter.incrementAndGet();
}
public void update() {
final long time = System.currentTimeMillis();
batchesPool.execute(() -> {
for (Task task : tasks) {
final UpdateOption updateOption = task.getUpdateOption();
final long lastUpdate = task.getLastUpdateTime();
final boolean hasCooldown = CooldownUtils.hasCooldown(time, lastUpdate, updateOption.getTimeUnit(), updateOption.getValue());
if (!hasCooldown) {
final TaskRunnable runnable = task.getRunnable();
final int maxCallCount = task.getMaxCallCount();
final int callCount = runnable.getCallCount() + 1;
runnable.setCallCount(callCount);
runnable.run();
task.refreshLastUpdateTime(time);
if (callCount == maxCallCount) {
tasks.remove(task);
}
}
}
});
/**
* Increments the current shutdown counter value
*
* @return the updated shutdown counter value
*/
public int getShutdownCounterIdentifier() {
return this.shutdownCounter.incrementAndGet();
}
/**
* Gets a {@link List} with all registered tasks
*
* @return a {@link List} with all registered tasks
*/
public List<Task> getTasks() {
return tasks;
}
/**
* Gets a {@link List} with all registered shutdown tasks
*
* @return a {@link List} with all registered shutdown tasks
*/
public List<Task> getShutdownTasks() {
return shutdownTasks;
}
/**
* Gets the execution service for all registered tasks
*
* @return the execution service for all registered tasks
*/
public ExecutorService getBatchesPool() {
return batchesPool;
}
/**
* Gets the scheduled execution service for all registered tasks
*
* @return the scheduled execution service for all registered tasks
*/
public ScheduledExecutorService getTimerExecutionService() {
return timerExecutionService;
}
}

View File

@ -1,47 +1,131 @@
package net.minestom.server.timer;
import net.minestom.server.utils.time.UpdateOption;
import java.util.Objects;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class Task {
/**
* An Object that represents a task that is scheduled for execution on the application.
*/
public class Task implements Runnable {
private int id;
private TaskRunnable runnable;
private UpdateOption updateOption;
private int maxCallCount;
private long lastUpdateTime;
public Task(TaskRunnable runnable, UpdateOption updateOption, int maxCallCount) {
this.id = runnable.getId();
// Manages all tasks
private final SchedulerManager schedulerManager;
// The task logic
private final Runnable runnable;
// Task identifier
private final int id;
// True if the task planned for the application shutdown
private final boolean shutdown;
// Delay value for the task execution
private final long delay;
// Repeat value for the task execution
private final long repeat;
// Task completion/execution
private ScheduledFuture<?> future;
// The thread of the task
private volatile Thread currentThreadTask;
/**
* Creates a task
*
* @param schedulerManager The manager for the task
* @param runnable The task to run when scheduled
* @param shutdown Defines whether the task is a shutdown task
* @param delay The time to delay
* @param repeat The time until the repetition
*/
public Task(SchedulerManager schedulerManager, Runnable runnable, boolean shutdown, long delay, long repeat) {
this.schedulerManager = schedulerManager;
this.runnable = runnable;
this.updateOption = updateOption;
this.maxCallCount = maxCallCount;
this.shutdown = shutdown;
this.id = shutdown ? this.schedulerManager.getShutdownCounterIdentifier() : this.schedulerManager.getCounterIdentifier();
this.delay = delay;
this.repeat = repeat;
}
protected void refreshLastUpdateTime(long lastUpdateTime) {
this.lastUpdateTime = lastUpdateTime;
/**
* Executes the task
*/
@Override
public void run() {
this.schedulerManager.getBatchesPool().execute(() -> {
this.currentThreadTask = Thread.currentThread();
try {
this.runnable.run();
} catch (Exception e) {
System.err.println(
String.format(
"An exception in %s task %s is occurred! (%s)",
this.shutdown ? "shutdown" : "",
this.id,
e.getMessage()
)
);
e.printStackTrace();
} finally {
if (this.repeat == 0) this.finish();
this.currentThreadTask = null;
}
});
}
protected long getLastUpdateTime() {
return lastUpdateTime;
/**
* Sets up the task for correct execution
*/
public void schedule() {
this.future = this.repeat == 0L ?
this.schedulerManager.getTimerExecutionService().schedule(this, this.delay, TimeUnit.MILLISECONDS) :
this.schedulerManager.getTimerExecutionService().scheduleAtFixedRate(this, this.delay, this.repeat, TimeUnit.MILLISECONDS);
}
public int getId() {
return id;
/**
* Gets the current status of the task
*
* @return the current stats of the task
*/
public TaskStatus getStatus() {
if (this.future == null) return TaskStatus.SCHEDULED;
if (this.future.isCancelled()) return TaskStatus.CANCELLED;
if (this.future.isDone()) return TaskStatus.FINISHED;
return TaskStatus.SCHEDULED;
}
public TaskRunnable getRunnable() {
return runnable;
/**
* Cancels this task. If the task is already running, the thread in which it is running is interrupted.
* If the task is not currently running, Minestom will safely terminate it.
*/
public void cancel() {
if (this.future != null) {
this.future.cancel(false);
Thread current = this.currentThreadTask;
if (current != null) current.interrupt();
this.finish();
}
}
public UpdateOption getUpdateOption() {
return updateOption;
/**
* Removes the task
*/
private void finish() {
if (this.shutdown)
this.schedulerManager.removeShutdownTask(this);
else
this.schedulerManager.removeTask(this);
}
public int getMaxCallCount() {
return maxCallCount;
@Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
Task task = (Task) object;
return id == task.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}

View File

@ -0,0 +1,110 @@
package net.minestom.server.timer;
import net.minestom.server.utils.time.TimeUnit;
/**
* A builder which represents a fluent Object to schedule tasks
*/
public class TaskBuilder {
// Manager for the tasks
private final SchedulerManager schedulerManager;
// The logic behind every task
private final Runnable runnable;
// True if the task planned for the application shutdown
private final boolean shutdown;
// Delay value for the task execution
private long delay;
// Repeat value for the task execution
private long repeat;
/**
* Creates a task builder
* <br>
* <b>Note:</b> The task builder creates a normal task
*
* @param schedulerManager The manager for the tasks
* @param runnable The task to run when scheduled
*/
public TaskBuilder(SchedulerManager schedulerManager, Runnable runnable) {
this(schedulerManager, runnable, false);
}
/**
* Creates task builder
*
* @param schedulerManager The manager for the tasks
* @param runnable The task to run when scheduled
* @param shutdown Defines whether the task is a shutdown task
*/
public TaskBuilder(SchedulerManager schedulerManager, Runnable runnable, boolean shutdown) {
this.schedulerManager = schedulerManager;
this.runnable = runnable;
this.shutdown = shutdown;
}
/**
* Specifies that the task should delay its execution by the specified amount of time.
*
* @param time The time to delay
* @param unit The unit of time for {@code time}
* @return this builder, for chaining
*/
public TaskBuilder delay(long time, TimeUnit unit) {
this.delay = unit.toMilliseconds(time);
return this;
}
/**
* Specifies that the task should continue to run after waiting for the specified value until it is terminated.
*
* @param time The time until the repetition
* @param unit The unit of time for {@code time}
* @return this builder, for chaining
*/
public TaskBuilder repeat(long time, TimeUnit unit) {
this.repeat = unit.toMilliseconds(time);
return this;
}
/**
* Clears the delay interval of the task
*
* @return this builder, for chaining
*/
public TaskBuilder clearDelay() {
this.delay = 0L;
return this;
}
/**
* Clears the repeat interval of the task
*
* @return this builder, for chaining
*/
public TaskBuilder clearRepeat() {
this.repeat = 0L;
return this;
}
/**
* Builds this task for execution
*
* @return the built task
*/
public Task buildTask() {
Task task = new Task(
this.schedulerManager,
this.runnable,
this.shutdown,
this.delay,
this.repeat);
if (this.shutdown) {
this.schedulerManager.getShutdownTasks().add(task);
} else {
this.schedulerManager.getTasks().add(task);
task.schedule();
}
return task;
}
}

View File

@ -1,25 +0,0 @@
package net.minestom.server.timer;
public abstract class TaskRunnable {
private int id;
private int callCount;
public abstract void run();
public int getId() {
return id;
}
public int getCallCount() {
return callCount;
}
protected void setId(int id) {
this.id = id;
}
protected void setCallCount(int callCount) {
this.callCount = callCount;
}
}

View File

@ -0,0 +1,21 @@
package net.minestom.server.timer;
/**
* An enumeration that representing all available statuses for a task
*/
public enum TaskStatus {
/**
* The task is execution and is currently running
*/
SCHEDULED,
/**
* The task was cancelled with {@link Task#cancel()}
*/
CANCELLED,
/**
* The task has been completed. This only applies to tasks without repetition
*/
FINISHED,
}

View File

@ -1,9 +1,5 @@
package net.minestom.server.utils.thread;
import net.minestom.server.MinecraftServer;
import net.minestom.server.timer.SchedulerManager;
import net.minestom.server.timer.TaskRunnable;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;