an.build#
Incremental re-processing: content-addressed build stages (ADR 0004).
A draft must stay adjustable without re-processing everything: an edit
re-renders only what depends on it. The first slice is the shot cache —
an render skips any shot whose key (everything its render reads, digested;
never shot.id) already has an entry, and reuses that entry’s mp4.
an.build.keys— canonical digests, the project-wide fallback dependency, and the registry through which a renderer says what its shot render reads (register_shot_keyer()).an.build.reads— what a shot read: the recording view of the mall a keyer compiles against, and the digests of the entries it saw (an#316).an.build.shot_cache— theincremental=seam (IncrementalEngine), its built-in engineShotCache, and the entries, shaped aslacingartifacts in alacing.ArtifactStore.an.build.gc— garbage collection: what the current scene and the recorded renders reach, and deleting the rest (an cache gc,an cache info;an.build.cli).
The core names no renderer; the cut-out keyer lives with the cut-out backend
(an.stage.cache_key) and registers on its import.
>>> from an.build import ShotCache, resolve_incremental
>>> isinstance(resolve_incremental(True), ShotCache)
True
Functions
|
What |
|
The hex sha256 of |
|
Delete the shot-cache entries of |
|
The shot key: one digest over the named parts and the key's own version. |
|
Every asset in the project (decision 3's fallback), without the root files — those are |
|
The digest of |
A shot cache held in dicts — for tests, and for a mall with no disk. |
|
|
One digest over every asset store of the project (ADR 0004 decision 3). |
|
What every shot depends on project-wide: the project-root files ( |
|
|
|
Declare how shots of |
The renderer names that have a keyer. |
|
|
|
The record type ( |
|
|
A filesystem shot cache under |
|
The keyer that describes |
Classes
|
A view of |
|
Every shot's outcome, in timeline order. |
|
The |
|
The built-in engine: look a shot's key up in an ArtifactStore; record misses. |
|
What a renderer's keyer returns for one shot. |
|
What happened to one shot in one render, with its wall times (seconds). |
|
The engine's answer for one shot: its key, and what to reuse if anything. |
Exceptions
The collection cannot be done safely; nothing was deleted. |
|
The shot cache could not do something it should have; the render still stands. |
- class an.build.BuildReport(outcomes=<factory>, store_bytes=None, store_entries=None)[source]#
Bases:
objectEvery shot’s outcome, in timeline order.
- store_bytes: int | None = None#
The shot cache’s size after the render, when the engine measured it.
- store_line()[source]#
How big the shot cache is after the render, or
""if unmeasured.- Return type:
>>> BuildReport(store_bytes=3 * 1024**2, store_entries=4).store_line() '3.0 MB in 4 entries'
- summary()[source]#
One line: what was rendered, what reused, and WHY each rendered shot was not reused — a cache that silently re-renders everything reads as a broken cache (an#243 review, R2-1).
- Return type:
>>> BuildReport([ShotOutcome("a", "cutout", "uncached", reason=FRAMES_NOT_CACHED)]).summary() '1 shot(s): 1 rendered (a), 0 reused (-); not reused: frames not cached: film has transitions/sound; pass --cache-frames (a)'
- exception an.build.CacheGcError[source]#
Bases:
RuntimeErrorThe collection cannot be done safely; nothing was deleted.
- class an.build.IncrementalEngine(*args, **kwargs)[source]#
Bases:
ProtocolThe
incremental=seam ofan.render.render(ADR 0004 decision 5).beginonce per render with the project mall (and its root, for the root files every shot depends on);planonce per shot, BEFORE any shot renders (in the calling thread);recordonce per shot that was rendered (possibly from a worker thread);finishreturns the report.Two hooks are OPTIONAL (the render loop calls them when an engine has them):
record_parts(plan, parts)once per rendered shot whose film window is not whole (an#260), andrecord_root(output_name, profile=..., output=...)once the film is delivered (what the garbage collector keeps).
- class an.build.RecordingMall(mall, *, recorded=('characters', 'environments', 'props', 'styles', 'voices', 'sounds'))[source]#
Bases:
MappingA view of
mallthat records which asset entries are read through it.Stores named in
recordedcome back wrapped in aRecordingStore(one per store, so identity is stable across lookups); every other store comes back as it is.readsis the set of(store, key)read so far,keybeingWHOLE_STOREfor a store that was listed.
- class an.build.ShotCache(store=None, *, environment=<function default_environment_digest>, dependencies=<function project_dependencies>, fallback=<function every_asset_digest>, record_reads=True, cache_frames=False)[source]#
Bases:
objectThe built-in engine: look a shot’s key up in an ArtifactStore; record misses.
storeis the injectedlacing.ArtifactStore;Nonemeans the mall’sshot_cache(resolved inbegin()), and a mall without one renders every shot.environment(renderer_name) -> digestis the environment seam — injectable so a test (or a remote-render backend) can state its machine rather than probe this one. Three dependency seams (seeDependencies):record_reads(on by default) keys a shot whose keyer is registered withrecords_reads=Trueon the asset entries its compile read (an.build.reads, an#316: theassetspart), so an edit to an asset re-renders only the shots that read it;fallbackis theassetspart of every OTHER shot — a keyer that cannot vouch for its reads, or every shot underrecord_reads=False: by default every asset in the project (decision 3’s first slice);dependenciesis what EVERY shot depends on (theprojectpart): by default the library lockfile.Nonedrops it, andfallback=Nonekeys an unrecorded shot on its own parts alone — use either knowingly.
An engine built with other seams writes keys
an cache gc(which recomputes the current scene’s keys with the defaults) does not reach: collect such a cache with the same engine.cache_framesalso stores each shot’s whole PNG sequence when a caller plans withneeds_frames=True. The render loop no longer does (an#260): an assembled film takes a shot’s mp4 and, at a transition, its parts (window=), which are cached by default at a few MB. Kept for callers of the engine that need every frame;an cache gccollects old frames.After a render,
reportholds what happened to each shot.- entry_ids(shot, renderer, ctx, *, window=None)[source]#
The catalog ids a
plan()of this shot would read — computed by the same code, looking nothing up and rendering nothing. Whatan.build.gckeeps for the current scene. Callbegin()first.
- plan(shot, renderer, ctx, *, needs_frames=False, window=None, force=False)[source]#
windowis what an assembled film needs from this shot (an.assemble.ShotWindow;Nonefor a plain concat): a shot whose window is not whole is reused only when its parts entry is there too.- Return type:
- record_parts(plan, parts)[source]#
Store a freshly rendered shot’s
an.assemble.ShotPartsunderplan.parts_id: its body mp4 and its window’s PNGs, one stored zip. A failed write never fails the render (the next render renders it).- Return type:
- record_root(output_name, *, profile, output)[source]#
Record what this render used, as a ROOT for
an.build.gc; return its id.A root is keyed by the project (
project_id()), the output name, the renderprofile(the knobs as passed —Nonemeaning “the scene’s own”) and this machine (machine_id()), so the next render of the same output with the same knobs on the same machine replaces it — a browser upgrade included — while a render on another machine of a synced project, or of another project sharing the store, keeps its own. Its record is alacing.Artifactof the delivered film, derived from the shot keys. Also measures the store, for the summary.
- exception an.build.ShotCacheWarning[source]#
Bases:
UserWarningThe shot cache could not do something it should have; the render still stands.
- class an.build.ShotKeyInputs(parts, compile_s=None, details=<factory>)[source]#
Bases:
objectWhat a renderer’s keyer returns for one shot.
partsare named sha256 hex digests (the key’s content half); the engine addsrenderer,projectandenvironment.compile_sis the wall time of the compile the keyer ran to get its digest, orNonefor a renderer that has no compile stage.
- class an.build.ShotOutcome(shot_id, renderer, status, key=None, reason='', key_s=None, compile_s=None, render_s=None, cached_render_s=None)[source]#
Bases:
objectWhat happened to one shot in one render, with its wall times (seconds).
key_sis the whole key computation, of whichcompile_sis the compile;render_sis this render’s wall time (Nonewhen reused) andcached_render_sthe wall time of the render being reused.
- class an.build.ShotPlan(shot_id, renderer, key, inputs=<factory>, reads=<factory>, code=<factory>, cached=None, reason='', key_s=None, compile_s=None, cached_render_s=None, needs_frames=False, window=None, parts_id=None, parts=None)[source]#
Bases:
objectThe engine’s answer for one shot: its key, and what to reuse if anything.
- an.build.cache_info(project_dir, *, reachability=True, engine=None, now=None)[source]#
What
project_dir’s shot cache holds; never fails on reachability (it says why it is unknown instead).- Return type:
- an.build.collect_garbage(project_dir, *, dry_run=False, max_size=None, max_age=None, engine=None, now=None, force=False)[source]#
Delete the shot-cache entries of
project_dirthat nothing reaches.With no cap, every unreachable entry goes. With caps, unreachable history is kept within them, newest first, and both bind:
max_age(seconds) keeps only unreachable entries written more recently than that (and lets a recorded render older than that stop protecting its entries);max_size(bytes) keeps only those that fit in a cache of that size. Neither ever removes a reachable entry, so the cache can stay abovemax_size.dry_runreports and deletes nothing.forcecollects a cache no render of this project has recorded a root in (seereachable_entries()).See the module docstring for the reachability and concurrency argument.
- Return type:
- an.build.compose_shot_key(parts)[source]#
The shot key: one digest over the named parts and the key’s own version.
The parts are a MAPPING so the record can keep them by name, and a re-render can be explained (”
texturesmoved”) rather than merely observed.- Return type:
- an.build.default_environment_digest(renderer_name)[source]#
The digest of
renderer_name’s registered environment probe, once per process.Memoised per renderer NAME (each backend has its own machine: Chromium and ffmpeg for cut-out, a Manim install for Manim), and once per process, because the cut-out probe launches a browser and encodes a frame. So a long-lived host does not see a
playwright installor abrew upgrademade after its first render: restart it, or passforce_render. A renderer with no probe has the empty environment.- Return type:
- an.build.every_asset_digest(mall, *, project_root=None)[source]#
Every asset in the project (decision 3’s fallback), without the root files — those are
project_dependencies()’. Theassetspart of a shot whose keyer cannot vouch for its reads.- Return type:
>>> every_asset_digest({"props": {"a": 1}}) != every_asset_digest({"props": {"a": 2}}) True
- an.build.in_memory_shot_cache_store()[source]#
A shot cache held in dicts — for tests, and for a mall with no disk.
- Return type:
- an.build.project_assets_digest(mall, *, stores=('characters', 'environments', 'props', 'styles', 'voices', 'sounds'), project_root=None, root_files=('assets.lock.json',))[source]#
One digest over every asset store of the project (ADR 0004 decision 3).
The first slice’s dependency edge for every shot: safe — no asset can change without every shot’s key moving — at the price of the per-character saving, which read recording buys back. A store the mall does not have is recorded as absent rather than skipped, so adding one later moves the digest.
With
project_root, the files inroot_files(the library lockfile) are hashed by PATH as well — so a re-pin moves every key whether or not the mall has a store for the file yet.- Return type:
>>> a = project_assets_digest({"characters": {"c": {"v": 1}}}, stores=["characters"]) >>> b = project_assets_digest({"characters": {"c": {"v": 2}}}, stores=["characters"]) >>> a == b False
- an.build.project_dependencies(mall, *, project_root=None)[source]#
What every shot depends on project-wide: the project-root files (
PROJECT_ROOT_FILES, the library lockfile), read by path. A re-pin changes what is checked out, so it moves every key, conservatively.- Return type:
>>> project_dependencies({}) == project_dependencies({"props": {"a": 1}}) True
- an.build.read_digests(mall, reads)[source]#
{"store/key": digest}for every recorded read — the shot’s dependency edges.A read of a store the mall does not have is recorded as
ABSENT, so the day the store appears the key moves.>>> read_digests({}, [("props", "logo")]) {'props/logo': 'absent'}
- an.build.register_shot_keyer(renderer_name, keyer, *, environment=None, renderer_type=None, records_reads=False, replace=False)[source]#
Declare how shots of
renderer_nameare keyed, and how its machine is probed.The registration seam for every backend (cut-out here; Manim’s opaque shots, keyed on source hash + Manim version + quality, are the next).
renderer_typebinds the keyer to one renderer class: a renderer whose type is not exactly it is never cached.records_reads=Trueclaims that every asset the shot depends on is read throughctx.mall(or is already digested by one of the keyer’s parts): the engine then keys the shot on the entries it read rather than on the whole project. A second registration for a name is refused unlessreplace=True— a silent replacement would drop the first keyer’s parts from every key without anyone saying so.- Return type:
- an.build.registered_shot_keyers()[source]#
The renderer names that have a keyer.
A keyer registers beside its renderer, and a backend behind the import firewall registers when the renderer registry first loads it (an#247), so this loads the registry first.
>>> "cutout" in registered_shot_keyers() True
- an.build.resolve_incremental(incremental)[source]#
incremental=→ an engine, orNonefor “render every shot cold”.Trueis a freshShotCacheover the mall’s store;FalseorNoneis off; anything else must be anIncrementalEngine.- Return type:
>>> resolve_incremental(False) is None True >>> isinstance(resolve_incremental(True), ShotCache) True
- an.build.shot_artifact_type()[source]#
The record type (
lacing.Artifactsubclass), built on first use.
- an.build.shot_cache_store(root)[source]#
A filesystem shot cache under
root:catalog/+blobs/(lacing’s layout).- Return type:
- an.build.shot_keyer_for(renderer)[source]#
The keyer that describes
renderer(an instance, or a name), orNone.Nonemeans the shot is never cached: no keyer for the name, or a renderer whose class is not the one the keyer was registered for.- Return type:
_KeyerEntry|None
Modules
|
|
A renderer's derived stores, as |
|
Garbage collection of the shot cache: |
|
Cache keys for build stages: canonical digests, the project fallback, keyers. |
|
What a shot read: a read-recording view of the mall, and the digests of what it saw. |
|
The content-keyed shot cache: ADR 0004's first slice, behind the |