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 = '⏱ 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');