[IMP] script todo: stats d'install (durée/version) + moyennes par arch/distro
L'historique d'installation est désormais enregistré dans un fichier DÉDIÉ .venv.erplibre/qemu_install_stats.json (repli ~/.erplibre) : chaque run garde distro + version + architecture + durée + horodatage (500 derniers). - record_duration(distro, version, arch, secs) enregistre le run (appelé par le dashboard à la complétion d'une VM). - Menus de sélection enrichis : chaque architecture et chaque distribution affiche la DURÉE MOYENNE d'install historique « · ~5m moy (3) » quand la donnée existe. La DERNIÈRE install (distro version [arch] — durée) est rappelée en tête du déploiement. - eta_reference lit désormais les runs (médiane par arch, repli global). Nouveaux helpers : avg_by_arch, avg_by_distro, last_run. Validé : enregistrement + moyennes (amd64 ~5m (2), ubuntu ~13m (3)), dernière install affichée, distros sans données masquées. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4c04145c31
commit
b2a2db686e
3 changed files with 111 additions and 23 deletions
|
|
@ -192,8 +192,14 @@ def _read_new(path: str, offset: int) -> tuple[str, int]:
|
|||
# Télémétrie, historique de durées (ETA), navigateur CLI
|
||||
# --------------------------------------------------------------------------- #
|
||||
def _stats_path() -> Path:
|
||||
"""Fichier d'historique persistant (durées d'installation par arch)."""
|
||||
return session_dir() / "stats.json"
|
||||
"""Fichier d'historique persistant des installations. DÉDIÉ dans
|
||||
.venv.erplibre du dépôt (repli sur ~/.erplibre si le venv est absent)."""
|
||||
try:
|
||||
venv = Path(__file__).resolve().parents[2] / ".venv.erplibre"
|
||||
venv.mkdir(parents=True, exist_ok=True)
|
||||
return venv / "qemu_install_stats.json"
|
||||
except OSError:
|
||||
return session_dir() / "stats.json"
|
||||
|
||||
|
||||
def load_stats() -> dict:
|
||||
|
|
@ -203,34 +209,68 @@ def load_stats() -> dict:
|
|||
return {}
|
||||
|
||||
|
||||
def record_duration(arch: str, secs: float) -> None:
|
||||
"""Mémorise la durée d'une install (par architecture) pour estimer l'ETA
|
||||
des prochaines. On garde les 20 dernières valeurs par arch."""
|
||||
def record_duration(distro, version, arch, secs) -> None:
|
||||
"""Enregistre une install (distro + version + archi + durée + horodatage)
|
||||
dans l'historique, pour l'ETA et les moyennes par archi/distro. Garde les
|
||||
500 derniers runs."""
|
||||
data = load_stats()
|
||||
durations = data.setdefault("durations", {})
|
||||
key = arch or "unknown"
|
||||
lst = durations.setdefault(key, [])
|
||||
lst.append(int(secs))
|
||||
durations[key] = lst[-20:]
|
||||
runs = data.setdefault("runs", [])
|
||||
runs.append(
|
||||
{
|
||||
"distro": distro or "?",
|
||||
"version": version or "?",
|
||||
"arch": arch or "?",
|
||||
"seconds": int(secs),
|
||||
"ts": int(time.time()),
|
||||
}
|
||||
)
|
||||
data["runs"] = runs[-500:]
|
||||
try:
|
||||
_stats_path().write_text(json.dumps(data))
|
||||
_stats_path().write_text(json.dumps(data, ensure_ascii=False))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def eta_reference(stats: dict, arch: str) -> int | None:
|
||||
"""Durée d'install de RÉFÉRENCE (médiane) pour cette arch, d'après
|
||||
l'historique ; repli sur toutes archis confondues. None si aucun historique."""
|
||||
durations = stats.get("durations", {}) or {}
|
||||
lst = durations.get(arch or "unknown") or []
|
||||
if not lst:
|
||||
lst = [x for v in durations.values() for x in v]
|
||||
if not lst:
|
||||
def _runs(stats=None):
|
||||
return (stats or load_stats()).get("runs", []) or []
|
||||
|
||||
|
||||
def eta_reference(stats, arch):
|
||||
"""Durée d'install de RÉFÉRENCE (médiane) pour cette archi ; repli toutes
|
||||
archis confondues. None si aucun historique."""
|
||||
runs = _runs(stats)
|
||||
secs = [r["seconds"] for r in runs if r.get("arch") == arch]
|
||||
if not secs:
|
||||
secs = [r["seconds"] for r in runs]
|
||||
if not secs:
|
||||
return None
|
||||
s = sorted(lst)
|
||||
s = sorted(secs)
|
||||
return s[len(s) // 2]
|
||||
|
||||
|
||||
def _avg(field, value, stats=None):
|
||||
secs = [r["seconds"] for r in _runs(stats) if r.get(field) == value]
|
||||
return (sum(secs) / len(secs)) if secs else None
|
||||
|
||||
|
||||
def avg_by_arch(arch, stats=None):
|
||||
"""(moyenne_secondes, nb_runs) pour cette archi, ou (None, 0)."""
|
||||
secs = [r["seconds"] for r in _runs(stats) if r.get("arch") == arch]
|
||||
return (sum(secs) / len(secs), len(secs)) if secs else (None, 0)
|
||||
|
||||
|
||||
def avg_by_distro(distro, stats=None):
|
||||
"""(moyenne_secondes, nb_runs) pour cette distro, ou (None, 0)."""
|
||||
secs = [r["seconds"] for r in _runs(stats) if r.get("distro") == distro]
|
||||
return (sum(secs) / len(secs), len(secs)) if secs else (None, 0)
|
||||
|
||||
|
||||
def last_run(stats=None):
|
||||
"""Dernier run enregistré (dict) ou None."""
|
||||
runs = _runs(stats)
|
||||
return runs[-1] if runs else None
|
||||
|
||||
|
||||
def _fmt_size(nbytes) -> str:
|
||||
"""Octets -> « 1.2G » / « 345M » / « 12K »."""
|
||||
if nbytes is None:
|
||||
|
|
@ -540,7 +580,12 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
|||
elapsed = now - started
|
||||
self._final[name] = (state, code, elapsed)
|
||||
if state == "done":
|
||||
record_duration(vm.get("arch"), elapsed)
|
||||
record_duration(
|
||||
vm.get("distro"),
|
||||
vm.get("version"),
|
||||
vm.get("arch"),
|
||||
elapsed,
|
||||
)
|
||||
self._stats = load_stats()
|
||||
lbl = "✅" if state == "done" else f"❌ ({code})"
|
||||
self._set_cell(table, name, "state", lbl)
|
||||
|
|
|
|||
|
|
@ -947,6 +947,37 @@ class TODO:
|
|||
return self._QEMU_ARM64_DISTROS
|
||||
return None
|
||||
|
||||
def _qemu_last_run_line(self):
|
||||
"""Ligne « dernière install » (distro version [arch] en durée), depuis
|
||||
l'historique (.venv.erplibre) ; '' si aucune donnée."""
|
||||
try:
|
||||
from script.todo import qemu_install_monitor as mon
|
||||
|
||||
r = mon.last_run()
|
||||
if r:
|
||||
return (
|
||||
f" ℹ {t('Last install:')} {r.get('distro')} "
|
||||
f"{r.get('version')} [{r.get('arch')}] — "
|
||||
f"{mon._fmt_secs(r.get('seconds', 0))}"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
def _qemu_stat_avg(self, field, value):
|
||||
"""Suffixe « · ~5m moy (3) » : durée d'install MOYENNE historique pour
|
||||
cette archi/distro (fichier .venv.erplibre), ou '' si aucune donnée."""
|
||||
try:
|
||||
from script.todo import qemu_install_monitor as mon
|
||||
|
||||
fn = mon.avg_by_arch if field == "arch" else mon.avg_by_distro
|
||||
secs, n = fn(value)
|
||||
if secs:
|
||||
return f" · ~{mon._fmt_secs(secs)} {t('avg')} ({n})"
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
def _qemu_ask_arch(self, opts, native, allow_all=False):
|
||||
"""Affiche les architectures `opts` (natif marqué d'un *) et renvoie le
|
||||
choix. Si `allow_all`, propose aussi [all] = toutes les archis (renvoie
|
||||
|
|
@ -963,7 +994,7 @@ class TODO:
|
|||
label += f" ({t('ARM 64-bit — emulated, slow')})"
|
||||
else:
|
||||
label += f" ({t('emulated, slow')})"
|
||||
print(f" [{i}] {label}")
|
||||
print(f" [{i}] {label}{self._qemu_stat_avg('arch', a)}")
|
||||
if allow_all:
|
||||
print(f" [all] {t('All supported architectures')}")
|
||||
sel = (
|
||||
|
|
@ -2408,6 +2439,10 @@ class TODO:
|
|||
print(f"{t('Cannot load QEMU catalog: ')}{exc}")
|
||||
return
|
||||
distros = list(mod.DISTROS)
|
||||
# Rappel de la dernière installation enregistrée (si historique).
|
||||
last = self._qemu_last_run_line()
|
||||
if last:
|
||||
print(last)
|
||||
|
||||
# 0) Architecture du parc (défaut : native ; [all] = TOUTES les archis
|
||||
# supportées). Pour une arch précise non-amd64, on restreint le
|
||||
|
|
@ -2451,7 +2486,7 @@ class TODO:
|
|||
vers = ", ".join(
|
||||
(v + " *" if v == default_v else v) for v in mod.DISTROS[d][0]
|
||||
)
|
||||
print(f" [{i}] {d} ({vers})")
|
||||
print(f" [{i}] {d} ({vers}){self._qemu_stat_avg('distro', d)}")
|
||||
print(f" [all] {t('Whole catalog (every version)')}")
|
||||
print(
|
||||
f" [principal] {t('The main version of each distro (marked *)')}"
|
||||
|
|
|
|||
|
|
@ -1322,6 +1322,14 @@ TRANSLATIONS = {
|
|||
"fr": "vue",
|
||||
"en": "view",
|
||||
},
|
||||
"avg": {
|
||||
"fr": "moy",
|
||||
"en": "avg",
|
||||
},
|
||||
"Last install:": {
|
||||
"fr": "Dernière install :",
|
||||
"en": "Last install:",
|
||||
},
|
||||
"No command found.": {
|
||||
"fr": "Aucune commande trouvée.",
|
||||
"en": "No command found.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue