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]
|
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 ?"
|
||||||
|
|||||||
@@ -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)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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'); }
|
||||||
|
|
||||||
|
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(){
|
async function refresh(){
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/overview'); const o = await r.json();
|
const r = await fetch('/api/overview'); const o = await r.json();
|
||||||
|
lastOverview = o;
|
||||||
document.getElementById('err').textContent = o.error ? ('ошибка: '+o.error) : '';
|
document.getElementById('err').textContent = o.error ? ('ошибка: '+o.error) : '';
|
||||||
document.getElementById('c-total').textContent = o.total ?? 0;
|
document.getElementById('c-total').textContent = o.total ?? 0;
|
||||||
document.getElementById('c-feas').textContent = o.feasible ?? 0;
|
document.getElementById('c-feas').textContent = o.feasible ?? 0;
|
||||||
document.getElementById('c-infeas').textContent = o.infeasible ?? 0;
|
document.getElementById('c-infeas').textContent = o.infeasible ?? 0;
|
||||||
document.getElementById('c-pct').textContent = (o.feasible_pct??0).toFixed(1)+"%";
|
document.getElementById('c-pct').textContent = (o.feasible_pct??0).toFixed(1)+"%";
|
||||||
let best = (o.top && o.top.length) ? o.top[0].efficiency : null;
|
let best = (o.top && o.top.length) ? o.top[0].efficiency : null;
|
||||||
document.getElementById('c-best').textContent = fmtPct(best);
|
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)+' м/с' : '');
|
||||||
const top = document.getElementById('top'); top.innerHTML='';
|
renderTop(o);
|
||||||
(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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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>
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user