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
//
. 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 ?
`
Back to Genome Browser` : '';
// 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}
New BLAT search`;
@@ -523,30 +526,53 @@
// Keep the selected-row highlight after sort/filter. Header tooltips are wired once below (the
//
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 =
`Click a hit below to see its alignment details. ` +
`If you are missing matches that you think should be there, ` +
`
read our BLAT FAQ or ` +
`
contact us.
`;
+
+ // 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 = '⏱ 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