diff --git a/Spigot-Server-Patches/Improved-Async-Task-Scheduler.patch b/Spigot-Server-Patches/Improved-Async-Task-Scheduler.patch new file mode 100644 index 0000000000..80f2294be2 --- /dev/null +++ b/Spigot-Server-Patches/Improved-Async-Task-Scheduler.patch @@ -0,0 +1,384 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Aikar <aikar@aikar.co> +Date: Fri, 16 Mar 2018 22:59:43 -0400 +Subject: [PATCH] Improved Async Task Scheduler + +The Craft Scheduler still uses the primary thread for task scheduling. +This results in the main thread still having to do work as part of the +dispatching of async tasks. + +If plugins make use of lots of async tasks, such as particle emitters +that want to keep the logic off the main thread, the main thread still +receives quite a bit of load from processing all of these queued tasks. + +Additionally, resizing and managing the pending entries for all of +these asynchronous tasks takes up time on the main thread too. + +This commit replaces the implementation of the scheduler when working +with asynchronous tasks, by forwarding calls to the new scheduler. + +The Async Scheduler uses a single thread executor for "management" tasks. +The Management Thread is responsible for all adding and dispatching of +scheduled tasks. + +The mainThreadHeartbeat will send a heartbeat task to the management thread +with the currentTick value, so that it can find which tasks to execute. + +Scheduling of an async tasks also dispatches a management task, ensuring +that any Queue resizing operation occurs off of the main thread. + +The async queue uses a complete separate PriorityQueue, ensuring that resize +operations are decoupled from the sync tasks queue. + +Additionally, an optimization was made that if a plugin schedules +a single, non repeating, no delay task, that we immediately dispatch it +to the executor pool instead of scheduling it. This avoids an unnecessary +round trip through the queue, as well as will reduce the size growth of the +queue if a plugin schedules lots of asynchronous tasks. + +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java +new file mode 100644 +index 000000000..b1efbc3e7 +--- /dev/null ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java +@@ -0,0 +0,0 @@ ++/* ++ * Copyright (c) 2018 Daniel Ennis (Aikar) MIT License ++ * ++ * Permission is hereby granted, free of charge, to any person obtaining ++ * a copy of this software and associated documentation files (the ++ * "Software"), to deal in the Software without restriction, including ++ * without limitation the rights to use, copy, modify, merge, publish, ++ * distribute, sublicense, and/or sell copies of the Software, and to ++ * permit persons to whom the Software is furnished to do so, subject to ++ * the following conditions: ++ * ++ * The above copyright notice and this permission notice shall be ++ * included in all copies or substantial portions of the Software. ++ * ++ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ++ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ++ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ++ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE ++ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION ++ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION ++ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ++ */ ++ ++package org.bukkit.craftbukkit.scheduler; ++ ++import com.destroystokyo.paper.ServerSchedulerReportingWrapper; ++import com.google.common.util.concurrent.ThreadFactoryBuilder; ++import org.bukkit.plugin.Plugin; ++import org.bukkit.scheduler.BukkitTask; ++ ++import java.util.ArrayList; ++import java.util.Iterator; ++import java.util.List; ++import java.util.concurrent.Executor; ++import java.util.concurrent.Executors; ++ ++public class CraftAsyncScheduler extends CraftScheduler { ++ ++ private final Executor management = Executors.newFixedThreadPool(1, new ThreadFactoryBuilder() ++ .setNameFormat("Craft Scheduler Management Thread").build()); ++ CraftAsyncScheduler() { ++ super(true); ++ } ++ ++ @Override ++ public void cancelTask(int taskId) { ++ this.management.execute(() -> this.removeTask(taskId)); ++ } ++ ++ private synchronized void removeTask(int taskId) { ++ this.pending.removeIf((task) -> { ++ if (task.getTaskId() == taskId) { ++ task.cancel0(); ++ return true; ++ } ++ return false; ++ }); ++ } ++ ++ @Override ++ public void mainThreadHeartbeat(int currentTick) { ++ this.currentTick = currentTick; ++ this.management.execute(() -> this.runTasks(currentTick)); ++ } ++ ++ private synchronized void runTasks(int currentTick) { ++ final List<CraftTask> temp = new ArrayList<>(); ++ while (!this.pending.isEmpty() && this.pending.peek().getNextRun() <= currentTick) { ++ CraftTask task = this.pending.remove(); ++ this.runners.put(task.getTaskId(), task); ++ this.executor.execute(new ServerSchedulerReportingWrapper(task)); ++ final long period = task.getPeriod(); ++ if (period > 0) { ++ task.setNextRun(currentTick + period); ++ temp.add(task); ++ } ++ } ++ this.pending.addAll(temp); ++ } ++ ++ @Override ++ protected CraftTask handle(CraftTask task, final long delay) { ++ if (task.getPeriod() == -1L && delay == 0L) { ++ this.executor.execute(task); ++ return task; ++ } ++ task.setNextRun(this.currentTick + delay); ++ this.management.execute(() -> this.addTask(task)); ++ return task; ++ } ++ ++ private synchronized void addTask(CraftTask task) { ++ this.pending.add(task); ++ } ++ ++ @Override ++ public synchronized void cancelTasks(Plugin plugin) { ++ for (Iterator<CraftTask> iterator = this.pending.iterator(); iterator.hasNext(); ) { ++ CraftTask taskPending = iterator.next(); ++ if (taskPending.getTaskId() != -1 && (plugin == null || taskPending.getOwner().equals(plugin))) { ++ taskPending.cancel0(); ++ iterator.remove(); ++ } ++ } ++ } ++ ++ @Override ++ public synchronized void cancelAllTasks() { ++ cancelTasks(null); ++ } ++ ++ @Override ++ public synchronized List<BukkitTask> getPendingTasks() { ++ ArrayList<BukkitTask> list = new ArrayList<>(); ++ for (CraftTask task : this.runners.values()) { ++ if (isValid(task)) { ++ list.add(task); ++ } ++ } ++ for (CraftTask task : this.pending) { ++ if (isValid(task) && !list.contains(task)) { ++ list.add(task); ++ } ++ } ++ ++ return list; ++ } ++ ++ @Override ++ public synchronized boolean isQueued(int taskId) { ++ CraftTask runningTask = this.runners.get(taskId); ++ if (runningTask != null && isValid(runningTask)) { ++ return true; ++ } ++ for (CraftTask task : this.pending) { ++ if (task.getTaskId() == taskId) { ++ return isValid(task); // The task will run ++ } ++ } ++ return false; ++ } ++ ++ /** ++ * Task is not cancelled ++ * @param runningTask ++ * @return ++ */ ++ static boolean isValid(CraftTask runningTask) { ++ return runningTask.getPeriod() >= -1L; ++ } ++} +diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +index e47f4cca2..4a4159879 100644 +--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java ++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +@@ -0,0 +0,0 @@ import java.util.concurrent.atomic.AtomicReference; + import java.util.logging.Level; + + import co.aikar.timings.MinecraftTimings; // Paper +-import com.destroystokyo.paper.ServerSchedulerReportingWrapper; + import com.destroystokyo.paper.event.server.ServerExceptionEvent; + import com.destroystokyo.paper.exception.ServerSchedulerException; + import org.apache.commons.lang.Validate; +@@ -0,0 +0,0 @@ public class CraftScheduler implements BukkitScheduler { + /** + * Main thread logic only + */ +- private final PriorityQueue<CraftTask> pending = new PriorityQueue<CraftTask>(10, ++ final PriorityQueue<CraftTask> pending = new PriorityQueue<CraftTask>(10, // Paper + new Comparator<CraftTask>() { + public int compare(final CraftTask o1, final CraftTask o2) { + int value = Long.compare(o1.getNextRun(), o2.getNextRun()); +@@ -0,0 +0,0 @@ public class CraftScheduler implements BukkitScheduler { + /** + * These are tasks that are currently active. It's provided for 'viewing' the current state. + */ +- private final ConcurrentHashMap<Integer, CraftTask> runners = new ConcurrentHashMap<Integer, CraftTask>(); +- private volatile int currentTick = -1; +- private final Executor executor = Executors.newCachedThreadPool(new com.google.common.util.concurrent.ThreadFactoryBuilder().setNameFormat("Craft Scheduler Thread - %1$d").build()); // Spigot ++ final ConcurrentHashMap<Integer, CraftTask> runners = new ConcurrentHashMap<Integer, CraftTask>(); // Paper ++ volatile int currentTick = -1; // Paper ++ final Executor executor = Executors.newCachedThreadPool(new com.google.common.util.concurrent.ThreadFactoryBuilder().setNameFormat("Craft Scheduler Thread - %1$d").build()); // Spigot // Paper + //private CraftAsyncDebugger debugHead = new CraftAsyncDebugger(-1, null, null) {@Override StringBuilder debugTo(StringBuilder string) {return string;}}; // Paper + //private CraftAsyncDebugger debugTail = debugHead; // Paper + private static final int RECENT_TICKS; +@@ -0,0 +0,0 @@ public class CraftScheduler implements BukkitScheduler { + RECENT_TICKS = 30; + } + ++ // Paper start ++ private final CraftScheduler asyncScheduler; ++ private final boolean isAsyncScheduler; ++ public CraftScheduler() { ++ this(false); ++ } ++ ++ public CraftScheduler(boolean isAsync) { ++ this.isAsyncScheduler = isAsync; ++ if (isAsync) { ++ this.asyncScheduler = this; ++ } else { ++ this.asyncScheduler = new CraftAsyncScheduler(); ++ } ++ } ++ // Paper end ++ + public int scheduleSyncDelayedTask(final Plugin plugin, final Runnable task) { + return this.scheduleSyncDelayedTask(plugin, task, 0l); + } +@@ -0,0 +0,0 @@ public class CraftScheduler implements BukkitScheduler { + } else if (period < -1l) { + period = -1l; + } +- return handle(new CraftAsyncTask(runners, plugin, runnable, nextId(), period), delay); ++ return handle(new CraftAsyncTask(this.asyncScheduler.runners, plugin, runnable, nextId(), period), delay); // Paper + } + + public <T> Future<T> callSyncMethod(final Plugin plugin, final Callable<T> task) { +@@ -0,0 +0,0 @@ public class CraftScheduler implements BukkitScheduler { + if (taskId <= 0) { + return; + } ++ // Paper start ++ if (!this.isAsyncScheduler) { ++ this.asyncScheduler.cancelTask(taskId); ++ } ++ // Paper end + CraftTask task = runners.get(taskId); + if (task != null) { + task.cancel0(); +@@ -0,0 +0,0 @@ public class CraftScheduler implements BukkitScheduler { + + public void cancelTasks(final Plugin plugin) { + Validate.notNull(plugin, "Cannot cancel tasks of null plugin"); ++ // Paper start ++ if (!this.isAsyncScheduler) { ++ this.asyncScheduler.cancelTasks(plugin); ++ } ++ // Paper end + final CraftTask task = new CraftTask( + new Runnable() { + public void run() { +@@ -0,0 +0,0 @@ public class CraftScheduler implements BukkitScheduler { + } + + public void cancelAllTasks() { ++ // Paper start ++ if (!this.isAsyncScheduler) { ++ this.asyncScheduler.cancelAllTasks(); ++ } ++ // Paper end + final CraftTask task = new CraftTask( + new Runnable() { + public void run() { +@@ -0,0 +0,0 @@ public class CraftScheduler implements BukkitScheduler { + } + + public boolean isCurrentlyRunning(final int taskId) { ++ // Paper start ++ if (!isAsyncScheduler) { //noinspection TailRecursion ++ return this.asyncScheduler.isCurrentlyRunning(taskId); ++ } ++ // Paper end + final CraftTask task = runners.get(taskId); + if (task == null || task.isSync()) { + return false; +@@ -0,0 +0,0 @@ public class CraftScheduler implements BukkitScheduler { + if (taskId <= 0) { + return false; + } ++ // Paper start ++ if (!this.isAsyncScheduler && this.asyncScheduler.isQueued(taskId)) { ++ return true; ++ } ++ // Paper end + for (CraftTask task = head.getNext(); task != null; task = task.getNext()) { + if (task.getTaskId() == taskId) { + return task.getPeriod() >= -1l; // The task will run +@@ -0,0 +0,0 @@ public class CraftScheduler implements BukkitScheduler { + } + + public List<BukkitWorker> getActiveWorkers() { ++ // Paper start ++ if (!isAsyncScheduler) { ++ //noinspection TailRecursion ++ return this.asyncScheduler.getActiveWorkers(); ++ } ++ // Paper end + final ArrayList<BukkitWorker> workers = new ArrayList<BukkitWorker>(); + for (final CraftTask taskObj : runners.values()) { + // Iterator will be a best-effort (may fail to grab very new values) if called from an async thread +@@ -0,0 +0,0 @@ public class CraftScheduler implements BukkitScheduler { + pending.add(task); + } + } ++ // Paper start ++ if (!this.isAsyncScheduler) { ++ pending.addAll(this.asyncScheduler.getPendingTasks()); ++ } ++ // Paper end + return pending; + } + +@@ -0,0 +0,0 @@ public class CraftScheduler implements BukkitScheduler { + * This method is designed to never block or wait for locks; an immediate execution of all current tasks. + */ + public void mainThreadHeartbeat(final int currentTick) { ++ // Paper start ++ if (!this.isAsyncScheduler) { ++ this.asyncScheduler.mainThreadHeartbeat(currentTick); ++ } ++ // Paper end + this.currentTick = currentTick; + final List<CraftTask> temp = this.temp; + parsePending(); +@@ -0,0 +0,0 @@ public class CraftScheduler implements BukkitScheduler { + parsePending(); + } else { + //debugTail = debugTail.setNext(new CraftAsyncDebugger(currentTick + RECENT_TICKS, task.getOwner(), task.getTaskClass())); // Paper +- executor.execute(new ServerSchedulerReportingWrapper(task)); // Paper ++ task.getOwner().getLogger().log(Level.SEVERE, "Unexpected Async Task in the Sync Scheduler. Report this to Paper"); // Paper + // We don't need to parse pending + // (async tasks must live with race-conditions if they attempt to cancel between these few lines of code) + } +@@ -0,0 +0,0 @@ public class CraftScheduler implements BukkitScheduler { + tailTask.setNext(task); + } + +- private CraftTask handle(final CraftTask task, final long delay) { ++ protected CraftTask handle(final CraftTask task, final long delay) { // Paper ++ // Paper start ++ if (!this.isAsyncScheduler && !task.isSync()) { ++ this.asyncScheduler.handle(task, delay); ++ return task; ++ } ++ // Paper end + task.setNextRun(currentTick + delay); + addTask(task); + return task; +-- \ No newline at end of file