STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
summary.py
Go to the documentation of this file.
1"""What can be said about a run without exporting it.
2
3Opening a run used to trigger the parquet export — tens of seconds and a few hundred MB
4of cache for a big one, spent whether or not you meant to plot anything. So the export is
5now an explicit action, and this module answers "what am I about to open?" from the run
6directory alone, in well under a second.
7
8Everything here comes from filesystem metadata, deliberately. elodin-db's on-disk layout
9(a directory per component, each with sparse `data`/`index` files and a `metadata` blob)
10is undocumented and version-specific: an early attempt to read component names and row
11counts straight out of it decoded a plausible-looking row count that was wrong for every
12component — 2536 where the parquet had 314. Sizes, counts and mtimes cannot lie in that
13way, so that is all this reads.
14
15The one number that is an estimate is the duration, and it is marked as one.
16"""
17
18from __future__ import annotations
19
20import subprocess
21
22from . import config, export_cache, run_config
23
24
25def _size_bytes(run_id: str) -> int | None:
26 """On-disk size. `du` block counts, not apparent size — every component's data and
27 index file is a sparse 8 GiB, so `st_size` would report the run as ~8 TB."""
28 db = config.ELODIN_DIR / run_id
29 try:
30 out = subprocess.run(
31 ["du", "-sk", str(db)], capture_output=True, text=True, timeout=60
32 )
33 if out.returncode == 0:
34 return int(out.stdout.split()[0]) * 1024
35 except (ValueError, OSError, subprocess.SubprocessError):
36 pass
37 return None
38
39
40def _components_and_span(run_id: str) -> tuple[int, float | None]:
41 """Component count, and the wall time between the first and last write.
42
43 One stat per component. The span is when data was *written*, which trails the sample
44 times the plot is drawn on by the write latency at each end — measured 0.3-0.7 s low
45 on 20-35 s runs, i.e. within a percent. Good enough to answer "how long was this run"
46 before committing to an export; the exact figure replaces it once indexed.
47 """
48 db = config.ELODIN_DIR / run_id
49 oldest = newest = None
50 n = 0
51 try:
52 entries = list(db.iterdir())
53 except OSError:
54 return 0, None
55 for entry in entries:
56 # Component directories are named by a numeric id; `db_state` and `msgs` are not.
57 if not entry.name.isdigit():
58 continue
59 n += 1
60 try:
61 mt = (entry / "data").stat().st_mtime
62 except OSError:
63 continue
64 oldest = mt if oldest is None else min(oldest, mt)
65 newest = mt if newest is None else max(newest, mt)
66 span = (newest - oldest) if (oldest is not None and newest is not None) else None
67 return n, span
68
69
70def summarize(run_id: str) -> dict:
71 """Cheap facts about a run, whether or not it has ever been indexed."""
72 n_components, span = _components_and_span(run_id)
73 return {
74 "run_id": run_id,
75 "cached": export_cache.is_cached(run_id),
76 "size_bytes": _size_bytes(run_id),
77 # Every component the DB holds. Indexing reports a smaller number: the export
78 # only resolves components it can name, and the rest stay bare numeric hashes.
79 "n_components": n_components,
80 "duration_s": span,
81 # Never presented as exact; see _components_and_span.
82 "duration_approx": True,
83 # Whether <run_id>.toml exists, so the client can skip asking for a config that
84 # runs predating the snapshot never had, rather than eating a 404 per selection.
85 "has_config": run_config.snapshot_path(run_id).exists(),
86 }
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
tuple[int, float|None] _components_and_span(str run_id)
Definition summary.py:40
int|None _size_bytes(str run_id)
Definition summary.py:25
dict summarize(str run_id)
Definition summary.py:70