5a3be3af33b76d69d1fc98ee76b9a37f96e96fc7 max Wed Aug 26 00:36:52 2026 -0700 Add opt-in per-phase request timing to the new hgBlat and hgSession pages, shown in a dialog with &measureTiming=1. refs #38157 New reusable perfTimer collector (lib/perfTimer.c) records labeled wall-clock intervals via clock1000() and emits them as JSON; hgBlat and hgSession attach a timing array to their payload when the existing measureTiming cart/CGI var is set. A shared gbShowTimingDialog helper in utils.js renders the server phases plus a client render-time row in a house-style modal, which opens automatically when measureTiming is on. Confirms the position band/locus annotation loop dominates hgSession, and run-BLAT plus locus lookups dominate hgBlat. diff --git src/hg/js/utils.js src/hg/js/utils.js index 527b53e3ead..c7cf1aa7825 100644 --- src/hg/js/utils.js +++ src/hg/js/utils.js @@ -4321,30 +4321,108 @@ /* Actually hides the tooltip text */ ele.classList.remove("isShown"); ele.style.opacity = "0"; ele.style.visibility = "hidden"; } function titleTagToMouseover(mapEl) { /* for a given area tag, extract the title text into a div that can be positioned * like a standard tooltip mouseover next to the item */ if (mapEl.dataset.tooltip) addMouseover(mapEl, mapEl.dataset.tooltip); else addMouseover(mapEl, mapEl.title); } +function gbShowTimingDialog(serverRows, clientRows) { + /* Pop up a small modal that breaks down where a page spent its time. Shared by the + * client-rendered CGI pages (hgBlat, hgSession, ...) that emit a "timing" array when loaded + * with &measureTiming=1. serverRows and clientRows are each an array of {label, ms} (either may + * be null/empty); the server list normally ends with a {label:"total"} row from the C side. + * Renders one gbTable with a proportional bar per row, styled by the .gbTiming* rules in + * gbModern.css. Reuses the .gbModalBg/.gbModal machinery; closes on Close, Esc, backdrop. */ + serverRows = serverRows || []; + clientRows = clientRows || []; + // Scale the bars to the largest single interval (ignoring the grand-total rows, which would + // otherwise dwarf every step). Fall back to 1 to avoid divide-by-zero on an all-zero page. + var maxMs = 1; + function scan(rows) { + rows.forEach(function(r) { + if (r.label !== "total" && r.ms > maxMs) + maxMs = r.ms; + }); + } + scan(serverRows); + scan(clientRows); + + function rowHtml(r, extraClass) { + var pct = Math.min(100, Math.round((r.ms / maxMs) * 100)); + var isTotal = (r.label === "total"); + var label = isTotal ? "Total" : r.label; + return '<tr class="' + (extraClass || "") + (isTotal ? " gbTimeTotal" : "") + '">' + + '<td>' + htmlEncode(label) + '</td>' + + '<td class="num" title="' + htmlEncode(r.ms + " milliseconds") + '">' + + htmlEncode(commify(r.ms)) + '</td>' + + '<td class="gbTimeBarCell"><div class="gbTimeBar"><span style="width:' + pct + '%"></span>' + + '</div></td></tr>'; + } + function groupHtml(title, rows) { + if (!rows || !rows.length) + return ""; + var h = '<tr class="gbTimeGroup"><td colspan="3">' + htmlEncode(title) + '</td></tr>'; + rows.forEach(function(r) { h += rowHtml(r); }); + return h; + } + + var body = '<table class="gbTable gbTimingTable"><thead><tr>' + + '<th title="What was measured">Phase</th>' + + '<th class="num" title="Wall-clock milliseconds spent">ms</th>' + + '<th title="Time relative to the slowest phase"> </th>' + + '</tr></thead><tbody>' + + groupHtml("Server (C code)", serverRows) + + groupHtml("Browser (JavaScript)", clientRows) + + '</tbody></table>'; + + // Build a self-contained modal so this helper does not depend on any page-local markup. + $('#gbTimingBg').remove(); + var html = '<div id="gbTimingBg" class="gbModalBg gbApp" style="display:flex">' + + '<div class="gbModal gbTimingModal" role="dialog" aria-modal="true" aria-labelledby="gbTimingTitle">' + + '<div class="gbModalTitle" id="gbTimingTitle">Timing</div>' + + '<div class="gbModalText" style="max-width:none">Wall-clock time spent handling this ' + + 'request. Add or remove <code>&measureTiming=1</code> from the page URL to toggle ' + + 'this report.</div>' + + body + + '<div class="gbModalBtns"><button type="button" class="gbPill" id="gbTimingClose">Close' + + '</button></div></div></div>'; + $('body').append(html); + convertTitleTagsToMouseovers(); + + function close() { + $('#gbTimingBg').remove(); + $(document).off('keydown.gbTiming'); + } + $('#gbTimingClose').on('click', close); + $('#gbTimingBg').on('click', function(ev) { + if (ev.target === this) // backdrop click only, not clicks inside the card + close(); + }); + $(document).on('keydown.gbTiming', function(ev) { + if (ev.key === "Escape") + close(); + }); +} + function htmlEncode(s) { /* HTML-escape a value (&, <, >, ", ') so it is safe to insert as text or into an attribute * value in a string of HTML. Shared helper: prefer this over rolling a per-file escaper. * Uses the browser's own text->markup conversion via a detached element (jQuery required). * That conversion only escapes &, < and > - quotes need no escaping in text, so it leaves them * alone - hence the explicit quote handling below. Without it this function silently failed * the "safe in an attribute" half of its contract: a value containing a double quote closed the * attribute early, truncating it (and worse, allowing markup injection). */ return $('<div>').text(s === null || s === undefined ? '' : String(s)).html() .replace(/"/g, '"') .replace(/'/g, '''); } function convertTitleTagsToMouseovers() { /* make all the title tags in the document have mouseovers */