You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

415 lines
24 KiB

import discord
from discord.ext import commands
import aiohttp
import yaml
import re
import logging
TIMINGS_CHECK = None
YAML_ERROR = None
with open("cogs/timings_check.yml", 'r', encoding="utf8") as stream:
try:
TIMINGS_CHECK = yaml.safe_load(stream)
except yaml.YAMLError as exc:
logging.info(exc)
YAML_ERROR = exc
VERSION_REGEX = re.compile(r"\d+\.\d+\.\d+")
class Timings(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.TIMINGS_TITLE = "Timings Analysis"
# TODO: Check Tuinity.yml for spawn rate changes
async def analyze_timings(self, message):
words = message.content.replace("\n", " ").split(" ")
timings_url = ""
embed_var = discord.Embed(title=self.TIMINGS_TITLE, description="These are not magic values. Many of these settings have real consequences on your server's mechanics. See [YouHaveTrouble's guide](https://github.com/YouHaveTrouble/minecraft-optimization/blob/main/README.md) for detailed information on the functionality of each setting.")
embed_var.set_footer(text=f"Requested by {message.author.name}#{message.author.discriminator}", icon_url=message.author.avatar_url)
for word in words:
if word.startswith("https://timin") and "/d=" in word:
word.replace("/d=", "/?id=") # this seems to be a common issue when people post their links
if word.startswith("https://timin") and "/?id=" in word:
timings_url = word
break
if word.startswith("https://www.spigotmc.org/go/timings?url=") or word.startswith(
"https://timings.spigotmc.org/?url="):
embed_var.add_field(name="❌ Spigot",
value="Spigot timings have limited information. Switch to [Purpur](https://purpur.pl3x.net/downloads) for better timings analysis. All your plugins will be compatible, and if you don't like it, you can easily switch back.")
await message.reply(embed=embed_var)
return
if timings_url == "":
return
if "#" in timings_url:
timings_url = timings_url.split("#")[0]
if "?id=" not in timings_url:
return
logging.info(f'Timings analyzed from {message.author} ({message.author.id}): {timings_url}')
timings_host, timings_id = timings_url.split("?id=")
timings_json = timings_host + "data.php?id=" + timings_id
timings_url_raw = timings_url + "&raw=1"
async with aiohttp.ClientSession() as session:
async with session.get(timings_url_raw) as response:
request_raw = await response.json(content_type=None)
async with session.get(timings_json) as response:
request = await response.json(content_type=None)
if request is None or request_raw is None:
embed_var.add_field(name="❌ Invalid report",
value="Create a new timings report.")
await message.reply(embed=embed_var)
return
try:
try:
version = request["timingsMaster"]["version"] if "version" in request["timingsMaster"] else None
print(version)
if version.count('.') == 1:
version = version[:-1]
version = version + ".0)"
if "Gem" in version or "Stellar" in version or "iSpigot" in version:
embed_var.add_field(name="⚠️ Publicly Sold Custom Jar", value=f'You are using `{version}`. Publicly sold custom jars have been known to have major instability issues, often leading to crashes. Many have skirted the copyright law in the past, and have been implement plugin features for extremely large prices (x10-x20 market value). This does not necessarily mean they are bad, but please be careful.')
if "version" in TIMINGS_CHECK and version:
version_result = VERSION_REGEX.search(version)
version_result = version_result.group() if version_result else None
if version_result:
if compare_versions(version_result, TIMINGS_CHECK["version"]) == -1:
version = version.replace("git-", "").replace("MC: ", "")
embed_var.add_field(name="❌ Outdated", value=f'You are using `{version}`. Update to `{TIMINGS_CHECK["version"]}`.')
else:
embed_var.add_field(name="❗ Value Error",
value=f'Could not locate version from `{version}`')
if "servers" in TIMINGS_CHECK:
for server in TIMINGS_CHECK["servers"]:
if server["name"] in version:
embed_var.add_field(**create_field(server))
break
except KeyError as key:
logging.info("Missing: " + str(key))
try:
timing_cost = int(request["timingsMaster"]["system"]["timingcost"])
if timing_cost > 300:
embed_var.add_field(name="❌ Timingcost",
value=f"Your timingcost is {timing_cost}. Your cpu is overloaded and/or slow. Find a [better host](https://www.birdflop.com).")
except KeyError as key:
logging.info("Missing: " + str(key))
try:
jvm_version = request["timingsMaster"]["system"]["jvmversion"]
if "openj9" in jvm_version:
embed_var.add_field(name="⚠️ Java Version",
value=f"You are using OpenJ9. This JVM implementation favors low ram usage vs throughput. This can reduce RAM usage by up to 50%, but at the cost of max players. This is useful to offset servers where RAM is the bottleneck. This is one tool for reducing lag that requires extensive knowledge to use properly.")
elif jvm_version.startswith("1.8.") or jvm_version.startswith("9.") or jvm_version.startswith("10."):
embed_var.add_field(name="❌ Java Version",
value=f"You are using Java {jvm_version}. Update to [Java 16](https://adoptopenjdk.net/installation.html).")
except KeyError as key:
logging.info("Missing: " + str(key))
try:
flags = request["timingsMaster"]["system"]["flags"]
if "-XX:+UseZGC" in flags:
jvm_version = request["timingsMaster"]["system"]["jvmversion"]
java_version = jvm_version.split(".")[0]
if int(java_version) < 14:
embed_var.add_field(name="❌ Java " + java_version,
value="ZGC should only be used on Java 15+")
if "-Xmx" in flags:
max_mem = 0
flaglist = flags.split(" ")
for flag in flaglist:
if flag.startswith("-Xmx"):
max_mem = flag.split("-Xmx")[1]
max_mem = max_mem.replace("G", "000")
max_mem = max_mem.replace("M", "")
max_mem = max_mem.replace("g", "000")
max_mem = max_mem.replace("m", "")
if int(max_mem) < 10000:
embed_var.add_field(name="❌ Low Memory",
value="ZGC is only good with a lot of memory.")
elif "-Daikars.new.flags=true" in flags:
if "-XX:+PerfDisableSharedMem" not in flags:
embed_var.add_field(name="❌ Outdated Flags",
value="Add `-XX:+PerfDisableSharedMem` to flags.")
if "XX:G1MixedGCCountTarget=4" not in flags:
embed_var.add_field(name="❌ Outdated Flags",
value="Add `-XX:G1MixedGCCountTarget=4` to flags.")
jvm_version = request["timingsMaster"]["system"]["jvmversion"]
if "-XX:+UseG1GC" not in flags and jvm_version.startswith("1.8."):
embed_var.add_field(name="❌ Aikar's Flags",
value="You must use G1GC when using Aikar's flags.")
if "-Xmx" in flags:
max_mem = 0
flaglist = flags.split(" ")
for flag in flaglist:
if flag.startswith("-Xmx"):
max_mem = flag.split("-Xmx")[1]
max_mem = max_mem.replace("G", "000")
max_mem = max_mem.replace("M", "")
max_mem = max_mem.replace("g", "000")
max_mem = max_mem.replace("m", "")
if int(max_mem) < 5400:
embed_var.add_field(name="❌ Low Memory",
value="Allocate at least 6-10GB of ram to your server if you can afford it.")
index = 0
max_online_players = 0
while index < len(request["timingsMaster"]["data"]):
timed_ticks = request["timingsMaster"]["data"][index]["minuteReports"][0]["ticks"][
"timedTicks"]
player_ticks = request["timingsMaster"]["data"][index]["minuteReports"][0]["ticks"][
"playerTicks"]
players = (player_ticks / timed_ticks)
max_online_players = max(players, max_online_players)
index = index + 1
if 1000 * max_online_players / int(max_mem) > 8 and int(max_mem) < 10000:
embed_var.add_field(name="❌ Low memory",
value="You should be using more RAM with this many players.")
if "-Xms" in flags:
min_mem = 0
flaglist = flags.split(" ")
for flag in flaglist:
if flag.startswith("-Xms"):
min_mem = flag.split("-Xms")[1]
min_mem = min_mem.replace("G", "000")
min_mem = min_mem.replace("M", "")
min_mem = min_mem.replace("g", "000")
min_mem = min_mem.replace("m", "")
# if min_mem != max_mem:
# embed_var.add_field(name="❌ Aikar's Flags",
# value="Your Xmx and Xms values should be equal when using Aikar's flags.")
elif "-Dusing.aikars.flags=mcflags.emc.gs" in flags:
embed_var.add_field(name="❌ Outdated Flags",
value="Update [Aikar's flags](https://aikar.co/2018/07/02/tuning-the-jvm-g1gc-garbage-collector-flags-for-minecraft/).")
else:
embed_var.add_field(name="❌ Aikar's Flags",
value="Use [Aikar's flags](https://aikar.co/2018/07/02/tuning-the-jvm-g1gc-garbage-collector-flags-for-minecraft/).")
except KeyError as key:
logging.info("Missing: " + str(key))
try:
cpu = int(request["timingsMaster"]["system"]["cpu"])
if cpu == 1:
embed_var.add_field(name="❌ Too Few Threads",
value=f"You have only {cpu} thread. Even if you're virtualizing a bigger CPU, this is not enough to handle a larger playerbase.")
if cpu == 2:
embed_var.add_field(name️="⚠ Few Threads",
value=f"You have only {cpu} threads. This is on the border of the minimum, but if you have a hefty CPU, it can work well.")
except KeyError as key:
logging.info("Missing: " + str(key))
try:
handlers = request_raw["idmap"]["handlers"]
for handler in handlers:
handler_name = request_raw["idmap"]["handlers"][handler][1]
if handler_name.startswith("Command Function - ") and handler_name.endswith(":tick"):
handler_name = handler_name.split("Command Function - ")[1].split(":tick")[0]
embed_var.add_field(name=f"{handler_name}",
value=f"This datapack uses command functions which are laggy.")
except KeyError as key:
logging.info("Missing: " + str(key))
plugins = request["timingsMaster"]["plugins"] if "plugins" in request["timingsMaster"] else None
server_properties = request["timingsMaster"]["config"]["server.properties"] if "server.properties" in request["timingsMaster"]["config"] else None
bukkit = request["timingsMaster"]["config"]["bukkit"] if "bukkit" in request["timingsMaster"]["config"] else None
spigot = request["timingsMaster"]["config"]["spigot"] if "spigot" in request["timingsMaster"]["config"] else None
paper = request["timingsMaster"]["config"]["paper"] if "paper" in request["timingsMaster"]["config"] else None
tuinity = request["timingsMaster"]["config"]["tuinity"] if "tuinity" in request["timingsMaster"]["config"] else None
purpur = request["timingsMaster"]["config"]["purpur"] if "purpur" in request["timingsMaster"]["config"] else None
if not YAML_ERROR:
if "plugins" in TIMINGS_CHECK:
for server_name in TIMINGS_CHECK["plugins"]:
if server_name in request["timingsMaster"]["config"]:
for plugin in plugins:
for plugin_name in TIMINGS_CHECK["plugins"][server_name]:
if plugin == plugin_name:
stored_plugin = TIMINGS_CHECK["plugins"][server_name][plugin_name]
if isinstance(stored_plugin, dict):
stored_plugin["name"] = plugin_name
embed_var.add_field(**create_field(stored_plugin))
else:
eval_field(embed_var, stored_plugin, plugin_name, plugins,
server_properties, bukkit, spigot, paper, tuinity, purpur)
if "config" in TIMINGS_CHECK:
for config_name in TIMINGS_CHECK["config"]:
config = TIMINGS_CHECK["config"][config_name]
for option_name in config:
option = config[option_name]
eval_field(embed_var, option, option_name, plugins, server_properties, bukkit,
spigot, paper, tuinity, purpur)
else:
embed_var.add_field(name="Error loading YAML file",
value=YAML_ERROR)
try:
for plugin in plugins:
authors = request["timingsMaster"]["plugins"][plugin]["authors"]
if authors is not None and "songoda" in request["timingsMaster"]["plugins"][plugin]["authors"].casefold():
if plugin == "EpicHeads":
embed_var.add_field(name="❌ EpicHeads",
value="This plugin was made by Songoda. Songoda has a bad history- find an alternative such as [HeadsPlus](https://spigotmc.org/resources/headsplus-»-1-8-1-16-4.40265/) or [HeadDatabase](https://www.spigotmc.org/resources/head-database.14280/).")
elif plugin == "UltimateStacker":
embed_var.add_field(name="❌ UltimateStacker",
value="Stacking plugins actually causes more lag. "
"Remove UltimateStacker.")
else:
embed_var.add_field(name="" + plugin,
value="Songoda focuses mainly on small friends servers (under 10 players). Most of their plugins do not fare well in production systems, and are known to cause issues when used for many players.")
except KeyError as key:
logging.info("Missing: " + str(key))
try:
using_tweaks = "ViewDistanceTweaks" in plugins
if not using_tweaks:
worlds = request_raw["worlds"]
for world in worlds:
tvd = int(request_raw["worlds"][world]["ticking-distance"])
ntvd = int(request_raw["worlds"][world]["notick-viewdistance"])
if ntvd <= tvd and tvd >= 3:
if spigot["world-settings"]["default"]["view-distance"] == "default":
embed_var.add_field(name="❌ no-tick-view-distance",
value=f"Set in paper.yml. Recommended: {tvd}. "
f"And reduce view-distance from default ({tvd}) in spigot.yml. Recommended: 3.")
else:
embed_var.add_field(name="❌ no-tick-view-distance",
value=f"Set in paper.yml. Recommended: {tvd}. "
f"And reduce view-distance from {tvd} in spigot.yml. Recommended: 3.")
break
except KeyError as key:
logging.info("Missing: " + str(key))
try:
worlds = request_raw["worlds"]
high_mec = False
for world in worlds:
max_entity_cramming = int(request_raw["worlds"][world]["gamerules"]["maxEntityCramming"])
if max_entity_cramming >= 10:
high_mec = True
if high_mec:
embed_var.add_field(name="❌ maxEntityCramming",
value=f"Decrease this by running the /gamerule command in each world. Recommended: 6. ")
except KeyError as key:
logging.info("Missing: " + str(key))
try:
normal_ticks = request["timingsMaster"]["data"][0]["totalTicks"]
worst_tps = 20
for index in range(len(request["timingsMaster"]["data"])):
total_ticks = request["timingsMaster"]["data"][index]["totalTicks"]
if total_ticks == normal_ticks:
end_time = request["timingsMaster"]["data"][index]["end"]
start_time = request["timingsMaster"]["data"][index]["start"]
if end_time == start_time:
tps = 20
else:
tps = total_ticks / (end_time - start_time)
if tps < worst_tps:
worst_tps = tps
if worst_tps < 10:
red = 255
green = int(255 * (0.1 * worst_tps))
else:
red = int(255 * (-0.1 * worst_tps + 2))
green = 255
color = int(red*256*256 + green*256)
embed_var.color = color
except KeyError as key:
logging.info("Missing: " + str(key))
except ValueError as value_error:
logging.info(value_error)
embed_var.add_field(name="❗ Value Error",
value=value_error)
if len(embed_var.fields) == 0:
embed_var.add_field(name="✅ All good",
value="Analyzed with no recommendations")
await message.reply(embed=embed_var)
return
issue_count = len(embed_var.fields)
field_at_index = 24
if issue_count >= 25:
embed_var.insert_field_at(index=24, name=f"Plus {issue_count - 24} more recommendations",
value="Create a new timings report after resolving some of the above issues to see more.")
while len(embed_var) > 6000:
embed_var.insert_field_at(index=field_at_index,
name=f"Plus {issue_count - field_at_index} more recommendations",
value="Create a new timings report after resolving some of the above issues to see more.")
del embed_var._fields[(field_at_index + 1):]
field_at_index -= 1
await message.reply(embed=embed_var)
def eval_field(embed_var, option, option_name, plugins, server_properties, bukkit, spigot, paper, tuinity, purpur):
dict_of_vars = {"plugins": plugins, "server_properties": server_properties, "bukkit": bukkit, "spigot": spigot,
"paper": paper, "tuinity": tuinity, "purpur": purpur}
try:
for option_data in option:
add_to_field = True
for expression in option_data["expressions"]:
for config_name in dict_of_vars:
if config_name in expression and not dict_of_vars[config_name]:
add_to_field = False
break
if not add_to_field:
break
try:
if not eval(expression):
add_to_field = False
break
except ValueError as value_error:
add_to_field = False
logging.info(value_error)
embed_var.add_field(name="❗ Value Error",
value=f'`{value_error}`\nexpression:\n`{expression}`\noption:\n`{option_name}`')
except TypeError as type_error:
add_to_field = False
logging.info(type_error)
embed_var.add_field(name="❗ Type Error",
value=f'`{type_error}`\nexpression:\n`{expression}`\noption:\n`{option_name}`')
for config_name in dict_of_vars:
if config_name in option_data["value"] and not dict_of_vars[config_name]:
add_to_field = False
break
if add_to_field:
""" f strings don't like newlines so we replace the newlines with placeholder text before we eval """
option_data["value"] = eval('f"""' + option_data["value"].replace("\n", "\\|n\\") + '"""').replace(
"\\|n\\", "\n")
embed_var.add_field(**create_field({**{"name": option_name}, **option_data}))
break
except KeyError as key:
logging.info("Missing: " + str(key))
def create_field(option):
field = {"name": option["name"],
"value": option["value"]}
if "prefix" in option:
field["name"] = option["prefix"] + " " + field["name"]
if "suffix" in option:
field["name"] = field["name"] + option["suffix"]
if "inline" in option:
field["inline"] = option["inline"]
return field
# Returns -1 if version A is older than version B
# Returns 0 if version A and B are equivalent
# Returns 1 if version A is newer than version B
def compare_versions(version_a, version_b):
def normalize(v):
return [int(x) for x in re.sub(r'(\.0+)*$', '', v).split(".")]
return (normalize(version_a) > normalize(version_b)) - (normalize(version_a) < normalize(version_b))
def setup(bot):
bot.add_cog(Timings(bot))