Repository navigation
Look up supplements in an index kept with the CodeSystem map, instead of scanning every CodeSystem - #340
Merged
grahamegrieve merged 1 commit intoNov 4, 2026
Conversation
…nning every CodeSystem loadSupplements() walked every loaded CodeSystem (thousands, from the npm packages) on every code-system lookup, several times per $validate-code, asking each isSupplementFor(url, version). Under replayed IG-build traffic that scan was 18-24% of all CPU time. The provider's CodeSystem map is now a CodeSystemMap: a Map that also keeps, for each url, the keys whose CodeSystem has a supplements value with that url (the part before any '|'). It is updated in set, delete and clear, the only ways to change a Map, and Provider.addCodeSystem and deleteCodeSystem are the only code that changes this map. loadSupplements asks isSupplementFor of those candidates only, in the map's own order, so it returns the same objects in the same order as the scan; random sequences of adds, deletes, sets and clears check that. isSupplementFor is true only when supplements equals the url or starts with url + '|', and in both cases the parts before any '|' are equal, so no supplement the scan would find is missed, including for a url that carries |version. The index takes in any CodeSystem with a supplements value, whatever its content, and removes a key from the bucket it was put in, not one worked out again from the value. Request resources (tx-resource, cache-id) are not in this map, and are walked by the worker as before. The one difference: a CodeSystem marked as a supplement with no supplements element made isSupplementFor throw, so the scan threw on every lookup; the index skips it. Co-Authored-By: Claude Opus 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.
Register row P2.
What was wrong
Provider.loadSupplementswalks every CodeSystem the endpoint has loaded (thousands, from the npm packages), asking each oneisSupplementFor(url, version). It does this on every code-system lookup, several times per$validate-code. In a CPU profile of replayed IG Publisher traffic, this scan was 18–24% of all CPU time. One process kept up with about 80 requests/s with the scan, and about 160 with an index. That was measured on our development branch with an earlier version of this index, which had the same lookup cost.There's no single curl for this: it is a cost paid by every request, and it shows under load.
Code path
tx/provider.json main,Provider.loadSupplements(line 114), lines 119–125:CodeSystem.isSupplementFor(tx/library/codesystem.js:742) is true only whencontentissupplementandsupplementseither equalsurlor starts withurl + '|'.Change
provider.codeSystemsbecomes aCodeSystemMap: aMapthat also keeps, for each url, the set of its own keys whose CodeSystem has asupplementsvalue with that url. "That url" means the part before any|.loadSupplementsasksisSupplementForonly of those, in the map's own order. Everything else is unchanged: the sameisSupplementFor,isGlobalSupplementandisStatedSupplementchecks, and the samesupplements.set(cs.vurl, cs).Why this is safe
The aim is that, for every question, the index gives the same objects in the same order as the scan.
Nothing is missed. If
isSupplementFor(url, …)is true, thensupplementsequalsurlor starts withurl + '|'. Either way, the part ofsupplementsbefore its first|is the part ofurlbefore its first|, and that is the index key on both sides. So every CodeSystem the scan would accept is among the candidates. A url that itself carries|version(CodeSystem/$validate-code?url=http://x|1) works too: its key ishttp://x.Extra candidates do no harm. The index takes in any CodeSystem with a
supplementsvalue, whatever itscontent.isSupplementForstill decides, as before. A version mismatch, orcontentother thansupplement, is turned away exactly as the scan turned it away.The same order.
loadSupplementskeys its result byvurl. When two different objects share avurl(a core-package copy and a THO copy, say), the one seen last wins, so order matters. The index records the position at which each key first entered the map. Map semantics are that replacing a key keeps its place and deleting then re-adding moves it to the end; the index mirrors this. Candidates come back sorted by that position, which is the ordervalues()gives.Every change to the map updates the index. A
Mapchanges only throughset,deleteandclear.CodeSystemMapoverrides all three and updates the index in the same call. When a key is replaced or deleted, it is removed from the bucket it was put in, recorded at the time. That bucket is not worked out again from the value, so a value that changed since can't leave a stale entry. These are every place on main that changesprovider.codeSystems:tx/library.js:1175,Library.cloneWithFhirVersionnew CodeSystemMap()). One per endpointtx/provider.js:660,addCodeSystemdelete(cs.url)when a THO code system takes over from a core onetx/provider.js:668,addCodeSystemset(cs.url, cs)tx/provider.js:676,addCodeSystemset(cs.vurl, cs)tx/provider.js:731,732,deleteCodeSystemdelete(cs.vurl),delete(cs.url)tx/provider.js:746,deleteCodeSystemset(existing.url, existing): re-points the url at the latest survivorAnd every caller of those two methods:
tx/provider.js:212,loadNpmtx/library.js:1210,cloneWithFhirVersiontx/provider.js:620,623,626,updateCodeSystemListgetCodeSystemChanges(only OCL implements it), every 60 s (tx/tx.js:334)Nothing else changes the map. No code calls
Map.prototype.seton it directly, and no other code assignsprovider.codeSystems.Values aren't changed in place. The index reads a CodeSystem's
supplementswhen it is set. Nothing on main assignsjsonObj.supplementsafter a CodeSystem is built:codeSystemToR5runs once, in the constructor. OCL does changejsonObj.contentin place (tx/ocl/cs-ocl.cjs:440and others), but the index doesn't look atcontent.A supplement re-added with another
supplementsurl.addCodeSystemof the new objectsets its keys. If the url changed, the keys move from the old url's bucket to the new one.deleteCodeSystemdeletes them. Both are covered by the tests below.Versions. The index ignores versions on both sides.
isSupplementForstill does the version matching (versionMatchesByAlgorithm) on the candidates, exactly as before.Request-scoped supplements are not in this map. Supplements sent with the request (
tx-resource), or held under a cache-id ($cache-control,ResourceCacheintx/operation-context.js:83), go into the worker'sadditionalResources(tx/workers/worker.js:746,setupAdditionalResources). They never enterprovider.codeSystems.TerminologyWorker.#findSupplements(worker.js:343) walks them itself, after asking the provider. That walk is unchanged.The one difference. A CodeSystem marked
content: supplementwith nosupplementselement makesisSupplementForthrow (suppU.startsWithon undefined). With one of those loaded, the scan threw on every lookup of every code system. The index never offers it, so it is skipped.How this differs from #332's version
The index in #332 differed from the scan in three ways. Each was found by the comparison test below, run against that version:
|versionwas missed. It was looked up as-is, under a key without|.vurl, the other one could win.It also keyed on
content === 'supplement'and worked out the bucket again from the value when removing it. And it cached the request's supplement resources per request, a second invariant to argue for. That part is left out: it was a small saving next to this one.Test
tests/tx/codesystem-map.test.js:Property tests. The same random sequences run against a
Providerwhose map is a plainMap(the scan) and one whose map is aCodeSystemMap(the index):addCodeSystem/deleteCodeSystem, with code systems from THO, core and other packages;set/delete/clearon any key.The code systems are supplements or not, global or not, with or without a version, and with
supplementswith and without|version, pointing at several urls (includinghttp://x/aandhttp://x/a/sub). After every step, the test asks 60 questions:http://x/a,http://x/b,http://x/a/sub,http://x/a|1.0.0orhttp://x/c;1.0.0,2.0.0or1;The two must return the same objects in the same order, and the maps must hold the same entries. More than 50 steps per seed find supplements, so the comparison isn't vacuous. Run against Small fixes: requests no longer wait behind batches, one hung reply, regex alternation, and a few speed-ups #332's version, these tests fail. A count over the same 8 add/delete sequences found 1,672 differing answers for a
|versionurl, 306 that differ only in order, and 87 that return a different object.Cases:
supplementsurl is found under the new one only;url|2.0.0is found for 2.0.0 and not for 1.0.0;Existing suites pass unchanged:
provider-codesystem-list,provider-tho-precedence,supplement-filters,params-supplement-cachekey,lookupandocl-factory-discovery.Results
The touched suites, run in band: 74 of 74 pass. HL7 tx-ecosystem test cases (validator 7.0.3), with this branch merged with the other three from #332's split: 5,048 of 5,061 pass. The 13 failures are the same 13 main 9059fb9 fails:
cs-batch-validate,-badand-codings(R4 and R5, and R5 cached), andtranslate-reverse-r5+and-a(R5, plain and cached).🤖 Generated with Claude Code