From 85425e27bcb14b6c7b578750fd1c8cb5f4632e3f Mon Sep 17 00:00:00 2001 From: jze9 Date: Tue, 7 Jul 2026 02:04:58 +0500 Subject: [PATCH] Speed up dashboard and add exit-velocity ranking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Web was slow because run_detail and the plot/GIF renderers loaded the ENTIRE runs table to find one row by id (O(n) on a 200k-row DB, seconds per request). Now query by PRIMARY KEY (run_id): run_detail drops from seconds to ~1ms. The KPD histogram is aggregated in SQL instead of streaming every feasible row into Python. - Dashboard now toggles the top table between "по КПД" and "по скорости" (top_by_velocity, indexed on exit_velocity_mps); the best-of card shows both best efficiency and best exit velocity. Co-Authored-By: Claude Sonnet 5 --- src/gausse/storage/database.py | 12 ++++++++- src/gausse/storage/schema.py | 1 + src/gausse/web/page.py | 43 +++++++++++++++++++++---------- src/gausse/web/render.py | 4 +-- src/gausse/web/stats.py | 46 +++++++++++++++++----------------- 5 files changed, 67 insertions(+), 39 deletions(-) diff --git a/src/gausse/storage/database.py b/src/gausse/storage/database.py index 51657da..86533bc 100644 --- a/src/gausse/storage/database.py +++ b/src/gausse/storage/database.py @@ -65,11 +65,19 @@ def count_runs(conn: sqlite3.Connection, feasible: bool | None = None) -> int: return cursor.fetchone()[0] +def fetch_run_by_id(conn: sqlite3.Connection, run_id: str) -> RunRecord | None: + """Одна запись по PRIMARY KEY — быстро (без загрузки всей таблицы).""" + conn.row_factory = sqlite3.Row + row = conn.execute("SELECT * FROM runs WHERE run_id = ?", (run_id,)).fetchone() + return _row_to_record(row) if row is not None else None + + def fetch_runs( conn: sqlite3.Connection, feasible: bool | None = None, min_efficiency: float | None = None, order_by_efficiency_desc: bool = False, + order_by_velocity_desc: bool = False, limit: int | None = None, ) -> list[RunRecord]: conn.row_factory = sqlite3.Row @@ -81,7 +89,9 @@ def fetch_runs( if min_efficiency is not None: query += " AND efficiency >= ?" params.append(min_efficiency) - if order_by_efficiency_desc: + if order_by_velocity_desc: + query += " ORDER BY exit_velocity_mps DESC" + elif order_by_efficiency_desc: query += " ORDER BY efficiency DESC" if limit is not None: query += " LIMIT ?" diff --git a/src/gausse/storage/schema.py b/src/gausse/storage/schema.py index fb2524e..a8d76a7 100644 --- a/src/gausse/storage/schema.py +++ b/src/gausse/storage/schema.py @@ -29,6 +29,7 @@ CREATE TABLE IF NOT EXISTS runs ( CREATE_INDEXES_SQL = [ "CREATE INDEX IF NOT EXISTS idx_runs_feasible ON runs(feasible)", "CREATE INDEX IF NOT EXISTS idx_runs_efficiency ON runs(efficiency)", + "CREATE INDEX IF NOT EXISTS idx_runs_velocity ON runs(exit_velocity_mps)", "CREATE INDEX IF NOT EXISTS idx_runs_search_mode ON runs(search_mode)", ] diff --git a/src/gausse/web/page.py b/src/gausse/web/page.py index f182874..266d5c0 100644 --- a/src/gausse/web/page.py +++ b/src/gausse/web/page.py @@ -27,6 +27,9 @@ PAGE_HTML = r""" th,td { text-align:left; padding:7px 8px; border-bottom:1px solid #21262d; } th { color:#8b949e; font-weight:600; font-size:12px; } tr.clk:hover { background:#1c2230; cursor:pointer; } + .tgl { font-size:11px; padding:2px 8px; margin-left:6px; background:#21262d; color:#8b949e; + border:1px solid #30363d; border-radius:6px; cursor:pointer; text-transform:none; letter-spacing:0; } + .tgl.active { background:#238636; color:#fff; border-color:#238636; } .bar { height:16px; background:#238636; border-radius:3px; } .hist td { padding:3px 8px; } .log { background:#0a0d12; border:1px solid #30363d; border-radius:8px; padding:12px; @@ -57,12 +60,15 @@ PAGE_HTML = r"""
Реализуемо
Нереализуемо
Доля реализуемых
-
Лучший КПД
+
Лучший КПД / скорость
-

Топ конфигураций по КПД

+

Топ конфигураций + + +

@@ -95,26 +101,37 @@ PAGE_HTML = r""" diff --git a/src/gausse/web/render.py b/src/gausse/web/render.py index cdee419..a736253 100644 --- a/src/gausse/web/render.py +++ b/src/gausse/web/render.py @@ -23,7 +23,7 @@ from gausse.optim.search_space import SearchBounds, decode, genome_from_dict from gausse.report.animate import animate_run from gausse.report.plots import plot_stage_currents, plot_stage_fields, plot_velocity_vs_position from gausse.sim.coilgun import run_coilgun -from gausse.storage.database import fetch_runs, open_connection +from gausse.storage.database import fetch_run_by_id, open_connection _render_lock = threading.Lock() _db_cache: ComponentDatabase | None = None @@ -45,7 +45,7 @@ def _components_db() -> ComponentDatabase: def _rebuild_result(db_path: Path, run_id: str): conn = open_connection(db_path) try: - row = next((r for r in fetch_runs(conn) if r.run_id == run_id), None) + row = fetch_run_by_id(conn, run_id) finally: conn.close() if row is None: diff --git a/src/gausse/web/stats.py b/src/gausse/web/stats.py index b80011a..fb427f0 100644 --- a/src/gausse/web/stats.py +++ b/src/gausse/web/stats.py @@ -3,7 +3,7 @@ import json from pathlib import Path -from gausse.storage.database import fetch_runs, open_connection +from gausse.storage.database import fetch_run_by_id, fetch_runs, open_connection def _tail_log(log_path: Path, n_lines: int = 40) -> list[str]: @@ -35,28 +35,28 @@ def overview(db_path: Path, log_path: Path | None = None, top_n: int = 15) -> di short = reason.split(":", 1)[0] if reason else reason reasons.append({"reason": short, "count": cnt}) - # гистограмма КПД (реализуемые) + # гистограмма КПД — агрегируем в SQL (не тянем все строки в питон) buckets = [0] * 10 # 0-10%,...,90-100% - for (eff,) in conn.execute("SELECT efficiency FROM runs WHERE feasible=1 AND efficiency IS NOT NULL"): - idx = min(int(eff * 10), 9) - if idx >= 0: - buckets[idx] += 1 + for bucket, cnt in conn.execute( + "SELECT MIN(CAST(efficiency*10 AS INT), 9) AS b, COUNT(*) FROM runs " + "WHERE feasible=1 AND efficiency IS NOT NULL GROUP BY b" + ): + if bucket is not None and 0 <= bucket <= 9: + buckets[int(bucket)] = cnt - best_rows = fetch_runs(conn, feasible=True, order_by_efficiency_desc=True, limit=top_n) - top = [] - for r in best_rows: - detail = json.loads(r.decoded_summary_json) if r.decoded_summary_json else {} - top.append( - { - "run_id": r.run_id, - "search_mode": r.search_mode, - "efficiency": r.efficiency, - "exit_velocity_mps": r.exit_velocity_mps, - "cost_rub": r.cost_rub, - "n_stages": detail.get("n_stages"), - "timestamp": r.timestamp, - } - ) + def _summarize(rows): + out = [] + for r in rows: + detail = json.loads(r.decoded_summary_json) if r.decoded_summary_json else {} + out.append({ + "run_id": r.run_id, "search_mode": r.search_mode, "efficiency": r.efficiency, + "exit_velocity_mps": r.exit_velocity_mps, "cost_rub": r.cost_rub, + "n_stages": detail.get("n_stages"), "timestamp": r.timestamp, + }) + return out + + top = _summarize(fetch_runs(conn, feasible=True, order_by_efficiency_desc=True, limit=top_n)) + top_by_velocity = _summarize(fetch_runs(conn, feasible=True, order_by_velocity_desc=True, limit=top_n)) finally: conn.close() @@ -69,6 +69,7 @@ def overview(db_path: Path, log_path: Path | None = None, top_n: int = 15) -> di "infeasible_reasons": reasons, "efficiency_histogram": buckets, "top": top, + "top_by_velocity": top_by_velocity, "log_tail": _tail_log(log_path, 40) if log_path else [], } @@ -76,8 +77,7 @@ def overview(db_path: Path, log_path: Path | None = None, top_n: int = 15) -> di def run_detail(db_path: Path, run_id: str) -> dict | None: conn = open_connection(db_path) try: - rows = fetch_runs(conn) - row = next((r for r in rows if r.run_id == run_id), None) + row = fetch_run_by_id(conn, run_id) if row is None: return None return {
КПДСкор., м/сСтуп.Цена, ₽Режим