23790efe33021ccd1691bf5f4262c1396a1f6e9d braney Fri May 1 12:49:13 2026 -0700 quickLiftBench: phase_asserts mechanism + parallel-fetch regression case phase_asserts is a per-case schema in cases.yaml that declares regex matches against the per-iteration timing spans, with optional required/min_median_ms/max_median_ms bounds. When a case declares phase_asserts, the runner captures phase data automatically and runs the asserts after iterations complete; any failure prints to stderr and exits non-zero. The new regress_quickLift_parallel case uses this to assert that the "Waiting for parallel..." span fires for the lifted multi-track session Brianraney/benchQuickPara with a median between 500 and 15000 ms -- discriminating the working sandbox build from a pre-fix hgwdev where only non-quickLift tracks parallelize at ~50 ms. refs #37488, #37470 diff --git src/utils/qa/quickLiftBench/README.md src/utils/qa/quickLiftBench/README.md index a0230bc9a7b..53aedd3951c 100644 --- src/utils/qa/quickLiftBench/README.md +++ src/utils/qa/quickLiftBench/README.md @@ -1,159 +1,192 @@ # quickLiftBench Benchmark suite that compares hgTracks render times for two saved sessions on the same server. The intended pairing is a **native** session (tracks rendered on their source assembly) against a **lifted** session (the same tracks rendered on a different assembly via quickLift). Output TSVs are intended as the raw numbers behind tables and figures in a quickLift performance paper. ## What it measures For each benchmark case, two (or more) named variants — typically `native` and `lifted` — are timed across multiple iterations. Each request loads a saved session into a fresh cart and asks hgTracks for the per-request timing breakdown. The session renders at the position it was saved with; the runner does **not** override `position`, since native and quickLifted variants live on different assemblies and the same chr:start-end is not biologically equivalent across them. To benchmark multiple regions, save additional session pairs and add them as separate cases. Each response is parsed for: - **Overall total time** — the headline number, taken from the `Overall total time: NNN millis` footer span. - **Per-track load and draw times** — summed across all visible tracks from the `printTrackTiming()` table emitted into a `` block. - **HTTP wall time** — measured around the request itself. Each variant cell does `warmup` discarded requests followed by `iterations` recorded requests. Min / median / p90 are reported. ## Usage ``` ./quickLiftBench.py [--config FILE] [--cases ID,ID] [--server-override NAME] [--iterations N] [--warmup N] [--out DIR] [--verbose] [--phases] ``` Defaults: read `cases.yaml` next to the script, no server override, all cases, iterations and warmup from `defaults`, output to `./results//`. Examples: ``` # Run everything against the server in each case stanza: ./quickLiftBench.py # One case, against the sandbox, 10 iterations: ./quickLiftBench.py --cases bench1_hgwdev \ --server-override sandbox --iterations 10 # Quick smoke against a single existing saved session: ./quickLiftBench.py --cases smoke_session --iterations 1 --warmup 0 -v ``` ## Config schema ```yaml defaults: iterations: 5 warmup: 1 timeout: 60 servers: hgwdev: https://hgwdev.gi.ucsc.edu sandbox: https://hgwdev-braney.gi.ucsc.edu beta: https://hgwbeta.soe.ucsc.edu rr: https://genome.ucsc.edu cases: - id: case_id description: "..." server: hgwdev # one server for all variants in this case variants: native: User/sessionName_native # user/sessionName lifted: User/sessionName_lifted compare: - [native, lifted] ``` Each variant value is a saved-session reference of the form `user/sessionName` (the same form as the `/s//` short-link URL). Both `User/Name` and the prefix `/s/User/Name` are accepted. The URL the runner sends per iteration is: ``` {server}/cgi-bin/hgTracks? hgS_doOtherUser=submit &hgS_otherUserName=USER &hgS_otherUserSessionName=NAME &hgt.trackImgOnly=1 &measureTiming=1 ``` Notes on URL choices: - `hgS_doOtherUser=submit` plus the user/session name causes hgTracks to load the saved session into the cart (`cart.c:1715`). The session's saved position is used. - `hgt.trackImgOnly=1` is the JS-redraw fast path: hgTracks emits the image + map and returns without rendering the rest of the page. With `measureTiming=1` it also emits the per-track timing block. - A fresh `requests.Session()` per case mints a new hgsid (and thus a fresh cart) so cases do not contaminate each other. ## Adding a case 1. Save two sessions on the target server that differ only in the dimension you want to measure (typically: native vs. quickLifted versions of the same set of tracks). Each session should be saved at the position you want it benchmarked at. 2. Add a stanza to `cases.yaml` following the schema above. 3. Smoke-test with `--cases --iterations 1 --warmup 0 -v` to verify sessions load and timings parse out. ## Output Two TSVs are written to `results//`: - `results.tsv` — one row per (case, variant, iteration) with http_ms, load_ms_sum, draw_ms_sum, n_tracks, total_ms, status_code, error. - `summary.tsv` — two sections: 1. per (case, variant): n, n_ok, http/load_sum/draw_sum/total median and p90. 2. per (case, compare-pair): left vs right total medians and the `right/left` ratio for each metric. - `phases.tsv` (only with `--phases`) — long-form rows of every `label: NNN millis` marker emitted by hgTracks (chromAliasSetup, trackDbLoad, parallel data fetch, image generation, cart write, etc.), one row per (case, variant, iteration, phase). A per-(case, variant, phase) median+p90 summary is appended. Useful for localizing where time is going when total medians differ. +## Regression assertions: `phase_asserts` + +A case can declare assertions against the per-iteration phase timings, so +the bench acts as a tripwire for regressions instead of just emitting +numbers. When any case declares `phase_asserts`, that case's phase data is +captured automatically (no `--phases` flag needed) and assertions run after +all iterations complete. A failure prints to stderr and the script exits +non-zero. + +```yaml +- id: regress_my_thing + server: hgwdev + variants: + base: User/sessionName + phase_asserts: + - variant: base + phase: 'Waiting for parallel \(\d+ threads for \d+ tracks\) remote data fetch' + required: true # span must appear in every iteration + max_median_ms: 15000 # optional median upper bound + min_median_ms: 1 # optional median lower bound +``` + +Semantics: + +- `phase` is a Python regex matched against each phase label (the part + before `:` in `label: NNN millis`). +- `required: true` (default) — assert fails if the regex matches no phase + in any iteration of the variant. +- `max_median_ms` / `min_median_ms` — optional bounds on the median across + iterations. Per iteration, all matching phases' ms values are summed, + then the per-iteration sums are reduced via median. +- A FAIL prints `[FAIL] case/variant /pattern/ reason` and `sys.exit(1)`. + A short pairwise table is also printed to stderr at the end of a run. ## Dependencies ``` pip install requests pyyaml ``` ## Notes - The script does not parallelize requests against a single server. quickLift renders are single-threaded per request; parallel requests would measure contention rather than work. - If hgTracks returns the bot-block page or an `errAbort`, the row is written with `error` set and `*_ms` empty rather than aborting the run. - `total_ms` is the wall time inside hgTracks for the full request (cart load + track load + track draw + page assembly). `http_ms` adds network and CGI startup; treat it as a sanity check, not as the headline number. - Each request reloads the saved session into a fresh cart, so the per-request work includes session unmarshaling. That is consistent across variants, so it cancels out in the ratio. - For paper-quality numbers, run repeatedly across hours of the day or pin to a quiet host; render times on a shared dev server have noticeable load-dependent jitter.