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/hgSession.js src/hg/js/hgSession.js
index cc2942efb23..474beedc234 100644
--- src/hg/js/hgSession.js
+++ src/hg/js/hgSession.js
@@ -1,31 +1,31 @@
 // hgSession.js - the experimental client-rendered "My Sessions" page.
 //
 // An opt-in modern alternative to the classic server-rendered hgSession page, applying hgBlat's
 // facelift strategy (#37996): hgSession.c emits the session list and page config as an inline JSON
 // global (hgSessionData) into an empty #sessionApp container, and this file builds the UI - a
 // save-current-view card, a searchable/sortable DataTable of saved sessions with inline
 // Overwrite/Share/Edit/Delete, and an "Advanced" panel for loading and backup.
 //
 // The inline table actions POST to small JSON endpoints in hgSession.c (hgS_doDeleteJson, etc.) and
 // update the table in place.  Navigation actions (load a session, load from URL/file, save to file,
 // reset) are ordinary form submits/links against the existing hgSession actions.
 //
 // Styling: shared house-style components in gbModern.css (.gbPill, .gbCard, .gbModal*, .gbTable,
 // .gbBanner, .gbSection), session-specific layout in hgSession.css.
 
-/* global $, hgSessionData, convertTitleTagsToMouseovers, htmlEncode, commify */
+/* global $, hgSessionData, convertTitleTagsToMouseovers, htmlEncode, commify, gbShowTimingDialog */
 
 // Cart action variables (must match the hgs* defines in hgSession.h; hgSessionPrefix is "hgS_").
 var SESS_ACT = {
     save:      'hgS_doSaveSessionJson',
     rename:    'hgS_doRenameSessionJson',
     del:       'hgS_doDeleteJson',
     share:     'hgS_doShareJson',
     gallery:   'hgS_doGalleryJson',
     overwrite: 'hgS_doOverwriteJson',
     describe:  'hgS_doDescribeJson'
 };
 var SESS_P = {
     oldName:  'hgS_oldSessionName',
     newName:  'hgS_newSessionName',
     share:    'hgS_newSessionShare',
@@ -594,30 +594,33 @@
     return '<div class="sessRecent">Most recently saved session: <b>' + sessEnc(recent.name) +
         '</b> <span class="sessRecentTime">(' + sessEnc(recent.lastUse) + ')</span> ' +
         '<button type="button" class="gbPill" id="sessUpdateNow" ' +
         'title="Overwrite this session with the currently active view; keeps the session name and ' +
         'description identical">Update now</button></div>';
 }
 
 function sessTableHtml(C) {
     if (!C.loggedIn) { return ''; }
     return '<div class="gbSection">Your saved sessions</div>' +
         '<table id="sessionAppTable" class="gbTable" style="width:100%"></table>';
 }
 
 function sessionBuild() {
     sessData = hgSessionData;
+    // When the page was loaded with &measureTiming=1 the C side attaches sessData.timing; time the
+    // client-side render too so the dialog shows the full server+client picture.
+    var tBuildStart = (sessData.timing && window.performance) ? performance.now() : 0;
     var C = sessData.config;
     var app = $('#sessionApp');
 
     var intro = '<div class="sessIntro">' + sessAccountHtml(C) +
         (sessAccountHtml(C) ? '<br>' : '') +
         'A session is a stable link to a Genome Browser view that you can save, load later, share ' +
         'or copy into a manuscript. See the ' +
         '<a href="' + sessEnc(C.helpUrl) + '" target="_blank">Sessions User’s Guide</a> and the ' +
         '<a href="' + sessEnc(C.galleryUrl) + '" target="_blank">Session Gallery</a>.</div>';
 
     // The session most recently saved/overwritten (max lastUse), for the one-click "Update now".
     var recent = null;
     (sessData.sessions || []).forEach(function(s) {
         if (!recent || s.lastUseEpoch > recent.lastUseEpoch) { recent = s; }
     });
@@ -650,30 +653,51 @@
         });
     }
 
     // Advanced toggle.
     $('#sessAdvHead').on('click', function() {
         var body = document.getElementById('sessAdvBody');
         var open = body.style.display !== 'none';
         body.style.display = open ? 'none' : 'grid';
         $(this).find('.caret').html(open ? '▸' : '▾');
     });
 
     // Session table.
     if (C.loggedIn) { sessBuildTable(); }
 
     if (typeof convertTitleTagsToMouseovers === 'function') { convertTitleTagsToMouseovers(); }
+
+    // Timing report (only when loaded with &measureTiming=1): a pill that opens the shared dialog.
+    if (sessData.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 = 'sessTimingBtn';
+        pill.innerHTML = '&#9201; Timing';
+        pill.title = 'Show where this page spent its time (server and browser)';
+        pill.addEventListener('click', function() {
+            gbShowTimingDialog(sessData.timing, clientRows);
+        });
+        var bar = document.querySelector('#sessionApp .sessToolbar') ||
+                  document.getElementById('sessionApp');
+        bar.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(sessData.timing, clientRows);
+    }
 }
 
 // ---- bulk select / delete ------------------------------------------------
 
 function sessToggleSelect() {
     if (sessSelectMode) { sessDeleteSelected(); } else { sessSetSelectMode(true); }
 }
 
 function sessSetSelectMode(on) {
     sessSelectMode = on;
     sessDt.column(0).visible(on);   // the leading checkbox column
     var b = document.getElementById('sessSelectBtn');
     if (on) {
         b.textContent = 'Delete all selected';
         b.classList.add('primary');