mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
GPU realtime: cache endpoint TTL 4s + trim intel_gpu_top warmup
This commit is contained in:
@@ -107,6 +107,11 @@ interface LxcAppWatch {
|
||||
// Updates tab helper-scripts section find its matching registered
|
||||
// app to pull installed/upstream version data from.
|
||||
helper_slug?: string
|
||||
// Per-app opt-out of the CT's aggregate updates badge (default:
|
||||
// included). Independent from the app's notification toggle.
|
||||
exclude_from_badge?: boolean
|
||||
// Per-app opt-out for the `app_update_available` notification.
|
||||
notifications_enabled?: boolean
|
||||
}
|
||||
|
||||
interface VMData {
|
||||
@@ -1960,6 +1965,31 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
// (mirrors the Secure Gateway visual treatment). Security count
|
||||
// stays red because it's still an urgency cue independent of the
|
||||
// update theme.
|
||||
// Aggregate updates counter (OS packages + registered apps that
|
||||
// aren't opted out of the badge). Returns an update_check-shaped
|
||||
// object so `renderLxcUpdateBadge` and the tab count can consume it
|
||||
// without knowing about app entries. The underlying `update_check`
|
||||
// stays strictly OS-only on the backend — the Updates tab's "OS
|
||||
// packages" section needs a clean `available`/`count` to avoid a
|
||||
// false "N package pending" every time a registered app has an
|
||||
// upstream bump.
|
||||
const getAggregateUpdateCheck = (vm: VMData): LxcUpdateCheck | undefined => {
|
||||
const uc = vm.update_check
|
||||
const appCount = (vm.app_watches || []).filter(
|
||||
(a) => a.update_available === true && !a.exclude_from_badge,
|
||||
).length
|
||||
const osCount = uc?.count ?? 0
|
||||
const total = osCount + appCount
|
||||
if (!uc && appCount === 0) return undefined
|
||||
if (total === 0) return uc
|
||||
return {
|
||||
...(uc || {}),
|
||||
count: total,
|
||||
available: true,
|
||||
last_check: uc?.last_check ?? new Date().toISOString(),
|
||||
} as LxcUpdateCheck
|
||||
}
|
||||
|
||||
const renderLxcUpdateBadge = (
|
||||
uc?: LxcUpdateCheck,
|
||||
compact = false,
|
||||
@@ -2587,7 +2617,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
})}
|
||||
{vm.type === "lxc" && (
|
||||
<div className="ml-auto flex-shrink-0">
|
||||
{renderLxcUpdateBadge(vm.update_check)}
|
||||
{renderLxcUpdateBadge(getAggregateUpdateCheck(vm))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -2751,9 +2781,12 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{vm.type === "lxc" && vm.update_check?.available && (vm.update_check?.count ?? 0) > 0 && (
|
||||
<ArrowUpCircle className="h-3 w-3 text-violet-400 flex-shrink-0" />
|
||||
)}
|
||||
{vm.type === "lxc" && (() => {
|
||||
const agg = getAggregateUpdateCheck(vm)
|
||||
return agg?.available && (agg.count ?? 0) > 0 ? (
|
||||
<ArrowUpCircle className="h-3 w-3 text-violet-400 flex-shrink-0" />
|
||||
) : null
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2884,7 +2917,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
Uptime / Type / Status chips. */}
|
||||
{selectedVM.type === "lxc" &&
|
||||
renderLxcUpdateBadge(
|
||||
selectedVM.update_check,
|
||||
getAggregateUpdateCheck(selectedVM),
|
||||
false,
|
||||
() => setActiveModalTab("updates"),
|
||||
)}
|
||||
@@ -2931,7 +2964,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
)}
|
||||
{selectedVM.type === "lxc" &&
|
||||
renderLxcUpdateBadge(
|
||||
selectedVM.update_check,
|
||||
getAggregateUpdateCheck(selectedVM),
|
||||
false,
|
||||
() => setActiveModalTab("updates"),
|
||||
)}
|
||||
@@ -3011,11 +3044,14 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
|
||||
<span className={activeModalTab === "updates" ? "" : "hidden sm:inline"}>
|
||||
{t("vmLxc.tabs.updates")}
|
||||
</span>
|
||||
{typeof selectedVM.update_check?.count === "number" && selectedVM.update_check.count > 0 && (
|
||||
<Badge variant="secondary" className="text-xs h-5 ml-0.5 sm:ml-1">
|
||||
{selectedVM.update_check.count}
|
||||
</Badge>
|
||||
)}
|
||||
{(() => {
|
||||
const agg = getAggregateUpdateCheck(selectedVM)
|
||||
return typeof agg?.count === "number" && agg.count > 0 ? (
|
||||
<Badge variant="secondary" className="text-xs h-5 ml-0.5 sm:ml-1">
|
||||
{agg.count}
|
||||
</Badge>
|
||||
) : null
|
||||
})()}
|
||||
</button>
|
||||
)}
|
||||
{/* Mount Points tab — LXC only, and only when at least
|
||||
|
||||
@@ -3315,8 +3315,8 @@
|
||||
"switchMode": "Modo de cambio",
|
||||
"currentDriver": "Controlador actual",
|
||||
"consumer": "Consumidor",
|
||||
"graphicsClock": "Reloj de gráficos",
|
||||
"memoryClock": "Reloj de memoria",
|
||||
"graphicsClock": "Frecuencia GPU",
|
||||
"memoryClock": "Frecuencia de memoria",
|
||||
"powerDraw": "Consumo de energía",
|
||||
"temperature": "Temperatura",
|
||||
"memory": "Memoria",
|
||||
|
||||
@@ -6215,36 +6215,20 @@ def get_proxmox_vms():
|
||||
# apps per CT (0..N). Populates header badge,
|
||||
# Updates modal connected row, and the App
|
||||
# tab. Absent key = no apps registered.
|
||||
# The list-card aggregate badge folds app
|
||||
# updates on the frontend side so the
|
||||
# OS-only `update_check.available` / `.count`
|
||||
# stay clean — the Updates tab reads them to
|
||||
# decide whether "OS packages pending" +
|
||||
# "Apply OS update" should show, and
|
||||
# inflating them with app pending was
|
||||
# producing a false "1 package pending"
|
||||
# every time a registered app had a newer
|
||||
# upstream version.
|
||||
app_list = lxc_app_map.get(str(resource.get('vmid')))
|
||||
if app_list:
|
||||
vm_data['app_watches'] = app_list
|
||||
|
||||
# Fold registered-app updates into the CT's
|
||||
# aggregate updates badge so the list card
|
||||
# counter reflects OS + apps in one number.
|
||||
# Apps flagged `exclude_from_badge` are
|
||||
# omitted from the count (pinned versions,
|
||||
# tracker-locked apps, etc.) — see the
|
||||
# validator in lxc_apps.py for the full
|
||||
# rationale. Independent from
|
||||
# `notifications_enabled`.
|
||||
if app_list:
|
||||
app_upd_count = sum(
|
||||
1 for a in app_list
|
||||
if a.get('update_available') is True
|
||||
and not a.get('exclude_from_badge')
|
||||
)
|
||||
if app_upd_count:
|
||||
uc = vm_data.get('update_check') or {}
|
||||
# Synthesize a minimal update_check
|
||||
# entry when the CT has no apt/apk
|
||||
# data (OCI, non-Debian, checker off)
|
||||
# but at least one counted app.
|
||||
uc = dict(uc) if uc else {}
|
||||
uc['count'] = int(uc.get('count') or 0) + app_upd_count
|
||||
uc['available'] = True
|
||||
vm_data['update_check'] = uc
|
||||
|
||||
# PVE's cluster resources API reports disk=0 for most
|
||||
# QEMU VMs — it can't see inside the guest filesystem
|
||||
# for the common storage backends. For running QEMU
|
||||
@@ -7015,12 +6999,16 @@ def get_detailed_gpu_info(gpu):
|
||||
# print(f"[v0] Process started with PID: {process.pid}", flush=True)
|
||||
pass
|
||||
|
||||
# print(f"[v0] Waiting 1 second for intel_gpu_top to initialize and detect processes...", flush=True)
|
||||
pass
|
||||
time.sleep(1)
|
||||
|
||||
# intel_gpu_top needs a small warmup for the first JSON
|
||||
# object to hit stdout. 300 ms is enough on every host
|
||||
# tested — the previous 1 s was tuned when the tool was
|
||||
# slower to boot and doubled the modal open latency for
|
||||
# no gain. Combined with the shorter read timeout below
|
||||
# this halves the worst-case blocking time.
|
||||
time.sleep(0.3)
|
||||
|
||||
start_time = time.time()
|
||||
timeout = 3
|
||||
timeout = 1.5
|
||||
json_objects = []
|
||||
buffer = ""
|
||||
brace_count = 0
|
||||
@@ -13773,30 +13761,40 @@ def api_hardware_live():
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
_gpu_realtime_cache: dict[str, tuple[float, dict]] = {}
|
||||
_gpu_realtime_cache_lock = threading.Lock()
|
||||
# Frontend polls this endpoint every 3 s per open GPU modal. For
|
||||
# Intel and AMD the underlying tool call (intel_gpu_top / rocm-smi)
|
||||
# blocks ~2 s per invocation, so a naked request-per-poll makes the
|
||||
# modal feel sluggish and stacks CPU. A 4 s TTL means the second and
|
||||
# third poll of any 4 s window serve straight from memory while the
|
||||
# first still pays the tool cost; NVIDIA (nvidia-smi ~200 ms) also
|
||||
# benefits by dropping the second nvidia-smi spawn.
|
||||
_GPU_REALTIME_TTL = 4.0
|
||||
|
||||
@app.route('/api/gpu/<slot>/realtime', methods=['GET'])
|
||||
@require_auth
|
||||
def api_gpu_realtime(slot):
|
||||
"""Get real-time GPU monitoring data for a specific GPU"""
|
||||
try:
|
||||
# print(f"[v0] /api/gpu/{slot}/realtime - Getting GPU info...")
|
||||
pass
|
||||
|
||||
now = time.time()
|
||||
with _gpu_realtime_cache_lock:
|
||||
hit = _gpu_realtime_cache.get(slot)
|
||||
if hit and (now - hit[0]) < _GPU_REALTIME_TTL:
|
||||
return jsonify(hit[1])
|
||||
|
||||
gpus = get_gpu_info()
|
||||
|
||||
|
||||
gpu = None
|
||||
for g in gpus:
|
||||
# Match by slot or if the slot is a substring of the GPU's slot (e.g., '00:01.0' matching '00:01')
|
||||
if g.get('slot') == slot or slot in g.get('slot', ''):
|
||||
gpu = g
|
||||
break
|
||||
|
||||
|
||||
if not gpu:
|
||||
# print(f"[v0] GPU with slot matching '{slot}' not found")
|
||||
pass
|
||||
return jsonify({'error': 'GPU not found'}), 404
|
||||
|
||||
# print(f"[v0] Getting detailed monitoring data for GPU at slot {gpu.get('slot')}...")
|
||||
pass
|
||||
|
||||
detailed_info = get_detailed_gpu_info(gpu)
|
||||
gpu.update(detailed_info)
|
||||
|
||||
@@ -13840,10 +13838,11 @@ def api_gpu_realtime(slot):
|
||||
'sriov_consumer': gpu.get('sriov_consumer'),
|
||||
}
|
||||
|
||||
with _gpu_realtime_cache_lock:
|
||||
_gpu_realtime_cache[slot] = (time.time(), realtime_data)
|
||||
|
||||
return jsonify(realtime_data)
|
||||
except Exception as e:
|
||||
# print(f"[v0] Error getting real-time GPU data: {e}")
|
||||
pass
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
Reference in New Issue
Block a user