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.
432 lines
24 KiB
432 lines
24 KiB
import discord
|
|
from discord.ext import commands
|
|
import aiohttp
|
|
import yaml
|
|
import re
|
|
import logging
|
|
import requests
|
|
|
|
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=f"[Click here to view]({paste_excess_problems(embed_var)})")
|
|
while len(embed_var) > 6000:
|
|
embed_var.insert_field_at(index=field_at_index,
|
|
name=f"Plus {issue_count - field_at_index} more recommendations, see below.",
|
|
value=f"[Click here to view]({paste_excess_problems(embed_var)})")
|
|
del embed_var._fields[(field_at_index + 1):]
|
|
field_at_index -= 1
|
|
await message.reply(embed=embed_var)
|
|
|
|
|
|
def paste_excess_problems(embed_var):
|
|
url = "https://api.mclo.gs/1/log"
|
|
paste_content = {"content": stringify_embed_contents(embed_var)}
|
|
|
|
response = requests.post(url, paste_content)
|
|
return response.json()["url"]
|
|
|
|
|
|
def stringify_embed_contents(embed_var):
|
|
embed_string = ""
|
|
for embed in embed_var.fields:
|
|
embed_string += embed.name + ":\n"
|
|
embed_string += embed.value + "\n\n"
|
|
return embed_string
|
|
|
|
|
|
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))
|