// 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)); } }