d2a2afcd6c5c19257f8ed2f746c3f94474f4ab78
braney
  Tue Sep 1 13:37:34 2026 -0700
hgTracks: let hg.conf set how big a step malloc takes, refs #38225

hgTracks loads tracks in parallel threads, and each thread grows its own
memory pool in the 128 kB steps glibc uses by default.  A heavy render asks
the kernel to enlarge a pool about 133,000 times, work that does nothing for
the reader.

The new hg.conf setting mallocTopPad is a number of bytes.  When it is set,
cfgSetMallocTopPad() passes it to mallopt(M_TOP_PAD) before anything else in
main() has a chance to allocate, so the heap grows in that size step instead.
Absent or zero, glibc is left alone and nothing changes.

Measured with the #38094 harness over eight sessions and two positions, at
32 MB: renders take 0.926 of the time they did, against a noise floor of
1.006.  With the setting absent the same build measures 1.004, inside that
noise floor.  Through apache on a sandbox it measures 0.906.  The cost is
about 65 MB more resident memory on a heavy session and 8 to 20 MB on an
ordinary view, where it also saves no measurable time; the saving and the
cost are the same effect and appear together.  The rendered image is byte
identical either way.

Off by default, so a server opts in.

diff --git src/hg/lib/hgConfig.c src/hg/lib/hgConfig.c
index 5cf96c65a68..5ee8c485a4d 100644
--- src/hg/lib/hgConfig.c
+++ src/hg/lib/hgConfig.c
@@ -2,30 +2,33 @@
  * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */
 
 #include <stdio.h>
 
 
 #include "common.h"
 #include "hgConfig.h"
 #include "hash.h"
 #include "cheapcgi.h"
 #include "portable.h"
 #include "linefile.h"
 #include "htmshell.h"
 #include "customTrack.h"
 #include <sys/types.h>
 #include <sys/stat.h>
+#if defined(__GLIBC__)
+#include <malloc.h>
+#endif
 
 /* the file to read the global configuration info from */
 #define GLOBAL_CONFIG_PATH "."
 #define GLOBAL_CONFIG_FILE "hg.conf"
 //#define GLOBAL_CONFIG_FILE "/usr/local/apache/cgi-bin/hg.conf"
 /* the file to read the user configuration info from, starting at the user's home */
 #define USER_CONFIG_FILE ".hg.conf"
 
 // forwards
 static void parseConfigFile(char *filename, int depth);
 
 /* the hash holding the config options */
 static struct hash* cfgOptionsHash = NULL;
 
 static boolean isBrowserCgi()
@@ -368,30 +371,50 @@
 }
 
 void cfgSetMaxMem()
 /* Check hg.conf for maxMem.  If not set, don't limit memory.  Otherwise
  * limit memory usage to that number. */
 {
 char *maxMemStr = NULL;
 
 if ((maxMemStr = cfgOption("maxMem")) != NULL)
     {
     unsigned long maxMem = atol(maxMemStr);
     setMemLimit(maxMem);
     }
 }
 
+void cfgSetMallocTopPad()
+/* Check hg.conf for mallocTopPad.  If set to a positive number of bytes, ask
+ * the C library to grow the heap in steps that size instead of its 128 kB
+ * default.  hgTracks loads tracks in parallel threads, and every thread grows
+ * its own pool, so the small default step costs a heavy render more than a
+ * hundred thousand system calls that do nothing for the reader.  Bigger steps
+ * buy that time back and cost resident memory on the same renders.  See
+ * #38225. */
+{
+#if defined(__GLIBC__)
+char *topPadStr = cfgOption("mallocTopPad");
+if (topPadStr != NULL)
+    {
+    long topPad = atol(topPadStr);
+    if (topPad > 0)
+        mallopt(M_TOP_PAD, topPad);
+    }
+#endif
+}
+
 void cfgSetLogCgiVars()
 /* optionally activate dumping of all CGI variables to stderr log */
 {
 cgiSetMaxLogLen(atoi(cfgOptionDefault("logCgiVarMaxLen", "0")));
 }
 
 void cfgInitCgi()
 /* init kent libraries for CGI programs: max RAM, CGI logging, etc*/
 {
 cfgSetLogCgiVars();
 cfgSetMaxMem();
 
 if (cfgOptionBooleanDefault("showEarlyErrors", FALSE))
     errAbortSetDoContentType(TRUE);