From b5db5d7e818b2dec6e8f269b81e045fb388a6185 Mon Sep 17 00:00:00 2001 From: Aitor Date: Mon, 27 Jul 2026 15:38:03 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20CPU=20real=20por=20proceso=20en=20el=20m?= =?UTF-8?q?onitor,=20y=20UTXO=20Map=20usando=20la=20cach=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La columna %CPU de ps aux es la media desde que arrancó el proceso, no el consumo actual: un proceso que trabajó mucho hace días seguía apareciendo alto para siempre. Se detectó porque los porcentajes no cambiaban nunca entre lecturas mientras la CPU global sí variaba. Ahora se mide con dos lecturas de /proc/PID/stat separadas 500 ms, el mismo método que ya usaba getCpuUsage para el total. La UI muestra el % sobre el total de la máquina, con el % por núcleo y la media de ps en el tooltip. Verificado con carga artificial: 100% de un núcleo (25% de 4) frente al 152% que reportaba ps, imposible para un proceso de un solo hilo. Además, el UTXO Map era el único punto que se saltaba la caché usando fetchWithTimeout directo. Ahora pasa por get(). --- CHANGELOG.md | 25 ++++++++++++++++++++++++ dashboard.html | 23 ++++++++++++++++++----- system-metrics.js | 48 +++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03b02e0..499899e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,31 @@ y el versionado sigue [Versionado Semántico](https://semver.org/lang/es/): - **MENOR** — características nuevas que no rompen lo anterior - **PARCHE** — arreglos de errores +--- +## [1.12.1] — 2026-07-27 +### Corregido +- **El monitor mostraba una CPU por proceso que engañaba.** La columna `%CPU` + de `ps aux` no es el consumo actual sino la **media desde que el proceso + arrancó** (tiempo de CPU dividido por tiempo de vida). Un proceso que trabajó + mucho hace tres días seguía apareciendo alto para siempre, y se leía como si + estuviera saturando la máquina. Se detectó porque los porcentajes por proceso + no cambiaban NUNCA entre lecturas mientras la CPU global sí variaba. + - Ahora se mide de verdad: dos lecturas de `/proc/PID/stat` separadas 500 ms, + el mismo método que ya usaba `getCpuUsage` para el total. Las dos esperas + corren en paralelo dentro del mismo `Promise.all`, así que no cuesta tiempo. + - La interfaz muestra el **porcentaje sobre el total de la máquina**, que es + lo que suele querer saberse, con el porcentaje de un núcleo y la media de + `ps` en el tooltip para poder comparar con `top`. + - Comprobado con carga artificial: el proceso ocupado marcaba 100% de un + núcleo (25% de un sistema de 4) mientras `ps` daba 152%, imposible para un + proceso de un solo hilo. + +### Cambiado +- El UTXO Map pedía las transacciones de contexto con `fetchWithTimeout` + directo, saltándose la caché añadida en la 1.11.0. Era el único punto que no + la aprovechaba. Ahora pasa por `get()`, así que esas transacciones —ya + confirmadas, e inmutables— se guardan toda la sesión. + --- ## [1.12.0] — 2026-07-27 ### Añadido diff --git a/dashboard.html b/dashboard.html index 0f80c04..14ceaab 100644 --- a/dashboard.html +++ b/dashboard.html @@ -2837,11 +2837,22 @@
{p.command}
- CPU {p.cpu}% + {/* Se muestra el % del sistema (lo que de verdad se + quiere saber: cuánto de la máquina se lleva). El + % de un núcleo va en el tooltip, para comparar con + top si hace falta. */} + 25?C.red:p.cpuSistema>10?C.amber:C.t2}> + + CPU {p.cpuSistema!=null?p.cpuSistema:p.cpu}% + + 30?C.red:p.mem>15?C.amber:C.t2}>RAM {p.mem}%
))} +
+ CPU sobre el total de la máquina ({sysData.cpu?.count||"?"} núcleos), medida en una ventana de 500 ms — no es la media desde el arranque que muestra ps, que engaña porque un proceso que trabajó mucho hace días sigue apareciendo alto. +
)} @@ -3571,13 +3582,15 @@ if(sorted.length>MAX_UTXOS) setErr(`Mostrando ${MAX_UTXOS} de ${sorted.length} UTXOs — se muestran los de mayor valor.`); // Cargar txs de origen para contexto (máx 10, las más grandes) + // Vía get() y no fetch directo: así pasa por la caché compartida. + // Las transacciones confirmadas son inmutables y se guardan toda la + // sesión, de modo que volver a este UTXO Map (o analizar una de estas + // txs en otra pestaña) no cuesta ni una petición más al nodo. const cache={}; const toFetch=limited.slice(0,10); await Promise.all(toFetch.map(async u=>{ - try{ - const res=await fetchWithTimeout(`${base}/api/tx/${u.txid}`); - if(res.ok) cache[u.txid]=await res.json(); - }catch{} + const t = await get(`/api/tx/${u.txid}`, null); + if (t) cache[u.txid] = t; })); setTxCache(cache); diff --git a/system-metrics.js b/system-metrics.js index 303c781..afc54bf 100644 --- a/system-metrics.js +++ b/system-metrics.js @@ -168,10 +168,33 @@ async function getDisk() { } // ── Procesos destacados (nombre limpio del binario) ──────────────────────── +// Tiempo de CPU consumido por un proceso, en ticks, desde /proc/PID/stat. +// Campos 14 (utime) y 15 (stime). El nombre del proceso va entre paréntesis y +// puede contener espacios, así que se corta por el ÚLTIMO ')' antes de partir. +function readProcCpuTicks(pid) { + try { + const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8"); + const resto = stat.slice(stat.lastIndexOf(")") + 2).split(" "); + // resto[0] es el campo 3 (estado), así que utime=campo14 → resto[11] + const utime = parseInt(resto[11], 10); + const stime = parseInt(resto[12], 10); + if (Number.isNaN(utime) || Number.isNaN(stime)) return null; + return utime + stime; + } catch { return null; } +} + +// OJO con la columna %CPU de `ps`: NO es el consumo actual, sino la media del +// proceso desde que arrancó (tiempo de CPU / tiempo de vida). Un proceso que +// trabajó mucho al principio y ahora está ocioso sigue mostrando un número +// alto días después, y se lee como si estuviera saturando la máquina. +// Aquí se mide de verdad: dos lecturas de /proc separadas 500 ms, el mismo +// método que ya usa getCpuUsage para el total del sistema. Las dos esperas +// corren en paralelo (van dentro del mismo Promise.all), así que no cuesta +// tiempo extra. async function getProcesses() { const result = await sh("ps aux --no-headers --sort=-%mem | head -8"); if (!result) return []; - return result.split("\n").map(line => { + const filas = result.split("\n").map(line => { const parts = line.trim().split(/\s+/); // Nombre limpio: basename del ejecutable; si es un intérprete (node, // python...), añade el basename del script que ejecuta. @@ -190,12 +213,33 @@ async function getProcesses() { } command = command.slice(0, 24); return { + pid: parts[1], user: parts[0], - cpu: parseFloat(parts[2]), + cpuMedia: parseFloat(parts[2]), // media desde el arranque (lo que da ps) mem: parseFloat(parts[3]), command, }; }).filter(p => p.mem > 0.5); + + const ticks1 = filas.map(p => readProcCpuTicks(p.pid)); + await sleep(500); + const ticks2 = filas.map(p => readProcCpuTicks(p.pid)); + + const USER_HZ = 100; // estándar en Linux + const VENTANA_S = 0.5; + const nucleos = os.cpus().length || 1; + + return filas.map((p, i) => { + let cpu = null, cpuSistema = null; + if (ticks1[i] !== null && ticks2[i] !== null) { + const seg = (ticks2[i] - ticks1[i]) / USER_HZ; + // % de UN núcleo (criterio de top/ps: puede pasar de 100 si va en varios) + cpu = Math.max(0, Math.round((seg / VENTANA_S) * 1000) / 10); + // % del total de la máquina, que es lo que suele querer saberse + cpuSistema = Math.round((cpu / nucleos) * 10) / 10; + } + return { user: p.user, command: p.command, mem: p.mem, cpu, cpuSistema, cpuMedia: p.cpuMedia }; + }); } // ── Load / Uptime ───────────────────────────────────────────────────────────