1
mirror of https://github.com/StarWishsama/Slimefun4.git synced 2024-09-19 19:25:48 +00:00

Added a better profiler

This commit is contained in:
TheBusyBiscuit 2020-06-30 19:36:55 +02:00
parent 0aa99df8c2
commit d82b66a842
14 changed files with 463 additions and 225 deletions

View File

@ -40,6 +40,7 @@
* (1.16+) Added Warped and Crimson Fungus to the fuel list for the Bio Generator
* Added an AoE damage effect to the Explosive Bow
* Added runtime deprecation warnings for ItemHandlers and Attributes used by Addons
* Added a proper lag profiler
#### Changes
* Coolant Cells now last twice as long
@ -54,6 +55,7 @@
* Small performance improvements to the Cargo Net
* Slimefun no longer supports CraftBukkit
* Item Energy is now also stored persistently via NBT
* General performance improvements for ticking blocks
#### Fixes
* Fixed #2005
@ -72,6 +74,9 @@
* Fixed Grappling hooks making Bat sounds
* Fixed #1959
* Fixed Melon Juice requiring Melons instead of Melon Slices
* Fixed Cargo networks not showing up in /sf timings
* Fixed /sf timings reporting slightly inaccurate timings
* Fixed concurrency-related issues with the profiling
## Release Candidate 13 (16 Jun 2020)
https://thebusybiscuit.github.io/builds/TheBusyBiscuit/Slimefun4/stable/#13

View File

