From f3edc77165e3473effd0dc4ede4cb379c29855bd Mon Sep 17 00:00:00 2001 From: ElVatoEste Date: Thu, 20 Aug 2026 16:55:43 -0600 Subject: [PATCH] feat(performance): add CPU/performance panel via Performance.getMetrics deltas --- src/main/cdp/attach.ts | 9 +++- src/main/events/performance.ts | 63 ++++++++++++++++++++++ src/renderer/overlay/Overlay.tsx | 7 ++- src/renderer/overlay/PerformancePanel.tsx | 65 +++++++++++++++++++++++ src/renderer/overlay/usePerformance.ts | 31 +++++++++++ src/shared/events.ts | 29 +++++++++- 6 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 src/main/events/performance.ts create mode 100644 src/renderer/overlay/PerformancePanel.tsx create mode 100644 src/renderer/overlay/usePerformance.ts diff --git a/src/main/cdp/attach.ts b/src/main/cdp/attach.ts index 589eec3..4aece1f 100644 --- a/src/main/cdp/attach.ts +++ b/src/main/cdp/attach.ts @@ -2,6 +2,7 @@ import type { WebContents } from 'electron' import { handleCdpNetwork, resetNetwork } from '../events/network' import { handleCdpConsole } from '../events/console' import { createMemoryPoller } from '../events/memory' +import { createPerformancePoller } from '../events/performance' // ============================================================================ // Capa CDP (D-002) — DESACOPLADA de la base (REQ-062): si mañana se migra a @@ -32,19 +33,23 @@ export function attachCdp(page: WebContents): void { }) const memory = createMemoryPoller(dbg) + const performance = createPerformancePoller(dbg) dbg.on('detach', (_event, reason) => { console.warn('[cdp] debugger detached:', reason) memory.stop() + performance.stop() }) - // Dominios habilitados: Network (D-015) + Runtime/Log para consola (REQ-021). + // Dominios habilitados: Network (D-015) + Runtime/Log (REQ-021) + Performance (REQ-023). dbg.sendCommand('Network.enable').catch((e) => console.error('[cdp] Network.enable', e)) dbg.sendCommand('Runtime.enable').catch((e) => console.error('[cdp] Runtime.enable', e)) dbg.sendCommand('Log.enable').catch((e) => console.error('[cdp] Log.enable', e)) + dbg.sendCommand('Performance.enable').catch((e) => console.error('[cdp] Performance.enable', e)) - // Sondeo periódico de memoria (REQ-022). + // Sondeos periódicos (REQ-022 / REQ-023). memory.start() + performance.start() // Al navegar a otra página, limpiamos el estado de red acumulado. page.on('did-start-navigation', (_e, _url, isInPlace, isMainFrame) => { diff --git a/src/main/events/performance.ts b/src/main/events/performance.ts new file mode 100644 index 0000000..5443721 --- /dev/null +++ b/src/main/events/performance.ts @@ -0,0 +1,63 @@ +import type { Debugger } from 'electron' +import { bus } from './bus' + +// ============================================================================ +// Sondeo del dominio Performance (REQ-023). `Performance.getMetrics` da +// contadores ACUMULADOS; las señales por intervalo se derivan por diferencia +// entre dos sondeos. Todas atribuibles a la página (no CPU% de proceso del SO). +// ============================================================================ + +type Metrics = Record + +function toMap(metrics: { name: string; value: number }[]): Metrics { + const m: Metrics = {} + for (const { name, value } of metrics) m[name] = value + return m +} + +export function createPerformancePoller(dbg: Debugger, intervalMs = 1000): { start: () => void; stop: () => void } { + let timer: ReturnType | null = null + let prev: Metrics | null = null + + const tick = async (): Promise => { + if (!dbg.isAttached()) return + try { + const res = (await dbg.sendCommand('Performance.getMetrics')) as { metrics: { name: string; value: number }[] } + const cur = toMap(res.metrics) + if (prev) { + const dt = (cur.Timestamp ?? 0) - (prev.Timestamp ?? 0) // segundos de wall time + if (dt > 0) { + const d = (k: string): number => (cur[k] ?? 0) - (prev![k] ?? 0) + bus.emitEvent({ + domain: 'performance', + record: { + cpuPct: Math.min(100, Math.max(0, (d('TaskDuration') / dt) * 100)), + jsMs: d('ScriptDuration') * 1000, + renderMs: (d('LayoutDuration') + d('RecalcStyleDuration')) * 1000, + layouts: d('LayoutCount'), + fps: d('Frames') / dt + } + }) + } + } + prev = cur + } catch { + // debugger despegándose / navegando: se reintenta al próximo tick. + } + } + + return { + start(): void { + if (timer) return + timer = setInterval(tick, intervalMs) + void tick() + }, + stop(): void { + if (timer) { + clearInterval(timer) + timer = null + } + prev = null + } + } +} diff --git a/src/renderer/overlay/Overlay.tsx b/src/renderer/overlay/Overlay.tsx index 040c01b..61cdff7 100644 --- a/src/renderer/overlay/Overlay.tsx +++ b/src/renderer/overlay/Overlay.tsx @@ -2,13 +2,15 @@ import { useEffect, useRef, useState, type CSSProperties, type PointerEvent as R import { NetworkPanel } from './NetworkPanel' import { ConsolePanel } from './ConsolePanel' import { MemoryPanel } from './MemoryPanel' +import { PerformancePanel } from './PerformancePanel' import { useNetwork } from './useNetwork' import { useConsole } from './useConsole' import { useMemory } from './useMemory' +import { usePerformance } from './usePerformance' type Tab = 'Network' | 'Console' | 'Memory' | 'CPU' | 'Storage' const TABS: Tab[] = ['Network', 'Console', 'Memory', 'CPU', 'Storage'] -const ENABLED: Tab[] = ['Network', 'Console', 'Memory'] // v1 en progreso +const ENABLED: Tab[] = ['Network', 'Console', 'Memory', 'CPU'] // v1 en progreso // Arrastre del overlay: captura el puntero y manda deltas de pantalla al main, // que reposiciona la vista. `moved` distingue drag de click (para el pill). @@ -62,6 +64,7 @@ export function Overlay(): JSX.Element { const net = useNetwork() const con = useConsole() const mem = useMemory() + const perf = usePerformance() useEffect(() => window.overrun.onOverlayState(setCollapsed), []) @@ -97,7 +100,7 @@ export function Overlay(): JSX.Element { })} - {tab === 'Network' ? : tab === 'Console' ? : tab === 'Memory' ? : } + {tab === 'Network' ? : tab === 'Console' ? : tab === 'Memory' ? : tab === 'CPU' ? : } {/* footer */}
diff --git a/src/renderer/overlay/PerformancePanel.tsx b/src/renderer/overlay/PerformancePanel.tsx new file mode 100644 index 0000000..51dd2ff --- /dev/null +++ b/src/renderer/overlay/PerformancePanel.tsx @@ -0,0 +1,65 @@ +import { Throughput } from './Throughput' +import type { PerformanceView } from './usePerformance' + +// Panel CPU / Performance (REQ-023): gauge de utilización + JS/render/FPS + timeline. +// Señales atribuibles a la página; NO es CPU% de proceso del SO. +export function PerformancePanel({ perf }: { perf: PerformanceView }): JSX.Element { + const { latest, series } = perf + + if (!latest) { + return
Sondeando performance…
+ } + + return ( +
+ {/* gauge + breakdown */} +
+ +
+ + + +
+
+ + {/* timeline cpu% */} +
+
+ MAIN-THREAD % + {Math.round(latest.cpuPct)}% +
+ +
+ + {/* nota honesta de alcance */} +
+ Utilización del hilo principal de la página (task time / wall time). No incluye CPU del + proceso a nivel SO ni long tasks (Tracing) — se suman después. +
+
+ ) +} + +function Gauge({ pct }: { pct: number }): JSX.Element { + const r = 42 + const circ = 2 * Math.PI * r + const off = circ * (1 - Math.min(100, pct) / 100) + const color = pct >= 80 ? 'oklch(0.7 0.19 25)' : pct >= 50 ? 'oklch(0.82 0.15 85)' : 'var(--cyan)' + return ( + + + + {Math.round(pct)}% + CPU + + ) +} + +function Metric({ label, value }: { label: string; value: string }): JSX.Element { + return ( +
+
{label}
+
{value}
+
+ ) +} diff --git a/src/renderer/overlay/usePerformance.ts b/src/renderer/overlay/usePerformance.ts new file mode 100644 index 0000000..7c67664 --- /dev/null +++ b/src/renderer/overlay/usePerformance.ts @@ -0,0 +1,31 @@ +import { useEffect, useRef, useState } from 'react' +import type { OverrunEvent, PerformanceRecord } from '../../shared/events' + +export interface PerformanceView { + latest: PerformanceRecord | null + series: { t: number[]; cpu: number[] } +} + +const WINDOW = 120 + +/** Colecta snapshots de performance (REQ-023). Llegan ~1/s. */ +export function usePerformance(): PerformanceView { + const t = useRef([]) + const cpu = useRef([]) + const [view, setView] = useState({ latest: null, series: { t: [], cpu: [] } }) + + useEffect(() => { + return window.overrun.onEvent((evt: OverrunEvent) => { + if (evt.domain !== 'performance') return + t.current.push(evt.ts / 1000) + cpu.current.push(evt.record.cpuPct) + if (t.current.length > WINDOW) { + t.current.shift() + cpu.current.shift() + } + setView({ latest: evt.record, series: { t: t.current.slice(), cpu: cpu.current.slice() } }) + }) + }, []) + + return view +} diff --git a/src/shared/events.ts b/src/shared/events.ts index 341a06d..9e0a000 100644 --- a/src/shared/events.ts +++ b/src/shared/events.ts @@ -127,12 +127,37 @@ export interface MemoryEvent extends OverrunEventBase { record: MemoryRecord } +// --------------------------------------------------------------------------- +// Dominio: performance (v1 — REQ-023) +// +// Señales ATRIBUIBLES A LA PÁGINA (no CPU% de proceso del SO). Se derivan de +// `Performance.getMetrics` (contadores acumulados) por diferencia entre sondeos. +// --------------------------------------------------------------------------- + +export interface PerformanceRecord { + /** utilización del hilo principal en el intervalo: task time / wall time (%). */ + cpuPct: number + /** ms de ejecución de JS en el intervalo (ScriptDuration). */ + jsMs: number + /** ms de layout + recalc de estilos en el intervalo. */ + renderMs: number + /** layouts en el intervalo. */ + layouts: number + /** frames por segundo (delta de Frames / intervalo). */ + fps: number +} + +export interface PerformanceEvent extends OverrunEventBase { + domain: 'performance' + record: PerformanceRecord +} + // --------------------------------------------------------------------------- // Unión de todos los eventos del bus. Otros dominios se suman aquí sin rediseño. // --------------------------------------------------------------------------- -export type OverrunEvent = NetworkEvent | ConsoleEvent | MemoryEvent -// | PerformanceEvent | StorageEvent | SecurityEvent (próximas fases) +export type OverrunEvent = NetworkEvent | ConsoleEvent | MemoryEvent | PerformanceEvent +// | StorageEvent | SecurityEvent (próximas fases) /** Canales IPC — un único punto para no tipear strings sueltos. */ export const IPC = {