/*
* 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.FULL_SERVER_TICK;
import static co.aikar.timings.TimingsManager.MINUTE_REPORTS;
import co.aikar.util.JSONUtil;
import co.aikar.util.LoadingMap;
import co.aikar.util.MRUMapCache;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import net.canarymod.Canary;
import net.canarymod.api.entity.Entity;
import net.canarymod.api.entity.EntityType;
import net.canarymod.api.entity.living.humanoid.Player;
import net.canarymod.api.world.blocks.BlockType;
import net.canarymod.api.world.blocks.TileEntity;
import org.neptunepowered.vanilla.interfaces.minecraft.world.chunk.IMixinChunk;
import java.lang.management.ManagementFactory;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
public class TimingHistory {
public static long lastMinuteTime;
public static long timedTicks;
public static long playerTicks;
public static long entityTicks;
public static long tileEntityTicks;
public static long activatedEntityTicks;
static int worldIdPool = 1;
static Map<String, Integer> worldMap = LoadingMap.newHashMap((input) -> worldIdPool++);
final long endTime;
final long startTime;
final long totalTicks;
// Represents all time spent running the server this history
final long totalTime;
final MinuteReport[] minuteReports;
final TimingHistoryEntry[] entries;
final Set<BlockType> blockTypeSet = Sets.newHashSet();
final Set<EntityType> entityTypeSet = Sets.newHashSet();
final JsonObject worlds;
TimingHistory() {
this.endTime = System.currentTimeMillis() / 1000;
this.startTime = TimingsManager.historyStart / 1000;
if (timedTicks % 1200 != 0 || MINUTE_REPORTS.isEmpty()) {
this.minuteReports = MINUTE_REPORTS.toArray(new MinuteReport[MINUTE_REPORTS.size() + 1]);
this.minuteReports[this.minuteReports.length - 1] = new MinuteReport();
} else {
this.minuteReports = MINUTE_REPORTS.toArray(new MinuteReport[MINUTE_REPORTS.size()]);
}
long ticks = 0;
for (MinuteReport mp : this.minuteReports) {
ticks += mp.ticksRecord.timed;
}
this.totalTicks = ticks;
this.totalTime = FULL_SERVER_TICK.record.totalTime;
this.entries = new TimingHistoryEntry[TimingsManager.HANDLERS.size()];
int i = 0;
for (TimingHandler handler : TimingsManager.HANDLERS) {
this.entries[i++] = new TimingHistoryEntry(handler);
}
final Map<EntityType, Counter> entityCounts = MRUMapCache.of(LoadingMap.of(Maps.newHashMap(), Counter.loader()));
final Map<BlockType, Counter> tileEntityCounts = MRUMapCache.of(LoadingMap.of(Maps.newHashMap(), Counter.loader()));
// Information about all loaded chunks/entities
this.worlds = JSONUtil.mapArrayToObject(Canary.getServer().getWorldManager().getAllWorlds(), (world) -> {
return JSONUtil.singleObjectPair(String.valueOf(worldMap.get(world.getName())), JSONUtil.mapArray(world.getLoadedChunks(), (chunk) -> {
entityCounts.clear();
tileEntityCounts.clear();
for (Entity entity : ((IMixinChunk) chunk).getEntities()) {
if (entity.getEntityType() == null) {
Canary.log.error("Entity is not registered {}", entity);
continue;
}
entityCounts.get(entity.getEntityType()).increment();
}
for (TileEntity tileEntity : ((IMixinChunk) chunk).getTileEntities()) {
tileEntityCounts.get(tileEntity.getBlock().getType()).increment();
}
if (tileEntityCounts.isEmpty() && entityCounts.isEmpty()) {
return null;
}
return JSONUtil.arrayOf(
chunk.getX(),
chunk.getZ(),
JSONUtil.mapArrayToObject(entityCounts.entrySet(), (entry) -> {
if (entry.getKey() == EntityType.GENERIC_ENTITY) {
return null;
}
this.entityTypeSet.add(entry.getKey());
return JSONUtil.singleObjectPair(entry.getKey().getEntityID(), entry.getValue().count());
}),
JSONUtil.mapArrayToObject(tileEntityCounts.entrySet(), (entry) -> {
this.blockTypeSet.add(entry.getKey());
return JSONUtil.singleObjectPair(entry.getKey().getId(), entry.getValue().count());
}));
}));
});
}
public static void resetTicks(boolean fullReset) {
if (fullReset) {
// Non full is simply for 1 minute reports
timedTicks = 0;
}
lastMinuteTime = System.nanoTime();
playerTicks = 0;
tileEntityTicks = 0;
entityTicks = 0;
activatedEntityTicks = 0;
}
JsonObject export() {
return JSONUtil.objectBuilder()
.add("s", this.startTime)
.add("e", this.endTime)
.add("tk", this.totalTicks)
.add("tm", this.totalTime)
.add("w", this.worlds)
.add("h", JSONUtil.mapArray(this.entries, (entry) -> entry.data.count == 0 ? null : entry.export()))
.add("mp", JSONUtil.mapArray(this.minuteReports, MinuteReport::export))
.build();
}
static class MinuteReport {
final long time = System.currentTimeMillis() / 1000;
final TicksRecord ticksRecord = new TicksRecord();
final PingRecord pingRecord = new PingRecord();
final TimingData fst = TimingsManager.FULL_SERVER_TICK.minuteData.clone();
final double tps = 1E9 / (System.nanoTime() - lastMinuteTime) * this.ticksRecord.timed;
final double usedMemory = TimingsManager.FULL_SERVER_TICK.avgUsedMemory;
final double freeMemory = TimingsManager.FULL_SERVER_TICK.avgFreeMemory;
final double loadAvg = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage();
public JsonArray export() {
return JSONUtil.arrayOf(
this.time,
Math.round(this.tps * 100D) / 100D,
Math.round(this.pingRecord.avg * 100D) / 100D,
this.fst.export(),
JSONUtil.arrayOf(this.ticksRecord.timed,
this.ticksRecord.player,
this.ticksRecord.entity,
this.ticksRecord.activatedEntity,
this.ticksRecord.tileEntity),
this.usedMemory,
this.freeMemory,
this.loadAvg);
}
}
static class TicksRecord {
final long timed;
final long player;
final long entity;
final long tileEntity;
final long activatedEntity;
TicksRecord() {
this.timed = timedTicks - (TimingsManager.MINUTE_REPORTS.size() * 1200);
this.player = playerTicks;
this.entity = entityTicks;
this.tileEntity = tileEntityTicks;
this.activatedEntity = activatedEntityTicks;
}
}
static class PingRecord {
final double avg;
PingRecord() {
final Collection<Player> onlinePlayers = Canary.getServer().getPlayerList();
int totalPing = 0;
for (Player player : onlinePlayers) {
totalPing += player.getPing();
}
this.avg = onlinePlayers.isEmpty() ? 0 : totalPing / onlinePlayers.size();
}
}
static class Counter {
int count = 0;
private static final Function<?, Counter> LOADER = new LoadingMap.Feeder<Counter>() {
@Override
public Counter apply() {
return new Counter();
}
};
@SuppressWarnings("unchecked")
static <T> Function<T, Counter> loader() {
return (Function<T, Counter>) LOADER;
}
public int increment() {
return ++this.count;
}
public int count() {
return this.count;
}
}
}