Memory and retention
TL;DR: Every ceiling on what JBrowse retains is scoped to a JS context, since a per-file ceiling multiplies by the open track count and bounds nothing. Those ceilings bound retained bytes; a tab's peak is made of things they do not reach.
Optimizations is where a track's time goes; this page is what stays resident afterwards.
What retains
| layer | unit | scope | bounded by |
|---|---|---|---|
RemoteFileWithRangeCache | compressed bytes | module | its size and idle timeout |
| the parser's own chunk cache | decompressed bytes | JS context | decompressedBytesBudget |
| the CRAM record cache | decoded records | JS context | decodedRecordsBudget |
rpcDataMap | one region's packed columns | display model | the regions the view shows |
| GPU buffers | uploaded vertex data | display | per-object guards only |
The two named budgets live in packages/core/src/util/cacheBudgets.ts.
Cache budget scope
@gmod/bam, @gmod/tabix and @gmod/cram each take a per-file budget, and
dataAdapterCache holds one adapter per open track, so those ceilings multiply
with nothing summing them. Three moderately deep alignments tracks over eight
windows, every cache still well under its own 1 GB ceiling throughout:
| window (of 8) | aggregate held | RSS |
|---|---|---|
| 0 | 303 MB | 567 MB |
| 3 | 610 MB | 994 MB |
| 7 | 1109 MB | 1665 MB |
The idle timeouts reclaim what has gone quiet, and nothing is quiet while the
reader browses. So both budgets are one SharedBudget per JS context — one
per RPC worker plus one on the main thread.
Splitting a budget by track count
Same three tracks, browsing then panning back, counting refills on the revisit:
| per-file budget | aggregate held | revisit refills |
|---|---|---|
| 128 MB | 348 MB | 101 |
| 256 MB | 609 MB | 30 |
| 512 MB | 918 MB | 8 |
| 1024 MB | 1109 MB | 0 |
The cold pass cost 98 refills, so the smallest share is worse than no cache at all: the divisor makes each share too small to hold one working set. A shared budget yields only what is globally least-recently-used, so idle tracks hand their space to the one being panned.
Two budgets, because SharedBudget.total sums over its members and cram weighs
decoded records where bam and tabix weigh bytes.
What no budget bounds
- Reads in flight. Eviction never touches an unsettled read.
- A query's working set. It holds every chunk it parsed until it returns.
- Records that grow after weighing.
end,CIGARandtagsmemoize onto a record when a renderer first reads them. - Grow-only wasm memory.
@gmod/bgzf-filehandle's module-global heap never shrinks, and is the source of the hundreds-of-MB peaks.
Across six tracks, bounding retention moved held bytes 31% and RSS 12%. Bound total memory somewhere that can see the whole process.
GPU memory guards
Nothing sums uploaded bytes across displays, so OOM there is reportable rather
than preventable; both backends route through OomReporter to a
zoom-in-or-reduce-height message. WebGPU refuses past the adapter's own
maxBufferSize, WebGL2 past a fixed 256 MiB, so a region can banner on WebGL2
while rendering on WebGPU.
The largest allocation is the render target: WebGPUHal holds one 4x MSAA color
attachment per display, sized from canvas area, so an empty tall track costs
what a full one does — 79.2 MiB for one, counted nowhere. How many GPU displays
a page can open at all is a separate ceiling,
GPU displays.
ADR-064 owns every measurement here. ARCHITECTURAL_LIMITS.md is the live register of the ceilings, each with the condition that retires it.