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