aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/frontend/html/pages/metrics.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/zenserver/frontend/html/pages/metrics.js')
-rw-r--r--src/zenserver/frontend/html/pages/metrics.js232
1 files changed, 232 insertions, 0 deletions
diff --git a/src/zenserver/frontend/html/pages/metrics.js b/src/zenserver/frontend/html/pages/metrics.js
new file mode 100644
index 000000000..e7a2eca67
--- /dev/null
+++ b/src/zenserver/frontend/html/pages/metrics.js
@@ -0,0 +1,232 @@
+// 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 { PropTable, Toolbar } from "../util/widgets.js"
+
+////////////////////////////////////////////////////////////////////////////////
+class TemporalStat
+{
+ constructor(data, as_bytes)
+ {
+ this._data = data;
+ this._as_bytes = as_bytes;
+ }
+
+ toString()
+ {
+ const columns = [
+ /* count */ {},
+ /* rate */ {},
+ /* t */ {}, {},
+ ];
+ const data = this._data;
+ for (var key in data)
+ {
+ var out = columns[0];
+ if (key.startsWith("rate_")) out = columns[1];
+ else if (key.startsWith("t_p")) out = columns[3];
+ else if (key.startsWith("t_")) out = columns[2];
+ out[key] = data[key];
+ }
+
+ var friendly = this._as_bytes ? Friendly.bytes : Friendly.sep;
+
+ var content = "";
+ for (var i = 0; i < columns.length; ++i)
+ {
+ const column = columns[i];
+ for (var key in column)
+ {
+ var value = column[key];
+ if (i)
+ {
+ value = Friendly.sep(value, 2);
+ key = key.padStart(9);
+ content += key + ": " + value;
+ }
+ else
+ content += friendly(value);
+ content += "\r\n";
+ }
+ }
+
+ return content;
+ }
+
+ tag()
+ {
+ return "pre";
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+export class Page extends ZenPage
+{
+ async main()
+ {
+ this.set_title("metrics");
+
+ const metrics_section = this.add_section("metrics");
+ const top_toolbar = metrics_section.add_widget(Toolbar);
+ const tb_right = top_toolbar.right();
+ this._refresh_label = tb_right.add("", "label");
+ this._pause_btn = tb_right.add("pause").on_click(() => this._toggle_pause());
+
+ this._paused = false;
+ this._last_refresh = Date.now();
+ this._provider_views = [];
+
+ const providers_data = await new Fetcher().resource("stats").json();
+ const providers = providers_data["providers"] || [];
+
+ const stats_list = await Promise.all(providers.map((provider) =>
+ new Fetcher()
+ .resource("stats", provider)
+ .param("cidstorestats", "true")
+ .param("cachestorestats", "true")
+ .json()
+ .then((stats) => ({ provider, stats }))
+ ));
+
+ for (const { provider, stats } of stats_list)
+ {
+ this._condense(stats);
+ this._provider_views.push(this._render_provider(provider, stats));
+ }
+
+ this._last_refresh = Date.now();
+ this._update_refresh_label();
+
+ this._timer_id = setInterval(() => this._refresh(), 5000);
+ this._tick_id = setInterval(() => this._update_refresh_label(), 1000);
+
+ document.addEventListener("visibilitychange", () => {
+ if (document.hidden)
+ this._pause_timer(false);
+ else if (!this._paused)
+ this._resume_timer();
+ });
+ }
+
+ _render_provider(provider, stats)
+ {
+ const section = this.add_section(provider);
+ const toolbar = section.add_widget(Toolbar);
+
+ toolbar.right().add("detailed →").on_click(() => {
+ window.location = "?page=stat&provider=" + provider;
+ });
+
+ const table = section.add_widget(PropTable);
+ let current_stats = stats;
+ let current_category = undefined;
+
+ const show_category = (cat) => {
+ current_category = cat;
+ table.clear();
+ table.add_object(current_stats[cat], true, 3);
+ };
+
+ var first = undefined;
+ for (var name in stats)
+ {
+ first = first || name;
+ toolbar.left().add(name).on_click(show_category, name);
+ }
+
+ if (first)
+ show_category(first);
+
+ return {
+ provider,
+ set_stats: (new_stats) => {
+ current_stats = new_stats;
+ if (current_category && current_stats[current_category])
+ show_category(current_category);
+ },
+ };
+ }
+
+ async _refresh()
+ {
+ const updates = await Promise.all(this._provider_views.map((view) =>
+ new Fetcher()
+ .resource("stats", view.provider)
+ .param("cidstorestats", "true")
+ .param("cachestorestats", "true")
+ .json()
+ .then((stats) => ({ view, stats }))
+ ));
+
+ for (const { view, stats } of updates)
+ {
+ this._condense(stats);
+ view.set_stats(stats);
+ }
+
+ this._last_refresh = Date.now();
+ this._update_refresh_label();
+ }
+
+ _update_refresh_label()
+ {
+ const elapsed = Math.floor((Date.now() - this._last_refresh) / 1000);
+ this._refresh_label.inner().textContent = "refreshed " + elapsed + "s ago";
+ }
+
+ _toggle_pause()
+ {
+ if (this._paused)
+ this._resume_timer();
+ else
+ this._pause_timer(true);
+ }
+
+ _pause_timer(user_paused=true)
+ {
+ clearInterval(this._timer_id);
+ this._timer_id = undefined;
+ if (user_paused)
+ {
+ this._paused = true;
+ this._pause_btn.inner().textContent = "resume";
+ }
+ }
+
+ _resume_timer()
+ {
+ this._paused = false;
+ this._pause_btn.inner().textContent = "pause";
+ this._timer_id = setInterval(() => this._refresh(), 5000);
+ this._refresh();
+ }
+
+ _condense(stats)
+ {
+ const impl = function(node)
+ {
+ for (var name in node)
+ {
+ const candidate = node[name];
+ if (!(candidate instanceof Object))
+ continue;
+
+ if (candidate["rate_mean"] != undefined)
+ {
+ const as_bytes = (name.indexOf("bytes") >= 0);
+ node[name] = new TemporalStat(candidate, as_bytes);
+ continue;
+ }
+
+ impl(candidate);
+ }
+ }
+
+ for (var name in stats)
+ impl(stats[name]);
+ }
+}