Repository navigation
Conversation
…on demand Factors the row -> ValueSet construction (and the rejection of unacceptable content) out of loadAllValueSets so it can be applied to one row at a time, and lets search() read the matched rows' content itself when the provider doesn't hold a map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lding them all in memory The provider loaded all ~40,000 VSAC value sets into a Map at startup (about 1 GB of RSS), and built a second one alongside it at the end of every sync. The definitions are already in the provider's SQLite database, indexed by url/version and id, so read each one when it is asked for, with the same version choice and the same changes (no code system versions in the compose, an id in the provider's id space) as before. A sync compares against the stored content hashes rather than the map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ider do it The ConceptUsageTracker scan at startup and /problems.html walk listAllValueSets() and fetchValueSet() each url. With VSAC read on demand that read and parsed ~40,000 value sets one query at a time (about 10 s, against under 1 s from the map). scanAllValueSets() does the same walk; the default is the old loop, and VSAC reads the latest version of each url once, in batches, and visits it once per listed version, so the results are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The search worker asks every provider for all its matches, sorts them, and returns one page (200 by default). Read on demand, that meant reading and parsing every VSAC match: ValueSet?status=active read ~40,000 definitions to return 200. The VSAC provider now returns matches that carry only what a search is sorted by (id, url, version, date, name, from the indexed columns), with a load() for the value set, and the worker reads the matches on the page it returns. Totals, order across providers, _sort, _elements, _summary and the paging links are unchanged; a match that turns out not to be readable is dropped as before (every match is then read, so the page and total are right). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
so I chose to preload the vsac value sets because of the parsing time. And it does take time, but the numbers here are mostly persuasive that this is not a good trade-off. However there's one case in which it is a very good trade-off, which is repeated $validate-code hammering on a value set. I'd like to keep a small MRU cache of loaded VSAC value sets for this, particularly for a batch-validate-code where the vsac code is an import etc. |
initialize() counted the value sets only when the database already existed, so after creating a new one database.vsCount was unset, and `vsCount == 0` was false: a fresh install served no VSAC value sets until the first refresh timer (24 hours by default). Main synced at once, since its map was empty. Count after creating too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read on demand, every fetchValueSet re-read and parsed the value set: 15 ms for Medication Brand Name (33,341 codes), 3.6 ms for a 9,500-code list. And $batch-validate-code reads it once per entry, so 1,000 entries against the 9,500-code list took 5.3 s, against 1.15 s with the map. The provider now keeps the value sets it has read, by row id, and drops the least recently used once their stored JSON adds up to more than valueSetCacheMB (default 64, next to refreshIntervalHours in the VSAC config; 0 keeps none). - Which row a url (and version) refers to is still worked out from listVersions, so the version rules are unchanged. - The sync drops a row when it writes it (new, updated, or a hash backfill); an operator resync drops that url's rows; a full sync, a new database and close() drop them all. A read that was under way when anything was dropped is not kept. - Search reads the page through what is kept, but doesn't add to it, so a broad search doesn't push out the value sets in use. - What is kept is shared between requests, as the map's value sets were. Nothing writes to them: expand clones, the R4 conversion clones, and tests and a server run with them deep-frozen gave the same results. Small value sets are kept too: they cost little room, and a 1,000-entry batch against a 3 KB value set took 395 ms kept, against 700 ms not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, you were right that repeated use needed it. It's done (e62c9f2): the VSAC provider keeps the value sets it has read, parsed, up to 64 MB of their stored JSON ( Measured on main / on demand without the cache / with the cache:
How it stays correct:
|
|
should the size by configurable? |
|
Yes, it is: For scale: Medication Brand Name, one of the largest, is about 3.6 MB in the heap once cached. 64 MB covered the 20-largest-set round robin in the measurements. Only about 175 of the 18,176 VSAC urls are over 100 KB, so the default holds every large set in use plus plenty of small ones. |
Decisions for you
$batch-validate-codetook 5.3 s instead of 1.15 s; with the cache it takes 1.08 sscanAllValueSets(), for the usage-tracker scan and/problems.html; its default is the old loopfetchValueSetstatus=activewould parse about 40,000 definitions to return 200resource-ids.jssay it shouldn'tWhy
vsac-valuesets.db; this reads each one when a request asks for it.$validate-codeagainst the 57,017-code Medical Cohort goes from 10 to 53 ms, and against a 331-member grouping value set from 20 to 100 ms. A typical VSAC value set costs 0.1 ms. Value sets used again are kept parsed (see "Cache for repeated use"), so repeated use costs what it did with the map. Over 13,718 replayed IG Publisher calls the responses were identical and the total time was the same.It applies cleanly on main and on #331. 7 commits, 37 new tests.
Cause
In
tx/vs/vs-vsac.js,_reloadMap()callsdatabase.loadAllValueSets(). That parses all ~40,000 rows ofvsac-valuesets.db(about 400 MB of JSON) into aMapkeyed by id, url, and url|version._reloadMap()runs again at the end of every sync and builds the new map while the old one is still in use. The heap goes up by another ~1 GB until the old map is collected.Every value set is already stored in that database, indexed by url/version and by id. The map is used for three things: picking the version a url refers to,
map.get(id)after the search SQL has run, and the sync's "have we seen this url|version / has its hash changed" test.Measured
The test server ran the production source list with two SNOMED CT editions, plus
internal:vsacwith a pre-synced snapshot of 40,267 value sets. Each process was capped at 6 GB. The replay was the IG Publisher calls from build.fhir.org'sqa-tx.htmltranscripts, one call at a time.ValueSet?url=for a VSAC value set$validate-codeagainst the largest list (Medical Cohort, 57,017 codes)$validate-codeagainst a 331-member grouping value setValueSet?status=active(no_elements)These were measured before the cache was added. Without it, the biggest lists pay a read and parse on every request: 38 ms for the 6 MB Medical Cohort, 11–16 ms for the 2.5–4.6 MB lists, and 0.1 ms for a typical value set. Grouping value sets pay about 0.25 ms for each child value set. With the cache, they pay this only when they are not among the recently used ones.
Cache for repeated use
Read on demand with no cache, every
fetchValueSetre-read and parsed the value set: 15 ms for Medication Brand Name (33,341 codes), 3.6 ms for a 9,500-code list.$batch-validate-codemakes a new worker for each entry, so it parsed the value set once per entry.The provider now keeps the value sets it has read, parsed and prepared, by row id (the oid-version id), up to 64 MB of their stored JSON. When it is over that, it drops the least recently used. The size is
valueSetCacheMBin the VSAC config, next torefreshIntervalHours; 0 turns the cache off.listVersions, about 0.2 ms) before the cache is looked at.close()empty it. A read that was under way when anything was dropped is not kept. Nothing checks the hash on each request: readingcontent_hashfrom a 2.5 MB row costs 0.54 ms.$expandand the R4 conversion work on copies. A server run with every cached value set deep-frozen, covering$validate-code,$batch-validate-code,$expand, read, search, and HTML and XML output, gave the same results with no errors.Measured on the same snapshot and machine as above, one request at a time. Median of 200 calls, or the best of 3 runs for batches:
$validate-code, Medication Brand Name (33,341 codes), same code 200 times$validate-code, a 20-code value set$batch-validate-code, 200 entries, Medication Brand Name$batch-validate-code, 1,000 entries, a 9,500-code list$batch-validate-code, 1,000 entries, a 20-code value set$validate-coderound robin over 20 large lists (15 MB of JSON), 10 roundsWhen full, 64 MB of stored JSON is about 90 MB of heap if it is all large lists, and up to about 190 MB if it is all small value sets.
Change
fetchValueSet(url, version)reads only the ids and versions stored for that url. It picks one with the sameaddToMap/isNewerVersionrules as before: exact version, then major.minor, then latest. Then it reads and parses that one row and applies the same changes_reloadMap()applied: it stripscompose.include/exclude[].version, prefixes the id, and attachessourcePackageandcontentHash. A row that failsnew ValueSet()is logged and skipped, as it was when the map was loaded, and the next candidate version is used.fetchValueSetByIdreads by id.status=activeread about 40,000 definitions to return 200, taking 680 ms and +200 MB. Now:_buildSearchQuerySQL and returns matches carrying the fields a search sorts by (id, url, version, date, name), each with a non-enumerableload().SearchWorker.loadPagereads only the matches on the page, in parallel._summary,_total,_sort, order across providers,_elementsand paging links are unchanged._elementsfast path is untouched, and providers that hold their resources in memory work as before.content_hashin SQLite and no longer builds a map. After a sync, only the counts are refreshed./problems.htmlused to callfetchValueSetfor every row oflistAllValueSets(). They now go through a provider method,scanAllValueSets(), whose default is that same loop. VSAC's version reads the latest version of each url once, in batches, so the scan takes 1.9 s instead of 10 s on demand (0.9 s with the map). The tracker's counts are identical.Behaviour differences:
fetchValueSet()with a url that is really a raw VSAC id used to find that value set by accident, because the map was keyed by id as well as by url. Now it finds nothing, which is what the id-space rules inresource-ids.jsrequire.Files:
tx/vs/vs-vsac.js,tx/vs/vs-database.js,tx/vs/vs-api.js,tx/usage-tracker.js,tx/problems.js,tx/workers/search.js, plus two new test files. 7 commits.Testing
tests/vsandtests/txhave 5,693 tests before and 5,730 after. The VSAC suites pass, and so does everything else apart from 69 SNOMED, LOINC and language-pack cases intest-cases.test.jsthat need terminology data my latest checkout lacks; they fail the same way without the cache. The new tests are 21 intests/vs/vs-vsac-on-demand.test.jsand 16 intests/tx/search-deferred.test.js. The search tests compare the worker's bundles for providers returning full and deferred matches across 14 query shapes, and check that only the page is read. The provider tests run on a real ValueSetDatabase file with no network and cover version choice, normalisation, ids, rejected rows, search, counts, the hash comparison in sync, the sync's "already known" test, the scan, and the startup sync of a new database. The cache tests check that repeated fetches parse once, that a row the sync writes is dropped and the next fetch sees the new content, that a read under way when a row is dropped isn't kept, that the size limit drops the least recently used, that search doesn't fill the cache, that a full sync, an operator resync, a new database andclose()empty it, that cached value sets can be frozen without anything failing, and that the version rules (exact, then major.minor, then latest) give the same answers whether or not the value set is cached.vs-vsac-resync,vs-vsac-next-linkandvs-packagepass unchanged.?url=, every 7th?url=&version=, and name/title/publisher/status/system/identifier/description/date/version searches, each with and without_elements) were identical, including order.status=activewith_count/_offset,_sort=url,_summary=count,_summary=true,_elements, andtitle/name/publisher/?url=.$validate-code,$expandand$batch-validate-code. The VSAC entries of a separate expansion catalog (30 requests) also gave identical responses.A follow-up could do the same for npm-package value sets (
PackageValueSetProvider), which hold about 230 MB of heap, 196 MB of it PHIN VADS. That needs its own version rule, THO precedence, and a decision about theisCachedfilter memoisation, so it isn't in this PR.🤖 Generated with Claude Code