diff options
Diffstat (limited to 'src/zenserver/frontend/html/pages/network.js')
| -rw-r--r-- | src/zenserver/frontend/html/pages/network.js | 247 |
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)); + } +} |