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/hgSession/hgSession.c src/hg/hgSession/hgSession.c
index 683bfe1ad64..56c7a75a81d 100644
--- src/hg/hgSession/hgSession.c
+++ src/hg/hgSession/hgSession.c
@@ -30,32 +30,35 @@
 #include "customTrack.h"
 #include "customFactory.h"
 #include "udc.h"
 #include "hgSession.h"
 #include "hgConfig.h"
 #include "sessionThumbnail.h"
 #include "filePath.h"
 #include "obscure.h"
 #include "trashDir.h"
 #include "hubConnect.h"
 #include "trackHub.h"
 #include "errCatch.h"
 #include "sessionData.h"
 #include "jsonParse.h"
 #include "jsonWrite.h"
+#include "perfTimer.h"
 
 char *database = NULL;
+struct perfTimer *hgSessionTiming = NULL;	/* Non-NULL when &measureTiming is set; times the page
+						 * and is emitted as hgSessionData.timing for the JS. */
 
 void usage()
 /* Explain usage and exit. */
 {
 errAbort(
   "hgSession - Interface with wiki login and do session saving/loading.\n"
   "usage:\n"
   "    hgSession <various CGI settings>\n"
   );
 }
 
 /* Global variables. */
 struct cart *cart;
 char *excludeVars[] = {"Submit", "submit", hgsSessionDataDbSuffix, NULL};
 
@@ -2152,49 +2155,51 @@
             jsonWriteString(jw, "changePasswordUrl", wikiLinkChangePasswordUrl(cartSessionId(cart)));
         }
     jsonWriteString(jw, "signupUrl", wikiLinkUserSignupUrl(cartSessionId(cart)));
     }
 jsonWriteStringf(jw, "classicUrl", "hgSession?sessionNewPage=0&%s=%s",
                  cartSessionVarName(), cartSessionId(cart));
 jsonWriteString(jw, "helpUrl", "../goldenPath/help/hgSessionHelp.html");
 jsonWriteString(jw, "galleryUrl", "../goldenPath/help/sessions.html");
 jsonWriteStringf(jw, "publicSessionsUrl", "../cgi-bin/hgPublicSessions?%s", cartSidUrlString(cart));
 /* Reset-to-defaults link, same as showCartLinks(). */
 char returnAddress[512];
 safef(returnAddress, sizeof(returnAddress), "%s?%s", hgSessionName(), cartSidUrlString(cart));
 jsonWriteStringf(jw, "resetUrl", "../cgi-bin/cartReset?%s&destination=%s",
                  cartSidUrlString(cart), cgiEncodeFull(returnAddress));
 jsonWriteObjectEnd(jw);   // config
