Performance
This page describes what we measure on @cosyte/dicom, how we measure it, and how to run the
benchmarks yourself. 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, the repository our shared configuration lives in.
The numbers for the current release are in Current numbers below.
Three things run today: the benchmarks, which compare a pull request's build with its base; the scaling gates, which fail a pull request when parsing, reading or writing grows faster than its input; and a reading with a memory profile on a GitHub-hosted runner when the benchmarks change.
What a parse costs depends on what you read. parseDicom decodes an element's value the first time
something reads it, and keeps Pixel Data as raw bytes that are never decoded. To index large files,
readDicomHeader reads only as far as the Pixel Data; Recipes
shows it.
The benchmarks
bench/dicom.bench.mjs is our benchmark suite, run by the shared @cosyte/bench tool against the
built dist/index.mjs, the entry a consumer imports. It measures synthetic files in the shapes real
traffic has, each built in code by bench/shapes.mjs:
- a Modality Worklist entry, the small object a modality reads all day;
- a CT slice of 512 by 512 pixels at 16 bits;
- an Enhanced CT series of many frames, at two sizes;
- a header-only structured report of many small tags in Sequences nested four deep.
Each shape is parsed, parsed then read tag by tag, and written. A reading reports throughput,
latency at p50, p95 and p99, allocations, peak heap, a long run for leaks, and the cold import with
its gzip size. The peak heap is the V8 heap only, so Pixel Data, which is a Buffer, is not in it:
bench/profile.mjs measures each large size's peak resident memory and single-call time in a fresh
process, then profiles the hottest paths. bench/budgets.mjs judges each shape's peak memory
against a budget.
test/perf/bench-shapes.test.ts holds that every shape reads cleanly and writes back to its own
bytes, so the benchmarks measure correct work.
In CI
- On every pull request, CI builds the base and the head and compares them at each shape's first size. The comparison is advisory: it reports, it does not fail.
- The scaling gates (
test/perf/scaling-gate.perf.ts) run in their own job on every pull request and fail whenparseDicom, reading every tag orserializeDicomgrows faster than its input. - When
bench/changes,.github/workflows/bench.ymltakes a full reading and a profile on a GitHub-hosted runner, one job per shape, and keeps the results as artifacts. It is advisory too.
Run them yourself
pnpm install --frozen-lockfile
pnpm build
pnpm bench:reading --out bench-results/reading.json --summary bench-results/reading.md
node bench/budgets.mjs bench-results/reading.json
pnpm bench:compare --base <base checkout>/dist/index.mjs --head dist/index.mjs
pnpm bench:profile --out bench-results/profile
pnpm test:perf
A reading names the machine it ran on. Compare readings from the same machine only.
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.