Architecture¶
AnnZarro is a Flask server that reads AnnData stores slice by slice and a browser client that asks for exactly the slices on screen. Nothing else is in between: no database, no precomputation step, no copy of the data.
One request, top to bottom. A click sends one GET per affected panel. The server checks
access, answers an unchanged repeat with 304 before reading, refuses slices above the size
limit, serves from its result cache or reads only the overlapping chunks, and encodes one
vector.¶
What happens on a click¶
Clicking a gene in a Gene Plot that is linked to a Cell Plot coloured by a layer:
The focused gene changes (header selector, history). Every panel whose configuration depends on the focused gene asks
DataManagerfor its slice, hereGET /api/v1/data/layer/<key>?dataset_path=…&cols=<gene>&format=f32.DataManagerreturns a cached decoded slice if it has one (60 s), or joins a request already in flight for the same URL, or starts one.The browser sends
If-None-Matchif it holds an earlier reply.Server: login check (when enabled) and path confinement (hosted servers only), then
http_cache.conditionalcomputes the ETag from the URL and astat()of the store and answers304on a match, without reading.The route parses indices, applies the per-request caps (
400) and the size guard (413), both from metadata only.get_readerpicks the zarr or h5ad reader. The reader’s result cache (core/caching.py) answers a repeat of the same slice; otherwise zarr decompresses the chunks overlapping the requested column, or for a CSC matrix onlydata[indptr[j]:indptr[j+1]].array_responseencodes the vector: binary float32 (float64 when not exact), dense or sparse, or JSON.wire.jsdecodes it into a typed array; the panel restyles the existing plot.
Only step 6 touches the data, and only one chunk column of it.
Server¶
Module |
Role |
|---|---|
|
|
|
Flask app factory and route registration; the built-in threaded server for |
|
slice routes, dataset listing, size guard, error mapping, panel-set routes |
|
config, status, auth/me, cache info/reset, remote-URL check, the single-page app |
|
ETag/304 and gzip |
|
users and login, panel-set ownership, keeping a hosted server inside its data directory |
|
|
|
opens stores (consolidated metadata, zarr format 2, and 3 under zarr 3), slices dense arrays, CSR/CSC/COO groups, dataframes, categoricals and nullable encodings |
|
the same interface over h5py for local h5ad files; sparse rows and columns are read lazily, each request opens and closes the file |
|
remote-store policy (allowlist, timeouts, credentials) and fsspec access |
|
per-dataset result cache with LRU eviction under |
|
the wire format (Wire format) |
The server is stateless per request apart from caches: any process can answer any request, which is what makes several gunicorn workers behind one proxy work (Lab server).
Caches, from browser to disk¶
Cache |
Where |
Keyed by |
Bound |
Invalidated by |
|---|---|---|---|---|
decoded-slice cache |
browser, |
request URL |
1,000 entries, 1 GB, 60 s each |
expiry; dataset change |
in-flight table |
browser, |
request URL |
requests currently running |
completion |
HTTP cache |
browser |
URL + ETag |
browser policy |
ETag changes when the store is rewritten |
result cache |
server, |
dataset, method and arguments |
|
|
open stores and metadata |
server, reader |
dataset path |
the same |
|
remote chunk cache |
server, per remote store (zarr 3) |
chunk key |
|
restart |
page cache |
operating system |
file |
free RAM |
the OS |
The result cache is safe under concurrent threads (the threaded annzarro start server serves
every request thread from one cache; each gunicorn worker process has its own); each entry’s size is recorded when it is
added and exactly that is freed on eviction, so memory_usage_mb in GET /api/v1/cache/info is
what the cache holds. cache_memory_mb, cache_enabled and cache_dataset_limit take effect
when the app is configured, also for modules that imported the reader earlier.
All server caches assume a store does not change while the server runs. After rewriting a
store in place, call POST /api/v1/cache/reset or restart. On a shared server the reset is
admins only, and under gunicorn it clears only the worker that answers, so restart there.
Memory¶
Server memory follows the chunks being read, not the store. The paper’s lab deployment served 33 datasets totalling 2.2 TiB on disk with three server processes of 0.58-0.75 GB resident memory each. On the paper’s HPC benchmark, serving one row of a 160 GB dense matrix peaked at 109 MB. A selection on a sparse matrix’s other axis (a gene column of CSR, a cell row of CSC), in zarr and in h5ad, is a bounded scan that holds at most one block of stored indices, not the whole matrix; it still decompresses every chunk, so it stays slow. The one pattern that still needs memory on the order of the matrix is a cell row of a dense layer stored in whole-gene chunks (12 GB at 1M × 5,000 in the paper’s laptop sweep). See Performance.
Browser¶
The client is plain JavaScript modules served by the same Flask app (static/js), with
plotly.js for plots, DataTables (with SearchBuilder) for tables, select2 and chroma.js.
Module |
Role |
|---|---|
|
boot, header (dataset menu, Focused Gene / Focused Cell with history), deep-link handling, Share Link |
|
tiled panels, splits, drag handles; |
|
panel sets: save, load, import, export; autosave |
|
the Welcome tile and “Create New Panel” |
|
the two plot primitives (2D or 3D scatter); sources obs, obsm, obsp, layer and var, varm, varp, layer |
|
DataTables with filters; a plot can mask to a table’s rows |
|
controls, colour scales, aesthetics, listeners |
|
every request, coalescing, focus state and history |
|
the decoded-slice cache |
|
binary decoding |
|
the |
|
every panel says what is missing and why instead of drawing an empty plot |
|
API endpoints, defaults, limits |
The browser holds only the vectors on screen: one float per cell per displayed vector, plus the cell and gene name lists. At 1.17 million cells the cell-name list alone is 36 MB, which is the slow part of opening such a dataset (Troubleshooting).
What AnnZarro does not do¶
It never writes to a dataset and never runs user code. The only files it writes are panel sets
in <data_dir>/sessions/, server logs, and (with login) the user file. That is what lets a lab
host datasets for many users without granting write or compute access
(Deployment).