25ff975568a24a89dc2dcd4836875a448beaec9f
braney
Thu Aug 6 15:23:11 2026 -0700
Docent: hide a composite as a unit under hideKids, and warn on an over-long nav, refs #37892
`{cCREs: hideKids}` on hg38 sent 1701 cart variables in a 42,020-character
GET. The walk behind hideKids went to true leaves, straight through the
ENCODE4 Core Collection composite, and enumerated all 850 of its ENCFF
subtracks. Apache's LimitRequestLine is 8190, so the server answered 414 and
the next shot: photographed "Request-URI Too Long" -- and nothing failed: the
page loaded, the still got written, the build exited 0. Only looking at the
figure caught it.
Hiding a composite already reaches its subtracks, so the expansion now stops
at the first container that propagates its own visibility and keeps descending
only through superTracks, which do not. hubApi never lists a superTrack
container, so the flag tdbParse already sets when it synthesizes one answers
the question without new data. That step is two variables now.
This is a separate walk rather than a change to tdbLeaves, whose other caller
resolves a container to a row on the page and does need true leaves -- a
composite has no img_data_ of its own.
nav() also warns when a URL passes 7800 characters. A 414 is not an
exception, it is a page, so the run continues and the damage is a wrong
figure; the one thing that helps is saying so at the time.
diff --git src/hg/utils/docent/docent.js src/hg/utils/docent/docent.js
index 838534b0b57..81e1be10da5 100755
--- src/hg/utils/docent/docent.js
+++ src/hg/utils/docent/docent.js
@@ -383,31 +383,46 @@
// raise that ceiling in trackDb for one that should not (clinvarSubLolly does this).
const heightsSent = new Set();
let inHeightNav = false;
async function scaleHeights() {
if (!HEIGHTPER || inHeightNav) return;
const drawn = await page.evaluate(() =>
[...document.querySelectorAll('#imgTbl [id^="img_data_"]')].map(e => e.id.replace('img_data_', ''))
).catch(() => []);
const fresh = (drawn || []).filter(n => n && !heightsSent.has(n));
if (!fresh.length) return; // same rows as last time: no second load
fresh.forEach(n => heightsSent.add(n));
inHeightNav = true; // the nav below must not recurse
try { await nav(`/cgi-bin/hgTracks?${fresh.map(n => `${n}.heightPer=${HEIGHTPER}`).join('&')}&${IMGVARS}`); }
finally { inHeightNav = false; }
}
- async function nav(u) { pinnedTips.length = 0; await page.goto(absurl(u), { waitUntil: 'load' }); await captureState(); await page.mouse.move(cur.x, cur.y); }
+ // Apache's LimitRequestLine defaults to 8190 bytes for the whole request line, and a
+ // step that derives a lot of cart variables can sail past it. The server then answers 414
+ // and the page LOADS -- so nothing throws, captureState finds no image, and the next
+ // shot: quietly photographs "Request-URI Too Long". Say so, since only an eyeball on the
+ // still would otherwise catch it.
+ const URL_WARN = 7800;
+ async function nav(u) {
+ const full = absurl(u);
+ if (full.length > URL_WARN)
+ console.warn(`nav: URL is ${full.length} chars, over Apache's usual ${8190} limit `
+ + `-- expect a 414 "Request-URI Too Long" page instead of the view`);
+ pinnedTips.length = 0;
+ await page.goto(full, { waitUntil: 'load' });
+ await captureState();
+ await page.mouse.move(cur.x, cur.y);
+ }
async function glide(x, y) {
if (FAST) { await page.mouse.move(x, y); cur.x = x; cur.y = y; return; }
const steps = Math.max(10, Math.round(Math.hypot(x - cur.x, y - cur.y) / 9));
for (let i = 1; i <= steps; i++) { await page.mouse.move(cur.x + (x - cur.x) * i / steps, cur.y + (y - cur.y) * i / steps); await sleep(15); }
cur.x = x; cur.y = y;
}
async function glideTo(sel) {
const b = await page.locator(sel).first().boundingBox({ timeout: 8000 }).catch(() => null);
if (b) await glide(b.x + b.width / 2, b.y + b.height / 2);
}
async function clickGlide(sel) { await glideTo(sel); await sleep(160); await page.click(sel); }
async function checkGlide(sel, want) {
await glideTo(sel); await sleep(140);
if (want) await page.check(sel).catch(() => {}); else await page.uncheck(sel).catch(() => {});
}
@@ -550,30 +565,56 @@
// Leaf descendants of a container, in trackDb order (a container draws no pixels of its
// own, so a `mouseover:`/`click:` naming one has to be resolved to a row that does).
async function tdbLeaves(name) {
const idx = await tdbIndex(state.db);
const n = idx && idx.get(name);
if (!n || !n.children.length) return [];
const out = [];
const walk = k => {
const c = idx.get(k);
if (!c || !c.children.length) out.push(k);
else c.children.forEach(walk);
};
n.children.forEach(walk);
return out;
}
+ // What `hideKids` actually has to send. NOT the same thing as tdbLeaves(): hiding a
+ // COMPOSITE already reaches its subtracks, so the walk stops at the first container that
+ // propagates its own visibility and only keeps descending through superTracks, which do
+ // not. hubApi never lists a superTrack container -- tdbParse synthesizes it and flags
+ // superTrack -- so anything else holding children came from hubApi's own nesting and is a
+ // composite or a view.
+ //
+ // Descending all the way to leaves here is how `{cCREs: hideKids}` on hg38 came to send
+ // 1701 cart variables in a 42,020-character GET: the walk went straight through the
+ // ENCODE4 Core Collection composite and enumerated all 850 of its ENCFF subtracks. Apache
+ // answered 414 and the next shot: photographed the error page, with nothing failing the
+ // build. Stopping at the composite makes that same step three names.
+ async function tdbHideTargets(name) {
+ const idx = await tdbIndex(state.db);
+ const n = idx && idx.get(name);
+ if (!n || !n.children.length) return [];
+ const out = [];
+ const walk = k => {
+ const c = idx.get(k);
+ if (!c || !c.children.length) { out.push(k); return; } // a real leaf
+ if (!c.superTrack) { out.push(k); return; } // composite/view: hide as a unit
+ c.children.forEach(walk); // superTrack: does not propagate
+ };
+ n.children.forEach(walk);
+ return out;
+ }
// The track-controls dropdown to open for the visible gesture. Composite children have no
// dropdown of their own (the container carries it), so walk up until the DOM has one.
async function ctrlSelect(name) {
const idx = await tdbIndex(state.db);
for (let k = name; k; ) {
const sel = `select[name="${k}"]`;
if (await page.locator(sel).count()) return sel;
const n = idx && idx.get(k);
k = n ? n.parent : null;
}
return null;
}
async function shot(name) {
const p = path.join(STILLDIR, name + '.png');
// Drop any click ripple still fading: it reads as a red blob over whatever was just
@@ -1331,31 +1372,31 @@
// What the author spelled out AS A VISIBILITY. A `hideKids` container is deliberately
// NOT in here: it names no mode of its own, so it must not suppress the container
// variable derived from a child below it (`pubtator: pack` is what turns varsInPubs on).
const named = new Set(entries.filter(e => !isKidHide(e)).map(([n]) => n));
const idx = await tdbIndex(state.db);
// `hideKids` is not a visibility -- it is "hide everything under this container", so
// that a child named alongside it is left the only one drawn. A superTrack needs it:
// unlike a composite, its own mode does NOT reach its children, so every child comes
// up at its own trackDb visibility and an earlier `hide: all` does not stick
// (`{varsInPubs: show}` alone draws all eight of its members). The expansion skips any
// child the step names itself, and runs in a round of its own AFTER the rest, because
// a subtrack hide travelling in the same request as its container can be dropped by
// the cart (#37953) -- so the container goes on first and the hides follow.
const kidHides = [];
for (const e of entries.filter(isKidHide)) {
- const leaves = (await tdbLeaves(e[0])).filter(k => !named.has(k));
+ const leaves = (await tdbHideTargets(e[0])).filter(k => !named.has(k));
if (!leaves.length)
console.warn(`track ${e[0]}: hideKids -- trackDb gives it no children to hide`);
for (const k of leaves) kidHides.push([k, 'hide']);
}
entries = entries.filter(e => !isKidHide(e));
// Visible gesture first: drive the real track-controls dropdowns so the mouse is
// seen turning the tracks on. State is still applied by the nav()s below (which
// carry the container/checkbox vars too), so these opens are non-committing.
if (doc.trackAnim !== false)
for (const [name, mode] of entries) {
const csel = await ctrlSelect(name);
if (csel) await openSelectVisible(csel, mode, 6, false);
}
// hgTracks RESHAPES a composite when its container visibility changes, and that wipes
// per-subtrack overrides arriving in the same request (`clinvar=pack&clinvarCnv=hide`