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]
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 ?"

View File

@@ -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)",
]

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 { 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"""<!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-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-best">—</div></div>
<div class="card"><div class="k">Лучший КПД / скорость</div><div class="v" id="c-best" style="font-size:20px">—</div></div>
</div>
<div class="grid2">
<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>
<thead><tr><th>КПД</th><th>Скор., м/с</th><th>Ступ.</th><th>Цена, ₽</th><th>Режим</th></tr></thead>
<tbody id="top"></tbody>
@@ -95,26 +101,37 @@ PAGE_HTML = r"""<!doctype html>
<script>
const fmtPct = x => x==null ? "" : (x*100).toFixed(2)+"%";
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(){
try {
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);
function renderTop(o){
const rows = rankMode==='vel' ? (o.top_by_velocity||[]) : (o.top||[]);
const top = document.getElementById('top'); top.innerHTML='';
(o.top||[]).forEach(t=>{
rows.forEach(t=>{
const tr=document.createElement('tr'); tr.className='clk';
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>`;
tr.onclick=()=>showDetail(t.run_id);
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 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'});
}
refresh(); setInterval(refresh, 3000);
setRank('eff'); refresh(); setInterval(refresh, 3000);
</script>
</body>
</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.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:

View File

@@ -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:
def _summarize(rows):
out = []
for r in 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,
}
)
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 {