an.build.shot_cache#
The content-keyed shot cache: ADR 0004’s first slice, behind the incremental= seam.
render_project asks an incremental engine, per shot, for a plan: the
shot’s key, and — when an entry with that key exists — the rendered shot to
reuse. It renders only the misses, and hands each fresh render back to be
recorded. The engine is a seam (incremental= on an.render.render), and
ShotCache is its built-in default; an nw-backed engine is the
planned alternative (ADR 0004 decision 5).
**Data model: lacing’s** (decision 4). An entry is a ShotArtifact —
a lacing.Artifact (asset_id = sha256 of the mp4, W3C-PROV provenance
whose was_derived_from lists the key’s input digests) plus the named parts
of its key and the timings that produced it. Entries live in a
lacing.ArtifactStore: a catalog (key -> record) and a content-addressed
blob store, both injected dol mappings. In a project the store is
mall["shot_cache"] (artifacts/shot_cache/{catalog,blobs}/).
An ASSEMBLED film (transitions, a sound layer) is a stream-copy concat of
segments (an.assemble, an#260): a shot no transition touches needs only its
mp4, and a shot one touches also needs its parts — its body, encoded once,
and the PNGs inside its transition window — a second entry,
<key>.parts.<code>.h<head>.t<tail>, of a few MB. So such a film reuses its
shots by default. (The older whole-frames entry, <key>.frames, is read only
by a caller that asks for it with needs_frames=True; the render loop no
longer does.)
Each cached render also records a root (root.<digest>): which entries a
render of output <name> under one set of render knobs on one machine used.
Roots are what an.build.gc keeps alive, beside what the current scene reaches.
Invalidation is by digest, never by deletion (decision 6): a changed input
is a different key, and the old entry simply stops being asked for. The
pre-cache artifacts/shots/<shot.id>.mp4 archive is not read. Collecting
unreachable entries is a separate, explicit command: an cache gc
(an.build.gc).
>>> from an.build.shot_cache import BuildReport, ShotOutcome
>>> r = BuildReport([ShotOutcome("a", "cutout", "reused", key="k" * 64),
... ShotOutcome("b", "cutout", "rendered", key="j" * 64, reason="new or changed")])
>>> r.summary()
'2 shot(s): 1 rendered (b), 1 reused (a); not reused: new or changed (b)'
Module Attributes
The mall key of the shot cache. |
|
what an assembled film takes from a shot its transitions touch — its body, encoded once, and the PNGs inside its window. |
|
what one render used (see |
Functions
|
The digest of |
|
Why a shot keyed |
|
|
A shot cache held in dicts — for tests, and for a mall with no disk. |
|
A short digest naming this machine (its host name and hardware address), so each machine's renders of a synced project keep a root of their own. |
|
|
A short digest naming a project by its resolved directory, so two projects sharing one cache store keep a root each ( |
|
The catalog id of the parts entry of shot |
|
|
The record type ( |
|
|
A filesystem shot cache under |
|
|
Classes
|
Every shot's outcome, in timeline order. |
|
The |
|
The built-in engine: look a shot's key up in an ArtifactStore; record misses. |
|
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 shot cache could not do something it should have; the render still stands. |
- class an.build.shot_cache.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)'
- class an.build.shot_cache.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).
- an.build.shot_cache.PARTS_INFIX: str = '.parts.'#
what an assembled film takes from a shot its transitions touch — its body, encoded once, and the PNGs inside its window. See
parts_entry_id().- Type:
The catalog id infix of a shot’s PARTS entry (an#260)
- an.build.shot_cache.ROOT_PREFIX: str = 'root.'#
what one render used (see
ShotCache.record_root()).- Type:
The catalog id prefix of a ROOT
- class an.build.shot_cache.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.shot_cache.ShotCacheWarning[source]#
Bases:
UserWarningThe shot cache could not do something it should have; the render still stands.
- class an.build.shot_cache.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.shot_cache.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.shot_cache.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.shot_cache.explain_change(before, before_reads, after, after_reads, *, before_code=None, after_code=None)[source]#
Why a shot keyed
afteris not the entry keyedbefore, in words.The assets it read that moved come first, by name (
asset changed: props/logo), then every other key part that moved, appeared or went, byPART_LABELS(an optional part —fonts,runtime_extensions— comes and goes with what the shot draws). The parts that follow from an asset (FOLLOWS_ASSETS) are left out when one is named. Both keys are of one composition: a differentkey_versionisKEY_FORMAT_CHANGED, decided before this is called.- Return type:
>>> explain_change({"compiled": "a", "assets": "x"}, {"props/logo": "1"}, ... {"compiled": "b", "assets": "y"}, {"props/logo": "2"}) 'asset changed: props/logo' >>> explain_change({"knobs": "a", "compiled": "c"}, {}, {"knobs": "b", "compiled": "c"}, {}) 'render settings changed' >>> explain_change({"compiled": "a"}, {}, {"compiled": "a", "fonts": "f"}, {}) 'the system fonts changed' >>> explain_change({"assets": "a"}, {}, {"assets": "b"}, {}) # an unrecorded shot 'a project asset changed' >>> explain_change({"code": "a"}, {}, {"code": "b"}, {}, ... before_code={"an.motion": "1"}, after_code={"an.motion": "2"}) "an's render code (an.motion) changed"
- an.build.shot_cache.human_bytes(n)[source]#
nbytes for a person: 1024-based, one decimal.- Return type:
>>> human_bytes(0), human_bytes(1536), human_bytes(5 * 1024**3) ('0 B', '1.5 KB', '5.0 GB')
- an.build.shot_cache.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.shot_cache.machine_id()[source]#
A short digest naming this machine (its host name and hardware address), so each machine’s renders of a synced project keep a root of their own. A digest, never the names themselves.
- Return type:
>>> len(machine_id()) 16
- an.build.shot_cache.parts_entry_id(key, window)[source]#
The catalog id of the parts entry of shot
keyforwindow.The id names the window (the same shot cut for another neighbour is a different entry) and the digest of the code that cuts and encodes the parts (
parts_code_digest()): a parts entry is produced byan.assemble, not by the renderer, so the renderer’s owncodekey part does not cover it.- Return type:
>>> from an.assemble import ShotWindow >>> parts_entry_id("k" * 64, ShotWindow(frames=30, head=0, tail=4)).endswith(".h0.t4") True
- an.build.shot_cache.project_id(project_root)[source]#
A short digest naming a project by its resolved directory, so two projects sharing one cache store keep a root each (
""for none).- Return type:
>>> project_id(None) '' >>> len(project_id(".")) 16
- an.build.shot_cache.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_cache.shot_artifact_type()[source]#
The record type (
lacing.Artifactsubclass), built on first use.
- an.build.shot_cache.shot_cache_store(root)[source]#
A filesystem shot cache under
root:catalog/+blobs/(lacing’s layout).- Return type:
- an.build.shot_cache.store_usage(store)[source]#
(bytes, entries)of a shot cache store: every blob byte on disk (or, off disk, every distinct blob its records name) and its catalog entries.On a filesystem store this is a directory listing, not a read of every record, so the render summary can afford it on every render.