/*
* This file is part of Sponge, licensed under the MIT License (MIT).
*
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* 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 static co.aikar.timings.TimingsManager.HISTORY;
import co.aikar.util.JSONUtil;
import co.aikar.util.JSONUtil.JsonObjectBuilder;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import net.canarymod.Canary;
import net.canarymod.api.Server;
import net.canarymod.api.entity.EntityType;
import net.canarymod.api.world.blocks.BlockType;
import net.canarymod.chat.ChatFormat;
import net.canarymod.chat.MessageReceiver;
import net.canarymod.config.Configuration;
import net.canarymod.config.ConfigurationContainer;
import net.minecraft.network.rcon.RConConsoleSource;
import net.minecraft.server.MinecraftServer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import java.util.List;
import java.util.Set;
import java.util.zip.GZIPOutputStream;
class TimingsExport extends Thread {
private static final Joiner RUNTIME_FLAG_JOINER = Joiner.on(" ");
private final MessageReceiver sender;
private final JsonObject out;
private final TimingHistory[] history;
TimingsExport(MessageReceiver sender, JsonObject out, TimingHistory[] history) {
super("Timings paste thread");
this.sender = sender;
this.out = out;
this.history = history;
}
private static String getServerName() {
return Canary.getImplementationTitle() + " " + Canary.getImplementationVersion();
}
/**
* Builds an JSON report of the timings to be uploaded for parsing.
*
* @param sender Who to report to
*/
static void reportTimings(MessageReceiver sender) {
JsonObjectBuilder builder = JSONUtil.objectBuilder()
// Get some basic system details about the server
.add("version", Canary.getImplementationVersion())
.add("maxplayers", Configuration.getServerConfig().getMaxPlayers())
.add("start", TimingsManager.timingStart / 1000)
.add("end", System.currentTimeMillis() / 1000)
.add("sampletime", (System.currentTimeMillis() - TimingsManager.timingStart) / 1000);
if (!TimingsManager.privacy) {
builder.add("server", getServerName())
.add("motd", Configuration.getServerConfig().getMotd())
.add("online-mode", Configuration.getServerConfig().isOnlineMode())
.add("icon", MinecraftServer.getServer().getServerStatusResponse().getFavicon());
}
final Runtime runtime = Runtime.getRuntime();
RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean();
builder.add("system", JSONUtil.objectBuilder()
.add("timingcost", getCost())
.add("name", System.getProperty("os.name"))
.add("version", System.getProperty("os.version"))
.add("jvmversion", System.getProperty("java.version"))
.add("arch", System.getProperty("os.arch"))
.add("maxmem", runtime.maxMemory())
.add("cpu", runtime.availableProcessors())
.add("runtime", ManagementFactory.getRuntimeMXBean().getUptime())
.add("flags", RUNTIME_FLAG_JOINER.join(runtimeBean.getInputArguments()))
.add("gc", JSONUtil.mapArrayToObject(ManagementFactory.getGarbageCollectorMXBeans(), (input) -> {
return JSONUtil.singleObjectPair(input.getName(), JSONUtil.arrayOf(input.getCollectionCount(), input.getCollectionTime()));
})));
Set<BlockType> blockTypeSet = Sets.newHashSet();
Set<EntityType> entityTypeSet = Sets.newHashSet();
int size = HISTORY.size();
TimingHistory[] history = new TimingHistory[size + 1];
int i = 0;
for (TimingHistory timingHistory : HISTORY) {
blockTypeSet.addAll(timingHistory.blockTypeSet);
entityTypeSet.addAll(timingHistory.entityTypeSet);
history[i++] = timingHistory;
}
history[i] = new TimingHistory(); // Current snapshot
blockTypeSet.addAll(history[i].blockTypeSet);
entityTypeSet.addAll(history[i].entityTypeSet);
JsonObjectBuilder handlersBuilder = JSONUtil.objectBuilder();
for (TimingIdentifier.TimingGroup group : TimingIdentifier.GROUP_MAP.values()) {
for (TimingHandler id : group.handlers) {
if (!id.timed && !id.isSpecial()) {
continue;
}
handlersBuilder.add(id.id, JSONUtil.arrayOf(
group.id,
id.name));
}
}
builder.add("idmap", JSONUtil.objectBuilder()
.add("groups", JSONUtil.mapArrayToObject(TimingIdentifier.GROUP_MAP.values(), (group) -> {
return JSONUtil.singleObjectPair(group.id, group.name);
}))
.add("handlers", handlersBuilder)
.add("worlds", JSONUtil.mapArrayToObject(TimingHistory.worldMap.entrySet(), (entry) -> {
return JSONUtil.singleObjectPair(entry.getValue(), entry.getKey());
}))
.add("tileentity", JSONUtil.mapArrayToObject(blockTypeSet, (blockType) -> {
return JSONUtil.singleObjectPair(blockType.getId(), blockType.getId());
}))
.add("entity", JSONUtil.mapArrayToObject(entityTypeSet, (entityType) -> {
if (entityType == EntityType.GENERIC_ENTITY) {
return null;
}
return JSONUtil.singleObjectPair(entityType.getEntityID(), entityType.getEntityID());
})));
// Information about loaded plugins
builder.add("plugins", JSONUtil.mapArrayToObject(Canary.pluginManager().getPlugins(), (plugin) -> {
return JSONUtil.objectBuilder().add(plugin.getName().toLowerCase(), JSONUtil.objectBuilder()
.add("version", plugin.getVersion())
.add("description", "")
.add("website", "")
.add("authors", plugin.getAuthor())
).build();
}));
// Information on the users Config
builder.add("config", JSONUtil.objectBuilder()
.add("timings", serializeConfig(Configuration.getTimingsConfig()))
.add("worlds", JSONUtil.mapArrayToObject(Canary.getServer().getWorldManager().getAllWorlds(), (world) -> {
return JSONUtil.singleObjectPair(world.getFqName(), serializeConfig(Configuration.getWorldConfig(world.getFqName())));
})));
new TimingsExport(sender, builder.build(), history).start();
}
static long getCost() {
// Benchmark the users System.nanotime() for cost basis
int passes = 200;
TimingHandler SAMPLER1 = NeptuneTimingsFactory.ofSafe("Timings Sampler 1");
TimingHandler SAMPLER2 = NeptuneTimingsFactory.ofSafe("Timings Sampler 2");
TimingHandler SAMPLER3 = NeptuneTimingsFactory.ofSafe("Timings Sampler 3");
TimingHandler SAMPLER4 = NeptuneTimingsFactory.ofSafe("Timings Sampler 4");
TimingHandler SAMPLER5 = NeptuneTimingsFactory.ofSafe("Timings Sampler 5");
TimingHandler SAMPLER6 = NeptuneTimingsFactory.ofSafe("Timings Sampler 6");
long start = System.nanoTime();
for (int i = 0; i < passes; i++) {
SAMPLER1.startTiming();
SAMPLER2.startTiming();
SAMPLER3.startTiming();
SAMPLER3.stopTiming();
SAMPLER4.startTiming();
SAMPLER5.startTiming();
SAMPLER6.startTiming();
SAMPLER6.stopTiming();
SAMPLER5.stopTiming();
SAMPLER4.stopTiming();
SAMPLER2.stopTiming();
SAMPLER1.stopTiming();
}
long timingsCost = (System.nanoTime() - start) / passes / 6;
SAMPLER1.reset(true);
SAMPLER2.reset(true);
SAMPLER3.reset(true);
SAMPLER4.reset(true);
SAMPLER5.reset(true);
SAMPLER6.reset(true);
return timingsCost;
}
private static List<String> RESTRICTED_CONFIG_VALUES = ImmutableList.<String>builder()
.add("world-seed")
.build();
private static JsonElement serializeConfig(ConfigurationContainer configuration) {
return JSONUtil.mapArrayToObject(configuration.getFile().getPropertiesMap().entrySet(), (value) -> {
if (!RESTRICTED_CONFIG_VALUES.contains(value.getKey())) {
return JSONUtil.singleObjectPair(value.getKey(), value.getValue());
}
return null;
});
}
@Override
public synchronized void start() {
if (this.sender instanceof RConConsoleSource) {
this.sender.message(ChatFormat.RED + "Warning: Timings report done over RCON will cause lag spikes.");
this.sender.message(ChatFormat.RED + "You should use " + ChatFormat.YELLOW,
"/timings report" + ChatFormat.RED + " in game or console.");
run();
} else {
super.start();
}
}
@Override
public void run() {
this.sender.message(ChatFormat.GREEN + "Preparing Timings Report...");
this.out.add("data", JSONUtil.mapArray(this.history, TimingHistory::export));
String response = null;
try {
HttpURLConnection con = (HttpURLConnection) new URL("http://timings.aikar.co/post").openConnection();
con.setDoOutput(true);
con.setRequestProperty("User-Agent", "Neptune/" + getServerName() + "/" + InetAddress.getLocalHost().getHostName());
con.setRequestMethod("POST");
con.setInstanceFollowRedirects(false);
OutputStream request = new GZIPOutputStream(con.getOutputStream()) {
{
this.def.setLevel(7);
}
};
request.write(JSONUtil.toString(this.out).getBytes("UTF-8"));
request.close();
response = getResponse(con);
if (con.getResponseCode() != 302) {
this.sender.message(ChatFormat.RED + "Upload Error: " + con.getResponseCode() + ": " + con.getResponseMessage());
this.sender.message(ChatFormat.RED + "Check your logs for more information");
if (response != null) {
Canary.log.fatal(response);
}
return;
}
String location = con.getHeaderField("Location");
this.sender.message(ChatFormat.GREEN + "View Timings Report: " + location);
if (!(this.sender instanceof Server)) {
Canary.log.info("View Timings Report: " + location);
}
if (response != null && !response.isEmpty()) {
Canary.log.info("Timing Response: " + response);
}
} catch (IOException ex) {
this.sender.message(ChatFormat.RED + "Error uploading timings, check your logs for more information");
if (response != null) {
Canary.log.fatal(response);
}
Canary.log.fatal("Could not paste timings", ex);
}
}
private String getResponse(HttpURLConnection con) throws IOException {
InputStream is = null;
try {
is = con.getInputStream();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int bytesRead;
while ((bytesRead = is.read(b)) != -1) {
bos.write(b, 0, bytesRead);
}
return bos.toString();
} catch (IOException ex) {
this.sender.message(ChatFormat.RED + "Error uploading timings, check your logs for more information");
Canary.log.warn(con.getResponseMessage(), ex);
return null;
} finally {
if (is != null) {
is.close();
}
}
}
}