@ -26,7 +26,8 @@ class TimingsCommand extends SubCommand {
@Override
public void onExecute(CommandSender sender, String[] args) {
if (sender.hasPermission("slimefun.command.timings") || sender instanceof ConsoleCommandSender) {
SlimefunPlugin.getTickerTask().info(sender);
sender.sendMessage("Please wait a second... The results are coming in!");
SlimefunPlugin.getProfiler().requestSummary(sender);
}
else {
SlimefunPlugin.getLocalization().sendMessage(sender, "messages.no-permission", true);

View File

@ -166,7 +166,7 @@ public class CargoNet extends ChestTerminalNetwork {
destinations.addAll(output16);
}
Slimefun.runSync(() -> run(b, destinations, output));
run(b, destinations, output);
}
}

View File

@ -211,7 +211,7 @@ public class EnergyNet extends Network {
Set<Location> exploded = new HashSet<>();
for (Location source : generators) {
long timestamp = System.currentTimeMillis();
long timestamp = SlimefunPlugin.getProfiler().newEntry();
SlimefunItem item = BlockStorage.check(source);
if (item != null) {
@ -247,7 +247,7 @@ public class EnergyNet extends Network {
new ErrorReport(t, source, item);
}
SlimefunPlugin.getTickerTask().addBlockTimings(source, System.currentTimeMillis() - timestamp);
SlimefunPlugin.getProfiler().closeEntry(source, item, timestamp);
}
else {
// This block seems to be gone now, better remove it to be extra safe

View File

@ -56,7 +56,7 @@ class GitHubTask implements Runnable {
}
}
if (requests >= MAX_REQUESTS_PER_MINUTE) {
if (requests >= MAX_REQUESTS_PER_MINUTE && SlimefunPlugin.instance != null && SlimefunPlugin.instance.isEnabled()) {
// Slow down API requests and wait a minute after more than x requests were made
Bukkit.getScheduler().runTaskLaterAsynchronously(SlimefunPlugin.instance, this::grabTextures, 2 * 60 * 20L);
}

View File

@ -85,7 +85,7 @@ class PlaceholderAPIHook extends PlaceholderExpansion {
}
if (params.equals("timings_lag")) {
return SlimefunPlugin.getTickerTask().getTime() + "ms";
return SlimefunPlugin.getProfiler().getTime();
}
if (params.equals("language")) {

View File

@ -0,0 +1,49 @@
package io.github.thebusybiscuit.slimefun4.core.services.profiler;
import org.bukkit.block.Block;
import io.github.thebusybiscuit.cscorelib2.blocks.BlockPosition;
import io.github.thebusybiscuit.slimefun4.api.SlimefunAddon;
import me.mrCookieSlime.Slimefun.Objects.SlimefunItem.SlimefunItem;
class ProfiledBlock {
private final BlockPosition position;
private final SlimefunItem item;
ProfiledBlock(BlockPosition position, SlimefunItem item) {
this.position = position;
this.item = item;
}
ProfiledBlock(Block b) {
this(new BlockPosition(b), null);
}
public BlockPosition getPosition() {
return position;
}
public String getId() {
return item.getID();
}
public SlimefunAddon getAddon() {
return item.getAddon();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ProfiledBlock) {
return position.equals(((ProfiledBlock) obj).position);
}
return false;
}
@Override
public int hashCode() {
return position.hashCode();
}
}

View File

@ -0,0 +1,317 @@
package io.github.thebusybiscuit.slimefun4.core.services.profiler;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang.Validate;
import org.bukkit.ChatColor;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.block.Block;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import io.github.thebusybiscuit.cscorelib2.blocks.BlockPosition;
import io.github.thebusybiscuit.cscorelib2.chat.ChatColors;
import io.github.thebusybiscuit.slimefun4.api.SlimefunAddon;
import io.github.thebusybiscuit.slimefun4.implementation.SlimefunPlugin;
import io.github.thebusybiscuit.slimefun4.implementation.tasks.TickerTask;
import io.github.thebusybiscuit.slimefun4.utils.NumberUtils;
import me.mrCookieSlime.Slimefun.Objects.SlimefunItem.SlimefunItem;
import me.mrCookieSlime.Slimefun.api.Slimefun;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
/**
* The {@link SlimefunProfiler} works closely to the {@link TickerTask} and is responsible for
* monitoring that task.
* It collects timings data for any ticked {@link Block} and the corresponding {@link SlimefunItem}.
* This allows developers to identify laggy {@link SlimefunItem SlimefunItems} or {@link SlimefunAddon SlimefunAddons}.
* But it also enabled Server Admins to locate lag-inducing areas on the {@link Server}.
*
* @author TheBusyBiscuit
*
* @see TickerTask
*
*/
public class SlimefunProfiler {
// The threshold at which a Block or Chunk is significant enough to appear in /sf timings
private static final int VISIBILITY_THRESHOLD = 275_000;
private final ExecutorService executor = Executors.newFixedThreadPool(3);
private final AtomicBoolean running = new AtomicBoolean(false);
private final AtomicInteger queued = new AtomicInteger(0);
private long totalElapsedTime;
private final Map<ProfiledBlock, Long> timings = new ConcurrentHashMap<>();
private final Queue<CommandSender> requests = new ConcurrentLinkedQueue<>();
/**
* This method starts the profiling, data from previous runs will be cleared.
*/
public void start() {
running.set(true);
queued.set(0);
timings.clear();
}
/**
* This method starts a new profiler entry.
*
* @return A timestamp, best fed back into {@link #closeEntry(Location, SlimefunItem, long)}
*/
public long newEntry() {
if (!running.get()) {
return 0;
}
queued.incrementAndGet();
return System.nanoTime();
}
/**
* This method closes a previously started entry.
* Make sure to call {@link #newEntry()} to get the timestamp in advance.
*
* @param l
* The {@link Location} of our {@link Block}
* @param item
* The {@link SlimefunItem} at this {@link Location}
* @param timestamp
* The timestamp marking the start of this entry, you can retrieve it using {@link #newEntry()}
*/
public void closeEntry(Location l, SlimefunItem item, long timestamp) {
if (timestamp == 0) {
return;
}
long elapsedTime = System.nanoTime() - timestamp;
executor.execute(() -> {
ProfiledBlock block = new ProfiledBlock(new BlockPosition(l), item);
timings.put(block, elapsedTime);
queued.decrementAndGet();
});
}
/**
* This stops the profiling.
*/
public void stop() {
running.set(false);
if (SlimefunPlugin.instance == null || !SlimefunPlugin.instance.isEnabled()) {
// Slimefun has been disabled
return;
}
// Since we got more than one Thread in our pool, blocking this one is completely fine
executor.execute(() -> {
// Wait for all timing results to come in
while (queued.get() > 0 && !running.get()) {
try {
Thread.sleep(1);
}
catch (InterruptedException e) {
Slimefun.getLogger().log(Level.SEVERE, "A waiting Thread was interrupted", e);
Thread.currentThread().interrupt();
}
}
if (running.get()) {
// Looks like the next profiling has already started, abort!
return;
}
totalElapsedTime = timings.values().stream().mapToLong(Long::longValue).sum();
Iterator<CommandSender> iterator = requests.iterator();
while (iterator.hasNext()) {
sendSummary(iterator.next());
iterator.remove();
}
});
}
/**
* This method requests a summary for the given {@link CommandSender}.
* The summary will be sent upon the next available moment in time.
*
* @param sender
* The {@link CommandSender} who shall receive this summary.
*/
public void requestSummary(CommandSender sender) {
requests.add(sender);
}
private Map<String, Long> getByItem() {
Map<String, Long> map = new HashMap<>();
for (Map.Entry<ProfiledBlock, Long> entry : timings.entrySet()) {
map.merge(entry.getKey().getId(), entry.getValue(), Long::sum);
}
return map;
}
private Map<String, Long> getByChunk() {
Map<String, Long> map = new HashMap<>();
for (Map.Entry<ProfiledBlock, Long> entry : timings.entrySet()) {
String world = entry.getKey().getPosition().getWorld().getName();
int x = entry.getKey().getPosition().getChunkX();
int z = entry.getKey().getPosition().getChunkZ();
map.merge(world + " (" + x + ',' + z + ')', entry.getValue(), Long::sum);
}
return map;
}
private int getBlocksInChunk(String chunk) {
int blocks = 0;
for (ProfiledBlock block : timings.keySet()) {
String world = block.getPosition().getWorld().getName();
int x = block.getPosition().getChunkX();
int z = block.getPosition().getChunkZ();
if (chunk.equals(world + " (" + x + ',' + z + ')')) {
blocks++;
}
}
return blocks;
}
private int getBlocks(String id) {
int blocks = 0;
for (ProfiledBlock block : timings.keySet()) {
if (block.getId().equals(id)) {
blocks++;
}
}
return blocks;
}
private void sendSummary(CommandSender sender) {
Map<String, Long> chunks = getByChunk();
Map<String, Long> machines = getByItem();
sender.sendMessage(ChatColors.color("&2== &aSlimefun Lag Profiler &2=="));
sender.sendMessage(ChatColors.color("&6Running: &e&l" + String.valueOf(!SlimefunPlugin.getTickerTask().isHalted()).toUpperCase(Locale.ROOT)));
sender.sendMessage("");
sender.sendMessage(ChatColors.color("&6Impact: &e" + NumberUtils.getAsMillis(totalElapsedTime)));
sender.sendMessage(ChatColors.color("&6Ticked Chunks: &e" + chunks.size()));
sender.sendMessage(ChatColors.color("&6Ticked Blocks: &e" + timings.size()));
sender.sendMessage("");
sender.sendMessage(ChatColors.color("&6Ticking Machines:"));
summarizeTimings(sender, entry -> {
int count = getBlocks(entry.getKey());
String time = NumberUtils.getAsMillis(entry.getValue());
String average = NumberUtils.getAsMillis(entry.getValue() / count);
return entry.getKey() + " - " + count + "x (" + time + ", " + average + " avg/block)";
}, machines.entrySet().stream());
sender.sendMessage("");
sender.sendMessage(ChatColors.color("&6Ticking Chunks:"));
summarizeTimings(sender, entry -> {
int count = getBlocksInChunk(entry.getKey());
String time = NumberUtils.getAsMillis(entry.getValue());
return entry.getKey() + " - " + count + "x (" + time + ")";
}, chunks.entrySet().stream());
}
private void summarizeTimings(CommandSender sender, Function<Map.Entry<String, Long>, String> formatter, Stream<Map.Entry<String, Long>> stream) {
List<Entry<String, Long>> results = stream.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())).collect(Collectors.toList());
if (sender instanceof Player) {
TextComponent component = new TextComponent(" Hover for more details...");
component.setColor(net.md_5.bungee.api.ChatColor.GRAY);
component.setItalic(true);
StringBuilder builder = new StringBuilder();
int hidden = 0;
for (Map.Entry<String, Long> entry : results) {
if (entry.getValue() > VISIBILITY_THRESHOLD) {
builder.append("\n&e").append(formatter.apply(entry));
}
else {
hidden++;
}
}
builder.append("\n\n&c+ &6").append(hidden).append(" more");
component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, TextComponent.fromLegacyText(ChatColors.color(builder.toString()))));
sender.spigot().sendMessage(component);
}
else {
int hidden = 0;
for (Map.Entry<String, Long> entry : results) {
if (entry.getValue() > VISIBILITY_THRESHOLD) {
sender.sendMessage(" " + ChatColor.stripColor(formatter.apply(entry)));
}
else {
hidden++;
}
}
sender.sendMessage("+ " + hidden + " more");
}
}
public String getTime() {
return NumberUtils.getAsMillis(totalElapsedTime);
}
public String getTime(Block b) {
Validate.notNull("Cannot get timings for a null Block");
long time = timings.getOrDefault(new ProfiledBlock(b), 0L);
return NumberUtils.getAsMillis(time);
}
public String getTime(Chunk chunk) {
Validate.notNull("Cannot get timings for a null Chunk");
long time = getByChunk().getOrDefault(chunk.getWorld().getName() + " (" + chunk.getX() + ',' + chunk.getZ() + ')', 0L);
return NumberUtils.getAsMillis(time);
}
public String getTime(SlimefunItem item) {
Validate.notNull("Cannot get timings for a null SlimefunItem");
long time = getByItem().getOrDefault(item.getID(), 0L);
return NumberUtils.getAsMillis(time);
}
}

