[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
|
# Télémétrie, historique de durées (ETA), navigateur CLI
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
def _stats_path() -> Path:
|
def _stats_path() -> Path:
|
||||||
"""Fichier d'historique persistant (durées d'installation par arch)."""
|
"""Fichier d'historique persistant des installations. DÉDIÉ dans
|
||||||
return session_dir() / "stats.json"
|
.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:
|
def load_stats() -> dict:
|
||||||
|
|
@ -203,34 +209,68 @@ def load_stats() -> dict:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def record_duration(arch: str, secs: float) -> None:
|
def record_duration(distro, version, arch, secs) -> None:
|
||||||
"""Mémorise la durée d'une install (par architecture) pour estimer l'ETA
|
"""Enregistre une install (distro + version + archi + durée + horodatage)
|
||||||
des prochaines. On garde les 20 dernières valeurs par arch."""
|
dans l'historique, pour l'ETA et les moyennes par archi/distro. Garde les
|
||||||
|
500 derniers runs."""
|
||||||
data = load_stats()
|
data = load_stats()
|
||||||
durations = data.setdefault("durations", {})
|
runs = data.setdefault("runs", [])
|
||||||
key = arch or "unknown"
|
runs.append(
|
||||||
lst = durations.setdefault(key, [])
|
{
|
||||||
lst.append(int(secs))
|
"distro": distro or "?",
|
||||||
durations[key] = lst[-20:]
|
"version": version or "?",
|
||||||
|
"arch": arch or "?",
|
||||||
|
"seconds": int(secs),
|
||||||
|
"ts": int(time.time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
data["runs"] = runs[-500:]
|
||||||
try:
|
try:
|
||||||
_stats_path().write_text(json.dumps(data))
|
_stats_path().write_text(json.dumps(data, ensure_ascii=False))
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def eta_reference(stats: dict, arch: str) -> int | None:
|
def _runs(stats=None):
|
||||||
"""Durée d'install de RÉFÉRENCE (médiane) pour cette arch, d'après
|
return (stats or load_stats()).get("runs", []) or []
|
||||||
l'historique ; repli sur toutes archis confondues. None si aucun historique."""
|
|
||||||
durations = stats.get("durations", {}) or {}
|
|
||||||
lst = durations.get(arch or "unknown") or []
|
def eta_reference(stats, arch):
|
||||||
if not lst:
|
"""Durée d'install de RÉFÉRENCE (médiane) pour cette archi ; repli toutes
|
||||||
lst = [x for v in durations.values() for x in v]
|
archis confondues. None si aucun historique."""
|
||||||
if not lst:
|
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
|
return None
|
||||||
s = sorted(lst)
|
s = sorted(secs)
|
||||||
return s[len(s) // 2]
|
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:
|
def _fmt_size(nbytes) -> str:
|
||||||
"""Octets -> « 1.2G » / « 345M » / « 12K »."""
|
"""Octets -> « 1.2G » / « 345M » / « 12K »."""
|
||||||
if nbytes is None:
|
if nbytes is None:
|
||||||
|
|
@ -540,7 +580,12 @@ def run_monitor(manifest_path: str, run_app: bool = True):
|
||||||
elapsed = now - started
|
elapsed = now - started
|
||||||
self._final[name] = (state, code, elapsed)
|
self._final[name] = (state, code, elapsed)
|
||||||
if state == "done":
|
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()
|
self._stats = load_stats()
|
||||||
lbl = "✅" if state == "done" else f"❌ ({code})"
|
lbl = "✅" if state == "done" else f"❌ ({code})"
|
||||||
self._set_cell(table, name, "state", lbl)
|
self._set_cell(table, name, "state", lbl)
|
||||||
|
|
|
||||||
|
|
@ -947,6 +947,37 @@ class TODO:
|
||||||
return self._QEMU_ARM64_DISTROS
|
return self._QEMU_ARM64_DISTROS
|
||||||
return None
|
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):
|
def _qemu_ask_arch(self, opts, native, allow_all=False):
|
||||||
"""Affiche les architectures `opts` (natif marqué d'un *) et renvoie le
|
"""Affiche les architectures `opts` (natif marqué d'un *) et renvoie le
|
||||||
choix. Si `allow_all`, propose aussi [all] = toutes les archis (renvoie
|
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')})"
|
label += f" ({t('ARM 64-bit — emulated, slow')})"
|
||||||
else:
|
else:
|
||||||
label += f" ({t('emulated, slow')})"
|
label += f" ({t('emulated, slow')})"
|
||||||
print(f" [{i}] {label}")
|
print(f" [{i}] {label}{self._qemu_stat_avg('arch', a)}")
|
||||||
if allow_all:
|
if allow_all:
|
||||||
print(f" [all] {t('All supported architectures')}")
|
print(f" [all] {t('All supported architectures')}")
|
||||||
sel = (
|
sel = (
|
||||||
|
|
@ -2408,6 +2439,10 @@ class TODO:
|
||||||
print(f"{t('Cannot load QEMU catalog: ')}{exc}")
|
print(f"{t('Cannot load QEMU catalog: ')}{exc}")
|
||||||
return
|
return
|
||||||
distros = list(mod.DISTROS)
|
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
|
# 0) Architecture du parc (défaut : native ; [all] = TOUTES les archis
|
||||||
# supportées). Pour une arch précise non-amd64, on restreint le
|
# supportées). Pour une arch précise non-amd64, on restreint le
|
||||||
|
|
@ -2451,7 +2486,7 @@ class TODO:
|
||||||
vers = ", ".join(
|
vers = ", ".join(
|
||||||
(v + " *" if v == default_v else v) for v in mod.DISTROS[d][0]
|
(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" [all] {t('Whole catalog (every version)')}")
|
||||||
print(
|
print(
|
||||||
f" [principal] {t('The main version of each distro (marked *)')}"
|
f" [principal] {t('The main version of each distro (marked *)')}"
|
||||||
|
|
|
||||||
|
|
@ -1322,6 +1322,14 @@ TRANSLATIONS = {
|
||||||
"fr": "vue",
|
"fr": "vue",
|
||||||
"en": "view",
|
"en": "view",
|
||||||
},
|
},
|
||||||
|
"avg": {
|
||||||
|
"fr": "moy",
|
||||||
|
"en": "avg",
|
||||||
|
},
|
||||||
|
"Last install:": {
|
||||||
|
"fr": "Dernière install :",
|
||||||
|
"en": "Last install:",
|
||||||
|
},
|
||||||
"No command found.": {
|
"No command found.": {
|
||||||
"fr": "Aucune commande trouvée.",
|
"fr": "Aucune commande trouvée.",
|
||||||
"en": "No command found.",
|
"en": "No command found.",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue