Repository navigation
Browser engine: write a region of a view into a store as an OME-Zarr (Engine.materialize) - #39
Merged
Merged
Conversation
…arr of its own: Engine.materialize(view, store, {level, lo, hi, format, compression, skipEmpty, concurrency, signal, onProgress, attributes}) computes one level of a served view over a voxel box (rounded out to whole chunks, clipped to the level; the whole level by default) and writes it as zarr v2 with OME-Zarr 0.4 (the default) or zarr v3 with OME-Zarr 0.5, gzipped through CompressionStream, chunks of nothing but zeros left out, into a WritableStore: directoryStore() over a FileSystemDirectoryHandle (a folder picked with showDirectoryPicker, or the origin's private file system), each file written whole through createWritable, or memoryStore(). The metadata is the layout the engine serves for the view, now built in one module for the routes and the writer (web/src/layout.ts), the level written as the group's one dataset at 0 with its own scale and translation and the level's whole shape, and it is written first, so a store stopped part way is still an OME-Zarr. Its chunks come from the producer the routes use, through the same queue and kept chunks, but in the background: the queue (demand.ts) takes background claims, which no client giving up releases, a job only they hold waiting behind every job a client waits for and running in all the slots but one, so browsing goes on while a region is written; a client's claim brings such a job forward. An abort gives up the chunks still waiting and resolves at once with aborted; a chunk that fails rejects naming its key and why, once the writes under way have ended. The tests cover the box's chunks, both formats' files, gzip, skipEmpty, progress, abort, failure, the stores and the queue's background rule, and read the written store back with zarrita; the formats, caching and design pages and the roadmap (a first piece of item 2) say so, and the CHANGELOG has a line for it
…ten: the kept chunks (256 MB) are for clients that fetch a chunk again, and a region written chunk by chunk pushed what the viewer was looking at out of them for chunks no client might ask for; the engine notes the keys of chunks on their way that a client asked for (chunk() without background) and keeps only those, so a chunk a viewer asked for while materialize had it on its way is kept as before; caching.md says so (the staged stage outputs, 512 MB, are kept as before), as the chunkmirage session suggested in its review of the materialize proposal
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
cellmap-flow's browser app serves ONNX models as engine views (through
serveStages). Its users want Run on a region: compute a served view over a box, or over a whole level, and save the result as a real OME-Zarr in a folder they pick withshowDirectoryPicker({mode: "readwrite"}). They can then open it again or copy it to a cluster filesystem. Today the engine computes only the chunks a client asks for, and nothing is saved. Walking the chunk grid and writing zarr is engine work, so it belongs here rather than in cellmap-flow.This is a first, explicit piece of roadmap item 2. The disk cache, sharing across workers and the filler driven by browsing are not part of it, and the roadmap says so.
The API
The new code is in
web/src/materialize.ts, plus one method onEngine.engine.tsre-exports the store helpers and the types.directoryStorewrites into a picked folder, or into the origin's private file system (navigator.storage.getDirectory()). Each subdirectory is made once. Each file is written whole throughcreateWritable()and aborted if the write fails. Paths with.or..are refused.memoryStorewrites into aMap, for tests and for zipping a small result.level: the finest by default.lo,hi: a voxel box on that level, clipped to the level and rounded out to whole chunks. The whole level by default.format:"zarr2"(the default) or"zarr3".compression:"gzip"(the default) ornull.skipEmpty: true by default.concurrency: the queue's slot count by default.signal: anAbortSignal.onProgress: called with{done, total, written, bytes}.attributes: merged into the group's attributes. These cannot replaceomeormultiscales.{chunks, written, skipped, bytes, aborted}.aborted: true. The chunks already written stay.What it writes
It writes the layout the engine already serves for the view, as files:
.zgroup,.zattrs,0/.zarray(compressor: {"id": "gzip", "level": 6},dimension_separator: "/"),0/.zattrs, and the chunks at0/i/j/k.zarr.json,0/zarr.json(codecsbytesthengzip), and the chunks at0/c/i/j/k."0". The level is the group's only dataset, with its own scale and translation and its whole shape. The box therefore lands where the view has it, and the chunks outside the box are simply missing, which zarr reads as the fill value.CompressionStream, or none.skipEmpty, a chunk that is all zeros is not written.Scheduling in
web/src/demand.tsThis is the part most worth reviewing.
submit,claimandreleasetake an optionalbackgroundflag. It defaults to false, so existing callers (the routes,contrib/blocks.ts) are unchanged. The rules:max(1, slots - 1)slots, so a client's request finds a free slot, or gets the next one that frees.concurrencychunks at a time, never the whole region. The next chunk is waiting while one is being written.I measured this in headless Chromium, with 4 slots and a fake producer taking 60 ms per chunk:
All of these ran before the second commit. With the first commit alone, a second materialize of the same level also came entirely from kept chunks, with 0 recomputed. With the second commit, only chunks a client asked for are kept, so a repeat run recomputes the rest.
Second commit: background-only chunks are not kept
If only a background claim asked for a chunk, the chunk is not put in the 256 MB kept-chunk cache once it is written. That cache is for clients that fetch a chunk again. Without this change, a large region pushed what the viewer was looking at out of the cache, in favour of chunks no client might ask for.
The engine records the keys of in-flight chunks that a client asked for, and keeps only those. So a chunk a viewer asked for while materialize had it on its way is kept as before. A staged view's stage outputs (512 MB) are kept as usual, so a large region still cycles through those.
caching.mdsays this.layout.tsrefactorThe builders for the group, array and chunk-key metadata moved from
engine.tsinto a newweb/src/layout.ts, so the routes and the writer share one copy. The served JSON is unchanged.TO_SECONDSandisTimemoved with them and are still exported fromengine.ts.Docs
docs/concepts/formats.md: a new section, "A region written to a store (browser engine)".docs/concepts/caching.md: the background class, after thedemand.gatherparagraph.docs/design.md: a bullet under "Another page on the engine".docs/roadmap.md: item 2 now names this as a first piece.CHANGELOG.md: a line for it.Known limits and open questions
CompressionStream, at about 10 ms per MB chunk. Should it move to a worker?"0". Is a downsampled pyramid worth adding later? Should the level keep its own index instead?Engine.materializehas no automated test of its own. Node can't importengine.tsbecause of its JSON import. The pure parts and the write loop are tested over a fake producer, and the output is read back with zarrita. The method itself was checked on a throwaway Chromium page.Checks, after rebasing on main with #37 and #38:
npm run checkinweb/passes: both typechecks, 56/56 tests, and the build.mkdocs build --strictpasses.pytest -q tests/test_docs.pypasses.tests/test_demand.pyandtests/test_chained_passes.pypass.