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/hgBlat.js src/hg/js/hgBlat.js
index aae33b07a69..6b78ff212a3 100644
--- src/hg/js/hgBlat.js
+++ src/hg/js/hgBlat.js
@@ -1,27 +1,27 @@
 // hgBlat.js - client-side rendering of the hgBlat "Table" output mode.
 //
 // hgBlat.c emits an inline object  var hgBlatData = { config, hits }  and an empty
 // <div id="blatResults">.  This script builds the whole results UI from that data:
 //   - a card with a summary strip (query / length / assembly / hit count + actions)
 //   - a sortable, filterable DataTable whose cells are rendered here (identity bar,
 //     query-coverage bar, linked loci, action links, comma-formatted position)
 //   - a docked "selected hit" detail panel updated on row click
 // Header tooltips reuse the Genome Browser's own mechanism (title + convertTitleTagsToMouseovers).
 
 /* jshint esnext: true */
-/* global $, hgBlatData, convertTitleTagsToMouseovers, htmlEncode */
+/* global $, hgBlatData, convertTitleTagsToMouseovers, htmlEncode, commify, gbShowTimingDialog */
 
 var blatSelectedRank = null;   // rank of the row shown in the detail panel
 
 function blatFmt(n) {
     // 12345 -> "12,345"
     return Number(n).toLocaleString('en-US');
 }
 
 function blatIdColor(id) {
     // UCSC identity semantic colors
     if (id >= 98) { return '#1f7a34'; }
     if (id >= 95) { return '#4d7c0f'; }
     if (id >= 90) { return '#b45309'; }
     return '#b1301f';
 }
@@ -407,30 +407,33 @@
         box.style.display = 'none';
     });
 }
 
 function blatShowQuerySeq() {
     var box = document.getElementById('blatSeqBox');
     if (box.style.display === 'flex') { box.style.display = 'none'; return; }   // toggle off
     blatShowFasta(box, hgBlatData.config.querySeqs, 'blatQuery.fa');
 }
 
 // ---- build ---------------------------------------------------------------
 
 function blatBuild() {
     var cfg = hgBlatData.config;
     var hits = hgBlatData.hits;
+    // When loaded with &measureTiming=1 the C side attaches hgBlatData.timing; time the client
+    // render too so the dialog shows the full server+client picture.
+    var tBuildStart = (hgBlatData.timing && window.performance) ? performance.now() : 0;
 
     // Pin a stable, shareable URL into the address bar (no server redirect) so refresh, bookmark and
     // "Share a link" all use the trash-backed reopen link instead of the transient POST/search URL.
     if (cfg.shareUrl) {
         try { history.replaceState(null, '', cfg.shareUrl); } catch (e) { /* older browsers: ignore */ }
     }
 
     var back = cfg.backUrl ?
         `<a class="gbPill" title="Return to the Genome Browser at your previous location (${htmlEncode(cfg.backPos)})" ` +
         `href="${htmlEncode(cfg.backUrl)}">Back to Genome Browser</a>` : '';
     // The page actions live in the gold main-header bar (framework #sectTtl), next to the title -
     // so there is no separate toolbar (.blatHead is gone).  Injected into #sectTtl below.
     var headActions =
         `${back}<a class="gbPill primary" title="Start a new BLAT search" href="${htmlEncode(cfg.newSearchUrl)}">New BLAT search</a>`;
 
@@ -523,30 +526,53 @@
 
     // Keep the selected-row highlight after sort/filter.  Header tooltips are wired once below (the
     // <thead> persists across draws); we deliberately do NOT re-run convertTitleTagsToMouseovers on
     // every draw, as it re-scans the whole document and adds global listeners on each call.
     dt.on('draw', function() {
         if (blatSelectedRank !== null) { blatSelect(dt, blatSelectedRank); }
     });
 
     // No hit is pre-selected: several hits are often tied on score/identity, so picking one for the
     // user is misleading.  The detail panel shows a prompt until a row is clicked.
     document.getElementById('blatDetail').innerHTML =
         `<div class="blatSelectHint">Click a hit below to see its alignment details. ` +
         `If you are missing matches that you think should be there, ` +
         `<a target="_blank" href="../FAQ/FAQblat.html#blat1b">read our BLAT FAQ</a> or ` +
         `<a href="mailto:genome@soe.ucsc.edu">contact us</a>.</div>`;
+
+    // Timing report (only when loaded with &measureTiming=1): a pill in the summary strip that opens
+    // the shared dialog with the server phases plus the client render time.
+    if (hgBlatData.timing) {
+        var clientRows = [{ label: 'build page (JS)',
+                            ms: Math.round(performance.now() - tBuildStart) }];
+        var pill = document.createElement('button');
+        pill.type = 'button';
+        pill.className = 'gbPill';
+        pill.id = 'blatTimingBtn';
+        pill.innerHTML = '&#9201; Timing';
+        pill.title = 'Show where this page spent its time (server and browser)';
+        pill.addEventListener('click', function() {
+            gbShowTimingDialog(hgBlatData.timing, clientRows);
+        });
+        var strip = document.querySelector('#blatResults .gbStripActions') ||
+                    document.querySelector('#blatResults .gbStrip');
+        if (strip) { strip.appendChild(pill); }
+        // measureTiming=1 on the URL is an explicit request to see the numbers, so open the dialog
+        // right away; the pill stays for reopening it after Close.
+        gbShowTimingDialog(hgBlatData.timing, clientRows);
+    }
+
     blatApplyTooltips();
 }
 
 // ==== search form (the input page) ========================================
 // hgBlat.c emits  var hgBlatFormData = {...}  together with a real <form name="mainForm"> that
 // contains an empty <div id="blatFormBox"> and the C-generated genome search bar.  We build the
 // controls as real form fields *inside that form*, so the browser serializes them natively -
 // including the file input - and Submit / I'm feeling lucky / Clear stay plain submit buttons
 // handled by the existing C code.  There is no shadow form and no copying of values on submit.
 // Styling comes from hgBlat.css (loaded by webIncludeResourceFile in hgBlat.c), shared with the results page.
 
 // The Genome Browser's standard info icon, copied from printInfoIconSvg() in hg/lib/hui.c so the
 // form's icons are pixel-identical to the C-rendered ones elsewhere in the browser.
 var BLAT_INFO_SVG =
     "<svg style='height:1.1em; vertical-align:top' viewBox='0 0 24 24' fill='none' " +