View File

@ -0,0 +1,9 @@
/**
* This package holds classes related to the
* {@link io.github.thebusybiscuit.slimefun4.core.services.profiler.SlimefunProfiler}.
* The {@link io.github.thebusybiscuit.slimefun4.core.services.profiler.SlimefunProfiler} is used to determine
* {@link org.bukkit.block.Block Blocks}, {@link org.bukkit.Chunk Chunks} or
* {@link me.mrCookieSlime.Slimefun.Objects.SlimefunItem.SlimefunItem SlimefunItems} that cause lag or performance
* drops.
*/
package io.github.thebusybiscuit.slimefun4.core.services.profiler;

View File

@ -43,6 +43,7 @@ import io.github.thebusybiscuit.slimefun4.core.services.UpdaterService;
import io.github.thebusybiscuit.slimefun4.core.services.github.GitHubService;
import io.github.thebusybiscuit.slimefun4.core.services.metrics.MetricsService;
import io.github.thebusybiscuit.slimefun4.core.services.plugins.ThirdPartyPluginService;
import io.github.thebusybiscuit.slimefun4.core.services.profiler.SlimefunProfiler;
import io.github.thebusybiscuit.slimefun4.implementation.items.altar.AncientAltar;
import io.github.thebusybiscuit.slimefun4.implementation.items.backpacks.Cooler;
import io.github.thebusybiscuit.slimefun4.implementation.items.electric.BasicCircuitBoard;
@ -126,6 +127,7 @@ public final class SlimefunPlugin extends JavaPlugin implements SlimefunAddon {
private final PerWorldSettingsService worldSettingsService = new PerWorldSettingsService(this);
private final ThirdPartyPluginService thirdPartySupportService = new ThirdPartyPluginService(this);
private final MinecraftRecipeService recipeService = new MinecraftRecipeService(this);
private final SlimefunProfiler profiler = new SlimefunProfiler();
private LocalizationService local;
private GPSNetwork gpsNetwork;
@ -612,6 +614,10 @@ public final class SlimefunPlugin extends JavaPlugin implements SlimefunAddon {
return instance.command;
}
public static SlimefunProfiler getProfiler() {
return instance.profiler;
}
/**
* This returns the currently installed version of Minecraft.
*

View File

@ -41,7 +41,7 @@ public class CargoManager extends SlimefunItem {
@Override
public boolean isSynchronized() {
return false;
return true;
}
}, new BlockUseHandler() {

View File

@ -1,6 +1,5 @@
package io.github.thebusybiscuit.slimefun4.implementation.listeners;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.Skull;
@ -15,12 +14,11 @@ import org.bukkit.inventory.EquipmentSlot;
import io.github.thebusybiscuit.cscorelib2.chat.ChatColors;
import io.github.thebusybiscuit.cscorelib2.skull.SkullBlock;
import io.github.thebusybiscuit.slimefun4.core.attributes.EnergyNetComponent;
import io.github.thebusybiscuit.slimefun4.core.networks.energy.EnergyNet;
import io.github.thebusybiscuit.slimefun4.implementation.SlimefunItems;
import io.github.thebusybiscuit.slimefun4.implementation.SlimefunPlugin;
import io.github.thebusybiscuit.slimefun4.implementation.tasks.TickerTask;
import io.github.thebusybiscuit.slimefun4.utils.HeadTexture;
import io.github.thebusybiscuit.slimefun4.utils.NumberUtils;
import io.github.thebusybiscuit.slimefun4.utils.SlimefunUtils;
import me.mrCookieSlime.Slimefun.Objects.SlimefunItem.SlimefunItem;
import me.mrCookieSlime.Slimefun.api.BlockStorage;
@ -28,14 +26,14 @@ import me.mrCookieSlime.Slimefun.api.energy.ChargableBlock;
public class DebugFishListener implements Listener {
private final String enabledTooltip;
private final String disabledTooltip;
private final String greenCheckmark;
private final String redCross;
public DebugFishListener(SlimefunPlugin plugin) {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
enabledTooltip = "&2\u2714";
disabledTooltip = "&4\u2718";
greenCheckmark = "&2\u2714";
redCross = "&4\u2718";
}
@EventHandler
@ -81,7 +79,7 @@ public class DebugFishListener implements Listener {
p.sendMessage(ChatColors.color("&dPlugin: " + "&e" + item.getAddon().getName()));
if (b.getState() instanceof Skull) {
p.sendMessage(ChatColors.color("&dSkull: " + enabledTooltip));
p.sendMessage(ChatColors.color("&dSkull: " + greenCheckmark));
// Check if the skull is a wall skull, and if so use Directional instead of Rotatable.
if (b.getType() == Material.PLAYER_WALL_HEAD) {
@ -93,40 +91,40 @@ public class DebugFishListener implements Listener {
}
if (BlockStorage.getStorage(b.getWorld()).hasInventory(b.getLocation())) {
p.sendMessage(ChatColors.color("&dInventory: " + enabledTooltip));
p.sendMessage(ChatColors.color("&dInventory: " + greenCheckmark));
}
else {
p.sendMessage(ChatColors.color("&dInventory: " + disabledTooltip));
p.sendMessage(ChatColors.color("&dInventory: " + redCross));
}
TickerTask ticker = SlimefunPlugin.getTickerTask();
if (item.isTicking()) {
p.sendMessage(ChatColors.color("&dTicker: " + enabledTooltip));
p.sendMessage(ChatColors.color(" &dAsync: &e" + (BlockStorage.check(b).getBlockTicker().isSynchronized() ? disabledTooltip : enabledTooltip)));
p.sendMessage(ChatColors.color(" &dTimings: &e" + NumberUtils.getAsMillis(ticker.getTimings(b))));
p.sendMessage(ChatColors.color(" &dTotal Timings: &e" + NumberUtils.getAsMillis(ticker.getTimings(BlockStorage.checkID(b)))));
p.sendMessage(ChatColors.color(" &dChunk Timings: &e" + NumberUtils.getAsMillis(ticker.getTimings(b.getChunk()))));
p.sendMessage(ChatColors.color("&dTicker: " + greenCheckmark));
p.sendMessage(ChatColors.color(" &dAsync: &e" + (item.getBlockTicker().isSynchronized() ? redCross : greenCheckmark)));
p.sendMessage(ChatColors.color(" &dTimings: &e" + SlimefunPlugin.getProfiler().getTime(b)));
p.sendMessage(ChatColors.color(" &dTotal Timings: &e" + SlimefunPlugin.getProfiler().getTime(item)));
p.sendMessage(ChatColors.color(" &dChunk Timings: &e" + SlimefunPlugin.getProfiler().getTime(b.getChunk())));
}
else if (item.getEnergyTicker() != null) {
p.sendMessage(ChatColors.color("&dTicking: " + "&3Indirect"));
p.sendMessage(ChatColors.color(" &dTimings: &e" + NumberUtils.getAsMillis(ticker.getTimings(b))));
p.sendMessage(ChatColors.color(" &dChunk Timings: &e" + NumberUtils.getAsMillis(ticker.getTimings(b.getChunk()))));
p.sendMessage(ChatColors.color(" &dTimings: &e" + SlimefunPlugin.getProfiler().getTime(b)));
p.sendMessage(ChatColors.color(" &dChunk Timings: &e" + SlimefunPlugin.getProfiler().getTime(b.getChunk())));
}
else {
p.sendMessage(ChatColors.color("&dTicker: " + disabledTooltip));
p.sendMessage(ChatColor.translateAlternateColorCodes('&', "&dTicking: " + disabledTooltip));
p.sendMessage(ChatColors.color("&dTicker: " + redCross));
p.sendMessage(ChatColors.color("&dTicking: " + redCross));
}
if (ChargableBlock.isChargable(b)) {
p.sendMessage(ChatColors.color("&dChargeable: " + enabledTooltip));
p.sendMessage(ChatColors.color("&dChargeable: " + greenCheckmark));
p.sendMessage(ChatColors.color(" &dEnergy: &e" + ChargableBlock.getCharge(b) + " / " + ChargableBlock.getMaxCharge(b)));
}
else {
p.sendMessage(ChatColors.color("&dChargeable: " + disabledTooltip));
p.sendMessage(ChatColors.color("&dChargeable: " + redCross));
}
p.sendMessage(ChatColors.color(" &dEnergyNet Type: &e" + EnergyNet.getComponent(b.getLocation())));
if (item instanceof EnergyNetComponent) {
p.sendMessage(ChatColors.color(" &dEnergyNet Type: &e" + EnergyNet.getComponent(b.getLocation())));
}
p.sendMessage(ChatColors.color("&6" + BlockStorage.getBlockInfoAsJson(b)));
p.sendMessage(" ");

View File

@ -1,70 +1,38 @@
package io.github.thebusybiscuit.slimefun4.implementation.tasks;
import java.util.AbstractMap;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import io.github.thebusybiscuit.cscorelib2.chat.ChatColors;
import io.github.thebusybiscuit.slimefun4.api.ErrorReport;
import io.github.thebusybiscuit.slimefun4.implementation.SlimefunPlugin;
import io.github.thebusybiscuit.slimefun4.utils.NumberUtils;
import io.github.thebusybiscuit.slimefun4.utils.PatternUtils;
import me.mrCookieSlime.CSCoreLibPlugin.Configuration.Config;
import me.mrCookieSlime.Slimefun.Objects.SlimefunItem.SlimefunItem;
import me.mrCookieSlime.Slimefun.Objects.handlers.BlockTicker;
import me.mrCookieSlime.Slimefun.api.BlockStorage;
import me.mrCookieSlime.Slimefun.api.Slimefun;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
public class TickerTask implements Runnable {
private static final int VISIBILITY_THRESHOLD = 225_000;
private final Set<BlockTicker> tickers = new HashSet<>();
// These are "Queues" of blocks that need to be removed or moved
private final ConcurrentMap<Location, Location> movingQueue = new ConcurrentHashMap<>();
private final ConcurrentMap<Location, Boolean> deletionQueue = new ConcurrentHashMap<>();
private final ConcurrentMap<Location, Integer> buggedBlocks = new ConcurrentHashMap<>();
private final ConcurrentMap<Location, Long> blockTimings = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Integer> machineCount = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Long> machineTimings = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Long> chunkTimings = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Integer> chunkItemCount = new ConcurrentHashMap<>();
private final Set<String> skippedChunks = new HashSet<>();
private final Map<Location, Location> movingQueue = new ConcurrentHashMap<>();
private final Map<Location, Boolean> deletionQueue = new ConcurrentHashMap<>();
private final Map<Location, Integer> buggedBlocks = new ConcurrentHashMap<>();
private boolean halted = false;
private int skippedBlocks = 0;
private int chunks = 0;
private int blocks = 0;
private long time = 0;
private boolean running = false;
public void abortTick() {
@ -78,18 +46,7 @@ public class TickerTask implements Runnable {
}
running = true;
long timestamp = System.nanoTime();
skippedBlocks = 0;
chunks = 0;
blocks = 0;
chunkItemCount.clear();
machineCount.clear();
time = 0;
chunkTimings.clear();
skippedChunks.clear();
machineTimings.clear();
blockTimings.clear();
SlimefunPlugin.getProfiler().start();
Map<Location, Integer> bugs = new HashMap<>(buggedBlocks);
buggedBlocks.clear();
@ -103,24 +60,23 @@ public class TickerTask implements Runnable {
if (!halted) {
for (String chunk : BlockStorage.getTickingChunks()) {
long chunkTimestamp = System.nanoTime();
chunks++;
try {
Set<Location> locations = BlockStorage.getTickingLocations(chunk);
String[] components = PatternUtils.SEMICOLON.split(chunk);
Set<Location> locations = BlockStorage.getTickingLocations(chunk);
World world = Bukkit.getWorld(components[0]);
int x = Integer.parseInt(components[components.length - 2]);
int z = Integer.parseInt(components[components.length - 1]);
for (Location l : locations) {
if (l.getWorld().isChunkLoaded(l.getBlockX() >> 4, l.getBlockZ() >> 4)) {
tick(l, chunk, bugs);
}
else {
skippedBlocks += locations.size();
skippedChunks.add(chunk);
chunks--;
break;
if (world != null && world.isChunkLoaded(x, z)) {
for (Location l : locations) {
tick(l, bugs);
}
}
}
chunkTimings.put(chunk, System.nanoTime() - chunkTimestamp);
catch (ArrayIndexOutOfBoundsException | NumberFormatException x) {
Slimefun.getLogger().log(Level.SEVERE, x, () -> "An Exception has occured while trying to parse Chunk: " + chunk);
}
}
}
@ -136,49 +92,27 @@ public class TickerTask implements Runnable {
iterator.remove();
}
time = System.nanoTime() - timestamp;
running = false;
SlimefunPlugin.getProfiler().stop();
}
private void tick(Location l, String tickedChunk, Map<Location, Integer> bugs) {
Block b = l.getBlock();
SlimefunItem item = BlockStorage.check(l);
private void tick(Location l, Map<Location, Integer> bugs) {
Config data = BlockStorage.getLocationInfo(l);
SlimefunItem item = SlimefunItem.getByID(data.getString("id"));
if (item != null && item.getBlockTicker() != null) {
blocks++;
try {
long timestamp = SlimefunPlugin.getProfiler().newEntry();
Block b = l.getBlock();
item.getBlockTicker().update();
if (item.getBlockTicker().isSynchronized()) {
Slimefun.runSync(() -> {
try {
long timestamp = System.nanoTime();
item.getBlockTicker().tick(b, item, BlockStorage.getLocationInfo(l));
long machinetime = NumberUtils.getLong(machineTimings.get(item.getID()), 0);
int chunk = NumberUtils.getInt(chunkItemCount.get(tickedChunk), 0);
int machine = NumberUtils.getInt(machineCount.get(item.getID()), 0);
machineTimings.put(item.getID(), machinetime + (System.nanoTime() - timestamp));
chunkItemCount.put(tickedChunk, chunk + 1);
machineCount.put(item.getID(), machine + 1);
blockTimings.put(l, System.nanoTime() - timestamp);
}
catch (Exception | LinkageError x) {
int errors = bugs.getOrDefault(l, 0);
reportErrors(l, item, x, errors);
}
});
// We are ignoring the timestamp from above because synchronized actions
// are always ran with a 50ms delay (1 game tick)
Slimefun.runSync(() -> tickBlock(bugs, l, b, item, data, System.nanoTime()));
}
else {
long timestamp = System.nanoTime();
item.getBlockTicker().tick(b, item, BlockStorage.getLocationInfo(l));
machineTimings.merge(item.getID(), (System.nanoTime() - timestamp), Long::sum);
chunkItemCount.merge(tickedChunk, 1, Integer::sum);
machineCount.merge(item.getID(), 1, Integer::sum);
blockTimings.put(l, System.nanoTime() - timestamp);
tickBlock(bugs, l, b, item, data, timestamp);
}
tickers.add(item.getBlockTicker());
@ -188,8 +122,18 @@ public class TickerTask implements Runnable {
reportErrors(l, item, x, errors);
}
}
else {
skippedBlocks++;
}
private void tickBlock(Map<Location, Integer> bugs, Location l, Block b, SlimefunItem item, Config data, long timestamp) {
try {
item.getBlockTicker().tick(b, item, data);
}
catch (Exception | LinkageError x) {
int errors = bugs.getOrDefault(l, 0);
reportErrors(l, item, x, errors);
}
finally {
SlimefunPlugin.getProfiler().closeEntry(l, item, timestamp);
}
}
@ -215,101 +159,6 @@ public class TickerTask implements Runnable {
}
}
public String getTime() {
return NumberUtils.getAsMillis(time);
}
public void info(CommandSender sender) {
sender.sendMessage(ChatColors.color("&2== &aSlimefun Diagnostic Tool &2=="));
sender.sendMessage(ChatColors.color("&6Halted: &e&l" + String.valueOf(halted).toUpperCase(Locale.ROOT)));
sender.sendMessage("");
sender.sendMessage(ChatColors.color("&6Impact: &e" + NumberUtils.getAsMillis(time)));
sender.sendMessage(ChatColors.color("&6Ticked Chunks: &e" + chunks));
sender.sendMessage(ChatColors.color("&6Ticked Machines: &e" + blocks));
sender.sendMessage(ChatColors.color("&6Skipped Machines: &e" + skippedBlocks));
sender.sendMessage("");
sender.sendMessage(ChatColors.color("&6Ticking Machines:"));
summarizeTimings(sender, entry -> {
int count = machineCount.get(entry.getKey());
String timings = NumberUtils.getAsMillis(entry.getValue());
String average = NumberUtils.getAsMillis(entry.getValue() / count);
return entry.getKey() + " - " + count + "x (" + timings + ", " + average + " avg/machine)";
}, machineCount.keySet().stream().map(key -> new AbstractMap.SimpleEntry<>(key, machineTimings.getOrDefault(key, 0L))));
sender.sendMessage("");
sender.sendMessage(ChatColors.color("&6Ticking Chunks:"));
summarizeTimings(sender, entry -> {
int count = chunkItemCount.getOrDefault(entry.getKey(), 0);
String timings = NumberUtils.getAsMillis(entry.getValue());
return formatChunk(entry.getKey()) + " - " + count + "x (" + timings + ")";
}, chunkTimings.entrySet().stream().filter(entry -> !skippedChunks.contains(entry.getKey())));
}
private void summarizeTimings(CommandSender sender, Function<Map.Entry<String, Long>, String> formatter, Stream<Map.Entry<String, Long>> stream) {
List<Entry<String, Long>> timings = stream.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())).collect(Collectors.toList());
if (sender instanceof Player) {
TextComponent component = new TextComponent(" Hover for more Info");
component.setColor(net.md_5.bungee.api.ChatColor.GRAY);
component.setItalic(true);
StringBuilder builder = new StringBuilder();
int hidden = 0;
for (Map.Entry<String, Long> entry : timings) {
if (entry.getValue() > VISIBILITY_THRESHOLD) {
builder.append("\n&c").append(formatter.apply(entry));
}
else {
hidden++;
}
}
builder.append("\n\n&c+ &4").append(hidden).append(" Hidden");
component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, TextComponent.fromLegacyText(ChatColors.color(builder.toString()))));
sender.spigot().sendMessage(component);
}
else {
int hidden = 0;
for (Map.Entry<String, Long> entry : timings) {
if (entry.getValue() > VISIBILITY_THRESHOLD) {
sender.sendMessage(" " + ChatColor.stripColor(formatter.apply(entry)));
}
else {
hidden++;
}
}
sender.sendMessage("+ " + hidden + " Hidden");
}
}
private String formatChunk(String chunk) {
String[] components = PatternUtils.SEMICOLON.split(chunk);
return components[0] + " [" + components[2] + ',' + components[3] + ']';
}
public long getTimings(Block b) {
return blockTimings.getOrDefault(b.getLocation(), 0L);
}
public long getTimings(String item) {
return machineTimings.getOrDefault(item, 0L);
}
public long getTimings(Chunk c) {
String id = c.getWorld().getName() + ';' + c.getX() + ';' + c.getZ();
return chunkTimings.getOrDefault(id, 0L);
}
public void addBlockTimings(Location l, long time) {
blockTimings.put(l, time);
}
public boolean isHalted() {
return halted;
}
@ -320,7 +169,7 @@ public class TickerTask implements Runnable {
@Override
public String toString() {
return "TickerTask {\n" + " HALTED = " + halted + "\n" + " tickers = " + tickers + "\n" + " move = " + movingQueue + "\n" + " delete = " + deletionQueue + "\n" + " chunks = " + chunkItemCount + "\n" + " machines = " + machineCount + "\n" + " machinetime = " + machineTimings + "\n" + " chunktime = " + chunkTimings + "\n" + " skipped = " + skippedChunks + "\n" + "}";
return "TickerTask {\n" + " HALTED = " + halted + "\n" + " tickers = " + tickers + "\n" + " move = " + movingQueue + "\n" + " delete = " + deletionQueue + "}";
}
public void queueMove(Location from, Location to) {

View File

@ -69,6 +69,10 @@ public final class NumberUtils {
}
public static String getAsMillis(long nanoseconds) {
if (nanoseconds == 0) {
return "0ms";
}
String number = DECIMAL_FORMAT.format(nanoseconds / 1000000.0);
String[] parts = PatternUtils.NUMBER_SEPERATOR.split(number);
@ -76,7 +80,7 @@ public final class NumberUtils {
return parts[0];
}
else {
return parts[0] + ',' + ChatColor.GRAY + parts[1] + "ms";
return parts[0] + ',' + parts[1] + "ms";
}
}