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:
@@ -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 ?"
|
||||
|
||||
@@ -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)",
|
||||
]
|
||||
|
||||
|
||||
@@ -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'); }
|
||||
|
||||
function renderTop(o){
|
||||
const rows = rankMode==='vel' ? (o.top_by_velocity||[]) : (o.top||[]);
|
||||
const top = document.getElementById('top'); top.innerHTML='';
|
||||
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;
|
||||
document.getElementById('c-best').textContent = fmtPct(best);
|
||||
|
||||
const top = document.getElementById('top'); top.innerHTML='';
|
||||
(o.top||[]).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);
|
||||
});
|
||||
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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user