e5697049b1e077237d1f83c8293e23d62ded0820 braney Tue May 12 11:40:26 2026 -0700 quickLiftBench: Mode C reference hub + hub-variant case schema, refs #37445 Adds a synthetic 4-track hub (bigBed native + lifted, bigWig native + lifted) at utils/qa/quickLiftBench/testHub/, regenerated via buildTestHub.sh, and extends the bench schema so a variant can be a mapping (hubUrl + db + position + tracks) in addition to the existing saved-session string. The two new cases mode_c_hs1_bb and mode_c_hs1_bw exercise quickLift on hs1 against an hg38-sourced chain, with both variants rendering at the same hs1 coords so the only difference is whether the chain-remap step runs. First numbers (n=10, hgwdev, chr22:15M-50M, 5000 source features / 34000 bedGraph bins): case native total lifted total parallel-fetch delta mode_c_hs1_bb 1376 ms 1154 ms +900 ms (chain remap) mode_c_hs1_bw 26 ms 27 ms negligible bigBed scales per-feature (~180 us/feature for 5000 hg38 features remapped); bigWig is essentially free. Also moves the regress_quickLift_parallel case from server: sandbox to server: hgwdev now that #37470 has landed on dev. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> diff --git src/utils/qa/quickLiftBench/quickLiftBench.py src/utils/qa/quickLiftBench/quickLiftBench.py index 1001d39bdfc..8e61246b389 100755 --- src/utils/qa/quickLiftBench/quickLiftBench.py +++ src/utils/qa/quickLiftBench/quickLiftBench.py @@ -138,69 +138,111 @@ if "variant" not in a or "phase" not in a: sys.exit(f"{ctx} missing required key (variant, phase)") if a["variant"] not in variant_names: sys.exit(f"{ctx}: variant {a['variant']!r} not declared in variants") try: re.compile(a["phase"]) except re.error as e: sys.exit(f"{ctx}: phase regex invalid: {e}") for k in ("max_median_ms", "min_median_ms"): if k in a and not isinstance(a[k], int): sys.exit(f"{ctx}: {k} must be an integer") if "required" in a and not isinstance(a["required"], bool): sys.exit(f"{ctx}: required must be true/false") -def parse_session(s): - """Split 'user/sessionName' (or '/s/user/name'). Returns (user, name).""" - if not isinstance(s, str): - raise ValueError(f"variant must be a 'user/sessionName' string, got: {s!r}") - s = s.strip() +def parse_variant(v): + """Return a dict describing one variant. + + Two forms supported: + - string: "user/sessionName" or "/s/user/name" -- saved-session reference. + Returns {"kind": "session", "user": ..., "session": ...}. + - mapping: {hubUrl, db, position, tracks: {trackName: vis, ...}} -- direct + URL for cases where both variants share an assembly and only differ in + track visibility (typically Mode C: same hub, lift on/off). + Returns {"kind": "hub", "hubUrl": ..., "db": ..., "position": ..., + "tracks": {...}}. + """ + if isinstance(v, str): + s = v.strip() if s.startswith("/s/"): s = s[3:] if "/" not in s: - raise ValueError(f"variant must contain '/': {s!r}") + raise ValueError(f"variant string must contain '/': {v!r}") user, name = s.split("/", 1) if not user or not name: - raise ValueError(f"empty user or session name in: {s!r}") - return user, name + raise ValueError(f"empty user or session name in: {v!r}") + return {"kind": "session", "user": user, "session": name} + if isinstance(v, dict): + missing = [k for k in ("hubUrl", "db", "position", "tracks") if k not in v] + if missing: + raise ValueError( + f"hub variant missing keys {missing}; got {sorted(v)}" + ) + if not isinstance(v["tracks"], dict) or not v["tracks"]: + raise ValueError("hub variant 'tracks' must be a non-empty mapping") + return { + "kind": "hub", + "hubUrl": v["hubUrl"], + "db": v["db"], + "position": v["position"], + "tracks": dict(v["tracks"]), + } + raise ValueError(f"variant must be a string or mapping, got: {type(v).__name__}") def resolve_server(case, defaults, server_override): server_key = server_override or case.get("server") if not server_key: raise ValueError(f"case '{case.get('id')}' has no server") servers = defaults.get("servers", {}) if server_key not in servers: raise ValueError( f"server '{server_key}' not in defaults.servers ({list(servers)})" ) return server_key, servers[server_key].rstrip("/") -def build_url(server_url, user, session_name): - """Build an hgTracks URL that loads a saved session at its saved position. +def build_url(server_url, variant): + """Build an hgTracks URL for one variant. - The position is NOT overridden via URL: a native session and its - quickLifted counterpart sit on different assemblies, so identical - chr:start-end ranges would not be biologically equivalent. Whatever - region the session was saved at is what gets rendered. + Saved-session variants (kind="session"): URL loads the named session at + its saved position. The position is NOT overridden -- a native session + and its quickLifted counterpart often sit on different assemblies, so + identical chr:start-end ranges would not be biologically equivalent. + + Hub variants (kind="hub"): URL attaches the hub at the named db and + position, with hideTracks=1 so only the explicitly named tracks render. """ + if variant["kind"] == "session": params = [ ("hgS_doOtherUser", "submit"), - ("hgS_otherUserName", user), - ("hgS_otherUserSessionName", session_name), + ("hgS_otherUserName", variant["user"]), + ("hgS_otherUserSessionName", variant["session"]), + ] + elif variant["kind"] == "hub": + params = [ + ("db", variant["db"]), + ("hubUrl", variant["hubUrl"]), + ("position", variant["position"]), + ("hideTracks", "1"), + ] + for track, vis in variant["tracks"].items(): + params.append((track, vis)) + else: + raise ValueError(f"unknown variant kind: {variant['kind']!r}") + params += [ ("hgt.trackImgOnly", "1"), ("measureTiming", "1"), ] return f"{server_url}/cgi-bin/hgTracks?{urlencode(params)}" def parse_track_timing_rows(html): """Return a list of (shortLabel, load_ms, draw_ms, total_ms) rows.""" if not html: return [] rows = [] for span in TRACK_TIMING_SPAN_RE.findall(html): text = span.replace(" ", " ") for raw in text.split("<br />"): raw = raw.strip() @@ -337,36 +379,40 @@ except ValueError as e: print(f" {e}", file=sys.stderr) continue variants = case.get("variants") or {} if not variants: print(f" skipped: no variants", file=sys.stderr) continue # Fresh session per case to keep an independent hgsid lineage. session = requests.Session() session.headers.update({"User-Agent": "quickLiftBench/1.0"}) for vname, vraw in variants.items(): try: - user, session_name = parse_session(vraw) + variant = parse_variant(vraw) except ValueError as e: print(f" variant {vname}: {e}", file=sys.stderr) continue - url = build_url(server_url, user, session_name) + url = build_url(server_url, variant) + # For the TSV's user/session columns, fill from the saved-session + # variant if present; for hub variants leave them blank. + user = variant.get("user", "") + session_name = variant.get("session", "") if args.verbose: print(f" URL: {url}", file=sys.stderr) for _ in range(warmup): run_request(session, url, timeout) for it in range(1, iterations + 1): http_ms, code, html, err = run_request(session, url, timeout) block = detect_block(html) if not err else None if block and not err: err = block load_sum = draw_sum = total_ms = None n_tracks = None phases = {}