Repository navigation
ValueSet page: list the first 500 codes of an include, and look those up together - #341
Merged
grahamegrieve merged 3 commits intoNov 4, 2026
Conversation
…t locate() resolveCode ran factory.build() on every call. A page calls it once per code it shows, and for a database-backed code system (RxNorm, LOINC, ...) each build opens a SQLite connection that is registered on the request and stays open until the request ends. The HTML of a value set listing 33,341 RxNorm codes opened 33,341 connections and about 2 GB outside the JS heap. The provider is now built once per factory (system and version) per operation, registered once, and reused. The build promise is kept, so overlapping calls share it; a failed build is not kept. It also called csp.locate(code) and csp.display(c) without await, so c was a Promise: any code "resolved", and the description was a Promise. locate() is awaited and its context used, and display() is awaited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
MAX_INCLUDE_CODE_LOOKUPS seems like workaround here. We control the renderer, so wouldn't it be better for us to do a deeper fix and batch up fetching the displays? We could run an expand internally? But there's a wider question here: if the value set is that big, do we really want to render it fully? I think the answer is no, we cut it off at X codes, and only render it fully if the user asks. Which is also a much deeper fix |
A page that lists the codes of an include asked resolveCode about each one in turn. resolveCodes(opContext, system, version, codes) finds the code system once, gets its provider from the per-operation cache (one build per system and version), and locates the codes eight at a time, returning the results in the order of the codes. resolveCode is now resolveCodes for one code. No CodeSystemProvider in main has a multi-code look-up, so the locates run concurrently with a small limit rather than in one query. A code from a factory gets a link only if the factory is iteratable (US states) or overrides codeLink(); none of the others does, so for RxNorm, LOINC, SNOMED CT and the rest what locate() found was thrown away. Those factories are no longer built or asked. This matters once locate() is awaited: RxNorm looks for a code it does not have in RXNATOMARCHIVE, with no index on RXCUI (36 ms a code), and 4,529 of the 33,341 codes in VSAC's Medication Brand Name are not in the current RxNorm. The results are the same as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… show all An include that lists its codes was rendered one row per code, each looked up in the code system: 33,341 rows and look-ups for VSAC's Medication Brand Name. The renderer now lists the first MAX_RENDERED_INCLUDE_CODES (500) codes of an include or exclude, then "... and 32,841 more codes. Show all". "Show all" is the same URL with _showAll=true, which lists every code. The parameter is underscored like the other parameters that control how a result is presented (_format, _summary, _elements); that also keeps it out of the way of $expand parameters, since the page passes its query string to the expansion it shows. A value set in a bundle links to its own page; a page from a POST gives the count without a link. The codes that are listed are looked up with one resolveCodes call per include, when the link resolver has it. Every listed code is still checked for validity, including those not shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jmandel
force-pushed
the
fix/render-include-lookups
branch
from
November 3, 2026 23:24
1288106 to
ffedf98
Compare
Contributor
Author
|
Agreed on both, and reworked:
Medication Brand Name (33k codes):
|
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 K14.
Reworked after review. The
MAX_INCLUDE_CODE_LOOKUPSworkaround is gone. The page now lists 500 codes per include by default, with a link to list them all, and the codes it lists are looked up in one call per include instead of one per code. Three commits:resolveCodebuilds one code system provider per system and version per request, and awaitslocate(). This is unchanged from the first version.Provider.resolveCodes(opContext, system, version, codes)looks up many codes of one code system together.What was wrong
The HTML page of VSAC's Medication Brand Name (2.16.840.1.113883.3.88.12.80.16, 33,341 RxNorm codes, all in one include) opened one RxNorm SQLite connection for each code. The connections stay open until the response is finished, at about 50 KB each. On main the server went from 3.6 GB to 5.1 GB and was killed by a 6 GB limit in 3 of 3 runs.
resolveCodealso calledcsp.locate(code)withoutawait. A Promise is always truthy, so every code counted as found, and the page didn't wait for the queries, which kept running after it was sent.What changed
Truncation (renderer.js, tx-html.js).
Renderer.renderIncludelists the firstMAX_RENDERED_INCLUDE_CODES(500) codes, then… and N more codes.with a "Show all" link. That link is the same URL plus_showAll=true, which lists every code. I chose the underscore because the other parameters that control how a result is shown (_format,_summary,_elements) use one. It also keeps the parameter apart from the$expandparameters, since the page passes its query string to the expansion it shows. A value set shown in a bundle links to its own page. A page from a POST gives the count with no link. Every code is still checked for validity, including codes not shown. The limit is a constant, because the renderer isn't given any configuration.Batched look-ups (provider.js).
resolveCodesfinds the code system once and takes its provider from the per-request cache from commit 1. It locates the codes eight at a time and returns them in order.resolveCodenow calls it with a single code. NoCodeSystemProviderin main has a lookup for many codes at once, so the lookups run concurrently up to a limit. AprefetchCodesstep (#334) could be added here later. The renderer makes oneresolveCodescall per include, for the codes it shows.It also skips code systems that can't link a code. A code from a factory gets a link only if the factory is
iteratable()(US states) or overridescodeLink(), and no other factory does. For RxNorm, LOINC, SNOMED CT and the rest, whateverlocate()found was thrown away, so these factories are no longer built or asked. The results are the same. This matters oncelocate()is awaited. RxNorm looks for a code it doesn't have inRXNATOMARCHIVE, which has no index onRXCUI, and each search takes 36 ms. 4,529 of Medication Brand Name's codes aren't in the current RxNorm. Without the skip, show-all took 241–246 s, and the default page took 11.5 s.Why not an internal expand. An expansion gives displays, but not the links that make up most of what a look-up adds. Those come from
codeLink(), thex-page for an iteratable code system, or aCodeSystemresource's anchors, so the locates would still be needed. An expansion also applies its own rules (inactive codes, excludes, size limits). This page's own expansion of Medication Brand Name is refused as "too many codes to produce (>1000)".Elsewhere. An expansion's
containsrows do no lookup per code: their$lookuplinks are built as strings, and the expansion is already bounded by the expand limits. I didn't change the CodeSystem page.Before and after
The setup was a local server with RxNorm 03/2026 and a VSAC snapshot (a private hard-linked copy, refreshIntervalHours 500), under a 6 GB memory limit, three runs each.
?_format=html&_showAll=trueThe two remaining RxNorm connections and nearly all of the 8 s come from the expansion that the page also runs, which is refused above 1,000 codes. This PR doesn't change that. Administrative Gender still links its codes to THO, for example
<a href="/r4/CodeSystem/tho-v3-AdministrativeGender#F">F</a>: Female.Tests
tests/tx/renderer-include-codes.test.js(9 tests):… and 32,841 more codes.with the link, and the link is left out when the page has none to give;resolveCodescall, for the 500 codes shown;resolveCodesis asked one code at a time;tests/tx/html-valueset-show-all.test.js(5 tests):_showAll=truelists all 33,341 codes;tests/tx/provider-resolve-code.test.js(12 tests, 5 new):CodeSystemresource still links its codes.22 of the 26 tests fail on main. The touched and nearby suites (
tests/tx/renderer*,provider*,html-*,read*,cs-provider-list), run under a 6 GB limit: 19 suites, 201 tests, all pass.Not changed here:
renderCoding(renderer.js, around line 376) readsdescandurlfrom whatresolveCodereturns, but it returnsdescriptionandlink, so a coding there is never linked. This is the same on main. It's a one-line fix if you'd like it.🤖 Generated with Claude Code