9
0
mirror of https://github.com/SparklyPower/SparklyPaper.git synced 2025-12-19 15:09:27 +00:00

Improve EntityScheduler optimization patch

This commit is contained in:
MrPowerGamerBR
2023-11-21 21:09:22 -03:00
parent 6dd174a103
commit 4d91dd604d
2 changed files with 15 additions and 30 deletions

View File

@@ -39,8 +39,8 @@ SparklyPaper's config file is `sparklypaper.yml`, the file is, by default, place
* Optimize `EntityScheduler`'s `executeTick` * Optimize `EntityScheduler`'s `executeTick`
* On each tick, Paper runs `EntityScheduler`'s `executeTick` of each entity. This is a bit expensive, due to `ArrayDeque`'s `size()` call because it ain't a simple "get the current queue size" function, and due to the thread checks. * On each tick, Paper runs `EntityScheduler`'s `executeTick` of each entity. This is a bit expensive, due to `ArrayDeque`'s `size()` call because it ain't a simple "get the current queue size" function, and due to the thread checks.
* To avoid those hefty calls, we check if we *really* need to execute the `executeTick`, by doing two changes: * To avoid those hefty calls, we check if we *really* need to execute the `executeTick`, by doing two changes:
* First, we check if the scheduler had a task scheduled to it at least once, if not, we bail out early and quickly. In this case, we don't even increase the scheduler's `tickCount`, nor do we check if the scheduler has been retired. This is not an issue, since `tickCount` is relative and not bound to the server's current tick, so it doesn't matter if we don't increase it. * First, we check if the scheduler actually has scheduled tasks to it by looking up `hasScheduledTasks`, if not, we bail out early and quickly. In this case, we don't even increase the scheduler's `tickCount`, nor do we check if the scheduler has been retired. This is not an issue, since `tickCount` is relative and not bound to the server's current tick, so it doesn't matter if we don't increase it.
* Then, we check if the scheduler has any pending tasks by checking if `currentlyExecuting` is not empty or if `oneTimeDelayed` is not empty. This avoids the thread checks and unnecessary `size()` calls. * Then, we check if the scheduler has any pending tasks by checking if `currentlyExecuting` is not empty or if `oneTimeDelayed` is not empty. If it isn't, then we process the tasks, if it is, then we set `hasScheduledTasks` to false, avoiding future `size()` checks. This avoids the thread checks and unnecessary `size()` calls.
* Most entities won't have any scheduled tasks, so this is a nice performance bonus. These optimizations, however, wouldn't work in a Folia environment, but because in SparklyPaper `executeTick` is always executed on the main thread, it ain't an issue for us (yay). With this change, the `executeTick` loop in `tickChildren` CPU % usage drops from 2.48% to 0.61% in a server with ~12k entities! * Most entities won't have any scheduled tasks, so this is a nice performance bonus. These optimizations, however, wouldn't work in a Folia environment, but because in SparklyPaper `executeTick` is always executed on the main thread, it ain't an issue for us (yay). With this change, the `executeTick` loop in `tickChildren` CPU % usage drops from 2.48% to 0.61% in a server with ~12k entities!
* Of course, this doesn't mean that `ArrayDeque#size()` is slow! It is mostly that because the `executeTick` function is called each tick for each entity, it would be better for us to avoid as many useless calls as possible. * Of course, this doesn't mean that `ArrayDeque#size()` is slow! It is mostly that because the `executeTick` function is called each tick for each entity, it would be better for us to avoid as many useless calls as possible.
* Blazingly Simple Farm Checks * Blazingly Simple Farm Checks

View File

