Repository navigation
fhir-benchmark: per-leg indexing, sql-run suite, run-time block, visual report - #2248
Merged
smunini merged 12 commits intoOct 27, 2026
Merged
Conversation
…rt row The web UI exposes two import processes: Batch/Transaction (/ui/batch) and Bulk Import (/ui/bulk-import, $bulk-submit). The benchmark report showed four import rows named after internal phases, and the per-leg Results table had no Bulk Import row at all (it only lists k6 suites). - Name the rows after the UI: "Batch/Transaction Import (`/ui/batch`: ...)" and "Bulk Import (`/ui/bulk-import`: `$bulk-submit`, until searchable)", with the two phases as indented sub-rows (ingest until Completed, search-index rebuild after Completed). - Add the Bulk Import row to the per-leg Results table from bulk-import.txt: resources/s end to end, Err% = failed/submitted, Checks = resources ok/failed; p50/p95/p99 are n/a because it is a single async job. The row is only printed for a complete job, so reports of runs without bulk import are unchanged. - Move "k6 drives it with 20 concurrent clients" from the row labels to the legend, and use "search-index rebuild" consistently (it was also called "initial index" / "index rebuild"). Numbers, formulas, row order, best-value bolding and markers are unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e, run history - Remove the opt-in `indexing` ($reindex) suite and its report row. - Set the bulk-import indexing mode explicitly per leg: deferred on the standalone backends; inline with 4 files x 4 ES bulk workers and batches of 1000 on every Elasticsearch leg. Document it in one table in crates/rest/README.md. - Rename the Bulk Import sub-row to `search indexing (seconds)`; Elasticsearch legs show `N s (= ingest)`. - Add the `sql-run` k6 suite (five views on Observation, Claim and ExplanationOfBenefit) with a per-view table in the leg summary. - Append a collapsed run history with Mermaid charts to the compare summary. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d gaps, docs) - sql-run: replace filter code 8480-6 (only a panel component in the corpus, matched no row) with 39156-5. - history: run the step after "Upload comparison" with a 5 minute timeout; do not invent chart points for missing values. - Document that sqlite-elasticsearch ingests one file at a time, clarify the refresh-interval wording and the benchmark's own settings. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
HFS used to answer a gzip-encoded $sql-run `_format=ndjson` request with 200 and an empty body (fixed in channel_body_response); the script asks for gzip like any real client and says why. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… gzip `channel_body_response` built its body with `futures::stream::unfold`, which panics when polled again after it returned `None`. tower-http's compression layer polls the body once more at the end, so a client sending `Accept-Encoding: gzip` got `200` with `content-encoding: gzip` and a zero-byte body. Fuse the stream. Covers every streamed `$sql-run` format (ndjson, csv, json, fhir, parquet, arrow). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…a joined run history - md_charts.py: collapsed `### Charts` section appended after the unchanged comparison and per-leg text (two charts per row, one colour per leg). - report_html.py + report_template.html: single-file dark HTML report with a summary at the top, uploaded with `archive: false` and linked as the last line of the summary. - history.py: joins renamed rows, single-series trend charts, safe truncation, `--data-dir` for the report trend. - workflow: capture the leg box, run the chart/report/history steps, never failing the run. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…es, summary, pipefail) - report_html.py: find comparison.md (copied into report-input), pass --expect-legs, apply the Postgres contention rule only to `postgres`, never show missing inputs as 0 or 00:00, fill prose numbers from data. - md_charts.py: no bulk bars for timed-out or failed Bulk Import, axes with units, n/a values are skipped. - history.py: the old "index rebuild" row no longer joins `search indexing`; the y axis carries a unit. - workflow: pipefail for the leg summary, `n/a` for absent latency keys. - README: report outputs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Add a "Run time" block at the top of the comparison (total, waiting for resources, working, per stage) computed from the Actions jobs API and the capacity gate wait, with sequencing behind earlier legs shown apart from idle waiting. The HTML report gets a matching tile and a timeline row per leg. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The capacity gate of run 40385370213 measured a 20000 MB Docker host (`docker info` MemTotal); the 11-12 GB figure in comments, the max_parallel description and the mongodb cache error dated from a smaller host. Keep the 4-CPU fact and point to the gate's MemTotal line instead of a number that goes stale. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lease build, test the helper scripts - build_features.py derives the cargo feature list (always R4) from the setup matrix; the build job uses it and restores/saves a release-profile cache. - New path-filtered workflow fhir-bench-tests.yml runs the helper-script unit tests and a syntax gate on pull requests that touch the benchmark. - README: run time block, build job, shared Docker host, form fields, tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ment, fetcher stream) - Key the release cache on the rustc version; restore the "upload first, analyse second" comment that explains why the shape step stays after the artifact upload; drop a duplicate header; fix stale wording and the sql-run ordering description. - README: reconcile the batch-size and refresh-interval notes with the recipe. - fhir-bench-tests: make the pyyaml fallback work under PEP 668. - bulk_submit_fetcher: fuse the unfold stream read through tokio-util's StreamReader, which re-polls after EOF (same class as the $sql-run fix). - test_report_html: read the real-run directory from FHIR_BENCH_REAL_RUN_DIR. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…_HOME actions/cache rejects keys containing commas; run 40415264411 failed at the cache step with "Key Validation Error" because the feature list is comma-separated. Join the list with + for the key and cache CARGO_HOME paths. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
smunini
deleted the
feat/2247-fhir-benchmark-explicit-per-leg-indexing
branch
October 27, 2026 12:50
eterceros
added a commit
that referenced
this pull request
Oct 27, 2026
Follow-up to #2248. - Always build the full feature set (R4,sqlite,postgres,mongodb,elasticsearch): narrowing it per leg saved only 2-4 min on a cold build and produced a different binary per dispatch. Remove build_features.py and its test. - Raise CARGO_BUILD_JOBS to 8 for the build job only. The runners have 8 CPUs and the compile is CPU-bound; measured cold, 2 jobs took 13-15 min, 4 jobs 7 min 03 s and 8 jobs 4 min 40 s with the same peak memory. - Drop the actions/cache step added in #2248: on a hit from another runner cargo still rebuilt the workspace crates (12 min 08 s in total), a restore cost 99-167 s and a miss paid 126-277 s to save. - The workflow still has a single cargo build, in the build job; every leg downloads its artifact. Keep needs: setup on the build job and update the comments that quoted a ~13-minute build. - README: rewrite the Build job section with the measurements. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #2247. One PR for the FHIR Server Benchmark (
.github/workflows/fhir-benchmark.yml,.github/scripts/fhir-bench/): the five requests from the benchmark review (drop the$reindextiming, explicit indexing mode per leg, an indexing-time row, a$sql-runsuite, a readable report with history), plus what was found while doing it (a server bug, a waiting-time block, a faster build, a test workflow). 12 commits, 30 files, +5,321 / -751.What this PR adds
main@b790972)$reindextimingindexingsuite (not part ofall) and areindexrow; up to ~5 min per legtests=indexingis skipped with a log line. Time until searchable is what the Bulk Import rows measurefhir-benchmark.yml,suite-lib.sh,summary_backends.py,compare_legs.py*-elasticsearchlegsbulk-import.sh.sqlite,postgres,mongodb: deferred. Every*-elasticsearchleg: inline withHFS_BULK_SUBMIT_FILE_CONCURRENCY=4,HFS_BULK_SUBMIT_BATCH_SIZE=1000,HFS_ELASTICSEARCH_BULK_CONCURRENCY=4. One per-backend table incrates/rest/README.md; other docs point to itbulk-import.sh,crates/rest/README.mdsearch-index rebuildrow: rebuild seconds only↳ search indexing (seconds): rebuild seconds on deferred legs,N s (= ingest)on inline legscompare_legs.py$sql-runsql-run(10 VUs x 1 min, read-only, betweensearchandinsert): five views on the biggest resource types (Observation x3, Claim, ExplanationOfBenefit; not Patient). Rows for requests/s, rows/s, p95, errors, plus a per-view table in each leg summaryk6/sql-run.js,compare_legs.py,summary_backends.pyBatch/Transaction Import, POST / Bundles,Bulk Import, $bulk-submit/ui/batch,/ui/bulk-import); Bulk Import row added to each leg's Results tablecompare_legs.py,fhir-benchmark.yml### Chartssection appended after the unchanged tables, in the comparison and in each leg (two charts per row, one colour per leg)md_charts.pysql-runviews, trend, full data, health, glossary), uploaded witharchive: falseand linked on the last line of the summaryreport_html.py,report_template.htmlmainrun; old and new row labels joined; trend data kept as an artifacthistory.pyRun timeblock under the report header: total, waiting for resources, working, and sequencing behind earlier legs; tile and timeline rows in the HTMLrun_timing.pyGET /$sql-run?_format=ndjsonwithAccept-Encoding: gzipanswered200with an empty body.fuse()on the streamed body) with a regression test; the same pattern in the bulk-submit fetcher is fused toocrates/rest/src/handlers/sof/run.rs,crates/rest/src/bulk_submit_fetcher.rsbuild_features.py,fhir-benchmark.ymlfhir-bench-tests.ymlMemTotal: 20000 MB); one source of truth for the indexing modeEvidence
1. Which CI run validates what
All runs are
workflow_dispatchruns on throwaway branches of this work (tmp/bench-steve-*), except the reference. The final tree of this PR equals the tree of4eb61b6(the 12 commits here were cherry-picked ontomain@b790972without changes); earlier runs ran earlier states of the same branch.main@e0bd5cbece668esql-runfirst run: found the gzip bug2aedc18sql-run99.9 % errors (empty gzip body)sql-runwith a client-side workarounda4f41e7sql-runwith the server fix, gzip requestedbb8688bRun timeblock on real runs83a7a2797b2f3f(push-triggered copy; the real trigger is thepull_requestpath filter)fb5f926actions/cachekeys cannot contain commas; fixed in4eb61b64eb61b64eb61b62. Results on the three Elasticsearch legs (run 40385370213, all suites)
↳ search indexing(= ingest)(= ingest)(= ingest)sql-run(req/s, 0 failed)sql-run(rows/s)sql-runp95 (ms)The shared Docker host was not quiet during this run (two other CI runs in progress when it started), so absolute crud/search numbers are indicative. mongodb+ES imported 996/1000 bundles in the Batch import (4 failed checks), the same count as in the reference run; the standalone
mongodbleg imported 989/1000 there.3. Bulk Import until searchable, before and after
main, deferred)Caveats: the reference run is
main@e0bd5cb, 143 commits (73 without merges) beforeb790972, and deferred, so the gap mixes the recipe with code changes; the controlled comparison is section 4. sqlite+ES shows almost no gain in CI: its SQLite primary ingests one file at a time whateverFILE_CONCURRENCYsays. Run 40353889556 (postgres+ES) took 463 s + a 27 s rebuild and was marked by the report: three bulk-index pages timed out at 30 s on a loaded host and 758 resources were rebuilt afterwards.4. Controlled experiment for the recipe (local, one server binary)
133,527-resource slice, seconds until searchable, one run per cell unless several are listed, same binary (
main@e0bd5cb) for every arm, 0 errors in all arms:wait_for)WRITE_REFRESH=false+ 1 s refreshC was chosen over D because D showed no repeatable gain and was unstable on postgres+ES (18 / 132 / 64 s). Run-to-run spread is large (for example 18-52 s for the same settings), so only differences above about 15 % count. Memory with C on postgres+ES: 2.15 GB peak RSS at 133k resources, 2.97 GB at 489k (203.5 s). Standalone backends: deferred was 26-27 % faster than inline on
postgresandsqlite, and a tie onmongodb(same slice).5. Build job: feature sets and cache
R4,sqlite,postgres,mongodb,elasticsearch(fixed)R4,sqliteR4,sqliteR4,mongodbR4+sqlitecache, saved its ownR4,postgres,elasticsearchR4,sqlite,elasticsearchEach of the last three linked a binary and ran a
prewarmleg successfully (mongodb,postgres-elasticsearch,sqlite-elasticsearch). A cold build of a narrower set is only moderately faster than the old fixed set; the large gain is the cache.backend=coreandallstill produce the full feature list.6. Local verification
python3 -m unittest discoveron.github/scripts/fhir-benchwith an emptyHOME: 154 tests, OK, 1 skipped (the real-data smoke test, which needsFHIR_BENCH_REAL_RUN_DIR).cargo test -p helios-rest --test http_compression: 13 passed, including the new gzip test, which failed withUnfold must not be polled after it returned Ready(None)before the fix.cargo check -p helios-hfs --no-default-features --features ...for all six backend sets:R4,sqlite95 s,R4,postgres95 s,R4,mongodb102 s,R4,sqlite,elasticsearch62 s,R4,postgres,elasticsearch64 s,R4,mongodb,elasticsearch75 s, no warnings (warm target; indicative).build_features.pyrun on the real output ofresolve-matrix.shfor everybackendoption.sql-run) changed exactly the matching bars and labels; no value from the reference run is hard-coded.cargo fmt --checkon the touched Rust files;bash -non every script; both workflow files parse.7. Why one run fell to half (run 40353889556)
Five other CI jobs (
Test Rust,Code Coverage) used the same Docker host during its crud and search suites: containers on the host went from 11 to 34 and free memory from 15.5 GB to 3.2 GB. Elasticsearch's own indexing time per document doubled (0.906 ms against 0.476 and 0.494 in the two quiet runs) and crud fell by the same factor (530 against 961 req/s), while Postgres stayed at 0.055-0.060 ms per call in all runs. The newRun timeblock does not measure this kind of contention; it measures queueing and the capacity gate wait only.Known limits
index rebuildrow is kept separate because it measures a different quantity).mongodbormongodb-elasticsearchdispatch builds without thesqlitefeature, so the bulk-export sidecar is off in that binary (the benchmark does not use it).sql-runis placed aftersearchonly fortests=all; with an explicit list it runs where listed.Run timeblock (checked on real runs offline and in a render test).Related issues
Closes #2247. Related, not closed here: #1899, #1957 and #2042 (about the
$reindexindexing suite removed here), #1953 (host contention marking), #1892 and #1902 (the insert and bulk-import suites).Where to look
crates/rest/README.md, section "$bulk-submitindexing mode per storage backend".github/scripts/fhir-bench/README.md🤖 Generated with Claude Code