Bugfix: Flickering overview charts because of bad choice of point
reducing method
This commit is contained in:
parent
6fdfde8dac
commit
9fa4e159af
6 changed files with 5210 additions and 50 deletions
|
|
@ -77,9 +77,10 @@ agent's own shell command line and kills the session.
|
|||
state.
|
||||
- `iw dev <if> link` prints `SSID: name` **unquoted**; the working regex is
|
||||
`SSID:\s+(\S.*)` (a `$` anchor fails without MULTILINE).
|
||||
- `/api/history` pads series with `null` for samples missing a key so all
|
||||
series stay aligned with the timestamps — keep that behaviour if you touch
|
||||
it.
|
||||
- `/api/history` window-averages the ring buffer down to at most
|
||||
`chart_max_points` (default 200) points, emitting `{avg, min, max}` per key,
|
||||
and pads each of the three arrays with `null` for windows missing a key so
|
||||
they stay aligned with the timestamps — keep that behaviour if you touch it.
|
||||
- AMD sysfs: GPU busy/VRAM/temp under
|
||||
`/sys/class/drm/card*/device` (+ `hwmon`), CPU temp from the `k10temp`
|
||||
hwmon (fallback `acpitz` thermal zone), both in millidegrees.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class Settings(BaseSettings):
|
|||
port: int = 8501
|
||||
sample_interval: float = 2.0
|
||||
retention_minutes: int = 60
|
||||
chart_max_points: int = 200
|
||||
|
||||
llama_base_url: str = "http://127.0.0.1:8080"
|
||||
llama_api_key: str = ""
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import asyncio
|
||||
import math
|
||||
import socket
|
||||
import time
|
||||
from typing import Any
|
||||
|
|
@ -8,10 +9,39 @@ from fastapi import APIRouter, Request
|
|||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
|
||||
from app.collect import net as net_col
|
||||
from app.config import get_settings
|
||||
from app.render import render, uptime_str
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["overview"])
|
||||
|
||||
RowAgg = dict[str, float | int | None]
|
||||
|
||||
|
||||
def _window(
|
||||
snap: list[tuple[float, dict[str, float | int | None]]], max_points: int
|
||||
) -> list[tuple[float, dict[str, RowAgg]]]:
|
||||
n = len(snap)
|
||||
w = max(1, math.ceil(n / max_points))
|
||||
out: list[tuple[float, dict[str, RowAgg]]] = []
|
||||
for start in range(0, n, w):
|
||||
chunk = snap[start : start + w]
|
||||
vals: dict[str, list[int | float]] = {}
|
||||
for _, sample in chunk:
|
||||
for k, v in sample.items():
|
||||
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
||||
vals.setdefault(k, []).append(v)
|
||||
row: dict[str, RowAgg] = {}
|
||||
for k, lst in vals.items():
|
||||
ints = all(isinstance(v, int) for v in lst)
|
||||
avg = sum(lst) / len(lst)
|
||||
row[k] = {
|
||||
"avg": round(avg) if ints else round(avg, 1),
|
||||
"min": min(lst) if ints else round(min(lst), 1),
|
||||
"max": max(lst) if ints else round(max(lst), 1),
|
||||
}
|
||||
out.append((chunk[-1][0], row))
|
||||
return out
|
||||
|
||||
|
||||
@router.get("/overview")
|
||||
async def overview(request: Request):
|
||||
|
|
@ -52,21 +82,17 @@ async def overview(request: Request):
|
|||
|
||||
@router.get("/history")
|
||||
async def history(request: Request):
|
||||
snap = request.app.state.store.snapshot()
|
||||
snap = _window(request.app.state.store.snapshot(), get_settings().chart_max_points)
|
||||
ts = [round(t, 1) for t, _ in snap]
|
||||
keys: set[str] = set()
|
||||
for _, sample in snap:
|
||||
for k, v in sample.items():
|
||||
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
||||
keys.add(k)
|
||||
series: dict[str, list[Any]] = {}
|
||||
for _, sample in snap:
|
||||
for _, row in snap:
|
||||
keys.update(row)
|
||||
series: dict[str, dict[str, list[Any]]] = {
|
||||
k: {"avg": [], "min": [], "max": []} for k in keys
|
||||
}
|
||||
for _, row in snap:
|
||||
for k in keys:
|
||||
v = sample.get(k)
|
||||
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
||||
if isinstance(v, float):
|
||||
v = round(v, 1)
|
||||
else:
|
||||
v = None
|
||||
series.setdefault(k, []).append(v)
|
||||
agg = row.get(k)
|
||||
for m in ("avg", "min", "max"):
|
||||
series[k][m].append(agg[m] if agg else None)
|
||||
return JSONResponse({"ts": ts, "series": series})
|
||||
|
|
|
|||
5144
opencode_session_chart_flicker_2026-08-30.txt
Normal file
5144
opencode_session_chart_flicker_2026-08-30.txt
Normal file
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 93 KiB |
|
|
@ -36,15 +36,6 @@
|
|||
return d.toLocaleTimeString([], { hour12: false });
|
||||
}
|
||||
|
||||
function downsampleIdx(len, max) {
|
||||
if (len <= max) return null;
|
||||
const step = Math.ceil(len / max);
|
||||
const idx = [];
|
||||
for (let i = 0; i < len; i += step) idx.push(i);
|
||||
if (idx[idx.length - 1] !== len - 1) idx.push(len - 1);
|
||||
return idx;
|
||||
}
|
||||
|
||||
// ---------- charts ----------
|
||||
const charts = {};
|
||||
|
||||
|
|
@ -64,14 +55,15 @@
|
|||
return o;
|
||||
}
|
||||
|
||||
function newDs(label, color, extra) {
|
||||
return Object.assign(
|
||||
{ label, data: [], borderColor: color, backgroundColor: color, borderWidth: 1.5, pointRadius: 0, tension: 0.25, fill: false },
|
||||
extra || {}
|
||||
);
|
||||
function envelopeDs(name, color, s) {
|
||||
s = s || {};
|
||||
return [
|
||||
{ label: name, data: s.max || [], borderColor: color, backgroundColor: color + "2e", borderWidth: 1, pointRadius: 0, tension: 0, fill: false, order: 0 },
|
||||
{ label: name + " min", band: true, data: s.min || [], borderColor: color, backgroundColor: color, borderWidth: 1, pointRadius: 0, tension: 0, fill: false, order: 1 },
|
||||
];
|
||||
}
|
||||
|
||||
const LEGEND = { display: true, labels: { boxWidth: 10, color: "#7d8a9c" } };
|
||||
const LEGEND = { display: true, labels: { boxWidth: 10, color: "#7d8a9c", filter: (item, data) => !data.datasets[item.datasetIndex]?.band } };
|
||||
|
||||
function initCharts() {
|
||||
if (typeof Chart === "undefined") return;
|
||||
|
|
@ -79,7 +71,7 @@
|
|||
type: "line",
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [newDs("CPU", "#4fc3f7"), newDs("GPU", "#ba68c8")],
|
||||
datasets: [].concat(envelopeDs("CPU", "#4fc3f7"), envelopeDs("GPU", "#ba68c8")),
|
||||
},
|
||||
options: baseOpts({ max: 100 }),
|
||||
});
|
||||
|
|
@ -88,7 +80,7 @@
|
|||
type: "line",
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [newDs("Memory", "#81c784"), newDs("VRAM", "#ffb74d")],
|
||||
datasets: [].concat(envelopeDs("Memory", "#81c784"), envelopeDs("VRAM", "#ffb74d")),
|
||||
},
|
||||
options: baseOpts({ max: 100 }),
|
||||
});
|
||||
|
|
@ -97,7 +89,7 @@
|
|||
type: "line",
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [newDs("read", "#e57373"), newDs("write", "#fff176")],
|
||||
datasets: [].concat(envelopeDs("read", "#e57373"), envelopeDs("write", "#fff176")),
|
||||
},
|
||||
options: baseOpts({
|
||||
ticks: { color: "#7d8a9c", callback: (v) => fmtBytes(v, 0) },
|
||||
|
|
@ -109,25 +101,21 @@
|
|||
|
||||
function updateCharts(hist) {
|
||||
if (!hist || !hist.ts || !hist.ts.length) return;
|
||||
const ts = hist.ts;
|
||||
const idx = downsampleIdx(ts.length, 400);
|
||||
const pick = (arr) => (arr && idx ? idx.map((i) => (i < arr.length ? arr[i] : null)) : arr);
|
||||
const labels = (idx ? idx.map((i) => ts[i]) : ts).map(fmtTime);
|
||||
|
||||
const set2 = (chart, keys) => {
|
||||
const labels = hist.ts.map(fmtTime);
|
||||
const series = hist.series || {};
|
||||
const build = (chart, pairs) => {
|
||||
chart.data.labels = labels;
|
||||
keys.forEach((k, i) => {
|
||||
chart.data.datasets[i].data = pick(hist.series[k]) || [];
|
||||
chart.data.datasets = [];
|
||||
pairs.forEach(([name, color, key], i) => {
|
||||
const ds = envelopeDs(name, color, series[key]);
|
||||
ds[0].fill = i * 2 + 1;
|
||||
chart.data.datasets.push(...ds);
|
||||
});
|
||||
chart.update("none");
|
||||
};
|
||||
set2(charts.cpu, ["cpu", "gpu"]);
|
||||
set2(charts.mem, ["mem_pct", "vram_pct"]);
|
||||
|
||||
charts.io.data.labels = labels;
|
||||
charts.io.data.datasets[0].data = pick(hist.series.io_read) || [];
|
||||
charts.io.data.datasets[1].data = pick(hist.series.io_write) || [];
|
||||
charts.io.update("none");
|
||||
build(charts.cpu, [["CPU", "#4fc3f7", "cpu"], ["GPU", "#ba68c8", "gpu"]]);
|
||||
build(charts.mem, [["Memory", "#81c784", "mem_pct"], ["VRAM", "#ffb74d", "vram_pct"]]);
|
||||
build(charts.io, [["read", "#e57373", "io_read"], ["write", "#fff176", "io_write"]]);
|
||||
}
|
||||
|
||||
function pollHistory() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue