From f8e01fa0f925a9e9eb41ebfef98bd03a8b0884f5 Mon Sep 17 00:00:00 2001 From: Sotr Date: Mon, 13 Aug 2018 00:37:29 +0800 Subject: [PATCH] Safety timings w/ Configurable ticking mode --- .../java/co/aikar/timings/TimingData.java | 127 +++++++++++ .../java/co/aikar/timings/TimingHandler.java | 215 ++++++++++++++++++ .../java/co/aikar/timings/TimingsManager.java | 200 ++++++++++++++++ .../java/co/aikar/util/LoadingIntMap.java | 72 ++++++ .../java/io/akarin/api/internal/Akari.java | 27 ++- .../server/core/AkarinGlobalConfig.java | 10 +- .../mixin/core/MixinMinecraftServer.java | 99 +++++--- .../server/mixin/core/MixinTimingHandler.java | 37 ++- .../java/org/bukkit/plugin/EventExecutor.java | 21 +- 9 files changed, 740 insertions(+), 68 deletions(-) create mode 100644 sources/src/main/java/co/aikar/timings/TimingData.java create mode 100644 sources/src/main/java/co/aikar/timings/TimingHandler.java create mode 100644 sources/src/main/java/co/aikar/timings/TimingsManager.java create mode 100644 sources/src/main/java/co/aikar/util/LoadingIntMap.java diff --git a/sources/src/main/java/co/aikar/timings/TimingData.java b/sources/src/main/java/co/aikar/timings/TimingData.java new file mode 100644 index 000000000..6178328f2 --- /dev/null +++ b/sources/src/main/java/co/aikar/timings/TimingData.java @@ -0,0 +1,127 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * 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 co.aikar.timings; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.LongAdder; + +import static co.aikar.util.JSONUtil.toArray; + +/** + * Akarin Changes Note + * 1) Thread safe timing (safety) + */ +/** + *

Lightweight object for tracking timing data

+ * + * This is broken out to reduce memory usage + */ +class TimingData { + private final int id; + private int count = 0; + private int lagCount = 0; + private long totalTime = 0; + private long lagTotalTime = 0; + private AtomicInteger curTickCount = new AtomicInteger(); // Akarin + private LongAdder curTickTotal = new LongAdder(); // Akarin + + TimingData(int id) { + this.id = id; + } + + private TimingData(TimingData data) { + this.id = data.id; + this.totalTime = data.totalTime; + this.lagTotalTime = data.lagTotalTime; + this.count = data.count; + this.lagCount = data.lagCount; + } + + void add(long diff) { + curTickCount.incrementAndGet(); + curTickTotal.add(diff); + } + + void processTick(boolean violated) { + totalTime += curTickTotal.sum(); + count += curTickCount.get(); + if (violated) { + lagTotalTime += curTickTotal.sum(); + lagCount += curTickCount.get(); + } + curTickTotal.reset(); + curTickCount.set(0); + } + + void reset() { + count = 0; + lagCount = 0; + curTickTotal.reset(); + curTickCount.set(0); + totalTime = 0; + lagTotalTime = 0; + } + + protected TimingData clone() { + return new TimingData(this); + } + + List export() { + List list = toArray( + id, + count, + totalTime); + if (lagCount > 0) { + list.add(lagCount); + list.add(lagTotalTime); + } + return list; + } + + boolean hasData() { + return count > 0; + } + + long getTotalTime() { + return totalTime; + } + + int getCurTickCount() { + return curTickCount.get(); + } + + void setCurTickCount(int curTickCount) { + this.curTickCount.getAndSet(curTickCount); + } + + long getCurTickTotal() { + return curTickTotal.sum(); + } + + void setCurTickTotal(long curTickTotal) { + this.curTickTotal.reset(); + this.curTickTotal.add(curTickTotal); + } +} diff --git a/sources/src/main/java/co/aikar/timings/TimingHandler.java b/sources/src/main/java/co/aikar/timings/TimingHandler.java new file mode 100644 index 000000000..afe0c2827 --- /dev/null +++ b/sources/src/main/java/co/aikar/timings/TimingHandler.java @@ -0,0 +1,215 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * 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 co.aikar.timings; + +import co.aikar.util.LoadingIntMap; +import org.bukkit.Bukkit; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; + +/** + * Akarin Changes Note + * 1) Thread safe timing (safety) + */ +class TimingHandler implements Timing { + + private static int idPool = 1; + final int id = idPool++; + + final String name; + private final boolean verbose; + + private final Map children = Collections.synchronizedMap(new LoadingIntMap<>(TimingData::new)); // Akarin - HashMap + + final TimingData record; + private final TimingHandler groupHandler; + + private AtomicLong start = new AtomicLong(); // Akarin + private AtomicInteger timingDepth = new AtomicInteger(); // Akarin + private volatile boolean added; // Akarin + private boolean timed; + private boolean enabled; + private volatile TimingHandler parent; // Akarin + + TimingHandler(TimingIdentifier id) { + if (id.name.startsWith("##")) { + verbose = true; + this.name = id.name.substring(3); + } else { + this.name = id.name; + verbose = false; + } + + this.record = new TimingData(this.id); + this.groupHandler = id.groupHandler; + + TimingIdentifier.getGroup(id.group).handlers.add(this); + checkEnabled(); + } + + final void checkEnabled() { + enabled = Timings.timingsEnabled && (!verbose || Timings.verboseEnabled); + } + + void processTick(boolean violated) { + if (timingDepth.get() != 0 || record.getCurTickCount() == 0) { + timingDepth.set(0); + start.set(0); + return; + } + + record.processTick(violated); + for (TimingData handler : children.values()) { + handler.processTick(violated); + } + } + + @Override + public Timing startTimingIfSync() { + if (Bukkit.isPrimaryThread()) { + startTiming(); + } + return this; + } + + @Override + public void stopTimingIfSync() { + if (Bukkit.isPrimaryThread()) { + stopTiming(); + } + } + + public Timing startTiming() { + if (enabled && timingDepth.incrementAndGet() == 1) { + start.getAndSet(System.nanoTime()); + parent = TimingsManager.CURRENT; + TimingsManager.CURRENT = this; + } + return this; + } + + public void stopTiming() { + if (enabled && timingDepth.decrementAndGet() == 0 && start.get() != 0) { + if (!Bukkit.isPrimaryThread()) { + Bukkit.getLogger().log(Level.SEVERE, "stopTiming called async for " + name); + new Throwable().printStackTrace(); + start.getAndSet(0); + return; + } + addDiff(System.nanoTime() - start.getAndSet(0)); + } + } + + @Override + public void abort() { + if (enabled && timingDepth.get() > 0) { + start.getAndSet(0); + } + } + + void addDiff(long diff) { + if (TimingsManager.CURRENT == this) { + TimingsManager.CURRENT = parent; + if (parent != null) { + parent.children.get(id).add(diff); + } + } + record.add(diff); + if (!added) { + added = true; + timed = true; + TimingsManager.HANDLERS.add(this); + } + if (groupHandler != null) { + groupHandler.addDiff(diff); + groupHandler.children.get(id).add(diff); + } + } + + /** + * Reset this timer, setting all values to zero. + * + * @param full + */ + void reset(boolean full) { + record.reset(); + if (full) { + timed = false; + } + start.set(0); + timingDepth.set(0); + added = false; + children.clear(); + checkEnabled(); + } + + @Override + public TimingHandler getTimingHandler() { + return this; + } + + @Override + public boolean equals(Object o) { + return (this == o); + } + + @Override + public int hashCode() { + return id; + } + + /** + * This is simply for the Closeable interface so it can be used with + * try-with-resources () + */ + @Override + public void close() { + stopTimingIfSync(); + } + + public boolean isSpecial() { + return this == TimingsManager.FULL_SERVER_TICK || this == TimingsManager.TIMINGS_TICK; + } + + boolean isTimed() { + return timed; + } + + public boolean isEnabled() { + return enabled; + } + + TimingData[] cloneChildren() { + int i = 0; + final TimingData[] clonedChildren = new TimingData[children.size()]; + for (TimingData child : children.values()) { + clonedChildren[i++] = child.clone(); + } + return clonedChildren; + } +} diff --git a/sources/src/main/java/co/aikar/timings/TimingsManager.java b/sources/src/main/java/co/aikar/timings/TimingsManager.java new file mode 100644 index 000000000..ea38d4249 --- /dev/null +++ b/sources/src/main/java/co/aikar/timings/TimingsManager.java @@ -0,0 +1,200 @@ +/* + * This file is licensed under the MIT License (MIT). + * + * Copyright (c) 2014 Daniel Ennis + * + * 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 co.aikar.timings; + +import com.google.common.base.Function; +import com.google.common.collect.EvictingQueue; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.command.Command; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.PluginClassLoader; +import co.aikar.util.LoadingMap; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; + +/** + * Akarin Changes Note + * 1) Thread safe timing (safety) + */ +public final class TimingsManager { + static final Map TIMING_MAP = + Collections.synchronizedMap(LoadingMap.newHashMap( + new Function() { + @Override + public TimingHandler apply(TimingIdentifier id) { + return (id.protect ? + new UnsafeTimingHandler(id) : + new TimingHandler(id) + ); + } + }, + 256, .5F + )); + public static final FullServerTickHandler FULL_SERVER_TICK = new FullServerTickHandler(); + public static final TimingHandler TIMINGS_TICK = Timings.ofSafe("Timings Tick", FULL_SERVER_TICK); + public static final Timing PLUGIN_GROUP_HANDLER = Timings.ofSafe("Plugins"); + public static List hiddenConfigs = new ArrayList(); + public static boolean privacy = false; + + static final Collection HANDLERS = Collections.synchronizedCollection(new ArrayDeque()); // Akarin + static final ArrayDeque MINUTE_REPORTS = new ArrayDeque(); + + static EvictingQueue HISTORY = EvictingQueue.create(12); + static TimingHandler CURRENT; + static long timingStart = 0; + static long historyStart = 0; + static boolean needsFullReset = false; + static boolean needsRecheckEnabled = false; + + private TimingsManager() {} + + /** + * Resets all timing data on the next tick + */ + static void reset() { + needsFullReset = true; + } + + /** + * Ticked every tick by CraftBukkit to count the number of times a timer + * caused TPS loss. + */ + static void tick() { + if (Timings.timingsEnabled) { + boolean violated = FULL_SERVER_TICK.isViolated(); + + for (TimingHandler handler : HANDLERS) { + if (handler.isSpecial()) { + // We manually call this + continue; + } + handler.processTick(violated); + } + + TimingHistory.playerTicks += Bukkit.getOnlinePlayers().size(); + TimingHistory.timedTicks++; + // Generate TPS/Ping/Tick reports every minute + } + } + static void stopServer() { + Timings.timingsEnabled = false; + recheckEnabled(); + } + static void recheckEnabled() { + synchronized (TIMING_MAP) { + for (TimingHandler timings : TIMING_MAP.values()) { + timings.checkEnabled(); + } + } + needsRecheckEnabled = false; + } + static void resetTimings() { + if (needsFullReset) { + // Full resets need to re-check every handlers enabled state + // Timing map can be modified from async so we must sync on it. + synchronized (TIMING_MAP) { + for (TimingHandler timings : TIMING_MAP.values()) { + timings.reset(true); + } + } + Bukkit.getLogger().log(Level.INFO, "Timings Reset"); + HISTORY.clear(); + needsFullReset = false; + needsRecheckEnabled = false; + timingStart = System.currentTimeMillis(); + } else { + // Soft resets only need to act on timings that have done something + // Handlers can only be modified on main thread. + for (TimingHandler timings : HANDLERS) { + timings.reset(false); + } + } + + HANDLERS.clear(); + MINUTE_REPORTS.clear(); + + TimingHistory.resetTicks(true); + historyStart = System.currentTimeMillis(); + } + + static TimingHandler getHandler(String group, String name, Timing parent, boolean protect) { + return TIMING_MAP.get(new TimingIdentifier(group, name, parent, protect)); + } + + + /** + *

Due to access restrictions, we need a helper method to get a Command TimingHandler with String group

+ * + * Plugins should never call this + * + * @param pluginName Plugin this command is associated with + * @param command Command to get timings for + * @return TimingHandler + */ + public static Timing getCommandTiming(String pluginName, Command command) { + Plugin plugin = null; + final Server server = Bukkit.getServer(); + if (!( server == null || pluginName == null || + "minecraft".equals(pluginName) || "bukkit".equals(pluginName) || + "spigot".equalsIgnoreCase(pluginName) || "paper".equals(pluginName) + )) { + plugin = server.getPluginManager().getPlugin(pluginName); + } + if (plugin == null) { + // Plugin is passing custom fallback prefix, try to look up by class loader + plugin = getPluginByClassloader(command.getClass()); + } + if (plugin == null) { + return Timings.ofSafe("Command: " + pluginName + ":" + command.getTimingName()); + } + + return Timings.ofSafe(plugin, "Command: " + pluginName + ":" + command.getTimingName()); + } + + /** + * Looks up the class loader for the specified class, and if it is a PluginClassLoader, return the + * Plugin that created this class. + * + * @param clazz Class to check + * @return Plugin if created by a plugin + */ + public static Plugin getPluginByClassloader(Class clazz) { + if (clazz == null) { + return null; + } + final ClassLoader classLoader = clazz.getClassLoader(); + if (classLoader instanceof PluginClassLoader) { + PluginClassLoader pluginClassLoader = (PluginClassLoader) classLoader; + return pluginClassLoader.getPlugin(); + } + return null; + } +} diff --git a/sources/src/main/java/co/aikar/util/LoadingIntMap.java b/sources/src/main/java/co/aikar/util/LoadingIntMap.java new file mode 100644 index 000000000..d2ea1b037 --- /dev/null +++ b/sources/src/main/java/co/aikar/util/LoadingIntMap.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2015. Starlis LLC / dba Empire Minecraft + * + * This source code is proprietary software and must not be redistributed without Starlis LLC's approval + * + */ +package co.aikar.util; + +import java.util.HashMap; +import com.google.common.base.Function; + +/** + * Allows you to pass a Loader function that when a key is accessed that doesn't exist, + * automatically loads the entry into the map by calling the loader Function. + * + * .get() Will only return null if the Loader can return null. + * + * You may pass any backing Map to use. + * + * This class is not thread safe and should be wrapped with Collections.synchronizedMap on the OUTSIDE of the LoadingMap if needed. + * + * Do not wrap the backing map with Collections.synchronizedMap. + * + * @param Value + */ +public class LoadingIntMap extends HashMap { // Akarin - HashMap + private static final long serialVersionUID = 1L; + + private final Function loader; + + public LoadingIntMap(Function loader) { + super(); + this.loader = loader; + } + + public LoadingIntMap(int expectedSize, Function loader) { + super(expectedSize); + this.loader = loader; + } + + public LoadingIntMap(int expectedSize, float loadFactor, Function loader) { + super(expectedSize, loadFactor); + this.loader = loader; + } + + + @Override + public V get(Object key) { + V res = super.get(key); + if (res == null) { + res = loader.apply((Integer) key); + if (res != null) { + put((Integer) key, res); + } + } + return res; + } + + /** + * Due to java stuff, you will need to cast it to (Function) for some cases + * + * @param Type + */ + public abstract static class Feeder implements Function { + @Override + public T apply(Object input) { + return apply(); + } + + public abstract T apply(); + } +} diff --git a/sources/src/main/java/io/akarin/api/internal/Akari.java b/sources/src/main/java/io/akarin/api/internal/Akari.java index 7b266696c..286be85ce 100644 --- a/sources/src/main/java/io/akarin/api/internal/Akari.java +++ b/sources/src/main/java/io/akarin/api/internal/Akari.java @@ -3,16 +3,9 @@ package io.akarin.api.internal; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Queue; -import java.util.concurrent.ExecutorCompletionService; -import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.ReentrantLock; - import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -21,15 +14,12 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import co.aikar.timings.Timing; import co.aikar.timings.Timings; -import io.akarin.api.internal.Akari.AssignableFactory; -import io.akarin.api.internal.Akari.AssignableThread; import io.akarin.api.internal.utils.ReentrantSpinningLock; import io.akarin.api.internal.utils.thread.SuspendableExecutorCompletionService; import io.akarin.api.internal.utils.thread.SuspendableThreadPoolExecutor; import io.akarin.server.core.AkarinGlobalConfig; import net.minecraft.server.MinecraftServer; import net.minecraft.server.World; -import net.minecraft.server.WorldServer; @SuppressWarnings("restriction") public abstract class Akari { @@ -91,7 +81,22 @@ public abstract class Akari { } public static void resizeTickExecutors(int worlds) { - STAGE_TICK = new SuspendableExecutorCompletionService<>(new SuspendableThreadPoolExecutor(worlds, worlds, + int parallelism; + switch (AkarinGlobalConfig.parallelMode) { + case -1: + return; + case 0: + parallelism = 1; + break; + case 1: + parallelism = worlds; + break; + case 2: + default: + parallelism = worlds * 2; + break; + } + STAGE_TICK = new SuspendableExecutorCompletionService<>(new SuspendableThreadPoolExecutor(parallelism, parallelism, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), new AssignableFactory("Akarin Parallel Ticking Thread - $"))); diff --git a/sources/src/main/java/io/akarin/server/core/AkarinGlobalConfig.java b/sources/src/main/java/io/akarin/server/core/AkarinGlobalConfig.java index 3a8ab0391..42f474bc7 100644 --- a/sources/src/main/java/io/akarin/server/core/AkarinGlobalConfig.java +++ b/sources/src/main/java/io/akarin/server/core/AkarinGlobalConfig.java @@ -160,11 +160,6 @@ public class AkarinGlobalConfig { playersPerIOThread = getInt("core.players-per-chunk-io-thread", 50); } - public static boolean silentAsyncTimings; - private static void silentAsyncTimings() { - silentAsyncTimings = getBoolean("core.always-silent-async-timing", false); - } - public static long timeUpdateInterval; private static void timeUpdateInterval() { timeUpdateInterval = getSeconds(getString("core.tick-rate.world-time-update-interval", "1s")) * 10; @@ -234,4 +229,9 @@ public class AkarinGlobalConfig { private static void fileIOThreads() { fileIOThreads = getInt("core.chunk-save-threads", 2); } + + public static int parallelMode; + private static void parallelMode() { + parallelMode = getInt("core.parallel-mode", 1); + } } diff --git a/sources/src/main/java/io/akarin/server/mixin/core/MixinMinecraftServer.java b/sources/src/main/java/io/akarin/server/mixin/core/MixinMinecraftServer.java index 0753e745a..023976a4f 100644 --- a/sources/src/main/java/io/akarin/server/mixin/core/MixinMinecraftServer.java +++ b/sources/src/main/java/io/akarin/server/mixin/core/MixinMinecraftServer.java @@ -89,6 +89,8 @@ public abstract class MixinMinecraftServer { private boolean tickEntities(WorldServer world) { try { world.tickEntities(); + world.getTracker().updatePlayers(); + world.explosionDensityCache.clear(); // Paper - Optimize explosions } catch (Throwable throwable) { CrashReport crashreport; try { @@ -147,35 +149,78 @@ public abstract class MixinMinecraftServer { Akari.worldTiming.startTiming(); if (cachedWorldSize != worlds.size()) Akari.resizeTickExecutors((cachedWorldSize = worlds.size())); - // Never tick one world concurrently! - for (int i = 0; i < cachedWorldSize; i++) { - // Impl Note: - // Entities ticking: index 1 -> ... -> 0 (parallel) - // World ticking: index 0 -> ... (parallel) - int interlace = i + 1; - WorldServer entityWorld = worlds.get(interlace < cachedWorldSize ? interlace : 0); - Akari.STAGE_TICK.submit(() -> { - synchronized (((IMixinLockProvider) entityWorld).lock()) { - tickEntities(entityWorld); - entityWorld.getTracker().updatePlayers(); - entityWorld.explosionDensityCache.clear(); // Paper - Optimize explosions + switch (AkarinGlobalConfig.parallelMode) { + case 1: + case 2: + default: + // Never tick one world concurrently! + for (int i = 0; i < cachedWorldSize; i++) { + // Impl Note: + // Entities ticking: index 1 -> ... -> 0 (parallel) + // World ticking: index 0 -> ... (parallel) + int interlace = i + 1; + WorldServer entityWorld = worlds.get(interlace < cachedWorldSize ? interlace : 0); + Akari.STAGE_TICK.submit(() -> { + synchronized (((IMixinLockProvider) entityWorld).lock()) { + tickEntities(entityWorld); + } + }, new TimingSignal(entityWorld, true)); + + WorldServer world = worlds.get(i); + switch (AkarinGlobalConfig.parallelMode) { + case 1: + world.timings.doTick.startTiming(); + synchronized (((IMixinLockProvider) world).lock()) { + tickWorld(world); + } + world.timings.doTick.stopTiming(); + break; + default: + Akari.STAGE_TICK.submit(() -> { + synchronized (((IMixinLockProvider) world).lock()) { + tickWorld(world); + } + }, new TimingSignal(world, false)); + break; + } } - }, new TimingSignal(entityWorld, true)); - - WorldServer world = worlds.get(i); - world.timings.doTick.startTiming(); - synchronized (((IMixinLockProvider) world).lock()) { - tickWorld(world); - } - world.timings.doTick.stopTiming(); - } - - for (int i = cachedWorldSize; i --> 0 ;) { - long startTiming = System.nanoTime(); - TimingSignal signal = Akari.STAGE_TICK.take().get(); - IMixinTimingHandler timing = (IMixinTimingHandler) (signal.isEntities ? signal.tickedWorld.timings.tickEntities : signal.tickedWorld.timings.doTick); - timing.stopTiming(startTiming); + + for (int i = AkarinGlobalConfig.parallelMode == 1 ? cachedWorldSize : cachedWorldSize * 2; i --> 0 ;) { + long startTiming = System.nanoTime(); + TimingSignal signal = Akari.STAGE_TICK.take().get(); + IMixinTimingHandler timing = (IMixinTimingHandler) (signal.isEntities ? signal.tickedWorld.timings.tickEntities : signal.tickedWorld.timings.doTick); + timing.stopTiming(startTiming); + } + + break; + case 0: + Akari.STAGE_TICK.submit(() -> { + for (int i = 1; i <= cachedWorldSize; ++i) { + WorldServer world = worlds.get(i < cachedWorldSize ? i : 0); + synchronized (((IMixinLockProvider) world).lock()) { + tickEntities(world); + } + } + }, null); + + for (int i = 0; i < cachedWorldSize; ++i) { + WorldServer world = worlds.get(i); + synchronized (((IMixinLockProvider) world).lock()) { + tickWorld(world); + } + } + + Akari.STAGE_TICK.take(); + break; + case -1: + for (int i = 0; i < cachedWorldSize; ++i) { + WorldServer world = worlds.get(i); + tickWorld(world); + tickEntities(world); + } + break; } + Akari.worldTiming.stopTiming(); Akari.callbackTiming.startTiming(); while ((runnable = Akari.callbackQueue.poll()) != null) runnable.run(); diff --git a/sources/src/main/java/io/akarin/server/mixin/core/MixinTimingHandler.java b/sources/src/main/java/io/akarin/server/mixin/core/MixinTimingHandler.java index 220295249..794d668e8 100644 --- a/sources/src/main/java/io/akarin/server/mixin/core/MixinTimingHandler.java +++ b/sources/src/main/java/io/akarin/server/mixin/core/MixinTimingHandler.java @@ -1,34 +1,36 @@ package io.akarin.server.mixin.core; -import java.util.logging.Level; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; -import org.bukkit.Bukkit; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.Shadow; import co.aikar.timings.Timing; -import io.akarin.api.internal.Akari; -import io.akarin.api.internal.Akari.AssignableThread; import io.akarin.api.internal.mixin.IMixinTimingHandler; -import io.akarin.server.core.AkarinGlobalConfig; -import net.minecraft.server.MinecraftServer; @Mixin(targets = "co.aikar.timings.TimingHandler", remap = false) public abstract class MixinTimingHandler implements IMixinTimingHandler { @Shadow @Final String name; @Shadow private boolean enabled; - @Shadow private long start; - @Shadow private int timingDepth; + @Shadow private AtomicLong start; + @Shadow private AtomicInteger timingDepth; @Shadow abstract void addDiff(long diff); @Shadow public abstract Timing startTiming(); + @Overwrite + public Timing startTimingIfSync() { + startTiming(); + return (Timing) this; + } + @Overwrite public void stopTimingIfSync() { - if (Akari.isPrimaryThread(false)) { + //if (Akari.isPrimaryThread(false)) { stopTiming(true); // Avoid twice thread check - } + //} } @Overwrite @@ -37,27 +39,24 @@ public abstract class MixinTimingHandler implements IMixinTimingHandler { } public void stopTiming(long start) { - if (enabled && --timingDepth == 0 && start != 0) { - addDiff(System.nanoTime() - start); - } + if (enabled) addDiff(System.nanoTime() - start); } public void stopTiming(boolean alreadySync) { - if (!enabled || --timingDepth != 0 || start == 0) return; - if (!alreadySync) { + if (!enabled || timingDepth.decrementAndGet() != 0 || start.get() == 0) return; + /*if (!alreadySync) { Thread curThread = Thread.currentThread(); if (curThread != MinecraftServer.getServer().primaryThread) { - if (!AkarinGlobalConfig.silentAsyncTimings) { + if (false && !AkarinGlobalConfig.silentAsyncTimings) { Bukkit.getLogger().log(Level.SEVERE, "stopTiming called async for " + name); Thread.dumpStack(); } start = 0; return; } - } + }*/ // Safety ensured - addDiff(System.nanoTime() - start); - start = 0; + addDiff(System.nanoTime() - start.getAndSet(0)); } } diff --git a/sources/src/main/java/org/bukkit/plugin/EventExecutor.java b/sources/src/main/java/org/bukkit/plugin/EventExecutor.java index 2adef1bb7..be5c77fff 100644 --- a/sources/src/main/java/org/bukkit/plugin/EventExecutor.java +++ b/sources/src/main/java/org/bukkit/plugin/EventExecutor.java @@ -19,6 +19,7 @@ import com.google.common.base.Preconditions; import io.akarin.api.internal.Akari; import io.akarin.api.internal.utils.thread.SuspendableExecutorCompletionService; // Paper end +import io.akarin.server.core.AkarinGlobalConfig; /** * Akarin Changes Note @@ -72,13 +73,21 @@ public interface EventExecutor { public void execute(Listener listener, Event event) throws EventException { if (!eventClass.isInstance(event)) return; try { - Akari.eventSuspendTiming.startTimingIfSync(); // Akarin - Akari.STAGE_TICK.suspend(); // Akarin - Akari.eventSuspendTiming.stopTimingIfSync(); // Akarin + // Akarin start + if (AkarinGlobalConfig.parallelMode != -1) { + Akari.eventSuspendTiming.startTiming(); + Akari.STAGE_TICK.suspend(); + Akari.eventSuspendTiming.stopTiming(); + } + // Akarin end asmExecutor.execute(listener, event); - Akari.eventResumeTiming.startTimingIfSync(); // Akarin - Akari.STAGE_TICK.resume(); // Akarin - Akari.eventResumeTiming.stopTimingIfSync(); // Akarin + // Akarin start + if (AkarinGlobalConfig.parallelMode != -1) { + Akari.eventResumeTiming.startTiming(); + Akari.STAGE_TICK.resume(); + Akari.eventResumeTiming.stopTiming(); + } + // Akarin end } catch (Exception e) { throw new EventException(e); }