@@ -11,14 +11,14 @@ To avoid the hefty ArrayDeque's size() call, we check if we *really* need to exe
Most entities won't have any scheduled tasks, so this is a nice performance bonus. These optimizations, however, wouldn't work in a Folia environment, but because in SparklyPaper executeTick is always executed on the main thread, it ain't an issue for us (yay). Most entities won't have any scheduled tasks, so this is a nice performance bonus. These optimizations, however, wouldn't work in a Folia environment, but because in SparklyPaper executeTick is always executed on the main thread, it ain't an issue for us (yay).
diff --git a/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java b/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java diff --git a/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java b/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java
index 62484ebf4550b05182f693a3180bbac5d5fd906d..9e15a2a9e47a5b50c3c9e2afce2eb0f48ed0fdaf 100644 index 62484ebf4550b05182f693a3180bbac5d5fd906d..4af6652edc0651ab8ad28a738055d407451e49bf 100644
--- a/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java --- a/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java
+++ b/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java +++ b/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java
@@ -45,6 +45,7 @@ public final class EntityScheduler { @@ -45,6 +45,7 @@ public final class EntityScheduler {
private final Long2ObjectOpenHashMap<List<ScheduledTask>> oneTimeDelayed = new Long2ObjectOpenHashMap<>(); private final Long2ObjectOpenHashMap<List<ScheduledTask>> oneTimeDelayed = new Long2ObjectOpenHashMap<>();
private final ArrayDeque<ScheduledTask> currentlyExecuting = new ArrayDeque<>(); private final ArrayDeque<ScheduledTask> currentlyExecuting = new ArrayDeque<>();
+ private boolean hasScheduledAtLeastOneTask = false; // SparklyPaper - skip EntityScheduler's executeTick checks if there isn't any tasks to be run + private boolean hasScheduledTasks = false; // SparklyPaper - skip EntityScheduler's executeTick checks if there isn't any tasks to be run
public EntityScheduler(final CraftEntity entity) { public EntityScheduler(final CraftEntity entity) {
this.entity = Validate.notNull(entity); this.entity = Validate.notNull(entity);
@@ -26,11 +26,11 @@ index 62484ebf4550b05182f693a3180bbac5d5fd906d..9e15a2a9e47a5b50c3c9e2afce2eb0f4
if (this.tickCount == RETIRED_TICK_COUNT) { if (this.tickCount == RETIRED_TICK_COUNT) {
return false; return false;
} }
+ hasScheduledAtLeastOneTask = true; // SparklyPaper - skip EntityScheduler's executeTick checks if there isn't any tasks to be run + hasScheduledTasks = true; // SparklyPaper - skip EntityScheduler's executeTick checks if there isn't any tasks to be run
this.oneTimeDelayed.computeIfAbsent(this.tickCount + Math.max(1L, delay), (final long keyInMap) -> { this.oneTimeDelayed.computeIfAbsent(this.tickCount + Math.max(1L, delay), (final long keyInMap) -> {
return new ArrayList<>(); return new ArrayList<>();
}).add(task); }).add(task);
@@ -138,15 +140,34 @@ public final class EntityScheduler { @@ -138,6 +140,23 @@ public final class EntityScheduler {
* @throws IllegalStateException If the scheduler is retired. * @throws IllegalStateException If the scheduler is retired.
*/ */
public void executeTick() { public void executeTick() {
@@ -39,33 +39,18 @@ index 62484ebf4550b05182f693a3180bbac5d5fd906d..9e15a2a9e47a5b50c3c9e2afce2eb0f4
+ // main thread, we don't need to care about concurrency + // main thread, we don't need to care about concurrency
+ // First, we use a simple boolean check here + // First, we use a simple boolean check here
+ // Yeah, this sounds dumb but, after benchmarking, it seems like a simple boolean check like this is actually more performant than calling both is empty checks + // Yeah, this sounds dumb but, after benchmarking, it seems like a simple boolean check like this is actually more performant than calling both is empty checks
+ // So first we check if the EntityScheduler has been used at least ONCE + // So first we check if the EntityScheduler has any scheduled tasks
+ // If it wasn't, then we skip all checks entirely + // If it doesn't, then we skip all checks entirely
+ // We don't even need to update the tick count because the tick count is actually relative to this scheduler, not bound to the global current tick, so it isn't a big deal if we don't update the tick count + // We don't even need to update the tick count because the tick count is actually relative to this scheduler, not bound to the global current tick, so it isn't a big deal if we don't update the tick count, as long
+ if (!this.hasScheduledAtLeastOneTask) + // as there isn't any pending tasks
+ if (!this.hasScheduledTasks)
+ return;
+ // We do have scheduled tasks then... but do we *really* have scheduled tasks tho?
+ if (this.currentlyExecuting.isEmpty() && this.oneTimeDelayed.isEmpty()) { // Check if we have any pending tasks and, if not, skip!
+ this.hasScheduledTasks = false; // We don't! Bye bye!!
+ return; + return;
+ if (this.tickCount == RETIRED_TICK_COUNT) {
+ throw new IllegalStateException("Ticking retired scheduler");
+ } + }
+ ++this.tickCount;
+ if (this.currentlyExecuting.isEmpty() && this.oneTimeDelayed.isEmpty()) // Check if we have any pending tasks and, if not, skip!
+ return;
+ // SparklyPaper end + // SparklyPaper end
final Entity thisEntity = this.entity.getHandleRaw(); final Entity thisEntity = this.entity.getHandleRaw();
TickThread.ensureTickThread(thisEntity, "May not tick entity scheduler asynchronously"); TickThread.ensureTickThread(thisEntity, "May not tick entity scheduler asynchronously");
final List<ScheduledTask> toRun;
synchronized (this.stateLock) {
- if (this.tickCount == RETIRED_TICK_COUNT) {
- throw new IllegalStateException("Ticking retired scheduler");
- }
- ++this.tickCount;
+ // SparklyPaper start - skip EntityScheduler's executeTick checks if there isn't any tasks to be run
+ // if (this.tickCount == RETIRED_TICK_COUNT) {
+ // throw new IllegalStateException("Ticking retired scheduler");
+ // }
+ // ++this.tickCount;
+ // SparklyPaper end
if (this.oneTimeDelayed.isEmpty()) {
toRun = null;
} else {