Performance
This page describes what we measure on @cosyte/terminology and how. The shared method every Cosyte
package follows (throughput, latency percentiles, memory, import time and bundle size, and how a pull
request is compared with main) is written down in the performance standard in
cosyte/config's decision records.
The numbers for the current release are in Current numbers below.
What matters for this package
The engine's cost has three parts, and they behave differently:
- Loading a release.
loadCodeSystem,loadRxNormGraph,loadGemsandloadComplexMapread a whole release file into memory and index it. This happens once per release, grows with the size of the file, and is where the memory goes: a loaded release stays in memory for as long as you hold it. - Answering a query.
lookup,validateCode,resolveNdc,applyGemandapplyComplexMapfind their answer through an index keyed by code.translatefinds a loaded map's elements through an index of each group by source code, andvalidateCodeInValueSetreads a filter through the release's hierarchy index: each is built on the first call that needs it and kept with the loaded map or release, so the first call pays for it once. A map or code system you build by hand, rather than load, is read in full on every call.expandwalks the release for a filter or a whole-system include, so its cost grows with the release and the value set. - Fixed costs. The bundled UCUM table is parsed the first time a UCUM call needs it and kept for the life of the process. The package has no runtime dependencies to load.
What runs today
bench/terminology.bench.mjs measures the built package, as a consumer imports it, with
@cosyte/bench. Every input is synthetic and built in code by bench/shapes.mjs:
- Loading a release: a LOINC-style CSV (up to 100,000 rows, 40 MiB), the same table with long quoted definitions, RXNCONSO-style RRF, an ICD-10-CM order file (98,000 lines), a hierarchical FHIR CodeSystem (up to 100,000 concepts), a GEM file (78,000 lines), an RxNorm release and a ConceptMap.
- Many queries in a row: lookups and validations, value set membership and expansion over an
is-afilter, translations, GEM lookups, RxNorm navigation and UCUM validation, against a small and a large model, asking the same questions at each size. - One query: the single call made for each coded field.
For each case it reports throughput, latency p50, p95 and p99, allocations per call, the peak heap of
one call, whether the heap stays flat over a long run, and the cold import with every file it loads
and its gzip size. bench/budgets.mjs reads a reading against proposed budgets, and
bench/profile.mjs takes a CPU and heap profile of the hottest paths.
pnpm build
pnpm bench:reading --out bench-results/reading.json --summary bench-results/reading.md
node bench/budgets.mjs bench-results/reading.json
node --enable-source-maps bench/profile.mjs dist/index.mjs --out bench-results/profiles
A full reading loads releases of tens of MiB many times and takes a long while; --only <benchmark>
narrows it. In CI, pnpm test:perf runs the scaling gates (does a load or an expansion grow faster
than its input?) beside the tests, a pull request's build is timed against its base (advisory), and
the Bench reading workflow takes a full reading, the budgets and a profile on a GitHub-hosted runner.
What these numbers are not
- Not a promise for your deployment. The size of the releases you load and the shape of your value sets decide what you see.
- Not a comparison with terminology servers or other libraries.
Current numbers
The numbers for this section are measured on main at each release and filled in when the release
is built. They are never typed by hand.