+perfTimerStep(hgSessionTiming, "page header + config");
 
 jsonWriteListStart(jw, "sessions");
 if (loggedIn)
     {
     struct sqlConnection *conn = hConnectCentral();
     if (sqlTableExists(conn, namedSessionTable))
         {
         char *encUserName = cgiEncodeFull(userName);
         boolean gotSettings = (sqlFieldIndex(conn, namedSessionTable, "settings") >= 0);
         char query[512];
         if (gotSettings)
             sqlSafef(query, sizeof(query),
                 "SELECT sessionName, shared, firstUse, useCount, contents, settings, lastUse FROM %s "
                 "WHERE userName = '%s' ORDER BY sessionName;", namedSessionTable, encUserName);
         else
             sqlSafef(query, sizeof(query),
                 "SELECT sessionName, shared, firstUse, useCount, contents, lastUse FROM %s "
                 "WHERE userName = '%s' ORDER BY sessionName;", namedSessionTable, encUserName);
         struct sqlResult *sr = sqlGetResult(conn, query);
+        perfTimerStep(hgSessionTiming, "load sessions from MySQL");
         char **row;
         /* Cache one connection per assembly db so the per-session band/locus lookups don't
          * re-open a connection for every row when many sessions share an assembly. */
         struct hash *dbConnCache = hashNew(0);
         while ((row = sqlNextRow(sr)) != NULL)
             {
             char *encSessionName = row[0];
             char *sessionName = cgiDecodeClone(encSessionName);
             int shared = atoi(row[1]);
             char *firstUse = cloneString(row[2]);
             struct tm firstUseTm;
             ZeroVar(&firstUseTm);
             strptime(firstUse, "%Y-%m-%d %T", &firstUseTm);
             long epoch = (long)mktime(&firstUseTm);
             /* created = date only for display; createdFull = date+minute for the hover. */
@@ -2301,71 +2306,78 @@
                 jsonWriteString(jw, "description", description);
             jsonWriteString(jw, "shareUrl", dyUrl->string);
             jsonWriteObjectEnd(jw);
 
             dyStringFree(&dyUrl);
             freez(&band);
             freez(&locus);
             freez(&firstUse);
             freez(&dateOnly);
             freez(&createdFull);
             freez(&lastUse);
             freez(&lastUseDate);
             freez(&sessionName);
             }
         sqlFreeResult(&sr);
+        perfTimerStep(hgSessionTiming, "annotate positions (band + locus) + build JSON");
         /* Release the cached per-assembly connections. */
         struct hashEl *hel, *helList = hashElListHash(dbConnCache);
         for (hel = helList; hel != NULL; hel = hel->next)
             {
             struct sqlConnection *dbConn = hel->val;
             hFreeConn(&dbConn);
             }
         hashElFreeList(&helList);
         hashFree(&dbConnCache);
         }
     hDisconnectCentral(&conn);
     }
 jsonWriteListEnd(jw);   // sessions
 }
 
 void doMainPageNew(char *userName, char *message)
 /* Render the experimental client-rendered Sessions page: framework header (gold "My Sessions"
  * band), the experimental banner, an empty #sessionApp container, and the hgSessionData JSON that
  * hgSession.js reads to build the UI. */
 {
+if (isNotEmpty(cartOptionalString(cart, "measureTiming")))
+    hgSessionTiming = perfTimerNew();   /* times the page; emitted as hgSessionData.timing */
 cspWriteResponseHeader();
 puts("Content-Type:text/html\n");
 cartWebStart(cart, NULL, "My Sessions");
 jsInit();
 jsIncludeDataTablesLibs();
 webIncludeResourceFile("gbModern.css");
 webIncludeResourceFile("hgSession.css");
 jsIncludeFile("hgSession.js", NULL);
 
 printSessionNewPageBanner(TRUE);
 if (isNotEmpty(message))
     printf("<div class='gbBanner'>%s</div>\n", message);
 
 printf("<div id='sessionApp' class='gbApp'></div>\n");
 
 struct jsonWrite *jw = jsonWriteNew();
 jsonWriteObjectStart(jw, NULL);
 sessionDataToJson(userName, jw);
+/* When &measureTiming is set, hand the per-phase timings to hgSession.js (it shows them in a
+ * dialog).  Emitted at the top level as hgSessionData.timing. */
+perfTimerJson(hgSessionTiming, jw, "timing");
 jsonWriteObjectEnd(jw);
 jsInlineF("var hgSessionData = %s;\n", jw->dy->string);
 jsonWriteFree(&jw);
+perfTimerFree(&hgSessionTiming);
 
 cartWebEnd();
 }
 
 /* ---- JSON action endpoints for the experimental page's inline table actions ---- */
 
 static void saveSessionJsonOk(struct sqlConnection *conn, char *extraFields)
 /* Emit {"success": true[, <extraFields>]} and disconnect.  extraFields (may be NULL) is inserted
  * verbatim after "success": true, e.g. ", \"shared\": 2". */
 {
 puts("Content-Type:application/json\n");
 printf("{\"success\": true%s}\n", extraFields ? extraFields : "");
 hDisconnectCentral(&conn);
 }