From 9ed0d179757469312f6b68a094b6cd7be67c849e Mon Sep 17 00:00:00 2001 From: Benjamin Elsdon Date: Sat, 7 May 2022 21:36:17 +0800 Subject: [PATCH] RAM usage and player count graphs. Now uses serverHooks --- .../main/java/com/benj4/gcgm/GCGMPlugin.java | 24 ++- .../gcgm/handlers/ServerTickHandler.java | 15 +- .../server/websocket/WebSocketServer.java | 5 +- .../gcgm/server/websocket/json/TickData.java | 9 ++ .../gcgm/server/websocket/json/WSData.java | 6 +- .../java/com/benj4/gcgm/utils/GCGMUtils.java | 13 +- .../com/benj4/gcgm/utils/web/WebUtils.java | 4 +- web-interface/src/App.js | 5 +- .../src/Components/Dashboard/StatsWidget.css | 7 +- .../src/Components/Dashboard/StatsWidget.js | 150 ++++++++++-------- .../src/Context/WebsocketProvider.js | 61 +++++++ web-interface/src/Dashboard.css | 5 + web-interface/src/Dashboard.js | 52 ++++-- web-interface/src/util/util.js | 24 +++ 14 files changed, 284 insertions(+), 96 deletions(-) create mode 100644 gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/json/TickData.java create mode 100644 web-interface/src/Context/WebsocketProvider.js create mode 100644 web-interface/src/util/util.js diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/GCGMPlugin.java b/gc-plugin/src/main/java/com/benj4/gcgm/GCGMPlugin.java index d98b28b..2694931 100644 --- a/gc-plugin/src/main/java/com/benj4/gcgm/GCGMPlugin.java +++ b/gc-plugin/src/main/java/com/benj4/gcgm/GCGMPlugin.java @@ -6,9 +6,12 @@ import com.benj4.gcgm.utils.*; import com.benj4.gcgm.utils.web.WebUtils; import emu.grasscutter.Grasscutter; import emu.grasscutter.plugin.Plugin; +import emu.grasscutter.plugin.api.ServerHook; +import emu.grasscutter.server.dispatch.DispatchServer; import emu.grasscutter.server.event.EventHandler; import emu.grasscutter.server.event.HandlerPriority; import emu.grasscutter.server.event.game.ServerTickEvent; +import emu.grasscutter.server.game.GameServer; import emu.grasscutter.utils.Utils; import express.Express; @@ -18,7 +21,7 @@ public class GCGMPlugin extends Plugin { private static GCGMPlugin INSTANCE; EventHandler serverTickEventHandler; - private static WebSocketServer webSocketServer; + private WebSocketServer webSocketServer; private File webData; @Override @@ -57,12 +60,11 @@ public class GCGMPlugin extends Plugin { @Override public void onEnable() { if(webData.exists()) { - Express app = Grasscutter.getDispatchServer().getServer(); - WebUtils.addStaticFiles(app, webData); + WebUtils.addStaticFiles(webData); webSocketServer = new WebSocketServer(); - webSocketServer.start(app); + webSocketServer.start(); - Grasscutter.getPluginManager().registerListener(serverTickEventHandler); + serverTickEventHandler.register(); Grasscutter.getLogger().info("[GCGM] GCGM Enabled"); Grasscutter.getLogger().info("[GCGM] You can access your GM panel by navigating to " + GCGMUtils.GetDispatchAddress() + WebUtils.PAGE_ROOT); @@ -78,11 +80,19 @@ public class GCGMPlugin extends Plugin { Grasscutter.getLogger().info("[GCGM] GCGM Disabled"); } - public static GCGMPlugin GetInstance() { + public static GCGMPlugin getInstance() { return INSTANCE; } - public static WebSocketServer getWebSocketServer() { + public WebSocketServer getWebSocketServer() { return webSocketServer; } + + public static GameServer getGameServer() { + return GCGMPlugin.getInstance().getServer(); + } + + public static DispatchServer getDispatchServer() { + return ServerHook.getInstance().getDispatchServer(); + } } diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/handlers/ServerTickHandler.java b/gc-plugin/src/main/java/com/benj4/gcgm/handlers/ServerTickHandler.java index bc04937..655cf15 100644 --- a/gc-plugin/src/main/java/com/benj4/gcgm/handlers/ServerTickHandler.java +++ b/gc-plugin/src/main/java/com/benj4/gcgm/handlers/ServerTickHandler.java @@ -1,24 +1,35 @@ package com.benj4.gcgm.handlers; import com.benj4.gcgm.GCGMPlugin; +import com.benj4.gcgm.server.websocket.json.TickData; import com.benj4.gcgm.server.websocket.json.WSData; +import com.benj4.gcgm.utils.GCGMUtils; +import com.google.gson.JsonObject; import emu.grasscutter.server.event.game.ServerTickEvent; import emu.grasscutter.utils.EventConsumer; import java.time.Instant; public class ServerTickHandler implements EventConsumer { + private static Instant firstTick; private static Instant lastTick; @Override public void consume(ServerTickEvent serverTickEvent) { if(lastTick != null) { Instant now = Instant.now(); - long timeTaken = now.toEpochMilli() - lastTick.toEpochMilli(); - GCGMPlugin.getWebSocketServer().broadcast(new WSData("tick", timeTaken)); + TickData data = new TickData(); + data.tickTimeElapsed = now.toEpochMilli() - lastTick.toEpochMilli(); + data.serverUptime = lastTick.toEpochMilli() - firstTick.toEpochMilli(); + data.getFreeMemory = GCGMUtils.GetFreeJVMMemory(); + data.getAllocatedMemory = GCGMUtils.GetAllocatedJVMMemory(); + data.playerCount = GCGMPlugin.getGameServer().getPlayers().size(); + + GCGMPlugin.getInstance().getWebSocketServer().broadcast(new WSData("tick", data)); lastTick = now; } else { lastTick = Instant.now(); + firstTick = Instant.now(); } } } diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/WebSocketServer.java b/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/WebSocketServer.java index 61d6e5f..64afffe 100644 --- a/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/WebSocketServer.java +++ b/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/WebSocketServer.java @@ -1,5 +1,6 @@ package com.benj4.gcgm.server.websocket; +import com.benj4.gcgm.GCGMPlugin; import com.benj4.gcgm.server.websocket.json.WSData; import emu.grasscutter.Grasscutter; import express.Express; @@ -16,7 +17,9 @@ public class WebSocketServer { //SocketIOServer socketIOServer; private static Map userUsernameMap = new ConcurrentHashMap<>(); - public void start(Express app) { + public void start() { + Express app = GCGMPlugin.getDispatchServer().getServer(); + app.ws("/gm", ws -> { ws.onConnect(ctx -> { String username = "Not logged in"; diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/json/TickData.java b/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/json/TickData.java new file mode 100644 index 0000000..05b4a97 --- /dev/null +++ b/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/json/TickData.java @@ -0,0 +1,9 @@ +package com.benj4.gcgm.server.websocket.json; + +public class TickData { + public long tickTimeElapsed; + public long serverUptime; + public long getFreeMemory; + public long getAllocatedMemory; + public int playerCount; +} diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/json/WSData.java b/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/json/WSData.java index e36a8bd..a2bd0bb 100644 --- a/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/json/WSData.java +++ b/gc-plugin/src/main/java/com/benj4/gcgm/server/websocket/json/WSData.java @@ -2,10 +2,10 @@ package com.benj4.gcgm.server.websocket.json; public class WSData { public String eventName; - public Object object; + public Object data; - public WSData(String eventName, Object object) { + public WSData(String eventName, Object data) { this.eventName = eventName; - this.object = object; + this.data = data; } } diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/utils/GCGMUtils.java b/gc-plugin/src/main/java/com/benj4/gcgm/utils/GCGMUtils.java index 51e3740..00e5737 100644 --- a/gc-plugin/src/main/java/com/benj4/gcgm/utils/GCGMUtils.java +++ b/gc-plugin/src/main/java/com/benj4/gcgm/utils/GCGMUtils.java @@ -2,6 +2,7 @@ package com.benj4.gcgm.utils; import com.benj4.gcgm.GCGMPlugin; import emu.grasscutter.Grasscutter; +import emu.grasscutter.server.game.GameServer; import java.io.File; import java.io.IOException; @@ -11,6 +12,8 @@ import java.nio.file.StandardCopyOption; public class GCGMUtils { + private static Runtime RUNTIME = Runtime.getRuntime(); + public static String GetDispatchAddress() { return "http" + (Grasscutter.getConfig().getDispatchOptions().FrontHTTPS ? "s" : "") + "://" + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().PublicIp) + @@ -20,7 +23,7 @@ public class GCGMUtils { public static boolean CopyFile(String resourceName, String copyLocation) { try { Grasscutter.getLogger().info("[GCGM] Copying 'DefaultWebApp.zip' to './plugins/GCGM'"); - Files.copy(GCGMPlugin.GetInstance().getResource(resourceName), Paths.get(new File(copyLocation).toURI()), StandardCopyOption.REPLACE_EXISTING); + Files.copy(GCGMPlugin.getInstance().getResource(resourceName), Paths.get(new File(copyLocation).toURI()), StandardCopyOption.REPLACE_EXISTING); return true; } catch (IOException e) { Grasscutter.getLogger().error(String.format("[GCGM] An error occurred while trying to copy '%s' to '%s'", resourceName, copyLocation)); @@ -28,4 +31,12 @@ public class GCGMUtils { return false; } } + + public static long GetFreeJVMMemory() { + return GCGMUtils.RUNTIME.freeMemory(); + } + + public static long GetAllocatedJVMMemory() { + return GCGMUtils.RUNTIME.totalMemory(); + } } diff --git a/gc-plugin/src/main/java/com/benj4/gcgm/utils/web/WebUtils.java b/gc-plugin/src/main/java/com/benj4/gcgm/utils/web/WebUtils.java index 442a9a3..da01ea9 100644 --- a/gc-plugin/src/main/java/com/benj4/gcgm/utils/web/WebUtils.java +++ b/gc-plugin/src/main/java/com/benj4/gcgm/utils/web/WebUtils.java @@ -1,5 +1,6 @@ package com.benj4.gcgm.utils.web; +import com.benj4.gcgm.GCGMPlugin; import emu.grasscutter.Grasscutter; import emu.grasscutter.utils.Utils; import express.Express; @@ -12,7 +13,8 @@ public class WebUtils { public static final String PAGE_ROOT = "/gm"; - public static void addStaticFiles(Express app, File staticRoot) { + public static void addStaticFiles(File staticRoot) { + Express app = GCGMPlugin.getDispatchServer().getServer(); app.raw().config.precompressStaticFiles = false; // MUST BE SET TO FALSE OR FILES SUCH AS IMAGES WILL APPEAR CORRUPTED app.raw().config.addStaticFiles(PAGE_ROOT, staticRoot.getAbsolutePath(), Location.EXTERNAL); app.raw().config.addSinglePageRoot(PAGE_ROOT, Utils.toFilePath(staticRoot.getPath() + "/index.html"), Location.EXTERNAL); diff --git a/web-interface/src/App.js b/web-interface/src/App.js index a2e6d61..d0829c0 100644 --- a/web-interface/src/App.js +++ b/web-interface/src/App.js @@ -1,8 +1,11 @@ +import { WebsocketProvider } from './Context/WebsocketProvider'; import Dashboard from './Dashboard'; function App() { return ( - + + + ); } diff --git a/web-interface/src/Components/Dashboard/StatsWidget.css b/web-interface/src/Components/Dashboard/StatsWidget.css index 9046264..63939d0 100644 --- a/web-interface/src/Components/Dashboard/StatsWidget.css +++ b/web-interface/src/Components/Dashboard/StatsWidget.css @@ -1,12 +1,11 @@ .graph { - position: absolute; - transform: translate(-50%, 0%); - top: 6%; - left: 50%; + float: left; border-color: grey; border-width: 1px; border-style: solid; border-radius: 8px; width: 800px; + margin: 10px; + margin-left: 50px; text-align: center; } \ No newline at end of file diff --git a/web-interface/src/Components/Dashboard/StatsWidget.js b/web-interface/src/Components/Dashboard/StatsWidget.js index d6170c4..efd031e 100644 --- a/web-interface/src/Components/Dashboard/StatsWidget.js +++ b/web-interface/src/Components/Dashboard/StatsWidget.js @@ -1,94 +1,114 @@ -import React, { useState, useEffect } from "react"; -import { - Chart as ChartJS, - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend, - } from 'chart.js'; -import { useWebSocket, ReadyState } from "react-use-websocket/dist/lib/use-websocket"; +import React, { Component, useState, useEffect } from "react"; +import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend } from "chart.js"; import { Line } from "react-chartjs-2"; import "./StatsWidget.css"; +import WebsocketContext, { useWebsocket } from "../../Context/WebsocketProvider"; +import { toHaveStyle } from "@testing-library/jest-dom/dist/matchers"; -ChartJS.register( - CategoryScale, - LinearScale, - PointElement, - LineElement, - ); +ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement); - const CHART_COLORS = { - red: 'rgb(255, 99, 132)', - orange: 'rgb(255, 159, 64)', - yellow: 'rgb(255, 205, 86)', - green: 'rgb(75, 192, 192)', - blue: 'rgb(54, 162, 235)', - purple: 'rgb(153, 102, 255)', - grey: 'rgb(201, 203, 207)' - }; +const CHART_COLORS = { + red: "rgb(255, 99, 132)", + orange: "rgb(255, 159, 64)", + yellow: "rgb(255, 205, 86)", + green: "rgb(75, 192, 192)", + blue: "rgb(54, 162, 235)", + purple: "rgb(153, 102, 255)", + grey: "rgb(201, 203, 207)", +}; -const TICK_COUNT = 50; +const TICK_COUNT = 25; const labels = []; for (let i = 0; i <= TICK_COUNT; ++i) { - labels.push(i.toString()); + labels.push(i.toString()); } -export default function StatsWidget() { - const protocol = window.location.protocol === "https:" ? "wss" : "wss" - const host = window.location.host; - const port = window.location.port; +export default class StatsWidget extends Component { + constructor(props) { + super(props); - const { sendMessage, lastMessage, readyState } = useWebSocket(protocol + "://"+ host +":" + port + "/gm"); + this.state = { + graphHistory: [], + }; + } + + componentDidUpdate(lastProps) { + if (this.props.data !== this.props.ignoreData) { + if (this.props.serverUptime !== lastProps.serverUptime) { + var newGraph = this.state.graphHistory; + console.log(this.props); + newGraph.push({ tick: this.props.data }); + + if (newGraph.length > TICK_COUNT) { + newGraph.splice(0, 1); + } + + this.setState({ + graphHistory: newGraph, + }); + } + } + } + + render() { + return ( +
+

{ this.props.title }

+

{JSON.stringify(this.state.data)}

+ this.state.graphHistory.length - i), + datasets: [ + { + label: "", + data: this.state.graphHistory.map((gh) => gh.tick), + borderColor: CHART_COLORS.red, + fill: false, + cubicInterpolationMode: "monotone", + tension: 0.4, + }, + ], + }} + options={{ + y: { + suggestedMin: this.props.suggestedYMin, + suggestedMax: this.props.suggestedYMax, + }, + }} + /> +
+ ); + } +} + +/*export default function StatsWidget() { const [graphHistory, setGraphHistory] = useState([]); + const [lastMessage, connectionStatus, sendMessage] = useWebsocket(); useEffect(() => { - if (lastMessage !== null) { - const data = JSON.parse(lastMessage.data); + console.log(lastMessage()) + if (lastMessage() !== null) { + const { data } = JSON.parse(lastMessage().data); var newGraph = graphHistory; - newGraph.push({ time: new Date().getTime(), tick: data.object}); + newGraph.push({ time: new Date().getTime(), tick: data.tickTimeElapsed }); if(newGraph.length > TICK_COUNT) { newGraph.splice(0, 1); } setGraphHistory(newGraph); } - }, [lastMessage, setGraphHistory]); + }, [graphHistory, lastMessage, setGraphHistory]); - /*const connectionStatus = { + const connectionStatus = { [ReadyState.CONNECTING]: "Connecting", [ReadyState.OPEN]: "Open", [ReadyState.CLOSING]: "Closing", [ReadyState.CLOSED]: "Closed", [ReadyState.UNINSTANTIATED]: "Uninstantiated", - }[readyState];*/ + }[readyState]; return ( -
-

Server Performance (Ticks)

- l.tick), - borderColor: CHART_COLORS.red, - fill: false, - cubicInterpolationMode: 'monotone', - tension: 0.4 - } - ] - }} - options={{ - y: { - suggestedMin: 995, - suggestedMax: 1005 - } - }}/> -
+ ); -} +}*/ diff --git a/web-interface/src/Context/WebsocketProvider.js b/web-interface/src/Context/WebsocketProvider.js new file mode 100644 index 0000000..0181c3a --- /dev/null +++ b/web-interface/src/Context/WebsocketProvider.js @@ -0,0 +1,61 @@ +import React, { useContext, useCallback, useState } from 'react' +import { useWebSocket as Websocket, ReadyState } from "react-use-websocket/dist/lib/use-websocket"; +import { ReactIsInDevelopmentMode } from '../util/util'; + +const WebsocketContext = React.createContext(); + +const protocol = window.location.protocol === "https:" ? "wss" : "wss" +const host = window.location.host; +const port = window.location.port; + +function getWebsocketURL() { + if(ReactIsInDevelopmentMode) { + return "wss://localhost:443/gm"; + } else { + return protocol + "://"+ host +":" + port + "/gm"; + } +} + +export function useWebsocket() { + return useContext(WebsocketContext); +} + +export function WebsocketProvider({ children }) { + const { sendMessage, lastMessage, readyState } = Websocket(getWebsocketURL()); + + const connectionStatus = useCallback(() => { + if(readyState === ReadyState.CONNECTING) { + return "Connecting"; + } else if(readyState === ReadyState.OPEN) { + return "Open"; + } else if(readyState === ReadyState.CLOSING) { + return "Closing"; + } else if(readyState === ReadyState.CLOSED) { + return "Closed"; + } else if(readyState === ReadyState.UNINSTANTIATED) { + return "Uninstantiated"; + } + }, [readyState]); + + const getLastMessage = useCallback(() => { + if(lastMessage != null) { + if(lastMessage.data != null) { + return JSON.parse(lastMessage.data); + } + } + + return {}; + }, [lastMessage]); + + const send = useCallback((data) => { + sendMessage(data, false); + }, [sendMessage]); + + return ( + + {children} + + ) +} + +export default WebsocketContext; \ No newline at end of file diff --git a/web-interface/src/Dashboard.css b/web-interface/src/Dashboard.css index e140fa0..89fdd6d 100644 --- a/web-interface/src/Dashboard.css +++ b/web-interface/src/Dashboard.css @@ -3,4 +3,9 @@ background-color: #F8F9FB; width: 100vw; height: 100vh; +} + +.content { + float: left; + width: calc(100vw - 350px); } \ No newline at end of file diff --git a/web-interface/src/Dashboard.js b/web-interface/src/Dashboard.js index 49663df..75f6865 100644 --- a/web-interface/src/Dashboard.js +++ b/web-interface/src/Dashboard.js @@ -1,16 +1,46 @@ -import React, { Component } from 'react' -import Sidepanel from './Components/Dashboard/Sidepanel/Sidepanel' -import StatsWidget from './Components/Dashboard/StatsWidget' +import React, { Component } from "react"; +import Sidepanel from "./Components/Dashboard/Sidepanel/Sidepanel"; +import StatsWidget from "./Components/Dashboard/StatsWidget"; +import WebsocketContext, { useWebsocket } from "./Context/WebsocketProvider"; -import './Dashboard.css' +import "./Dashboard.css"; +import { bytesToMegabytes } from "./util/util"; export default class Dashboard extends Component { - render() { - return ( -
- - + static contextType = WebsocketContext; + + constructor(props) { + super(props); + + this.state = { + lastMessage: { + serverUptime: 0, + }, + }; + } + + componentDidUpdate() { + if (typeof this.context.lastMessage().data !== "undefined") { + if (this.context.lastMessage().data.serverUptime !== this.state.lastMessage.serverUptime) { + //console.log(this.context.lastMessage().data); + this.setState({ + lastMessage: this.context.lastMessage().data, + }); + } + } + } + + render() { + return ( +
+ +
+ + + +
- ) - } +
+ ); + } } diff --git a/web-interface/src/util/util.js b/web-interface/src/util/util.js new file mode 100644 index 0000000..8e7e427 --- /dev/null +++ b/web-interface/src/util/util.js @@ -0,0 +1,24 @@ +import React from 'react'; + +export function ReactIsInDevelopmentMode() { + return '_self' in React.createElement('div'); +} + +export function formatBytesToString(bytes, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + +export function bytesToMegabytes(bytes, decimals = 2) { + const dm = decimals < 0 ? 0 : decimals; + + const k = 1024; + return parseFloat((bytes / Math.pow(k, 2)).toFixed(dm)); +} \ No newline at end of file