From 40d1d9e5d00f80d9d96447483726c5231f93f6ba Mon Sep 17 00:00:00 2001 From: Intybyte Date: Sat, 14 Mar 2026 20:12:23 +0100 Subject: [PATCH 01/11] Timing Wheel --- .../paper/util/concurrent/ScheduledTask.java | 5 + .../paper/util/concurrent/TimingWheel.java | 147 ++++++++++++++++++ .../scheduler/CraftAsyncScheduler.java | 9 +- .../craftbukkit/scheduler/CraftScheduler.java | 20 +-- .../craftbukkit/scheduler/CraftTask.java | 5 +- 5 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 paper-server/src/main/java/io/papermc/paper/util/concurrent/ScheduledTask.java create mode 100644 paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java diff --git a/paper-server/src/main/java/io/papermc/paper/util/concurrent/ScheduledTask.java b/paper-server/src/main/java/io/papermc/paper/util/concurrent/ScheduledTask.java new file mode 100644 index 000000000000..8077bb68ffe0 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/concurrent/ScheduledTask.java @@ -0,0 +1,5 @@ +package io.papermc.paper.util.concurrent; + +public interface ScheduledTask { + long getNextRun(); +} diff --git a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java new file mode 100644 index 000000000000..6f7210b08974 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java @@ -0,0 +1,147 @@ +package io.papermc.paper.util.concurrent; + +import org.jetbrains.annotations.NotNull; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Predicate; + +/** + * This class schedules tasks in ticks and executes them efficiently using a circular array (the wheel). + * Each slot in the wheel represents a specific tick modulo the wheel size. + * Tasks are placed into slots based on their target execution tick. + * On each tick, the wheel checks the current slot and runs any tasks whose execute tick has been reached. + * + * O(1) task scheduling and retrieval within a single wheel rotation. + * We are using power of 2 for faster operations than modulo. + * + */ +public class TimingWheel implements Iterable { + private final int wheelSize; + private final long mask; + private final ArrayDeque[] wheel; + + @SuppressWarnings("unchecked") + public TimingWheel(int exponent) { + this.wheelSize = 1 << exponent; + this.mask = wheelSize - 1L; + + this.wheel = (ArrayDeque[]) new ArrayDeque[wheelSize]; + for (int i = 0; i < wheelSize; i++) { + wheel[i] = new ArrayDeque<>(); + } + } + + public void add(T task) { + int slot = (int) (task.getNextRun() & mask); + wheel[slot].add(task); + } + + public void addAll(Collection tasks) { + for (T task : tasks) { + this.add(task); + } + } + + public @NotNull List popValid(long currentTick) { + int slot = (int) (currentTick & mask); + ArrayDeque bucket = wheel[slot]; + if (bucket.isEmpty()) return Collections.emptyList(); + + Iterator iter = bucket.iterator(); + List list = new ArrayList<>(); + while (iter.hasNext()) { + T task = iter.next(); + + if (task.getNextRun() <= currentTick) { + iter.remove(); + list.add(task); + } + } + + return list; + } + + public boolean isReady(long currentTick) { + int slot = (int) (currentTick & mask); + ArrayDeque bucket = wheel[slot]; + if (bucket.isEmpty()) return false; + + for (final T task : bucket) { + if (task.getNextRun() <= currentTick) { + return true; + } + } + + return false; + } + + public void removeIf(Predicate apply) { + Iterator itr = iterator(); + while (itr.hasNext()) { + T next = itr.next(); + if (apply.test(next)) { + itr.remove(); + } + } + } + + @SuppressWarnings("unchecked") + private class Itr implements Iterator { + private int index = 0; + private Iterator current = Collections.emptyIterator(); + private Iterator lastIterator = null; + + @Override + public boolean hasNext() { + if (current.hasNext()) { + return true; + } + + for (int i = index; i < wheelSize; i++) { + if (!wheel[i].isEmpty()) { + return true; + } + } + + return false; + } + + @Override + public T next() { + while (true) { + if (current.hasNext()) { + lastIterator = current; + return current.next(); + } + + if (index >= wheelSize) { + throw new NoSuchElementException(); + } + + current = wheel[index++].iterator(); + } + } + + @Override + public void remove() { + if (lastIterator == null) { + throw new NoSuchElementException(); + } + + lastIterator.remove(); + lastIterator = null; + } + } + + + @Override + public @NotNull Iterator iterator() { + return new Itr(); + } +} diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java index 27562fd66ae9..363d5df904fe 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java @@ -75,8 +75,10 @@ public void mainThreadHeartbeat() { private synchronized void runTasks(int currentTick) { parsePending(); - while (!this.pending.isEmpty() && this.pending.peek().getNextRun() <= currentTick) { - CraftTask task = this.pending.remove(); + // Paper start - Timing Wheel + List tasks = this.pending.popValid(currentTick); + for (CraftTask task : tasks) { + // Paper end - Timing Wheel if (executeTask(task)) { final long period = task.getPeriod(); if (period > 0) { @@ -84,8 +86,9 @@ private synchronized void runTasks(int currentTick) { temp.add(task); } } - parsePending(); } + parsePending(); + this.pending.addAll(temp); temp.clear(); } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java index 7ffb7a210bf8..ba154dda4eb7 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java @@ -75,16 +75,7 @@ public class CraftScheduler implements BukkitScheduler { /** * Main thread logic only */ - final PriorityQueue pending = new PriorityQueue(10, // Paper - new Comparator() { - @Override - public int compare(final CraftTask o1, final CraftTask o2) { - int value = Long.compare(o1.getNextRun(), o2.getNextRun()); - - // If the tasks should run on the same tick they should be run FIFO - return value != 0 ? value : Long.compare(o1.getCreatedAt(), o2.getCreatedAt()); - } - }); + final io.papermc.paper.util.concurrent.TimingWheel pending = new io.papermc.paper.util.concurrent.TimingWheel<>(12); // Paper - Timing wheel /** * Main thread logic only */ @@ -459,8 +450,10 @@ public void mainThreadHeartbeat() { // Paper end final List temp = this.temp; this.parsePending(); - while (this.isReady(this.currentTick)) { - final CraftTask task = this.pending.remove(); + // Paper start - Timing Wheel + final List tasks = this.pending.popValid(this.currentTick); + for (CraftTask task : tasks) { + // Paper end - Timing Wheel if (task.getPeriod() < CraftTask.NO_REPEATING) { if (task.isSync()) { this.runners.remove(task.getTaskId(), task); @@ -564,7 +557,8 @@ void parsePending() { // Paper } private boolean isReady(final int currentTick) { - return !this.pending.isEmpty() && this.pending.peek().getNextRun() <= currentTick; + // return !this.pending.isEmpty() && this.pending.peek().getNextRun() <= currentTick; + return this.pending.isReady(currentTick); // Paper - Timing wheel } @Override diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java index 17680f112d0c..6f9c3a24aac8 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java @@ -6,7 +6,8 @@ import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitTask; -public class CraftTask implements BukkitTask, Runnable { // Spigot +// Paper - Timing Wheel +public class CraftTask implements BukkitTask, Runnable, io.papermc.paper.util.concurrent.ScheduledTask { // Spigot private volatile CraftTask next = null; public static final int ERROR = 0; @@ -93,7 +94,7 @@ void setPeriod(long period) { this.period = period; } - long getNextRun() { + public long getNextRun() { // Paper - Timing Wheel return this.nextRun; } From d633ac1f200f21c75e4b3a844b32b199f3ea01df Mon Sep 17 00:00:00 2001 From: Intybyte Date: Sat, 14 Mar 2026 23:25:40 +0100 Subject: [PATCH 02/11] Remove comments --- .../org/bukkit/craftbukkit/scheduler/CraftScheduler.java | 8 +++----- .../java/org/bukkit/craftbukkit/scheduler/CraftTask.java | 6 +++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java index ba154dda4eb7..a7992c3e9b41 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java @@ -17,6 +17,7 @@ import java.util.function.Consumer; import java.util.function.IntUnaryOperator; import java.util.logging.Level; +import io.papermc.paper.util.concurrent.TimingWheel; import org.bukkit.plugin.IllegalPluginAccessException; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitRunnable; @@ -75,7 +76,7 @@ public class CraftScheduler implements BukkitScheduler { /** * Main thread logic only */ - final io.papermc.paper.util.concurrent.TimingWheel pending = new io.papermc.paper.util.concurrent.TimingWheel<>(12); // Paper - Timing wheel + final TimingWheel pending = new TimingWheel<>(12); /** * Main thread logic only */ @@ -450,10 +451,8 @@ public void mainThreadHeartbeat() { // Paper end final List temp = this.temp; this.parsePending(); - // Paper start - Timing Wheel final List tasks = this.pending.popValid(this.currentTick); for (CraftTask task : tasks) { - // Paper end - Timing Wheel if (task.getPeriod() < CraftTask.NO_REPEATING) { if (task.isSync()) { this.runners.remove(task.getTaskId(), task); @@ -557,8 +556,7 @@ void parsePending() { // Paper } private boolean isReady(final int currentTick) { - // return !this.pending.isEmpty() && this.pending.peek().getNextRun() <= currentTick; - return this.pending.isReady(currentTick); // Paper - Timing wheel + return this.pending.isReady(currentTick); } @Override diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java index 6f9c3a24aac8..a1db1f4b521e 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java @@ -2,12 +2,12 @@ import java.util.function.Consumer; +import io.papermc.paper.util.concurrent.ScheduledTask; import org.bukkit.Bukkit; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitTask; -// Paper - Timing Wheel -public class CraftTask implements BukkitTask, Runnable, io.papermc.paper.util.concurrent.ScheduledTask { // Spigot +public class CraftTask implements BukkitTask, Runnable, ScheduledTask { private volatile CraftTask next = null; public static final int ERROR = 0; @@ -94,7 +94,7 @@ void setPeriod(long period) { this.period = period; } - public long getNextRun() { // Paper - Timing Wheel + public long getNextRun() { return this.nextRun; } From 28f7c04635e83f77c30084d6600b1c13bfcda01e Mon Sep 17 00:00:00 2001 From: Intybyte Date: Sat, 14 Mar 2026 23:26:07 +0100 Subject: [PATCH 03/11] Rename interface --- .../concurrent/{ScheduledTask.java => TickBoundTask.java} | 2 +- .../java/io/papermc/paper/util/concurrent/TimingWheel.java | 3 +-- .../main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) rename paper-server/src/main/java/io/papermc/paper/util/concurrent/{ScheduledTask.java => TickBoundTask.java} (67%) diff --git a/paper-server/src/main/java/io/papermc/paper/util/concurrent/ScheduledTask.java b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TickBoundTask.java similarity index 67% rename from paper-server/src/main/java/io/papermc/paper/util/concurrent/ScheduledTask.java rename to paper-server/src/main/java/io/papermc/paper/util/concurrent/TickBoundTask.java index 8077bb68ffe0..83f793124e30 100644 --- a/paper-server/src/main/java/io/papermc/paper/util/concurrent/ScheduledTask.java +++ b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TickBoundTask.java @@ -1,5 +1,5 @@ package io.papermc.paper.util.concurrent; -public interface ScheduledTask { +public interface TickBoundTask { long getNextRun(); } diff --git a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java index 6f7210b08974..7b95cb1fa3d6 100644 --- a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java +++ b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java @@ -8,7 +8,6 @@ import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.function.Predicate; /** @@ -21,7 +20,7 @@ * We are using power of 2 for faster operations than modulo. * */ -public class TimingWheel implements Iterable { +public class TimingWheel implements Iterable { private final int wheelSize; private final long mask; private final ArrayDeque[] wheel; diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java index a1db1f4b521e..9a1450770207 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java @@ -2,12 +2,12 @@ import java.util.function.Consumer; -import io.papermc.paper.util.concurrent.ScheduledTask; +import io.papermc.paper.util.concurrent.TickBoundTask; import org.bukkit.Bukkit; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitTask; -public class CraftTask implements BukkitTask, Runnable, ScheduledTask { +public class CraftTask implements BukkitTask, Runnable, TickBoundTask { private volatile CraftTask next = null; public static final int ERROR = 0; From 6d09fb0a4ded020fbd73a820f038bf6d0bdec3ca Mon Sep 17 00:00:00 2001 From: Intybyte Date: Sat, 14 Mar 2026 23:26:42 +0100 Subject: [PATCH 04/11] Forgot one --- .../org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java index 363d5df904fe..297b5a9abfd8 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java @@ -75,10 +75,8 @@ public void mainThreadHeartbeat() { private synchronized void runTasks(int currentTick) { parsePending(); - // Paper start - Timing Wheel List tasks = this.pending.popValid(currentTick); for (CraftTask task : tasks) { - // Paper end - Timing Wheel if (executeTask(task)) { final long period = task.getPeriod(); if (period > 0) { From 3379a43f7ee442c4d66438979b90f5d38817905b Mon Sep 17 00:00:00 2001 From: Intybyte Date: Sun, 15 Mar 2026 10:42:33 +0100 Subject: [PATCH 05/11] Use LinkedList --- .../io/papermc/paper/util/concurrent/TimingWheel.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java index 7b95cb1fa3d6..b9ffda4a1547 100644 --- a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java +++ b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java @@ -6,6 +6,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; import java.util.function.Predicate; @@ -23,16 +24,16 @@ public class TimingWheel implements Iterable { private final int wheelSize; private final long mask; - private final ArrayDeque[] wheel; + private final LinkedList[] wheel; @SuppressWarnings("unchecked") public TimingWheel(int exponent) { this.wheelSize = 1 << exponent; this.mask = wheelSize - 1L; - this.wheel = (ArrayDeque[]) new ArrayDeque[wheelSize]; + this.wheel = (LinkedList[]) new LinkedList[wheelSize]; for (int i = 0; i < wheelSize; i++) { - wheel[i] = new ArrayDeque<>(); + wheel[i] = new LinkedList<>(); } } @@ -49,7 +50,7 @@ public void addAll(Collection tasks) { public @NotNull List popValid(long currentTick) { int slot = (int) (currentTick & mask); - ArrayDeque bucket = wheel[slot]; + LinkedList bucket = wheel[slot]; if (bucket.isEmpty()) return Collections.emptyList(); Iterator iter = bucket.iterator(); @@ -68,7 +69,7 @@ public void addAll(Collection tasks) { public boolean isReady(long currentTick) { int slot = (int) (currentTick & mask); - ArrayDeque bucket = wheel[slot]; + LinkedList bucket = wheel[slot]; if (bucket.isEmpty()) return false; for (final T task : bucket) { From fe3d809da39c6b59d5e908312f110c2a5c5d3889 Mon Sep 17 00:00:00 2001 From: Intybyte Date: Sun, 15 Mar 2026 10:53:48 +0100 Subject: [PATCH 06/11] Highlight FIFO --- .../java/io/papermc/paper/util/concurrent/TimingWheel.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java index b9ffda4a1547..6a171df0c47e 100644 --- a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java +++ b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java @@ -1,7 +1,6 @@ package io.papermc.paper.util.concurrent; import org.jetbrains.annotations.NotNull; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -39,7 +38,7 @@ public TimingWheel(int exponent) { public void add(T task) { int slot = (int) (task.getNextRun() & mask); - wheel[slot].add(task); + wheel[slot].addLast(task); } public void addAll(Collection tasks) { From 2390f4ba366560a253080e1eec762c08f7ccaa05 Mon Sep 17 00:00:00 2001 From: Intybyte Date: Sun, 15 Mar 2026 12:13:51 +0100 Subject: [PATCH 07/11] Fix supplying late task --- .../paper/util/concurrent/TickBoundTask.java | 1 + .../paper/util/concurrent/TimingWheel.java | 19 ++-- .../scheduler/CraftAsyncScheduler.java | 29 +++--- .../craftbukkit/scheduler/CraftScheduler.java | 95 ++++++++++--------- .../craftbukkit/scheduler/CraftTask.java | 2 +- 5 files changed, 82 insertions(+), 64 deletions(-) diff --git a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TickBoundTask.java b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TickBoundTask.java index 83f793124e30..15d0c4f15c62 100644 --- a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TickBoundTask.java +++ b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TickBoundTask.java @@ -2,4 +2,5 @@ public interface TickBoundTask { long getNextRun(); + void setNextRun(long next); } diff --git a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java index 6a171df0c47e..076d2c2a6688 100644 --- a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java +++ b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java @@ -36,18 +36,25 @@ public TimingWheel(int exponent) { } } - public void add(T task) { - int slot = (int) (task.getNextRun() & mask); + public void add(T task, int currentTick) { + long nextRun = task.getNextRun(); + + if (nextRun <= currentTick) { + nextRun = currentTick; + task.setNextRun(nextRun); + } + + int slot = (int) (nextRun & mask); wheel[slot].addLast(task); } - public void addAll(Collection tasks) { + public void addAll(Collection tasks, int currentTick) { for (T task : tasks) { - this.add(task); + this.add(task, currentTick); } } - public @NotNull List popValid(long currentTick) { + public @NotNull List popValid(int currentTick) { int slot = (int) (currentTick & mask); LinkedList bucket = wheel[slot]; if (bucket.isEmpty()) return Collections.emptyList(); @@ -66,7 +73,7 @@ public void addAll(Collection tasks) { return list; } - public boolean isReady(long currentTick) { + public boolean isReady(int currentTick) { int slot = (int) (currentTick & mask); LinkedList bucket = wheel[slot]; if (bucket.isEmpty()) return false; diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java index 297b5a9abfd8..253573fb45af 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java @@ -57,7 +57,7 @@ public void cancelTask(int taskId) { } private synchronized void removeTask(int taskId) { - parsePending(); + parsePending(this.currentTick); this.pending.removeIf((task) -> { if (task.getTaskId() == taskId) { task.cancel0(); @@ -74,20 +74,25 @@ public void mainThreadHeartbeat() { } private synchronized void runTasks(int currentTick) { - parsePending(); - List tasks = this.pending.popValid(currentTick); - for (CraftTask task : tasks) { - if (executeTask(task)) { - final long period = task.getPeriod(); - if (period > 0) { - task.setNextRun(currentTick + period); - temp.add(task); + parsePending(this.currentTick); + while (true) { + List tasks = this.pending.popValid(currentTick); + if (tasks.isEmpty()) break; + + for (CraftTask task : tasks) { + if (executeTask(task)) { + final long period = task.getPeriod(); + if (period > 0) { + task.setNextRun(currentTick + period); + temp.add(task); + } } } + + parsePending(this.currentTick); } - parsePending(); - this.pending.addAll(temp); + this.pending.addAll(temp, this.currentTick); temp.clear(); } @@ -102,7 +107,7 @@ private boolean executeTask(CraftTask task) { @Override public synchronized void cancelTasks(Plugin plugin) { - parsePending(); + parsePending(this.currentTick); for (Iterator iterator = this.pending.iterator(); iterator.hasNext(); ) { CraftTask task = iterator.next(); if (task.getTaskId() != -1 && (plugin == null || task.getOwner().equals(plugin))) { diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java index a7992c3e9b41..2e09ec35e23b 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java @@ -42,7 +42,7 @@ *
  • Async tasks are responsible for removing themselves from runners
  • *
  • Sync tasks are only to be removed from runners on the main thread when coupled with a removal from pending and temp.
  • *
  • Most of the design in this scheduler relies on queuing special tasks to perform any data changes on the main thread. - * When executed from inside a synchronous method, the scheduler will be updated before next execution by virtue of the frequent {@link #parsePending()} calls.
  • + * When executed from inside a synchronous method, the scheduler will be updated before next execution by virtue of the frequent {@link #parsePending(int)} calls. * */ public class CraftScheduler implements BukkitScheduler { @@ -450,54 +450,59 @@ public void mainThreadHeartbeat() { } // Paper end final List temp = this.temp; - this.parsePending(); - final List tasks = this.pending.popValid(this.currentTick); - for (CraftTask task : tasks) { - if (task.getPeriod() < CraftTask.NO_REPEATING) { - if (task.isSync()) { - this.runners.remove(task.getTaskId(), task); + while (true) { + this.parsePending(this.currentTick); + + final List tasks = this.pending.popValid(this.currentTick); + if (tasks.isEmpty()) break; + + for (CraftTask task : tasks) { + if (task.getPeriod() < CraftTask.NO_REPEATING) { + if (task.isSync()) { + this.runners.remove(task.getTaskId(), task); + } + this.parsePending(this.currentTick); + continue; } - this.parsePending(); - continue; - } - if (task.isSync()) { - this.currentTask = task; - try { - task.run(); - } catch (final Throwable throwable) { - // Paper start - final String logMessage = String.format( - "Task #%s for %s generated an exception", - task.getTaskId(), - task.getOwner().getDescription().getFullName()); - task.getOwner().getLogger().log( + if (task.isSync()) { + this.currentTask = task; + try { + task.run(); + } catch (final Throwable throwable) { + // Paper start + final String logMessage = String.format( + "Task #%s for %s generated an exception", + task.getTaskId(), + task.getOwner().getDescription().getFullName()); + task.getOwner().getLogger().log( Level.WARNING, - logMessage, + logMessage, throwable); - org.bukkit.Bukkit.getServer().getPluginManager().callEvent( - new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerSchedulerException(logMessage, throwable, task))); - // Paper end - } finally { - this.currentTask = null; + org.bukkit.Bukkit.getServer().getPluginManager().callEvent( + new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerSchedulerException(logMessage, throwable, task))); + // Paper end + } finally { + this.currentTask = null; + } + this.parsePending(this.currentTick); + } else { + // this.debugTail = this.debugTail.setNext(new CraftAsyncDebugger(this.currentTick + CraftScheduler.RECENT_TICKS, task.getOwner(), task.getTaskClass())); // 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) + } + final long period = task.getPeriod(); // State consistency + if (period > 0) { + task.setNextRun(this.currentTick + period); + temp.add(task); + } else if (task.isSync()) { + this.runners.remove(task.getTaskId()); } - this.parsePending(); - } else { - // this.debugTail = this.debugTail.setNext(new CraftAsyncDebugger(this.currentTick + CraftScheduler.RECENT_TICKS, task.getOwner(), task.getTaskClass())); // 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) - } - final long period = task.getPeriod(); // State consistency - if (period > 0) { - task.setNextRun(this.currentTick + period); - temp.add(task); - } else if (task.isSync()) { - this.runners.remove(task.getTaskId()); } + this.pending.addAll(temp, this.currentTick); + temp.clear(); + //this.debugHead = this.debugHead.getNextHead(this.currentTick); // Paper } - this.pending.addAll(temp); - temp.clear(); - //this.debugHead = this.debugHead.getNextHead(this.currentTick); // Paper } protected void addTask(final CraftTask task) { @@ -534,7 +539,7 @@ private int nextId() { return id; } - void parsePending() { // Paper + void parsePending(int currentTick) { // Paper CraftTask head = this.head; CraftTask task = head.getNext(); CraftTask lastTask = head; @@ -542,7 +547,7 @@ void parsePending() { // Paper if (task.getTaskId() == -1) { task.run(); } else if (task.getPeriod() >= CraftTask.NO_REPEATING) { - this.pending.add(task); + this.pending.add(task, currentTick); this.runners.put(task.getTaskId(), task); } } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java index 9a1450770207..c7fca5b80f52 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java @@ -98,7 +98,7 @@ public long getNextRun() { return this.nextRun; } - void setNextRun(long nextRun) { + public void setNextRun(long nextRun) { this.nextRun = nextRun; } From 95365e36321dd704c55495dad4c7a13d746fbb5b Mon Sep 17 00:00:00 2001 From: Intybyte Date: Sun, 15 Mar 2026 12:35:37 +0100 Subject: [PATCH 08/11] Add getCreatedAt to interface --- .../java/io/papermc/paper/util/concurrent/TickBoundTask.java | 1 + .../main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TickBoundTask.java b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TickBoundTask.java index 15d0c4f15c62..f33bde0000e3 100644 --- a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TickBoundTask.java +++ b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TickBoundTask.java @@ -3,4 +3,5 @@ public interface TickBoundTask { long getNextRun(); void setNextRun(long next); + long getCreatedAt(); } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java index c7fca5b80f52..fcb142b5a985 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java @@ -82,7 +82,7 @@ public void run() { } } - long getCreatedAt() { + public long getCreatedAt() { return this.createdAt; } From 6cf693abe2d5905b4a79b5c88c99111ee597abff Mon Sep 17 00:00:00 2001 From: Intybyte Date: Sun, 15 Mar 2026 12:47:16 +0100 Subject: [PATCH 09/11] Sorted linked list --- .../paper/util/concurrent/TimingWheel.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java index 076d2c2a6688..c56d14913228 100644 --- a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java +++ b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java @@ -7,6 +7,7 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.ListIterator; import java.util.NoSuchElementException; import java.util.function.Predicate; @@ -45,7 +46,24 @@ public void add(T task, int currentTick) { } int slot = (int) (nextRun & mask); - wheel[slot].addLast(task); + LinkedList bucket = wheel[slot]; + + if (bucket.isEmpty() || bucket.getLast().getCreatedAt() <= task.getCreatedAt()) { + bucket.addLast(task); // append if newest + return; + } + + ListIterator it = bucket.listIterator(bucket.size()); + while (it.hasPrevious()) { + T t = it.previous(); + if (t.getCreatedAt() <= task.getCreatedAt()) { + it.next(); + it.add(task); + return; + } + } + + bucket.addFirst(task); // oldest goes to front } public void addAll(Collection tasks, int currentTick) { From 54ce706be1be6e59a7e50ca577305c14438ee039 Mon Sep 17 00:00:00 2001 From: Intybyte Date: Sun, 15 Mar 2026 13:49:06 +0100 Subject: [PATCH 10/11] currentTick is a field come on brain --- .../craftbukkit/scheduler/CraftAsyncScheduler.java | 8 ++++---- .../bukkit/craftbukkit/scheduler/CraftScheduler.java | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java index 253573fb45af..0a731ef2445d 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncScheduler.java @@ -57,7 +57,7 @@ public void cancelTask(int taskId) { } private synchronized void removeTask(int taskId) { - parsePending(this.currentTick); + parsePending(); this.pending.removeIf((task) -> { if (task.getTaskId() == taskId) { task.cancel0(); @@ -74,7 +74,7 @@ public void mainThreadHeartbeat() { } private synchronized void runTasks(int currentTick) { - parsePending(this.currentTick); + parsePending(); while (true) { List tasks = this.pending.popValid(currentTick); if (tasks.isEmpty()) break; @@ -89,7 +89,7 @@ private synchronized void runTasks(int currentTick) { } } - parsePending(this.currentTick); + parsePending(); } this.pending.addAll(temp, this.currentTick); @@ -107,7 +107,7 @@ private boolean executeTask(CraftTask task) { @Override public synchronized void cancelTasks(Plugin plugin) { - parsePending(this.currentTick); + parsePending(); for (Iterator iterator = this.pending.iterator(); iterator.hasNext(); ) { CraftTask task = iterator.next(); if (task.getTaskId() != -1 && (plugin == null || task.getOwner().equals(plugin))) { diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java index 2e09ec35e23b..ef322e1592f0 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java @@ -42,7 +42,7 @@ *
  • Async tasks are responsible for removing themselves from runners
  • *
  • Sync tasks are only to be removed from runners on the main thread when coupled with a removal from pending and temp.
  • *
  • Most of the design in this scheduler relies on queuing special tasks to perform any data changes on the main thread. - * When executed from inside a synchronous method, the scheduler will be updated before next execution by virtue of the frequent {@link #parsePending(int)} calls.
  • + * When executed from inside a synchronous method, the scheduler will be updated before next execution by virtue of the frequent {@link #parsePending()} calls. * */ public class CraftScheduler implements BukkitScheduler { @@ -451,7 +451,7 @@ public void mainThreadHeartbeat() { // Paper end final List temp = this.temp; while (true) { - this.parsePending(this.currentTick); + this.parsePending(); final List tasks = this.pending.popValid(this.currentTick); if (tasks.isEmpty()) break; @@ -461,7 +461,7 @@ public void mainThreadHeartbeat() { if (task.isSync()) { this.runners.remove(task.getTaskId(), task); } - this.parsePending(this.currentTick); + this.parsePending(); continue; } if (task.isSync()) { @@ -484,7 +484,7 @@ public void mainThreadHeartbeat() { } finally { this.currentTask = null; } - this.parsePending(this.currentTick); + this.parsePending(); } else { // this.debugTail = this.debugTail.setNext(new CraftAsyncDebugger(this.currentTick + CraftScheduler.RECENT_TICKS, task.getOwner(), task.getTaskClass())); // Paper task.getOwner().getLogger().log(Level.SEVERE, "Unexpected Async Task in the Sync Scheduler. Report this to Paper"); // Paper @@ -539,7 +539,7 @@ private int nextId() { return id; } - void parsePending(int currentTick) { // Paper + void parsePending() { // Paper CraftTask head = this.head; CraftTask task = head.getNext(); CraftTask lastTask = head; @@ -547,7 +547,7 @@ void parsePending(int currentTick) { // Paper if (task.getTaskId() == -1) { task.run(); } else if (task.getPeriod() >= CraftTask.NO_REPEATING) { - this.pending.add(task, currentTick); + this.pending.add(task, this.currentTick); this.runners.put(task.getTaskId(), task); } } From ac5875b377b33110164003a76bdcb53dd0ee477c Mon Sep 17 00:00:00 2001 From: Intybyte Date: Sun, 15 Mar 2026 16:20:22 +0100 Subject: [PATCH 11/11] LinkedList sorting --- .../paper/util/concurrent/TimingWheel.java | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java index c56d14913228..512fee5df447 100644 --- a/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java +++ b/paper-server/src/main/java/io/papermc/paper/util/concurrent/TimingWheel.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -26,6 +27,8 @@ public class TimingWheel implements Iterable { private final long mask; private final LinkedList[] wheel; + private static final Comparator ORDERING = Comparator.comparingLong(TickBoundTask::getCreatedAt); + @SuppressWarnings("unchecked") public TimingWheel(int exponent) { this.wheelSize = 1 << exponent; @@ -47,23 +50,7 @@ public void add(T task, int currentTick) { int slot = (int) (nextRun & mask); LinkedList bucket = wheel[slot]; - - if (bucket.isEmpty() || bucket.getLast().getCreatedAt() <= task.getCreatedAt()) { - bucket.addLast(task); // append if newest - return; - } - - ListIterator it = bucket.listIterator(bucket.size()); - while (it.hasPrevious()) { - T t = it.previous(); - if (t.getCreatedAt() <= task.getCreatedAt()) { - it.next(); - it.add(task); - return; - } - } - - bucket.addFirst(task); // oldest goes to front + bucket.add(task); } public void addAll(Collection tasks, int currentTick) { @@ -88,6 +75,7 @@ public void addAll(Collection tasks, int currentTick) { } } + list.sort(ORDERING); return list; }