Network performance tab (#2009)

* Fixed disk medium threshold not showing color
* Added 'serverName' and 'serverUUID' to optimizedPerformance endpoint
* Added /v1/network/listServers endpoint
* Added /v1/network/performanceOverview?servers endpoint
* Hide negative values from performance graphs
* Allow json cache bypass by not providing timestamp parameter in URIQuery
* Ignore negative values in low tps spike count
* Added (Unavailable with Export) to exported network html performance tab title

Affects issues:
- Close #1693
This commit is contained in:
Risto Lahtela 2021-07-17 12:19:33 +03:00 committed by GitHub
parent c22874df34
commit 13823c044a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 772 additions and 35 deletions

View File

@ -170,7 +170,7 @@ public class TPSMutator {
for (TPS tpsObj : tpsData) {
double tps = tpsObj.getTicksPerSecond();
if (tps < threshold) {
if (0 <= tps && tps < threshold) {
if (!wasLow) {
spikeCount++;
wasLow = true;

View File

@ -98,8 +98,8 @@ public class NetworkPageExporter extends FileExporter {
// Fixes refreshingJsonRequest ignoring old data of export
String html = StringUtils.replaceEach(page.toHtml(),
new String[]{"loadPlayersOnlineGraph, 'network-overview', true);"},
new String[]{"loadPlayersOnlineGraph, 'network-overview');"});
new String[]{"loadPlayersOnlineGraph, 'network-overview', true);", "&middot; Performance"},
new String[]{"loadPlayersOnlineGraph, 'network-overview');", "&middot; Performance (Unavailable with Export)"});
export(to, exportPaths.resolveExportPaths(html));
}

View File

@ -48,6 +48,7 @@ import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Factory with different JSON creation methods placed to a single class.
@ -258,4 +259,16 @@ public class JSONFactory {
return tableEntries;
}
public Map<String, Object> listServers() {
Collection<Server> servers = dbSystem.getDatabase().query(ServerQueries.fetchPlanServerInformationCollection());
return Maps.builder(String.class, Object.class)
.put("servers", servers.stream()
.map(server -> Maps.builder(String.class, Object.class)
.put("serverUUID", server.getUuid().toString())
.put("serverName", server.getIdentifiableName())
.put("proxy", server.isProxy())
.build())
.collect(Collectors.toList()))
.build();
}
}

View File

@ -29,9 +29,12 @@ import com.djrapitops.plan.delivery.rendering.json.graphs.pie.Pie;
import com.djrapitops.plan.delivery.rendering.json.graphs.pie.WorldPie;
import com.djrapitops.plan.delivery.rendering.json.graphs.special.WorldMap;
import com.djrapitops.plan.delivery.rendering.json.graphs.stack.StackGraph;
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
import com.djrapitops.plan.delivery.web.resolver.request.URIQuery;
import com.djrapitops.plan.gathering.domain.FinishedSession;
import com.djrapitops.plan.gathering.domain.Ping;
import com.djrapitops.plan.gathering.domain.WorldTimes;
import com.djrapitops.plan.identification.Server;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.DataGatheringSettings;
@ -119,7 +122,9 @@ public class GraphJSONCreator {
"}}";
}
public Map<String, Object> optimizedPerformanceGraphJSON(ServerUUID serverUUID) {
public Map<String, Object> optimizedPerformanceGraphJSON(ServerUUID serverUUID, URIQuery query) {
long after = getAfter(query); // TODO Implement if performance issues become apparent.
long now = System.currentTimeMillis();
long twoMonthsAgo = now - TimeUnit.DAYS.toMillis(60);
long monthAgo = now - TimeUnit.DAYS.toMillis(30);
@ -131,6 +136,10 @@ public class GraphJSONCreator {
TPSMutator lowResolutionData = new TPSMutator(db.query(TPSQueries.fetchTPSDataOfServerInResolution(twoMonthsAgo, monthAgo, lowResolution, serverUUID)));
TPSMutator highResolutionData = new TPSMutator(db.query(TPSQueries.fetchTPSDataOfServer(monthAgo, now, serverUUID)));
String serverName = db.query(ServerQueries.fetchServerMatchingIdentifier(serverUUID))
.map(Server::getIdentifiableName)
.orElse(serverUUID.toString());
List<Number[]> values = lowestResolutionData.toArrays(new LineGraph.GapStrategy(
config.isTrue(DisplaySettings.GAPS_IN_GRAPH_DATA),
lowestResolution + TimeUnit.MINUTES.toMillis(1),
@ -172,9 +181,21 @@ public class GraphJSONCreator {
.put("diskThresholdMed", config.get(DisplaySettings.GRAPH_DISK_THRESHOLD_MED))
.put("diskThresholdHigh", config.get(DisplaySettings.GRAPH_DISK_THRESHOLD_HIGH))
.build())
.put("serverName", serverName)
.put("serverUUID", serverUUID)
.build();
}
private long getAfter(URIQuery query) {
try {
return query.get("after")
.map(Long::parseLong)
.orElse(0L) - 500L; // Some headroom for out-of-sync clock.
} catch (NumberFormatException badType) {
throw new BadRequestException("'after': " + badType.toString());
}
}
public String playersOnlineGraph(ServerUUID serverUUID) {
Database db = dbSystem.getDatabase();
long now = System.currentTimeMillis();

View File

@ -25,6 +25,7 @@ import com.djrapitops.plan.utilities.UnitSemaphoreAccessLock;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@ -62,7 +63,7 @@ public class AsyncJSONResolverService {
}
public <T> JSONStorage.StoredJSON resolve(
long newerThanTimestamp, DataID dataID, ServerUUID serverUUID, Function<ServerUUID, T> creator
Optional<Long> newerThanTimestamp, DataID dataID, ServerUUID serverUUID, Function<ServerUUID, T> creator
) {
String identifier = dataID.of(serverUUID);
Supplier<T> jsonCreator = () -> creator.apply(serverUUID);
@ -71,24 +72,29 @@ public class AsyncJSONResolverService {
public <T> JSONStorage.StoredJSON resolve(
long newerThanTimestamp, DataID dataID, Supplier<T> jsonCreator
Optional<Long> newerThanTimestamp, DataID dataID, Supplier<T> jsonCreator
) {
String identifier = dataID.name();
return getStoredOrCreateJSON(newerThanTimestamp, identifier, jsonCreator);
}
private <T> JSONStorage.StoredJSON getStoredOrCreateJSON(
long timestamp, String identifier, Supplier<T> jsonCreator
Optional<Long> givenTimestamp, String identifier, Supplier<T> jsonCreator
) {
JSONStorage.StoredJSON storedJSON = getNewFromCache(timestamp, identifier);
if (storedJSON != null) return storedJSON;
JSONStorage.StoredJSON storedJSON = null;
Future<JSONStorage.StoredJSON> updatedJSON = null;
if (givenTimestamp.isPresent()) {
long timestamp = givenTimestamp.get();
storedJSON = getNewFromCache(timestamp, identifier);
if (storedJSON != null) return storedJSON;
// No new enough version, let's refresh and send old version of the file
Future<JSONStorage.StoredJSON> updatedJSON = scheduleJSONForUpdate(timestamp, identifier, jsonCreator);
// No new enough version, let's refresh and send old version of the file
updatedJSON = scheduleJSONForUpdate(timestamp, identifier, jsonCreator);
storedJSON = getOldFromCache(timestamp, identifier);
}
storedJSON = getOldFromCache(timestamp, identifier);
if (storedJSON != null) {
return storedJSON;
return storedJSON; // Found old from cache
} else {
// Update not performed if the last update was recent and the file is deleted before next update
// Fall back to waiting for the updated file if old version of the file doesn't exist.

View File

@ -49,8 +49,8 @@ public enum DataID {
PLAYERBASE_OVERVIEW,
PERFORMANCE_OVERVIEW,
EXTENSION_NAV,
EXTENSION_TABS
;
EXTENSION_TABS,
LIST_SERVERS;
public String of(ServerUUID serverUUID) {
return name() + '-' + serverUUID;

View File

@ -22,6 +22,7 @@ import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.URIQuery;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.cache.AsyncJSONResolverService;
import com.djrapitops.plan.delivery.webserver.cache.DataID;
@ -87,14 +88,14 @@ public class GraphsJSONResolver implements Resolver {
}
private JSONStorage.StoredJSON getGraphJSON(Request request, DataID dataID) {
long timestamp = Identifiers.getTimestamp(request);
Optional<Long> timestamp = Identifiers.getTimestamp(request);
JSONStorage.StoredJSON storedJSON;
if (request.getQuery().get("server").isPresent()) {
ServerUUID serverUUID = identifiers.getServerUUID(request); // Can throw BadRequestException
storedJSON = jsonResolverService.resolve(
timestamp, dataID, serverUUID,
theServerUUID -> generateGraphDataJSONOfType(dataID, theServerUUID)
theServerUUID -> generateGraphDataJSONOfType(dataID, theServerUUID, request.getQuery())
);
} else {
// Assume network
@ -138,12 +139,12 @@ public class GraphsJSONResolver implements Resolver {
}
}
private Object generateGraphDataJSONOfType(DataID id, ServerUUID serverUUID) {
private Object generateGraphDataJSONOfType(DataID id, ServerUUID serverUUID, URIQuery query) {
switch (id) {
case GRAPH_PERFORMANCE:
return graphJSON.performanceGraphJSON(serverUUID);
case GRAPH_OPTIMIZED_PERFORMANCE:
return graphJSON.optimizedPerformanceGraphJSON(serverUUID);
return graphJSON.optimizedPerformanceGraphJSON(serverUUID, query);
case GRAPH_ONLINE:
return graphJSON.playersOnlineGraph(serverUUID);
case GRAPH_UNIQUE_NEW:

View File

@ -44,7 +44,8 @@ public class NetworkJSONResolver {
AsyncJSONResolverService asyncJSONResolverService, JSONFactory jsonFactory,
NetworkOverviewJSONCreator networkOverviewJSONCreator,
NetworkPlayerBaseOverviewJSONCreator networkPlayerBaseOverviewJSONCreator,
NetworkSessionsOverviewJSONCreator networkSessionsOverviewJSONCreator
NetworkSessionsOverviewJSONCreator networkSessionsOverviewJSONCreator,
NetworkPerformanceJSONResolver networkPerformanceJSONResolver
) {
this.asyncJSONResolverService = asyncJSONResolverService;
resolver = CompositeResolver.builder()
@ -53,6 +54,8 @@ public class NetworkJSONResolver {
.add("sessionsOverview", forJSON(DataID.SESSIONS_OVERVIEW, networkSessionsOverviewJSONCreator))
.add("servers", forJSON(DataID.SERVERS, jsonFactory::serversAsJSONMaps))
.add("pingTable", forJSON(DataID.PING_TABLE, jsonFactory::pingPerGeolocation))
.add("listServers", forJSON(DataID.LIST_SERVERS, jsonFactory::listServers))
.add("performanceOverview", networkPerformanceJSONResolver)
.build();
}

View File

@ -0,0 +1,194 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.domain.mutators.TPSMutator;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.gathering.domain.TPS;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.DisplaySettings;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.GenericLang;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.queries.objects.TPSQueries;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Creates JSON payload for /server-page Performance tab.
*
* @author AuroraLS3
*/
@Singleton
public class NetworkPerformanceJSONResolver implements Resolver {
private final PlanConfig config;
private final Locale locale;
private final DBSystem dbSystem;
private final Formatter<Double> decimals;
private final Formatter<Long> timeAmount;
private final Formatter<Double> percentage;
private final Formatter<Double> byteSize;
@Inject
public NetworkPerformanceJSONResolver(
PlanConfig config,
Locale locale,
DBSystem dbSystem,
Formatters formatters
) {
this.config = config;
this.locale = locale;
this.dbSystem = dbSystem;
decimals = formatters.decimals();
percentage = formatters.percentage();
timeAmount = formatters.timeAmount();
byteSize = formatters.byteSize();
}
@Override
public boolean canAccess(Request request) {
return request.getUser().orElse(new WebUser("")).hasPermission("page.network");
}
@Override
public Optional<Response> resolve(Request request) {
List<ServerUUID> serverUUIDs = request.getQuery().get("servers")
.map(this::getUUIDList)
.orElse(Collections.emptyList())
.stream().map(ServerUUID::from)
.collect(Collectors.toList());
return Optional.of(Response.builder()
.setJSONContent(createJSONAsMap(serverUUIDs))
.build());
}
private List<UUID> getUUIDList(String jsonString) {
return new Gson().fromJson(jsonString, new TypeToken<List<UUID>>() {}.getType());
}
public Map<String, Object> createJSONAsMap(Collection<ServerUUID> serverUUIDs) {
Map<String, Object> serverOverview = new HashMap<>();
Database db = dbSystem.getDatabase();
long now = System.currentTimeMillis();
long monthAgo = now - TimeUnit.DAYS.toMillis(30L);
Map<Integer, List<TPS>> tpsData = db.query(TPSQueries.fetchTPSDataOfServers(monthAgo, now, serverUUIDs));
serverOverview.put("numbers", createNumbersMap(tpsData));
return serverOverview;
}
private Map<String, Object> createNumbersMap(Map<Integer, List<TPS>> tpsData) {
long now = System.currentTimeMillis();
long dayAgo = now - TimeUnit.DAYS.toMillis(1L);
long weekAgo = now - TimeUnit.DAYS.toMillis(7L);
Map<String, Object> numbers = new HashMap<>();
List<TPS> tpsDataOfAllServers = new ArrayList<>();
tpsData.values().forEach(tpsDataOfAllServers::addAll);
TPSMutator tpsDataMonth = new TPSMutator(tpsDataOfAllServers);
TPSMutator tpsDataWeek = tpsDataMonth.filterDataBetween(weekAgo, now);
TPSMutator tpsDataDay = tpsDataWeek.filterDataBetween(dayAgo, now);
Map<Integer, TPSMutator> mutatorsOfServersMonth = new HashMap<>();
Map<Integer, TPSMutator> mutatorsOfServersWeek = new HashMap<>();
Map<Integer, TPSMutator> mutatorsOfServersDay = new HashMap<>();
for (Map.Entry<Integer, List<TPS>> entry : tpsData.entrySet()) {
TPSMutator mutator = new TPSMutator(entry.getValue());
mutatorsOfServersMonth.put(entry.getKey(), mutator);
mutatorsOfServersWeek.put(entry.getKey(), mutator.filterDataBetween(weekAgo, now));
mutatorsOfServersDay.put(entry.getKey(), mutator.filterDataBetween(dayAgo, now));
}
Integer tpsThreshold = config.get(DisplaySettings.GRAPH_TPS_THRESHOLD_MED);
numbers.put("low_tps_spikes_30d", tpsDataMonth.lowTpsSpikeCount(tpsThreshold));
numbers.put("low_tps_spikes_7d", tpsDataWeek.lowTpsSpikeCount(tpsThreshold));
numbers.put("low_tps_spikes_24h", tpsDataDay.lowTpsSpikeCount(tpsThreshold));
long downtimeMonth = getTotalDowntime(mutatorsOfServersMonth);
long downtimeWeek = getTotalDowntime(mutatorsOfServersWeek);
long downtimeDay = getTotalDowntime(mutatorsOfServersDay);
numbers.put("server_downtime_30d", timeAmount.apply(downtimeMonth));
numbers.put("server_downtime_7d", timeAmount.apply(downtimeWeek));
numbers.put("server_downtime_24h", timeAmount.apply(downtimeDay));
if (!tpsData.isEmpty()) {
numbers.put("avg_server_downtime_30d", timeAmount.apply(downtimeMonth / tpsData.size()));
numbers.put("avg_server_downtime_7d", timeAmount.apply(downtimeWeek / tpsData.size()));
numbers.put("avg_server_downtime_24h", timeAmount.apply(downtimeDay / tpsData.size()));
} else {
numbers.put("avg_server_downtime_30d", "-");
numbers.put("avg_server_downtime_7d", "-");
numbers.put("avg_server_downtime_24h", "-");
}
numbers.put("tps_30d", format(tpsDataMonth.averageTPS()));
numbers.put("tps_7d", format(tpsDataWeek.averageTPS()));
numbers.put("tps_24h", format(tpsDataDay.averageTPS()));
numbers.put("cpu_30d", formatPercentage(tpsDataMonth.averageCPU()));
numbers.put("cpu_7d", formatPercentage(tpsDataWeek.averageCPU()));
numbers.put("cpu_24h", formatPercentage(tpsDataDay.averageCPU()));
numbers.put("ram_30d", formatBytes(tpsDataMonth.averageRAM()));
numbers.put("ram_7d", formatBytes(tpsDataWeek.averageRAM()));
numbers.put("ram_24h", formatBytes(tpsDataDay.averageRAM()));
numbers.put("entities_30d", format((int) tpsDataMonth.averageEntities()));
numbers.put("entities_7d", format((int) tpsDataWeek.averageEntities()));
numbers.put("entities_24h", format((int) tpsDataDay.averageEntities()));
numbers.put("chunks_30d", format((int) tpsDataMonth.averageChunks()));
numbers.put("chunks_7d", format((int) tpsDataWeek.averageChunks()));
numbers.put("chunks_24h", format((int) tpsDataDay.averageChunks()));
return numbers;
}
private long getTotalDowntime(Map<Integer, TPSMutator> mutatorsOfServersMonth) {
long downTime = 0L;
for (TPSMutator tpsMutator : mutatorsOfServersMonth.values()) {
downTime += tpsMutator.serverDownTime();
}
return downTime;
}
private String format(double value) {
return value != -1 ? decimals.apply(value) : locale.get(GenericLang.UNAVAILABLE).toString();
}
private String formatBytes(double value) {
return value != -1 ? byteSize.apply(value) : locale.get(GenericLang.UNAVAILABLE).toString();
}
private String formatPercentage(double value) {
return value != -1 ? percentage.apply(value / 100.0) : locale.get(GenericLang.UNAVAILABLE).toString();
}
}

View File

@ -68,7 +68,7 @@ public class PlayerKillsJSONResolver implements Resolver {
private Response getResponse(Request request) {
ServerUUID serverUUID = identifiers.getServerUUID(request);
long timestamp = Identifiers.getTimestamp(request);
Optional<Long> timestamp = Identifiers.getTimestamp(request);
JSONStorage.StoredJSON storedJSON = jsonResolverService.resolve(timestamp, DataID.KILLS, serverUUID,
theUUID -> Collections.singletonMap("player_kills", jsonFactory.serverPlayerKillsAsJSONMap(theUUID))
);

View File

@ -78,7 +78,7 @@ public class PlayersTableJSONResolver implements Resolver {
}
private JSONStorage.StoredJSON getStoredJSON(Request request) {
long timestamp = Identifiers.getTimestamp(request);
Optional<Long> timestamp = Identifiers.getTimestamp(request);
JSONStorage.StoredJSON storedJSON;
if (request.getQuery().get("server").isPresent()) {
ServerUUID serverUUID = identifiers.getServerUUID(request); // Can throw BadRequestException

View File

@ -74,7 +74,7 @@ public class SessionsJSONResolver implements Resolver {
}
private JSONStorage.StoredJSON getStoredJSON(Request request) {
long timestamp = Identifiers.getTimestamp(request);
Optional<Long> timestamp = Identifiers.getTimestamp(request);
if (request.getQuery().get("server").isPresent()) {
ServerUUID serverUUID = identifiers.getServerUUID(request);
return jsonResolverService.resolve(timestamp, DataID.SESSIONS, serverUUID,

View File

@ -170,15 +170,21 @@ public class TPS implements DateHolder {
}
public Number[] toArray() {
double tps = getTicksPerSecond();
double cpu = getCPUUsage();
long ram = getUsedMemory();
int entities = getEntityCount();
int chunks = getChunksLoaded();
long disk = getFreeDiskSpace();
return new Number[]{
getDate(),
getPlayers(),
getTicksPerSecond(),
getCPUUsage(),
getUsedMemory(),
getEntityCount(),
getChunksLoaded(),
getFreeDiskSpace()
tps >= 0 ? tps : null,
cpu >= 0 ? cpu : null,
ram >= 0 ? ram : null,
entities >= 0 ? entities : null,
chunks >= 0 ? chunks : null,
disk >= 0 ? disk : null
};
}
}

View File

@ -105,16 +105,16 @@ public class Identifiers {
return uuidUtility.getUUIDOf(name);
}
public static long getTimestamp(Request request) {
public static Optional<Long> getTimestamp(Request request) {
try {
long currentTime = System.currentTimeMillis();
long timestamp = request.getQuery().get("timestamp")
.map(Long::parseLong)
.orElse(currentTime);
if (currentTime + TimeUnit.SECONDS.toMillis(10L) < timestamp) {
return currentTime;
return Optional.empty();
}
return timestamp;
return Optional.of(timestamp);
} catch (NumberFormatException nonNumberTimestamp) {
throw new BadRequestException("'timestamp' was not a number: " + nonNumberTimestamp.getMessage());
}

View File

@ -445,4 +445,31 @@ public class TPSQueries {
}
};
}
public static Query<Map<Integer, List<TPS>>> fetchTPSDataOfServers(long after, long before, Collection<ServerUUID> serverUUIDs) {
String sql = SELECT + "*" + FROM + TABLE_NAME +
WHERE + SERVER_ID + " IN " + ServerTable.selectServerIds(serverUUIDs) +
AND + DATE + ">=?" +
AND + DATE + "<=?" +
ORDER_BY + DATE;
System.out.println(sql);
return new QueryStatement<Map<Integer, List<TPS>>>(sql, 50000) {
@Override
public void prepare(PreparedStatement statement) throws SQLException {
statement.setLong(1, after);
statement.setLong(2, before);
}
@Override
public Map<Integer, List<TPS>> processResults(ResultSet set) throws SQLException {
Map<Integer, List<TPS>> data = new HashMap<>();
while (set.next()) {
int serverId = set.getInt(SERVER_ID);
data.computeIfAbsent(serverId, Lists::create)
.add(extractTPS(set));
}
return data;
}
};
}
}

View File

@ -17,11 +17,15 @@
package com.djrapitops.plan.storage.database.sql.tables;
import com.djrapitops.plan.identification.Server;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.storage.database.DBType;
import com.djrapitops.plan.storage.database.sql.building.CreateTableBuilder;
import com.djrapitops.plan.storage.database.sql.building.Insert;
import com.djrapitops.plan.storage.database.sql.building.Sql;
import com.djrapitops.plan.storage.database.sql.building.Update;
import org.apache.commons.text.TextStringBuilder;
import java.util.Collection;
import static com.djrapitops.plan.storage.database.sql.building.Sql.*;
@ -77,4 +81,12 @@ public class ServerTable {
.column(MAX_PLAYERS, Sql.INT).notNull().defaultValue("-1")
.toString();
}
public static String selectServerIds(Collection<ServerUUID> serverUUIDs) {
return '(' + SELECT + TABLE_NAME + '.' + SERVER_ID +
FROM + TABLE_NAME +
WHERE + TABLE_NAME + '.' + SERVER_UUID + " IN ('" +
new TextStringBuilder().appendWithSeparators(serverUUIDs, "','").build() +
"'))";
}
}

View File

@ -242,7 +242,9 @@ function loadservers(json, error) {
if (!servers || !servers.length) {
let elements = document.getElementsByClassName('nav-servers');
for (let i = 0; i < elements.length; i++) { elements[i].style.display = 'none'; }
for (let i = 0; i < elements.length; i++) {
elements[i].style.display = 'none';
}
document.getElementById('game-server-warning').classList.remove('hidden');
document.getElementById('data_server_list').innerHTML =
`<div class="card shadow mb-4"><div class="card-body"><p>No servers found in the database.</p><p>It appears that Plan is not installed on any game servers or not connected to the same database. See <a href="https://github.com/plan-player-analytics/Plan/wiki">wiki</a> for Network tutorial.</p></div></div>`
@ -444,4 +446,230 @@ function loadJoinAddressPie(json, error) {
} else if (error) {
document.getElementById('joinAddressPie').innerText = `Failed to load graph data: ${error}`;
}
}
function loadPerformanceServerOptions() {
const refreshElement = document.querySelector(`#performance .refresh-element`);
refreshElement.querySelector('i').addEventListener('click', () => {
if (refreshElement.querySelector('.refresh-notice').innerHTML.length) {
return;
}
onSelectPerformanceServers();
refreshElement.querySelector('.refresh-notice').innerHTML = '<i class="fa fa-fw fa-cog fa-spin"></i> Updating..';
});
const selector = document.getElementById('performance-server-selector');
jsonRequest('./v1/network/listServers', function (json, error) {
if (json) {
let options = ``;
for (let server of json.servers) {
options += `<option${server.proxy ? ' selected' : ''} data-plan-server-uuid="${server.serverUUID}">${server.serverName}</option>`
}
selector.innerHTML = options;
onSelectPerformanceServers();
} else if (error) {
selector.innerText = `Failed to load server list: ${error}`
}
});
}
async function onSelectPerformanceServers() {
const selector = document.getElementById('performance-server-selector');
const selectedServerUUIDs = [];
for (const option of selector.selectedOptions) {
selectedServerUUIDs.push(option.getAttribute('data-plan-server-uuid'));
}
const serverUUIDs = encodeURIComponent(JSON.stringify(selectedServerUUIDs));
const loadedJson = {
servers: [],
errors: [],
zones: {},
colors: {},
timestamp_f: ''
}
const time = new Date().getTime();
const monthMs = 2592000000;
const after = time - monthMs;
for (const serverUUID of selectedServerUUIDs) {
jsonRequest(`./v1/graph?type=optimizedPerformance&server=${serverUUID}&after=${after}`, (json, error) => {
if (json) {
loadedJson.servers.push(json);
loadedJson.zones = json.zones;
loadedJson.colors = json.colors;
loadedJson.timestamp_f = json.timestamp_f;
} else if (error) {
loadedJson.errors.push(error);
}
});
}
await awaitUntil(() => selectedServerUUIDs.length === (loadedJson.servers.length + loadedJson.errors.length));
jsonRequest(`./v1/network/performanceOverview?servers=${serverUUIDs}`, loadPerformanceValues);
if (loadedJson.errors.length) {
await loadPerformanceGraph(undefined, loadedJson.errors[0]);
} else {
await loadPerformanceGraph({
servers: loadedJson.servers,
zones: loadedJson.zones,
colors: loadedJson.colors
}, undefined);
}
const refreshElement = document.querySelector(`#performance .refresh-element`);
refreshElement.querySelector('.refresh-time').innerText = loadedJson.timestamp_f;
refreshElement.querySelector('.refresh-notice').innerHTML = "";
}
async function loadPerformanceGraph(json, error) {
if (json) {
const zones = {
tps: [{
value: json.zones.tpsThresholdMed,
color: json.colors.low
}, {
value: json.zones.tpsThresholdHigh,
color: json.colors.med
}, {
value: 30,
color: json.colors.high
}],
disk: [{
value: json.zones.diskThresholdMed,
color: json.colors.low
}, {
value: json.zones.diskThresholdHigh,
color: json.colors.med
}, {
value: Number.MAX_VALUE,
color: json.colors.high
}]
};
const serverData = [];
for (const server of json.servers) {
serverData.push({
serverName: server.serverName,
values: await mapToDataSeries(server.values)
});
}
const series = {
tps: [],
cpu: [],
ram: [],
entities: [],
chunks: [],
disk: []
}
for (const server of serverData) {
series.tps.push({
name: server.serverName, type: s.type.spline, tooltip: s.tooltip.twoDecimals,
data: server.values.tps, color: json.colors.high, zones: zones.tps, yAxis: 0
});
series.cpu.push({
name: server.serverName, type: s.type.spline, tooltip: s.tooltip.twoDecimals,
data: server.values.cpu, color: json.colors.cpu, yAxis: 0
});
series.ram.push({
name: server.serverName, type: s.type.spline, tooltip: s.tooltip.zeroDecimals,
data: server.values.ram, color: json.colors.ram, yAxis: 0
});
series.entities.push({
name: server.serverName, type: s.type.spline, tooltip: s.tooltip.zeroDecimals,
data: server.values.entities, color: json.colors.entities, yAxis: 0
});
series.chunks.push({
name: server.serverName, type: s.type.spline, tooltip: s.tooltip.zeroDecimals,
data: server.values.chunks, color: json.colors.chunks, yAxis: 0
});
series.disk.push({
name: server.serverName, type: s.type.spline, tooltip: s.tooltip.zeroDecimals,
data: server.values.disk, color: json.colors.high, zones: zones.disk, yAxis: 0
});
}
setTimeout(() => lineChart('tpsGraph', series.tps), 10);
setTimeout(() => lineChart('cpuGraph', series.cpu), 20);
setTimeout(() => lineChart('ramGraph', series.ram), 30);
setTimeout(() => lineChart('entityGraph', series.entities), 40);
setTimeout(() => lineChart('chunkGraph', series.chunks), 50);
setTimeout(() => lineChart('diskGraph', series.disk), 60);
} else if (error) {
const errorMessage = `Failed to load graph data: ${error}`;
document.getElementById('tpsGraph').innerText = errorMessage;
document.getElementById('cpuGraph').innerText = errorMessage;
document.getElementById('ramGraph').innerText = errorMessage;
document.getElementById('entityGraph').innerText = errorMessage;
document.getElementById('chunkGraph').innerText = errorMessage;
document.getElementById('diskGraph').innerText = errorMessage;
}
}
/* This function loads Performance tab */
function loadPerformanceValues(json, error) {
const tab = document.getElementById('performance');
if (error) {
displayError(tab, error);
return;
}
// as Numbers
let data = json.numbers;
let element = tab.querySelector('#data_numbers');
element.querySelector('#data_low_tps_spikes_30d').innerText = data.low_tps_spikes_30d;
element.querySelector('#data_low_tps_spikes_7d').innerText = data.low_tps_spikes_7d;
element.querySelector('#data_low_tps_spikes_24h').innerText = data.low_tps_spikes_24h;
element.querySelector('#data_server_downtime_30d').innerText = data.server_downtime_30d;
element.querySelector('#data_server_downtime_7d').innerText = data.server_downtime_7d;
element.querySelector('#data_server_downtime_24h').innerText = data.server_downtime_24h;
element.querySelector('#data_avg_server_downtime_30d').innerText = data.avg_server_downtime_30d;
element.querySelector('#data_avg_server_downtime_7d').innerText = data.avg_server_downtime_7d;
element.querySelector('#data_avg_server_downtime_24h').innerText = data.avg_server_downtime_24h;
element.querySelector('#data_tps_30d').innerText = data.tps_30d;
element.querySelector('#data_tps_7d').innerText = data.tps_7d;
element.querySelector('#data_tps_24h').innerText = data.tps_24h;
element.querySelector('#data_cpu_30d').innerText = data.cpu_30d;
element.querySelector('#data_cpu_7d').innerText = data.cpu_7d;
element.querySelector('#data_cpu_24h').innerText = data.cpu_24h;
element.querySelector('#data_ram_30d').innerText = data.ram_30d;
element.querySelector('#data_ram_7d').innerText = data.ram_7d;
element.querySelector('#data_ram_24h').innerText = data.ram_24h;
element.querySelector('#data_entities_30d').innerText = data.entities_30d;
element.querySelector('#data_entities_7d').innerText = data.entities_7d;
element.querySelector('#data_entities_24h').innerText = data.entities_24h;
element.querySelector('#data_chunks_30d').innerText = data.chunks_30d;
element.querySelector('#data_chunks_7d').innerText = data.chunks_7d;
element.querySelector('#data_chunks_24h').innerText = data.chunks_24h;
}
function loadPingGraph(json, error) {
if (json) {
const series = {
avgPing: {
name: s.name.avgPing,
type: s.type.spline,
tooltip: s.tooltip.twoDecimals,
data: json.avg_ping_series,
color: json.colors.avg
},
maxPing: {
name: s.name.maxPing,
type: s.type.spline,
tooltip: s.tooltip.zeroDecimals,
data: json.max_ping_series,
color: json.colors.max
},
minPing: {
name: s.name.minPing,
type: s.type.spline,
tooltip: s.tooltip.zeroDecimals,
data: json.min_ping_series,
color: json.colors.min
}
};
lineChart('pingGraph', [series.avgPing, series.maxPing, series.minPing]);
} else if (error) {
document.getElementById('pingGraph').innerText = `Failed to load graph data: ${error}`;
}
}

View File

@ -369,7 +369,7 @@ async function loadOptimizedPerformanceGraph(json, error) {
value: json.zones.diskThresholdMed,
color: json.colors.low
}, {
value: json.zones.tpsThresholdHigh,
value: json.zones.diskThresholdHigh,
color: json.colors.med
}, {
value: Number.MAX_VALUE,

View File

@ -120,4 +120,17 @@ function newConfiguredXHR(callback) {
};
return xhr;
}
function awaitUntil(predicateFunction) {
return new Promise((resolve => {
const handlerFunction = () => {
if (predicateFunction.apply()) {
resolve();
} else {
setTimeout(handlerFunction, 10)
}
};
handlerFunction();
}))
}

View File

@ -75,6 +75,9 @@
<a class="collapse-item nav-button" href="#tab-sessions-overview"><i
class="far fa-fw fa-calendar-alt"></i>
Sessions</a>
<a class="collapse-item nav-button" href="#tab-performance">
<i class="fas fa-fw fa-cogs"></i>
<span>Performance</span></a>
<hr class="nav-servers dropdown-divider mx-3 my-2">
<div class="nav-servers" id="navSrvContainer">
</div>
@ -471,6 +474,205 @@
</div>
</div> <!-- /.container-fluid -->
</div> <!-- End of Sessions tab -->
<!-- Begin Performance Tab -->
<div class="tab" id="performance">
<div class="container-fluid mt-4">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0 text-gray-800"><i class="sidebar-toggler fa fa-fw fa-bars"></i>${networkDisplayName}
&middot; Performance
<span class="refresh-element">
<i class="fa fa-fw fa-sync"></i> <span class="refresh-time"></span>
<span class="refresh-notice"><i class="fa fa-fw fa-cog fa-spin"></i> Updating..</span>
</span>
</h1>
</div>
<div class="row">
<!-- Performance Charts -->
<div class="col-xl-12 col-lg-12 col-sm-12">
<div class="card shadow mb-4">
<ul class="nav nav-tabs" id="performanceChartTab" role="tablist">
<li class="nav-item">
<a aria-controls="profile" aria-selected="true"
class="nav-link col-black active"
data-bs-toggle="tab"
href="#tps" id="performance-tps-tab" role="tab"><i
class="fa fa-fw fa-tachometer-alt col-deep-orange"></i> TPS</a>
</li>
<li class="nav-item">
<a aria-controls="contact" aria-selected="false" class="nav-link col-black"
data-bs-toggle="tab" href="#cpu" id="performance-cpu-tab"
role="tab"><i
class="fa fa-fw fa-tachometer-alt col-amber"></i>
CPU</a>
</li>
<li class="nav-item">
<a aria-controls="contact" aria-selected="false" class="nav-link col-black"
data-bs-toggle="tab" href="#ram" id="performance-ram-tab"
role="tab"><i
class="fa fa-fw fa-microchip col-light-green"></i>
RAM</a>
</li>
<li class="nav-item">
<a aria-controls="contact" aria-selected="false" class="nav-link col-black"
data-bs-toggle="tab" href="#entity" id="performance-entity-tab"
role="tab"><i class="fa fa-fw fa-dragon col-purple"></i>
Entities</a>
</li>
<li class="nav-item">
<a aria-controls="contact" aria-selected="false" class="nav-link col-black"
data-bs-toggle="tab" href="#chunk" id="performance-chunk-tab"
role="tab"><i class="fa fa-fw fa-map col-blue-grey"></i>
Chunks</a>
</li>
<li class="nav-item">
<a aria-controls="contact" aria-selected="false" class="nav-link col-black"
data-bs-toggle="tab" href="#ping" id="performance-ping-tab" role="tab"><i
class="fa fa-fw fa-signal col-amber"></i> Ping</a>
</li>
<li class="nav-item">
<a aria-controls="contact" aria-selected="false" class="nav-link col-black"
data-bs-toggle="tab" href="#disk" id="performance-disk-tab" role="tab"><i
class="fa fa-fw fa-hdd col-green"></i> Disk Space</a>
</li>
</ul>
<div class="tab-content" id="performanceChartTabContent">
<div aria-labelledby="performance-tps-tab" class="tab-pane show active" id="tps"
role="tabpanel">
<div class="chart-area" id="tpsGraph"><span class="loader"></span></div>
</div>
<div aria-labelledby="performance-hardware-tab" class="tab-pane" id="cpu"
role="tabpanel">
<div class="chart-area" id="cpuGraph"><span class="loader"></span></div>
</div>
<div aria-labelledby="performance-hardware-tab" class="tab-pane" id="ram"
role="tabpanel">
<div class="chart-area" id="ramGraph"><span class="loader"></span></div>
</div>
<div aria-labelledby="performance-entity-tab" class="tab-pane"
id="entity"
role="tabpanel">
<div class="chart-area" id="entityGraph"><span class="loader"></span></div>
</div>
<div aria-labelledby="performance-chunk-tab" class="tab-pane"
id="chunk"
role="tabpanel">
<div class="chart-area" id="chunkGraph"><span class="loader"></span></div>
</div>
<div aria-labelledby="performance-ping-tab" class="tab-pane" id="ping"
role="tabpanel">
<p>This data is from only the proxy server (bungeecord or velocity)</p>
<div class="chart-area" id="pingGraph"><span class="loader"></span></div>
</div>
<div aria-labelledby="performance-disk-tab" class="tab-pane" id="disk"
role="tabpanel">
<div class="chart-area" id="diskGraph"><span class="loader"></span></div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Performance as Numbers -->
<div class="col-lg-8 mb-8 col-sm-12">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold col-black"><i
class="fa fa-fw fa-book-open col-blue-grey"></i>
Performance as Numbers</h6>
</div>
<table class="table" id="data_numbers">
<thead>
<th></th>
<th>Last 30 days</th>
<th>Last 7 days</th>
<th>Last 24 hours</th>
</thead>
<tbody>
<tr>
<td><i class="fa fa-fw fa-exclamation-circle col-red"></i>
Low TPS Spikes
</td>
<td id="data_low_tps_spikes_30d"></td>
<td id="data_low_tps_spikes_7d"></td>
<td id="data_low_tps_spikes_24h"></td>
</tr>
<tr>
<td><i class="fa fa-fw fa-power-off col-red"></i>
Total Downtime (No Data)
</td>
<td id="data_server_downtime_30d"></td>
<td id="data_server_downtime_7d"></td>
<td id="data_server_downtime_24h"></td>
</tr>
<tr>
<td><i class="fa fa-fw fa-power-off col-red"></i>
Average Downtime / Server
</td>
<td id="data_avg_server_downtime_30d"></td>
<td id="data_avg_server_downtime_7d"></td>
<td id="data_avg_server_downtime_24h"></td>
</tr>
<tr>
<td><i class="fa fa-fw fa-tachometer-alt col-orange"></i> Average TPS</td>
<td id="data_tps_30d"></td>
<td id="data_tps_7d"></td>
<td id="data_tps_24h"></td>
</tr>
<tr>
<td><i class="fa fa-fw fa-tachometer-alt col-amber"></i> Average CPU Usage</td>
<td id="data_cpu_30d"></td>
<td id="data_cpu_7d"></td>
<td id="data_cpu_24h"></td>
</tr>
<tr>
<td><i class="fa fa-fw fa-microchip col-light-green"></i> Average RAM Usage</td>
<td id="data_ram_30d"></td>
<td id="data_ram_7d"></td>
<td id="data_ram_24h"></td>
</tr>
<tr>
<td><i class="fa fa-fw fa-dragon col-purple"></i> Average Entities</td>
<td id="data_entities_30d"></td>
<td id="data_entities_7d"></td>
<td id="data_entities_24h"></td>
</tr>
<tr>
<td>
<i class="fa fa-fw fa-map col-blue-grey"></i> Average Chunks
<i class="far fa-fw fa-eye hidden" id="sponge-chunk-warning"
title="Chunks unavailable on Sponge"></i>
</td>
<td id="data_chunks_30d"></td>
<td id="data_chunks_7d"></td>
<td id="data_chunks_24h"></td>
</tr>
</tbody>
</table>
</div>
</div> <!-- End of Performance as numbers-->
<!-- Insights -->
<div class="col-lg-4 mb-4 col-sm-12">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold col-black"><i
class="fa fa-fw fa-server col-light-green"></i>
Server selector</h6>
</div>
<select class="form-control" id="performance-server-selector" multiple>
<option selected>Proxy server</option>
<option>Server 1</option>
<option>Skyblock</option>
<option>Server 3</option>
</select>
<button class="btn bg-plan" onclick="onSelectPerformanceServers()">Apply</button>
</div>
</div>
</div>
</div> <!-- /.container-fluid -->
</div> <!-- End of Performance tab -->
<!-- Begin Playerbase Overview Tab -->
<div class="tab" id="playerbase-overview">
<div class="container-fluid mt-4">
@ -928,6 +1130,7 @@
refreshingJsonRequest("./v1/graph?type=joinAddressPie", loadJoinAddressPie, 'playerbase-overview');
refreshingJsonRequest("./v1/graph?type=activity", loadActivityGraphs, 'playerbase-overview');
refreshingJsonRequest("./v1/graph?type=geolocation", loadGeolocationGraph, 'geolocations');
refreshingJsonRequest("../v1/graph?type=aggregatedPing&server=${serverUUID}", loadPingGraph, 'performance');
setLoadingText('Sorting out plugin tables..');
@ -937,6 +1140,8 @@
responsive: true
});
loadPerformanceServerOptions();
setLoadingText('Almost done..');
openPage();
setLoadingText('Done.');

View File

@ -195,6 +195,8 @@ public class AccessControlTest {
"/v1/query,400",
"/v1/errors,200",
"/errors,200",
"/v1/network/listServers,200",
"/v1/network/performanceOverview?servers=[" + TestConstants.SERVER_UUID_STRING + "],200",
})
void levelZeroCanAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException {
int responseCode = access(resource, cookieLevel0);
@ -255,6 +257,8 @@ public class AccessControlTest {
"/v1/query,400",
"/v1/errors,403",
"/errors,403",
"/v1/network/listServers,403",
"/v1/network/performanceOverview?servers=[" + TestConstants.SERVER_UUID_STRING + "],403",
})
void levelOneCanAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException {
int responseCode = access(resource, cookieLevel1);
@ -315,6 +319,8 @@ public class AccessControlTest {
"/v1/query,403",
"/v1/errors,403",
"/errors,403",
"/v1/network/listServers,403",
"/v1/network/performanceOverview?servers=[" + TestConstants.SERVER_UUID_STRING + "],403",
})
void levelTwoCanAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException {
int responseCode = access(resource, cookieLevel2);
@ -372,7 +378,9 @@ public class AccessControlTest {
"/v1/players,403",
"/query,403",
"/v1/filters,403",
"/v1/query,403"
"/v1/query,403",
"/v1/network/listServers,403",
"/v1/network/performanceOverview?servers=[" + TestConstants.SERVER_UUID_STRING + "],403",
})
void levelHundredCanNotAccess(String resource, String expectedResponseCode) throws NoSuchAlgorithmException, IOException, KeyManagementException {
int responseCode = access(resource, cookieLevel100);