aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/frontend/html/pages/network.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/zenserver/frontend/html/pages/network.js')
-rw-r--r--src/zenserver/frontend/html/pages/network.js247
1 files changed, 247 insertions, 0 deletions
diff --git a/src/zenserver/frontend/html/pages/network.js b/src/zenserver/frontend/html/pages/network.js
new file mode 100644
index 000000000..c41a6ed85
--- /dev/null
+++ b/src/zenserver/frontend/html/pages/network.js
@@ -0,0 +1,247 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+"use strict";
+
+import { ZenPage } from "./page.js"
+import { Fetcher } from "../util/fetcher.js"
+import { Friendly } from "../util/friendly.js"
+import { Table } from "../util/widgets.js"
+
+////////////////////////////////////////////////////////////////////////////////
+export class Page extends ZenPage
+{
+ generate_crumbs() {}
+
+ async main()
+ {
+ this.set_title("network");
+
+ const providers_data = await new Fetcher().resource("stats").json();
+ const provider_list = providers_data["providers"] || [];
+ const all_stats = {};
+ await Promise.all(provider_list.map(async (provider) => {
+ all_stats[provider] = await new Fetcher().resource("stats", provider).json();
+ }));
+
+ this._all_stats = all_stats;
+
+ // Overview
+ this._render_overview(all_stats);
+
+ // Per-service request breakdown
+ this._render_services(all_stats, provider_list);
+
+ // Proxy connections (if available)
+ if (all_stats["proxy"])
+ {
+ this._render_proxy(all_stats["proxy"]);
+ }
+
+ // WebSocket live updates
+ this.connect_stats_ws((all_stats) => {
+ this._all_stats = all_stats;
+ this._populate_overview(all_stats);
+ this._populate_services(all_stats, Object.keys(all_stats));
+ });
+ }
+
+ _render_overview(all_stats)
+ {
+ const section = this.add_section("Overview");
+ this._overview_grid = section.tag().classify("grid").classify("info-tiles");
+ this._populate_overview(all_stats);
+ }
+
+ _populate_overview(all_stats)
+ {
+ this._overview_grid.inner().innerHTML = "";
+
+ const safe = (obj, path) => path.split(".").reduce((a, b) => a && a[b], obj);
+
+ // HTTP overview tile
+ {
+ const tile = this._overview_grid.tag().classify("card").classify("info-tile");
+ tile.tag().classify("card-title").text("HTTP");
+ const list = tile.tag().classify("info-props");
+
+ let totalRequests = 0;
+ let totalRate = 0;
+ for (const p in all_stats)
+ {
+ totalRequests += (safe(all_stats[p], "requests.count") || 0);
+ totalRate += (safe(all_stats[p], "requests.rate_1") || 0);
+ }
+
+ this._prop(list, "total requests", Friendly.sep(totalRequests));
+ if (totalRate > 0)
+ {
+ this._prop(list, "req/sec (1m)", Friendly.sep(totalRate, 1) + "/s");
+ }
+
+ const httpStats = all_stats["http"];
+ if (httpStats)
+ {
+ if (httpStats.distinct_clients)
+ {
+ this._prop(list, "distinct clients", Friendly.sep(httpStats.distinct_clients));
+ }
+ if (httpStats.distinct_sessions)
+ {
+ this._prop(list, "distinct sessions", Friendly.sep(httpStats.distinct_sessions));
+ }
+
+ const bytes = httpStats.bytes || {};
+ if (bytes.received)
+ {
+ this._prop(list, "received", Friendly.bytes(bytes.received));
+ }
+ if (bytes.sent)
+ {
+ this._prop(list, "sent", Friendly.bytes(bytes.sent));
+ }
+
+ const req = httpStats.requests || {};
+ if (req.t_avg)
+ {
+ this._prop(list, "avg latency", Friendly.duration(req.t_avg));
+ }
+ if (req.t_p95)
+ {
+ this._prop(list, "p95 latency", Friendly.duration(req.t_p95));
+ }
+ if (req.t_p99)
+ {
+ this._prop(list, "p99 latency", Friendly.duration(req.t_p99));
+ }
+ }
+ }
+
+ // WebSocket tile
+ {
+ const ws = all_stats["http"] ? (all_stats["http"]["websockets"] || {}) : {};
+ const tile = this._overview_grid.tag().classify("card").classify("info-tile");
+ tile.tag().classify("card-title").text("WebSocket");
+ const list = tile.tag().classify("info-props");
+
+ this._prop(list, "active connections", Friendly.sep(ws.active_connections || 0));
+ this._prop(list, "frames received", Friendly.sep(ws.frames_received || 0));
+ this._prop(list, "frames sent", Friendly.sep(ws.frames_sent || 0));
+
+ const totalBytes = (ws.bytes_received || 0) + (ws.bytes_sent || 0);
+ if (totalBytes > 0)
+ {
+ this._prop(list, "bytes received", Friendly.bytes(ws.bytes_received || 0));
+ this._prop(list, "bytes sent", Friendly.bytes(ws.bytes_sent || 0));
+ }
+ }
+ }
+
+ _render_services(all_stats, provider_list)
+ {
+ const section = this.add_section("Requests by Service");
+ this._services_host = section.tag();
+ this._populate_services(all_stats, provider_list);
+ }
+
+ _populate_services(all_stats, provider_list)
+ {
+ this._services_host.inner().innerHTML = "";
+
+ const safe = (obj, path) => path.split(".").reduce((a, b) => a && a[b], obj);
+
+ const table = new Table(this._services_host,
+ ["service", "requests", "req/sec", "avg", "p95", "p99", "received", "sent"],
+ Table.Flag_FitLeft | Table.Flag_PackRight | Table.Flag_AlignNumeric, -1);
+
+ // Sort providers by request count (descending)
+ const sorted = provider_list
+ .filter(p => all_stats[p] && (safe(all_stats[p], "requests.count") || 0) > 0)
+ .sort((a, b) => (safe(all_stats[b], "requests.count") || 0) - (safe(all_stats[a], "requests.count") || 0));
+
+ for (const provider of sorted)
+ {
+ const stats = all_stats[provider];
+ const req = stats.requests || {};
+ const bytes = stats.bytes || {};
+
+ table.add_row(
+ provider,
+ Friendly.sep(req.count || 0),
+ req.rate_1 ? Friendly.sep(req.rate_1, 1) + "/s" : "-",
+ req.t_avg ? Friendly.duration(req.t_avg) : "-",
+ req.t_p95 ? Friendly.duration(req.t_p95) : "-",
+ req.t_p99 ? Friendly.duration(req.t_p99) : "-",
+ bytes.received ? Friendly.bytes(bytes.received) : "-",
+ bytes.sent ? Friendly.bytes(bytes.sent) : "-",
+ );
+ }
+ }
+
+ _render_proxy(proxyStats)
+ {
+ const mappings = proxyStats.mappings || [];
+ if (mappings.length === 0)
+ {
+ return;
+ }
+
+ const section = this.add_section("Proxy");
+
+ for (const mapping of mappings)
+ {
+ const sub = section.tag().classify("card").style("marginBottom", "16px");
+ sub.tag().classify("card-title").text(mapping.name || "mapping");
+
+ const grid = sub.tag().classify("grid").classify("info-tiles");
+
+ // Overview tile
+ {
+ const tile = grid.tag().classify("info-tile");
+ tile.tag().style("fontWeight", "600").style("fontSize", "12px")
+ .style("color", "var(--theme_g1)").style("textTransform", "uppercase")
+ .style("letterSpacing", "0.5px").style("marginBottom", "8px")
+ .text("Stats");
+ const list = tile.tag().classify("info-props");
+
+ this._prop(list, "active connections", Friendly.sep(mapping.activeConnections || 0));
+ this._prop(list, "peak connections", Friendly.sep(mapping.peakActiveConnections || 0));
+ this._prop(list, "total connections", Friendly.sep(mapping.totalConnections || 0));
+ this._prop(list, "from client", Friendly.bytes(mapping.bytesFromClient || 0));
+ this._prop(list, "to client", Friendly.bytes(mapping.bytesToClient || 0));
+ if (mapping.requestRate1)
+ {
+ this._prop(list, "req/sec (1m)", Friendly.sep(mapping.requestRate1, 1) + "/s");
+ }
+ }
+
+ // Active connections table
+ const connections = mapping.connections || [];
+ if (connections.length > 0)
+ {
+ const connHost = sub.tag().style("marginTop", "12px");
+ const connTable = new Table(connHost,
+ ["client", "requests", "from client", "to client", "duration", "websocket"],
+ Table.Flag_FitLeft | Table.Flag_PackRight, -1);
+
+ for (const conn of connections)
+ {
+ connTable.add_row(
+ conn.client || "-",
+ Friendly.sep(conn.requests || 0),
+ Friendly.bytes(conn.bytesFromClient || 0),
+ Friendly.bytes(conn.bytesToClient || 0),
+ conn.durationMs ? Friendly.duration(conn.durationMs / 1000) : "-",
+ conn.websocket ? "yes" : "no",
+ );
+ }
+ }
+ }
+ }
+
+ _prop(parent, label, value)
+ {
+ const row = parent.tag().classify("info-prop");
+ row.tag().classify("info-prop-label").text(label);
+ row.tag().classify("info-prop-value").text(String(value));
+ }
+}