diff options
Diffstat (limited to 'src/zen/frontend/html/stats.js')
| -rw-r--r-- | src/zen/frontend/html/stats.js | 95 |
1 files changed, 95 insertions, 0 deletions
diff --git a/src/zen/frontend/html/stats.js b/src/zen/frontend/html/stats.js new file mode 100644 index 000000000..741ad7ef9 --- /dev/null +++ b/src/zen/frontend/html/stats.js @@ -0,0 +1,95 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// Sortable stats table view. + +const US_PER_MS = 1000; + +function escapeHtml(s) { + return String(s).replace(/[&<>"']/g, (c) => ({ + "&": "&", + "<": "<", + ">": ">", + "\"": """, + "'": "'", + }[c])); +} + +export class StatsView { + constructor(tbody, headerRow, model, onSelect) { + this.tbody = tbody; + this.headerRow = headerRow; + this.stats = model.scopeStats.slice(); + this.onSelect = onSelect; + this.sortKey = "count"; + this.sortAsc = false; + this.selectedName = null; + + for (const th of headerRow.querySelectorAll("th[data-sort]")) { + th.addEventListener("click", () => this.handleSort(th.dataset.sort)); + } + this.render(); + } + + handleSort(key) { + if (this.sortKey === key) { + this.sortAsc = !this.sortAsc; + } else { + this.sortKey = key; + this.sortAsc = key === "name"; + } + this.render(); + } + + selectByName(name) { + this.selectedName = name; + for (const tr of this.tbody.querySelectorAll("tr")) { + tr.classList.toggle("selected", tr.dataset.name === name); + if (tr.dataset.name === name) { + tr.scrollIntoView({ block: "nearest" }); + } + } + } + + render() { + const key = this.sortKey; + const asc = this.sortAsc; + this.stats.sort((a, b) => { + const av = a[key]; + const bv = b[key]; + if (typeof av === "string") { + return asc ? av.localeCompare(bv) : bv.localeCompare(av); + } + return asc ? av - bv : bv - av; + }); + + for (const th of this.headerRow.querySelectorAll("th[data-sort]")) { + th.classList.toggle("sorted", th.dataset.sort === key); + th.classList.toggle("asc", th.dataset.sort === key && asc); + } + + const rows = []; + for (const stat of this.stats) { + const selected = stat.name === this.selectedName ? " class=\"selected\"" : ""; + rows.push( + `<tr data-name="${escapeHtml(stat.name)}"${selected}>` + + `<td>${escapeHtml(stat.name)}</td>` + + `<td class="num">${stat.count.toLocaleString()}</td>` + + `<td class="num">${(stat.min_us / US_PER_MS).toFixed(3)}</td>` + + `<td class="num">${(stat.mean_us / US_PER_MS).toFixed(3)}</td>` + + `<td class="num">${(stat.max_us / US_PER_MS).toFixed(3)}</td>` + + `<td class="num">${(stat.stdev_us / US_PER_MS).toFixed(3)}</td>` + + `</tr>`, + ); + } + this.tbody.innerHTML = rows.join(""); + + for (const tr of this.tbody.querySelectorAll("tr")) { + tr.addEventListener("click", () => { + const name = tr.dataset.name; + this.selectByName(name); + if (this.onSelect) { + this.onSelect(name); + } + }); + } + } +} |