Speed up dashboard and add exit-velocity ranking

- 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 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-07-07 02:04:58 +05:00
parent b0a35e08d5
commit 85425e27bc
5 changed files with 67 additions and 39 deletions

View File

@@ -65,11 +65,19 @@ def count_runs(conn: sqlite3.Connection, feasible: bool | None = None) -> int:
return cursor.fetchone()[0] 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( def fetch_runs(
conn: sqlite3.Connection, conn: sqlite3.Connection,
feasible: bool | None = None, feasible: bool | None = None,
min_efficiency: float | None = None, min_efficiency: float | None = None,
order_by_efficiency_desc: bool = False, order_by_efficiency_desc: bool = False,
order_by_velocity_desc: bool = False,
limit: int | None = None, limit: int | None = None,
) -> list[RunRecord]: ) -> list[RunRecord]:
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
@@ -81,7 +89,9 @@ def fetch_runs(
if min_efficiency is not None: if min_efficiency is not None:
query += " AND efficiency >= ?" query += " AND efficiency >= ?"
params.append(min_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" query += " ORDER BY efficiency DESC"
if limit is not None: if limit is not None:
query += " LIMIT ?" query += " LIMIT ?"

View File

@@ -29,6 +29,7 @@ CREATE TABLE IF NOT EXISTS runs (
CREATE_INDEXES_SQL = [ CREATE_INDEXES_SQL = [
"CREATE INDEX IF NOT EXISTS idx_runs_feasible ON runs(feasible)", "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_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)", "CREATE INDEX IF NOT EXISTS idx_runs_search_mode ON runs(search_mode)",
] ]

View File

@@ -27,6 +27,9 @@ PAGE_HTML = r"""<!doctype html>
th,td { text-align:left; padding:7px 8px; border-bottom:1px solid #21262d; } th,td { text-align:left; padding:7px 8px; border-bottom:1px solid #21262d; }
th { color:#8b949e; font-weight:600; font-size:12px; } th { color:#8b949e; font-weight:600; font-size:12px; }
tr.clk:hover { background:#1c2230; cursor:pointer; } 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; } .bar { height:16px; background:#238636; border-radius:3px; }
.hist td { padding:3px 8px; } .hist td { padding:3px 8px; }
.log { background:#0a0d12; border:1px solid #30363d; border-radius:8px; padding:12px; .log { background:#0a0d12; border:1px solid #30363d; border-radius:8px; padding:12px;
@@ -57,12 +60,15 @@ PAGE_HTML = r"""<!doctype html>
<div class="card"><div class="k">Реализуемо</div><div class="v" id="c-feas">—</div></div> <div class="card"><div class="k">Реализуемо</div><div class="v" id="c-feas">—</div></div>
<div class="card"><div class="k">Нереализуемо</div><div class="v" id="c-infeas">—</div></div> <div class="card"><div class="k">Нереализуемо</div><div class="v" id="c-infeas">—</div></div>
<div class="card"><div class="k">Доля реализуемых</div><div class="v" id="c-pct">—</div></div> <div class="card"><div class="k">Доля реализуемых</div><div class="v" id="c-pct">—</div></div>
<div class="card"><div class="k">Лучший КПД</div><div class="v" id="c-best">—</div></div> <div class="card"><div class="k">Лучший КПД / скорость</div><div class="v" id="c-best" style="font-size:20px">—</div></div>
</div> </div>
<div class="grid2"> <div class="grid2">
<section> <section>
<h2>Топ конфигураций по КПД</h2> <h2>Топ конфигураций
<button class="tgl" id="tab-eff" onclick="setRank('eff')">по КПД</button>
<button class="tgl" id="tab-vel" onclick="setRank('vel')">по скорости</button>
</h2>
<table> <table>
<thead><tr><th>КПД</th><th>Скор., м/с</th><th>Ступ.</th><th>Цена, ₽</th><th>Режим</th></tr></thead> <thead><tr><th>КПД</th><th>Скор., м/с</th><th>Ступ.</th><th>Цена, ₽</th><th>Режим</th></tr></thead>
<tbody id="top"></tbody> <tbody id="top"></tbody>
@@ -95,26 +101,37 @@ PAGE_HTML = r"""<!doctype html>
<script> <script>
const fmtPct = x => x==null ? "" : (x*100).toFixed(2)+"%"; const fmtPct = x => x==null ? "" : (x*100).toFixed(2)+"%";
const fmt = (x,d=1) => x==null ? "" : Number(x).toFixed(d); const fmt = (x,d=1) => x==null ? "" : Number(x).toFixed(d);
let rankMode = 'eff'; // 'eff' | 'vel'
let lastOverview = null;
function setRank(m){ rankMode=m; if(lastOverview) renderTop(lastOverview);
document.getElementById('tab-eff').classList.toggle('active', m==='eff');
document.getElementById('tab-vel').classList.toggle('active', m==='vel'); }
async function refresh(){ function renderTop(o){
try { const rows = rankMode==='vel' ? (o.top_by_velocity||[]) : (o.top||[]);
const r = await fetch('/api/overview'); const o = await r.json();
document.getElementById('err').textContent = o.error ? ('ошибка: '+o.error) : '';
document.getElementById('c-total').textContent = o.total ?? 0;
document.getElementById('c-feas').textContent = o.feasible ?? 0;
document.getElementById('c-infeas').textContent = o.infeasible ?? 0;
document.getElementById('c-pct').textContent = (o.feasible_pct??0).toFixed(1)+"%";
let best = (o.top && o.top.length) ? o.top[0].efficiency : null;
document.getElementById('c-best').textContent = fmtPct(best);
const top = document.getElementById('top'); top.innerHTML=''; const top = document.getElementById('top'); top.innerHTML='';
(o.top||[]).forEach(t=>{ rows.forEach(t=>{
const tr=document.createElement('tr'); tr.className='clk'; const tr=document.createElement('tr'); tr.className='clk';
tr.innerHTML=`<td>${fmtPct(t.efficiency)}</td><td>${fmt(t.exit_velocity_mps,1)}</td>`+ tr.innerHTML=`<td>${fmtPct(t.efficiency)}</td><td>${fmt(t.exit_velocity_mps,1)}</td>`+
`<td>${t.n_stages??''}</td><td>${fmt(t.cost_rub,0)}</td><td><span class="pill">${t.search_mode}</span></td>`; `<td>${t.n_stages??''}</td><td>${fmt(t.cost_rub,0)}</td><td><span class="pill">${t.search_mode}</span></td>`;
tr.onclick=()=>showDetail(t.run_id); tr.onclick=()=>showDetail(t.run_id);
top.appendChild(tr); top.appendChild(tr);
}); });
}
async function refresh(){
try {
const r = await fetch('/api/overview'); const o = await r.json();
lastOverview = o;
document.getElementById('err').textContent = o.error ? ('ошибка: '+o.error) : '';
document.getElementById('c-total').textContent = o.total ?? 0;
document.getElementById('c-feas').textContent = o.feasible ?? 0;
document.getElementById('c-infeas').textContent = o.infeasible ?? 0;
document.getElementById('c-pct').textContent = (o.feasible_pct??0).toFixed(1)+"%";
let best = (o.top && o.top.length) ? o.top[0].efficiency : null;
let bestV = (o.top_by_velocity && o.top_by_velocity.length) ? o.top_by_velocity[0].exit_velocity_mps : null;
document.getElementById('c-best').textContent = fmtPct(best) + (bestV!=null ? ' / '+fmt(bestV,0)+' м/с' : '');
renderTop(o);
const hist=document.getElementById('hist'); hist.innerHTML=''; const hist=document.getElementById('hist'); hist.innerHTML='';
const mx=Math.max(1,...(o.efficiency_histogram||[0])); const mx=Math.max(1,...(o.efficiency_histogram||[0]));
@@ -164,7 +181,7 @@ async function showDetail(runId){
const el=document.getElementById('detail'); el.style.display='block'; el.scrollIntoView({behavior:'smooth'}); const el=document.getElementById('detail'); el.style.display='block'; el.scrollIntoView({behavior:'smooth'});
} }
refresh(); setInterval(refresh, 3000); setRank('eff'); refresh(); setInterval(refresh, 3000);
</script> </script>
</body> </body>
</html> </html>

View File

@@ -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.animate import animate_run
from gausse.report.plots import plot_stage_currents, plot_stage_fields, plot_velocity_vs_position from gausse.report.plots import plot_stage_currents, plot_stage_fields, plot_velocity_vs_position
from gausse.sim.coilgun import run_coilgun 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() _render_lock = threading.Lock()
_db_cache: ComponentDatabase | None = None _db_cache: ComponentDatabase | None = None
@@ -45,7 +45,7 @@ def _components_db() -> ComponentDatabase:
def _rebuild_result(db_path: Path, run_id: str): def _rebuild_result(db_path: Path, run_id: str):
conn = open_connection(db_path) conn = open_connection(db_path)
try: 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: finally:
conn.close() conn.close()
if row is None: if row is None:

View File

@@ -3,7 +3,7 @@
import json import json
from pathlib import Path 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]: 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 short = reason.split(":", 1)[0] if reason else reason
reasons.append({"reason": short, "count": cnt}) reasons.append({"reason": short, "count": cnt})
# гистограмма КПД (реализуемые) # гистограмма КПД — агрегируем в SQL (не тянем все строки в питон)
buckets = [0] * 10 # 0-10%,...,90-100% buckets = [0] * 10 # 0-10%,...,90-100%
for (eff,) in conn.execute("SELECT efficiency FROM runs WHERE feasible=1 AND efficiency IS NOT NULL"): for bucket, cnt in conn.execute(
idx = min(int(eff * 10), 9) "SELECT MIN(CAST(efficiency*10 AS INT), 9) AS b, COUNT(*) FROM runs "
if idx >= 0: "WHERE feasible=1 AND efficiency IS NOT NULL GROUP BY b"
buckets[idx] += 1 ):
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) def _summarize(rows):
top = [] out = []
for r in best_rows: for r in rows:
detail = json.loads(r.decoded_summary_json) if r.decoded_summary_json else {} detail = json.loads(r.decoded_summary_json) if r.decoded_summary_json else {}
top.append( out.append({
{ "run_id": r.run_id, "search_mode": r.search_mode, "efficiency": r.efficiency,
"run_id": r.run_id, "exit_velocity_mps": r.exit_velocity_mps, "cost_rub": r.cost_rub,
"search_mode": r.search_mode, "n_stages": detail.get("n_stages"), "timestamp": r.timestamp,
"efficiency": r.efficiency, })
"exit_velocity_mps": r.exit_velocity_mps, return out
"cost_rub": r.cost_rub,
"n_stages": detail.get("n_stages"), top = _summarize(fetch_runs(conn, feasible=True, order_by_efficiency_desc=True, limit=top_n))
"timestamp": r.timestamp, top_by_velocity = _summarize(fetch_runs(conn, feasible=True, order_by_velocity_desc=True, limit=top_n))
}
)
finally: finally:
conn.close() conn.close()
@@ -69,6 +69,7 @@ def overview(db_path: Path, log_path: Path | None = None, top_n: int = 15) -> di
"infeasible_reasons": reasons, "infeasible_reasons": reasons,
"efficiency_histogram": buckets, "efficiency_histogram": buckets,
"top": top, "top": top,
"top_by_velocity": top_by_velocity,
"log_tail": _tail_log(log_path, 40) if log_path else [], "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: def run_detail(db_path: Path, run_id: str) -> dict | None:
conn = open_connection(db_path) conn = open_connection(db_path)
try: try:
rows = fetch_runs(conn) row = fetch_run_by_id(conn, run_id)
row = next((r for r in rows if r.run_id == run_id), None)
if row is None: if row is None:
return None return None
return { return {