bdd18de186c0f34406ca395e7ba9b1b88980587d chmalee Mon Sep 23 16:44:07 2019 -0700 Optimizing the jstree on hgHubConnect by writing out a javascript object and letting ajax load the nodes of the tree on an as-needed basis. Also adding measureTiming to hgHubConnect, refs #23812 diff --git src/hg/hgHubConnect/hgHubConnect.c src/hg/hgHubConnect/hgHubConnect.c index 726b7af..f7b23fa 100644 --- src/hg/hgHubConnect/hgHubConnect.c +++ src/hg/hgHubConnect/hgHubConnect.c @@ -375,46 +375,45 @@ ourPrintCell(""); puts("</tr>"); } printf("</tbody></TABLE>\n"); printf("</div>"); } void doValidateNewHub(char *hubUrl) /* Run hubCheck on a hub. */ { struct dyString *cmd = dyStringNew(0); udcSetCacheTimeout(1); dyStringPrintf(cmd, "loader/hubCheck -htmlOut -noTracks %s", hubUrl); -printf("<tr><td>"); -printf("running command: '%s'\n", cmd->string); -printf("</td></tr></table>"); -printf("<div id=\"tracks\" class=\"hubTdbTree\" style=\"overflow: auto\"></div>"); +printf("</table>"); +printf("<div id=\"validateHubResult\" class=\"hubTdbTree\" style=\"overflow: auto\"></div>"); FILE *f = popen(cmd->string, "r"); if (f == NULL) errAbort("popen: error running command: \"%s\"", cmd->string); char buf[1024]; jsInline("trackData = [];"); while (fgets(buf, sizeof(buf), f)) { jsInlineF("%s", buf); } if (pclose(f) == -1) errAbort("pclose: error for command \"%s\"", cmd->string); -jsInline("hgCollection.init();"); +// the 'false' below prevents a few hub-search specific jstree configuration options +jsInline("hubSearchTree.init(false);"); dyStringFree(&cmd); } void hgHubConnectValidateNewHub() { // put out the top of our page char *hubUrl = cartOptionalString(cart, "validateHubUrl"); printf("<div id=\"validateHub\" class=\"hubList\"> \n" "<table id=\"validateHub\"> \n" "<thead><tr> \n"); printf("<th colspan=\"6\" id=\"addHubBar\"><label for=\"validateHubUrl\">URL:</label> \n"); if (hubUrl != NULL) { printf("<input name=\"validateText\" id=\"validateHubUrl\" class=\"hubField\" " "type=\"text\" size=\"65\" value=\"%s\"> \n", hubUrl); @@ -1060,128 +1059,291 @@ if (hst->textLength == hubSearchTextLong) dyStringPrintf(tdbOut->descriptionMatch, "%s", hst->text); else if (hst->textLength == hubSearchTextMeta) { if (isNotEmpty(dyStringContents(tdbOut->metaTags))) dyStringPrintf(tdbOut->metaTags, ", %s", hst->text); else dyStringPrintf(tdbOut->metaTags, "%s", hst->text); } } } } return hubOut; } +static char *tdbOutputStructureLabelToId(struct tdbOutputStructure *tdbOut) +/* Make an array name out of a tdbOutputStruct */ +{ +struct dyString *id = dyStringNew(0); +dyStringPrintf(id, "%s", htmlEncode(dyStringContents(tdbOut->shortLabel))); +if (tdbOut->childCount > 0) + { + dyStringPrintf(id, " (%d subtrack%s)", tdbOut->childCount, + tdbOut->childCount == 1 ? "" : "s"); + } +return dyStringCannibalize(&id); +} + +static void printTdbOutputStructureToDyString(struct tdbOutputStructure *tdbOut, struct dyString *dy, char *arrayName) +/* Print a tdbOutputStructure to a dyString*/ +{ +dyStringPrintf(dy, "trackData['%s'] = [", arrayName); + +if (tdbOut->childCount > 0) + { + struct dyString *subtrackDy = dyStringNew(0); + struct tdbOutputStructure *child = tdbOut->children; + while (child != NULL) + { + char *childId = tdbOutputStructureLabelToId(child); + dyStringPrintf(dy, "\n\t{\n\tid: '%s',\n\tparent: '%s',\n\t" + "li_attr: {nodetype:'track', configlink:'%s'},\n\ttext: \'%s ", + childId, arrayName, dyStringContents(child->configUrl), childId); + if (isNotEmpty(dyStringContents(child->metaTags))) + { + dyStringPrintf(dy, "<br><span class=\\'descriptionMatch\\'><em>Metadata: %s</em></span>", + htmlEncode(dyStringContents(child->metaTags))); + } + if (isNotEmpty(dyStringContents(child->descriptionMatch))) + { + dyStringPrintf(dy, "<br><span class=\\'descriptionMatch\\'><em>Description: %s</em></span>", + htmlEncode(dyStringContents(child->descriptionMatch))); + } + dyStringPrintf(dy, "\'"); + if (child->childCount > 0) + { + dyStringPrintf(dy, ",\n\tchildren: true"); + printTdbOutputStructureToDyString(child, subtrackDy, childId); + } + dyStringPrintf(dy, "\n\t},"); + child = child->next; + } + dyStringPrintf(dy, "];\n"); + if (isNotEmpty(dyStringContents(subtrackDy))) + dyStringPrintf(dy, "%s", subtrackDy->string); + } +else + dyStringPrintf(dy, "];\n"); +} + + +void printGenomeOutputStructureToDyString(struct genomeOutputStructure *genomeOut, struct dyString *dy, char *genomeNameId) +/* Print a genomeOutputStructure to a dyString */ +{ +struct tdbOutputStructure *tdbOut = NULL; +static struct dyString *tdbArrayDy = NULL; // the dyString for all of the tdb objects +static struct dyString *idString = NULL; // the special id of this track +if (tdbArrayDy == NULL) + tdbArrayDy = dyStringNew(0); +if (idString == NULL) + idString = dyStringNew(0); + +// The structure here is: +// trackData[genome] = [{track 1 obj}, {track2 obj}, {track3 obj}, ... ] +// trackData[track1] = [{search hit text}, {subtrack1 search hit}, {subtrack2 search hit}, ... ] +// +// if track1, track2, track3 are container tracks, then the recursive function +// tdbOutputStructureToDystring creates the above trackData[track1] = [{}] for +// each of the containers, otherwise a single child of the genome is sufficient +dyStringPrintf(dy, "trackData['%s'] = [", genomeNameId); +if (genomeOut->tracks != NULL) + { + tdbOut = genomeOut->tracks; + while (tdbOut != NULL) + { + dyStringPrintf(idString, "%s", tdbOutputStructureLabelToId(tdbOut)); + dyStringPrintf(dy, "\n\t{\n\t'id': '%s',\n\t'parent': '%s',\n\t" + "'li_attr': {'nodetype':'track', configlink: '%s'},\n\t'text': \'%s ", + idString->string, genomeNameId, dyStringContents(tdbOut->configUrl), idString->string); + if (isNotEmpty(dyStringContents(tdbOut->metaTags))) + { + dyStringPrintf(dy, "<br><span class=\\'descriptionMatch\\'><em>Metadata: %s</em></span>", + htmlEncode(dyStringContents(tdbOut->metaTags))); + } + if (isNotEmpty(dyStringContents(tdbOut->descriptionMatch))) + { + dyStringPrintf(dy, "<br><span class=\\'descriptionMatch\\'><em>Description: %s</em></span>", + htmlEncode(dyStringContents(tdbOut->descriptionMatch))); + } + dyStringPrintf(dy, "\'"); + + // above we took care of both non-heirarchical tracks and the top-level containers, + // now do container children, which also takes care of any deeper heirarchies + if (tdbOut->childCount > 0) + dyStringPrintf(dy, ",\n\t'children': true"); + dyStringPrintf(dy, "\n\t},\n"); + + if (tdbOut->childCount > 0) + printTdbOutputStructureToDyString(tdbOut, tdbArrayDy, idString->string); + tdbOut = tdbOut->next; + dyStringClear(idString); + } + } +dyStringPrintf(dy, "];\n"); // close off genome node +dyStringPrintf(dy, "%s\n", tdbArrayDy->string); +dyStringClear(tdbArrayDy); +dyStringClear(idString); +} + +void printHubOutputStructure(struct hubOutputStructure *hubOut, struct hubEntry *hubInfo) +/* Convert a hubOutputStructure to a jstree-readable string */ +{ +struct dyString *dy = dyStringNew(0); +// The leading '#' tells the javascript this is a 'root' node +dyStringPrintf(dy, "trackData['#_%d'] = [", hubInfo->id); +if (isNotEmpty(dyStringContents(hubOut->descriptionMatch))) + { + dyStringPrintf(dy, "{'id':'%d_descriptionMatchText','parent':'#_%d'," + "'state':{'opened': true},'text': 'Hub Description: " + "<span class=\"descriptionMatch\"><em>%s</em></span>'},", + hubInfo->id, hubInfo->id, dyStringContents(hubOut->descriptionMatch)); + } +struct genomeOutputStructure *genomeOut = hubOut->genomes; +struct dyString *genomeDy = dyStringNew(0); +if (genomeOut != NULL) + { + dyStringPrintf(dy, "{'id':'%d_assemblies', 'text':'%d Matching Assembl%s', 'parent':'#_%d', " + "'children':true, 'li_attr': {'state':{'opened': 'false'}}}];\n", + hubInfo->id, hubOut->genomeCount, hubOut->genomeCount == 1 ? "y" : "ies", hubInfo->id); + dyStringPrintf(dy, "trackData['%d_assemblies'] = [", hubInfo->id); + + while (genomeOut != NULL) + { + char *assemblyName = htmlEncode(dyStringContents(genomeOut->shortLabel)); + char genomeNameId[512]; + safef(genomeNameId, sizeof(genomeNameId), "%d_%s", hubInfo->id, assemblyName); + dyStringPrintf(dy, "{'id': '%s', 'parent': '%d_assemblies', 'children': true, " + "'li_attr': {'assemblylink': '%s','nodetype': 'assembly'}," + "'text': \"%s", + genomeNameId, hubInfo->id, dyStringContents(genomeOut->assemblyLink), assemblyName); + if (genomeOut->trackCount > 0) + { + dyStringPrintf(dy, " (%d track%s) ", genomeOut->trackCount, + genomeOut->trackCount == 1 ? "" : "s"); + } + if (isNotEmpty(dyStringContents(genomeOut->metaTags))) + { + dyStringPrintf(dy, "<br><span class='descriptionMatch'><em>%s</em></span>", + htmlEncode(dyStringContents(genomeOut->metaTags))); + } + if (isNotEmpty(dyStringContents(genomeOut->descriptionMatch))) + { + dyStringPrintf(dy, "<br><em>Assembly Description:</em> %s", + htmlEncode(dyStringContents(genomeOut->descriptionMatch))); + } + dyStringPrintf(dy, "\"},"); + printGenomeOutputStructureToDyString(genomeOut, genomeDy, genomeNameId); + genomeOut = genomeOut->next; + } + } +dyStringPrintf(dy, "];\n"); +dyStringPrintf(dy, "%s", genomeDy->string); +jsInline(dy->string); +dyStringClear(dy); +} static void printOutputForHub(struct hubEntry *hubInfo, struct hubSearchText *hubSearchResult, int count) /* Given a hub's info and a structure listing the search hits within the hub, first print * a basic line of hub information with a "connect" button. Then, if the search results * are non-NULL, write out information about the genomes and tracks from the search hits that * match the db filter. * If there are no search results to print, the basic hub lines are combined into a single HTML table * that is defined outside this function. * Otherwise, each hub line is printed in its own table followed by a <ul> containing details * about the search results. */ { if (hubSearchResult != NULL) printf("<table class='hubList'><tbody>\n"); outputPublicTableRow(hubInfo, count); if (hubSearchResult != NULL) { printf("</tbody></table>\n"); + printf("<div class=\"hubTdbTree\">\n"); + printf("<div id='tracks%d'></div>", hubInfo->id); // div for the jstree for this hub's search result(s) + printf("</div>\n"); struct trackHub *hub = fetchTrackHub(hubInfo); struct hubOutputStructure *hubOut = buildHubSearchOutputStructure(hub, hubSearchResult); if (dyStringIsEmpty(hubOut->descriptionMatch) && (hubOut->genomes == NULL)) return; // no detailed search results; hit must have been to hub short label or something - - printf("<div class=\"hubTdbTree\">\n"); - printf("<ul>\n"); - printf("<li>Search details ...\n<ul>\n"); - if (isNotEmpty(dyStringContents(hubOut->descriptionMatch))) - printf("<li>Hub Description: <span class='descriptionMatch'><em>%s</em></span></li>\n", dyStringContents(hubOut->descriptionMatch)); - - struct genomeOutputStructure *genomeOut = hubOut->genomes; - if (genomeOut != NULL) - { - printf("<li>%d Matching Assembl%s\n<ul>\n", hubOut->genomeCount, hubOut->genomeCount==1?"y":"ies"); - while (genomeOut != NULL) - { - printSearchOutputForGenome(genomeOut); - genomeOut = genomeOut->next; - } - printf("</ul></li>\n"); - } - printf("</ul></li></ul></div>\n"); + printHubOutputStructure(hubOut, hubInfo); } } int hubEntryCmp(const void *va, const void *vb) /* Compare to sort based on shortLabel */ { const struct hubEntry *a = *((struct hubEntry **)va); const struct hubEntry *b = *((struct hubEntry **)vb); return strcasecmp(a->shortLabel, b->shortLabel); } void printHubList(struct slName *hubsToPrint, struct hash *hubLookup, struct hash *searchResultHash) /* Print out a list of hubs, possibly along with search hits to those hubs. * hubLookup takes hub URLs to struct hubEntry * searchResultHash takes hub URLs to struct hubSearchText * (list of hits on that hub) */ { int count = 0; int udcTimeoutVal = udcCacheTimeout(); char *udcOldDir = cloneString(udcDefaultDir()); char *searchUdcDir = cfgOptionDefault("hgHubConnect.cacheDir", udcOldDir); udcSetDefaultDir(searchUdcDir); udcSetCacheTimeout(1<<30); struct hubEntry *hubList = NULL; struct hubEntry *hubInfo; +long slTime; +long printOutputForHubTime; +boolean measureTiming = cartUsualBoolean(cart, "measureTiming", FALSE); if (hubsToPrint != NULL) { printHubListHeader(); if (searchResultHash == NULL) // if not displaying search results, join the hub <tr>s into one table printf("<table class='hubList'><tbody>\n"); struct slName *thisHubName = NULL; for (thisHubName = hubsToPrint; thisHubName != NULL; thisHubName = thisHubName->next) { hubInfo = (struct hubEntry *) hashFindVal(hubLookup, thisHubName->name); if (hubInfo == NULL) { /* This shouldn't happen often, but maybe the search hits list was built from an outdated * search text file that includes hubs for which no info is available. * Skip this hub. */ continue; } slAddHead(&hubList, hubInfo); } slSort(&hubList, hubEntryCmp); + slTime = clock1000(); + jsInline("trackData = [];\n"); for (hubInfo = hubList; hubInfo != NULL; hubInfo = hubInfo->next) { struct hubSearchText *searchResult = NULL; if (searchResultHash != NULL) { searchResult = (struct hubSearchText *) hashMustFindVal(searchResultHash, hubInfo->hubUrl); } printOutputForHub(hubInfo, searchResult, count); count++; } + printOutputForHubTime = clock1000(); + if (measureTiming) + printf("hgHubConnect: printOutputForHubTime before js execution: %lu millis<BR>\n", printOutputForHubTime - slTime); if (searchResultHash == NULL) printf("</tbody></table>\n"); } udcSetCacheTimeout(udcTimeoutVal); udcSetDefaultDir(udcOldDir); if (hubsToPrint != NULL) { /* Write out the list of hubs in a single table inside a div that will be hidden by * javascript. This table is used (before being hidden) to set common column widths for * the individual hub tables when they're split by detailed search results. */ printf("<div id='hideThisDiv'>\n"); printf("<table class='hubList' id='hideThisTable'><tbody>\n"); for (hubInfo = hubList; hubInfo != NULL; hubInfo = hubInfo->next) { printOutputForHub(hubInfo, NULL, count); @@ -1482,31 +1644,30 @@ cartWebStart(cart, NULL, "%s", pageTitle); printf( "<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.7/themes/default/style.min.css\" />\n" "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.1/jquery.min.js\"></script>\n" "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.7/jstree.min.js\"></script>\n" "<style>.jstree-default .jstree-anchor { height: initial; } </style>\n" ); jsIncludeFile("utils.js", NULL); jsIncludeFile("jquery-ui.js", NULL); webIncludeResourceFile("jquery-ui.css"); jsIncludeFile("ajax.js", NULL); jsIncludeFile("hgHubConnect.js", NULL); webIncludeResourceFile("hgHubConnect.css"); jsIncludeFile("jquery.cookie.js", NULL); -jsIncludeFile("hgCollection.js", NULL); jsIncludeFile("spectrum.min.js", NULL); printf("<div id=\"hgHubConnectUI\"> <div id=\"description\"> \n"); printf( "<P>Track data hubs are collections of external tracks that can be imported into the UCSC Genome Browser. " "Hub tracks show up under the hub's own blue label bar on the main browser page, " "as well as on the configure page. For more information, including where to " "<A HREF=\"../goldenPath/help/hgTrackHubHelp.html#Hosting\">host</A> your track hub, see the " "<A HREF=\"../goldenPath/help/hgTrackHubHelp.html\" TARGET=_blank>" "User's Guide</A>." "To import a public hub click its \"Connect\" button below.</P>" "<P><B>NOTE: Because Track Hubs are created and maintained by external sources," " UCSC is not responsible for their content.</B></P>" ); printf("</div>\n"); @@ -1595,83 +1756,40 @@ hgHubConnectValidateNewHub(); printf("</div>"); printf("<div class=\"tabFooter\">"); printf("<span class=\"small\">" "Contact <a href=\"../contacts.html\">us</A> to add a public hub." "</span>\n"); printf("</div>"); cgiMakeHiddenVar(hgHubConnectRemakeTrackHub, "on"); puts("</FORM>"); printf("</div>\n"); -jsInline( -"var hubSearchTree = (function() {\n" -" // Effectively global vars set by init\n" -" var treeDiv; // Points to div we live in\n" -"\n" -" function hubSearchTreeContextMenuHandler (node, callback) {\n" -" var nodeType = node.li_attr.nodetype;\n" -" if (nodeType == 'track') {\n" -" callback({\n" -" 'openConfig': {\n" -" 'label' : 'Configure this track',\n" -" 'action' : function () {window.open(node.li_attr.configlink, '_blank'); }\n" -" }\n" -" });\n" -" }\n" -" else if (nodeType == 'assembly') {\n" -" callback({\n" -" 'openConfig': {\n" -" 'label' : 'Open this assembly',\n" -" 'action' : function () {window.open(node.li_attr.assemblylink, '_blank'); }\n" -" }\n" -" });\n" -" }\n" -" }\n" -" function toggleExpansion(node, event) {\n" -" var ident = '#' + node.id;\n" -" if (event.type != 'contextmenu')\n" -" $(ident).jstree(true).toggle_node(node);\n" -" return false;\n" -" }\n" -" function init() {\n" -" $.jstree.defaults.core.themes.icons = false;\n" -" $.jstree.defaults.core.themes.dots = true;\n" -" $.jstree.defaults.contextmenu.show_at_node = false;\n" -" $.jstree.defaults.contextmenu.items = hubSearchTreeContextMenuHandler\n" -" treeDiv=$('.hubTdbTree');\n" -" treeDiv.jstree({\n" -" 'conditionalselect' : function (node, event) { toggleExpansion(node, event); },\n" -" 'plugins' : ['conditionalselect', 'contextmenu'],\n" -" 'core' : { dblclick_toggle: false }\n" -" });\n" -" }\n\n" -" return { init: init};\n\n" -"}());\n" -"\n" -"$(function () {\n" -" hubSearchTree.init();\n" +jsInline("$(function () {\n" +" console.time(\"init time\");\n" +" hubSearchTree.init(true);\n" +" console.timeEnd(\"init time\");\n" "});\n" ); cartWebEnd(); } -char *excludeVars[] = {"Submit", "submit", "hc_one_url", +char *excludeVars[] = {"Submit", "submit", "hc_one_url", "validateHubUrl", hgHubCheckUrl, hgHubDoClear, hgHubDoDisconnect,hgHubDoRedirect, hgHubDataText, hgHubConnectRemakeTrackHub, NULL}; int main(int argc, char *argv[]) /* Process command line. */ { long enteredMainTime = clock1000(); oldVars = hashNew(10); cgiSpoof(&argc, argv); cartEmptyShell(doMiddle, hUserCookie(), excludeVars, oldVars); cgiExitTime("hgHubConnect", enteredMainTime); return 0; }