cache
tit.scene.cache ¶
The scene cache (plan ยง2.2): fingerprinted files under derivatives/.
Layout, exactly as decision S2 freezes it::
<project>/.ti-toolbox/cache/scene/sub-<id>/
skin.<fingerprint>.tvsc skin.<fingerprint>.gii + skin.<fingerprint>.json
gm.<fingerprint>.tvsc gm.<fingerprint>.gii + gm.<fingerprint>.json
labels-DK40.<fingerprint>.{tvsc,gii} + labels-DK40.<fingerprint>.json
One key, one fingerprint, one sidecar -- and one payload file per
serialisation in :data:FORMATS. tvsc remains the frozen compatibility
payload and gii is the standard GIfTI interchange mesh; the
sidecar describes the surface rather than its encoding, so there is exactly
one of it.
The fingerprint is a hash of (name, size, mtime_ns) of every source
file that fed the artifact, so a re-run of charm (new ernie.msh) or a
re-run of the atlas step (new .annot) produces a different name and the
stale file is deleted on the next write. Fingerprinting the content was
rejected: hashing a 184 MB mesh costs more than rebuilding the surface from
it (measured: 1.26 s to read and crop the whole mesh).
Concurrency, and the failure each measure prevents:
- Per-subject build lock (in-process
threading.Lock) -- FastAPI runs adefroute in a threadpool, so two panes opening at once would otherwise both read the 184 MB mesh and both spend ~600 MB of RSS. The lock is keyed by(project_dir, subject)so two different subjects still build in parallel. - Atomic publish (write
<name>.tmp-<pid>-<uid>,os.replace) -- a reader must never see a half-written.tvsc.os.replaceis atomic within a filesystem, and the temp file is created in the same directory so it always is. This is what makes the design correct even if a second process (a second uvicorn worker, a stray CLI) builds at the same time: the lock is an efficiency measure, the atomic rename is the correctness one.
The directory is regenerable bookkeeping, not a BIDS entity, so it lives in
the project's one dot-directory (.ti-toolbox/cache/), which
:mod:tit.paths owns, creates lazily and lists in the project's
.bidsignore.
CachedArtifact
dataclass
¶
One published cache entry: its bytes, its sidecar, and the sidecar's data.
cache_dir ¶
<project>/.ti-toolbox/cache/scene/sub-<id>/ -- from :mod:tit.paths.
Source code in tit/scene/cache.py
ensure_bidsignore ¶
Make sure the project's .bidsignore lists :data:BIDSIGNORE_LINE.
Idempotent, and appends without touching any existing line -- users curate that file by hand, so rewriting it would throw away their entries.
Source code in tit/scene/cache.py
fingerprint ¶
16 hex chars over (basename, size, mtime_ns) of each source file.
A missing source contributes "-" rather than raising, so a subject
with (say) no rh annotation still gets a stable, distinct fingerprint
instead of no cache at all.
version is the builder's own contribution, and it is not optional
for a real caller (:data:tit.scene.build.BUILDER_VERSION is what every
one of them passes). The source files say what went in; they cannot say
what the code made of it, so without a version salt an entry written by an
older builder is served for ever from unchanged inputs -- which is exactly
what happened to the inward-wound gm surface fixed on 2026-09-04: the
mesh had not changed, so neither had the fingerprint, so the corrected
build was never reached. It defaults to "" (no salt) only so the pure
cache tests can exercise the source half on its own.
Source code in tit/scene/cache.py
artifact_paths ¶
artifact_paths(project_dir: str | PathLike[str], subject_id: str, key: str, fp: str, ext: str = 'tvsc') -> tuple[Path, Path]
(<key>.<fp>.<ext>, <key>.<fp>.json) inside the subject's cache dir.
The sidecar is shared between formats: it describes the surface (vertex and triangle counts, the simplification, the bbox), not its encoding, and two sidecars would be two answers to "how many triangles is this".
Source code in tit/scene/cache.py
find_cached ¶
find_cached(project_dir: str | PathLike[str], subject_id: str, key: str, fp: str, ext: str = 'tvsc') -> CachedArtifact | None
The published entry for key/fp, or None if it is not there.
Both halves must exist: a payload whose sidecar is missing carries no triangle counts or build time, and the manifest would then have to lie about what it is serving.
Source code in tit/scene/cache.py
publish ¶
publish(project_dir: str | PathLike[str], subject_id: str, key: str, fp: str, blob: bytes, meta: dict, ext: str = 'tvsc') -> CachedArtifact
Write blob/meta atomically and delete this key's stale entries.
Source code in tit/scene/cache.py
prune_stale ¶
prune_stale(project_dir: str | PathLike[str], subject_id: str, key: str, keep_fp: str) -> list[Path]
Delete <key>.<other>.{tvsc,json} files, keeping keep_fp.
Without this the cache grows a new copy of every surface each time charm is re-run, and nothing ever reclaims the old ones.
Source code in tit/scene/cache.py
subject_lock ¶
The one build lock for this project+subject, created on first use.