From b2a2db686e98c729c1be40936d933fcafde3dfd5 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 29 Jul 2026 10:46:21 +0000 Subject: [PATCH] =?UTF-8?q?[IMP]=20script=20todo:=20stats=20d'install=20(d?= =?UTF-8?q?ur=C3=A9e/version)=20+=20moyennes=20par=20arch/distro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- script/todo/qemu_install_monitor.py | 87 ++++++++++++++++++++++------- script/todo/todo.py | 39 ++++++++++++- script/todo/todo_i18n.py | 8 +++ 3 files changed, 111 insertions(+), 23 deletions(-) diff --git a/script/todo/qemu_install_monitor.py b/script/todo/qemu_install_monitor.py index 37f891d..7705e78 100644 --- a/script/todo/qemu_install_monitor.py +++ b/script/todo/qemu_install_monitor.py @@ -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) diff --git a/script/todo/todo.py b/script/todo/todo.py index 8dd97cc..3940f48 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -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 *)')}" diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 2eef0aa..14873e1 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -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.",