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">&nbsp;</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>&amp;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, '&quot;')
         .replace(/'/g, '&#39;');
 }
 
 function convertTitleTagsToMouseovers() {
     /* make all the title tags in the document have mouseovers */