Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/main/cdp/attach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down
63 changes: 63 additions & 0 deletions src/main/events/performance.ts
Original file line number Diff line number Diff line change
@@ -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<string, number>

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<typeof setInterval> | null = null
let prev: Metrics | null = null

const tick = async (): Promise<void> => {
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
}
}
}
7 changes: 5 additions & 2 deletions src/renderer/overlay/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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), [])

Expand Down Expand Up @@ -97,7 +100,7 @@ export function Overlay(): JSX.Element {
})}
</div>

{tab === 'Network' ? <NetworkPanel net={net} /> : tab === 'Console' ? <ConsolePanel con={con} /> : tab === 'Memory' ? <MemoryPanel mem={mem} /> : <Placeholder tab={tab} />}
{tab === 'Network' ? <NetworkPanel net={net} /> : tab === 'Console' ? <ConsolePanel con={con} /> : tab === 'Memory' ? <MemoryPanel mem={mem} /> : tab === 'CPU' ? <PerformancePanel perf={perf} /> : <Placeholder tab={tab} />}

{/* footer */}
<div style={{ height: 34, display: 'flex', alignItems: 'center', gap: 8, padding: '0 14px', borderTop: '1px solid #22262e', fontFamily: 'var(--font-mono)' }}>
Expand Down
65 changes: 65 additions & 0 deletions src/renderer/overlay/PerformancePanel.tsx
Original file line number Diff line number Diff line change
@@ -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 <div style={{ padding: 20, fontSize: 11, color: 'var(--mute)', fontFamily: 'var(--font-mono)' }}>Sondeando performance…</div>
}

return (
<div style={{ display: 'flex', flexDirection: 'column', minHeight: 0, flex: 1, fontFamily: 'var(--font-mono)', overflowY: 'auto' }}>
{/* gauge + breakdown */}
<div style={{ display: 'flex', alignItems: 'center', gap: 16, padding: '16px 14px', borderBottom: '1px solid #22262e' }}>
<Gauge pct={latest.cpuPct} />
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 9 }}>
<Metric label="JS" value={`${Math.round(latest.jsMs)} ms`} />
<Metric label="RENDER" value={`${Math.round(latest.renderMs)} ms`} />
<Metric label="FPS" value={String(Math.round(latest.fps))} />
</div>
</div>

{/* timeline cpu% */}
<div style={{ padding: '12px 14px 6px', borderBottom: '1px solid #22262e' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
<span style={{ fontSize: 9.5, letterSpacing: '0.1em', color: 'var(--mute)' }}>MAIN-THREAD %</span>
<span style={{ fontSize: 11, color: 'var(--cyan-bright)' }}>{Math.round(latest.cpuPct)}%</span>
</div>
<Throughput t={series.t} bytes={series.cpu} />
</div>

{/* nota honesta de alcance */}
<div style={{ padding: '10px 14px', fontSize: 9.5, color: 'var(--mute)', lineHeight: 1.5 }}>
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.
</div>
</div>
)
}

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 (
<svg width="96" height="96" viewBox="0 0 100 100">
<circle cx="50" cy="50" r={r} fill="none" stroke="#20242c" strokeWidth="9" />
<circle cx="50" cy="50" r={r} fill="none" stroke={color} strokeWidth="9" strokeLinecap="round" strokeDasharray={circ} strokeDashoffset={off} transform="rotate(-90 50 50)" />
<text x="50" y="49" textAnchor="middle" fill="var(--text)" fontSize="21" fontFamily="JetBrains Mono, monospace" fontWeight="600">{Math.round(pct)}%</text>
<text x="50" y="63" textAnchor="middle" fill="var(--mute)" fontSize="8.5" fontFamily="JetBrains Mono, monospace" letterSpacing="1">CPU</text>
</svg>
)
}

function Metric({ label, value }: { label: string; value: string }): JSX.Element {
return (
<div>
<div style={{ fontSize: 9, color: 'var(--mute)', letterSpacing: '0.1em' }}>{label}</div>
<div style={{ fontSize: 14, color: '#cfd3d9', marginTop: 1 }}>{value}</div>
</div>
)
}
31 changes: 31 additions & 0 deletions src/renderer/overlay/usePerformance.ts
Original file line number Diff line number Diff line change
@@ -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<number[]>([])
const cpu = useRef<number[]>([])
const [view, setView] = useState<PerformanceView>({ 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
}
29 changes: 27 additions & 2 deletions src/shared/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading