perf(browser): acelera CDP — enable cacheado, wait_load por evento, timeout en sendCDP, escritura insertText
Optimiza el dominio browser para que el manejo del navegador via CDP sea mucho más rápido en automatización propia, manteniendo el camino sigiloso disponible. - CDPConn cachea los enable de Accessibility/Network/Page por conexión (ensureAX/ensureNetwork/ensurePage): elimina un round-trip redundante en cada percepción y espera, que son las operaciones más frecuentes del bucle percibir->actuar del agente. - sendCDP adquiere timeout (cdpCmdTimeout 30s): antes una respuesta que Chrome nunca enviaba colgaba la goroutine del tool indefinidamente; ahora falla limpio y el retry puede reconectar. - CdpWaitLoad pasa de polling de document.readyState cada 200ms a esperar el evento Page.loadEventFired, con fast path inicial de readyState y re-chequeo anti-carrera tras suscribir. Si la página ya está cargada retorna en microsegundos. - cdp_wait_idle usa ensureNetwork y deja de hacer Network.disable al salir (borraba el estado y forzaba el enable de nuevo). - Nuevas funciones de escritura rápida: CdpInsertText (todo el texto en un solo Input.insertText) y CdpTypeRefFast (focus + insertText). El chequeo de foco se extrajo a assertEditableFocus, compartido con CdpTypeText. - CdpTypeText pasa su pausa entre caracteres de 10ms fija a aleatoria 15-65ms (ritmo humano irregular). - El modo 'auto' se añade al perfil de ratón (MouseProfileForMode, mouseHumanDefaults, clickPauseMs) como alias rápido de 'fast'. No se tocan las firmas públicas existentes; CdpTypeRef y CdpTypeText conservan su comportamiento (camino human).
This commit is contained in:
@@ -14,8 +14,16 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// cdpCmdTimeout es el tope que sendCDP espera por la respuesta a un comando antes
|
||||
// de rendirse. Sin el, una respuesta que Chrome nunca envia (tab cerrada a media
|
||||
// peticion, proceso colgado) bloquearia la goroutine del tool para siempre — el
|
||||
// agente lo percibe como "lentitud infinita". Con el timeout, el tool falla limpio
|
||||
// y el retry de withConn puede reconectar.
|
||||
const cdpCmdTimeout = 30 * time.Second
|
||||
|
||||
// EventHandler es invocado cuando llega un evento CDP del metodo subscrito.
|
||||
// El handler corre en la goroutine del readLoop — debe ser rapido o despachar
|
||||
// a un canal/goroutine propio. params puede ser nil si Chrome no envia.
|
||||
@@ -36,6 +44,15 @@ type CDPConn struct {
|
||||
handlers map[string][]EventHandler
|
||||
hMu sync.Mutex
|
||||
|
||||
// axEnabled/netEnabled/pageEnabled cachean si ya enviamos el enable de cada
|
||||
// dominio CDP en esta conexion. enable/disable es idempotente pero cuesta un
|
||||
// round-trip; en el hot path del agente (percibir->actuar repetido) re-enviar
|
||||
// Accessibility.enable / Network.enable en cada llamada duplica los RTT.
|
||||
// Habilitar una vez y cachear el flag elimina ese coste por percepcion/espera.
|
||||
axEnabled atomic.Bool
|
||||
netEnabled atomic.Bool
|
||||
pageEnabled atomic.Bool
|
||||
|
||||
// frameCtx cachea el executionContextId del isolated world por frameID, para
|
||||
// que CdpEvalInFrame no cree un mundo aislado nuevo en cada llamada.
|
||||
// frameCtxMu protege solo el lazy-init del puntero (el cache tiene su mutex).
|
||||
@@ -250,12 +267,60 @@ func (c *CDPConn) sendCDP(method string, params map[string]any) (map[string]any,
|
||||
return nil, fmt.Errorf("cdp send %s: %w", method, err)
|
||||
}
|
||||
|
||||
// Esperar respuesta
|
||||
resp := <-ch
|
||||
if resp.Error != nil {
|
||||
return nil, fmt.Errorf("cdp %s: error %d: %s", method, resp.Error.Code, resp.Error.Message)
|
||||
// Esperar respuesta (con timeout para no colgar el tool indefinidamente).
|
||||
select {
|
||||
case resp := <-ch:
|
||||
if resp.Error != nil {
|
||||
return nil, fmt.Errorf("cdp %s: error %d: %s", method, resp.Error.Code, resp.Error.Message)
|
||||
}
|
||||
return resp.Result, nil
|
||||
case <-time.After(cdpCmdTimeout):
|
||||
c.pendMu.Lock()
|
||||
delete(c.pending, id)
|
||||
c.pendMu.Unlock()
|
||||
return nil, fmt.Errorf("cdp %s: sin respuesta tras %s (conexion colgada?)", method, cdpCmdTimeout)
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
|
||||
// ensureAX habilita el dominio Accessibility una sola vez por conexion (necesario
|
||||
// antes de Accessibility.getFullAXTree). Idempotente y cacheado: la segunda y
|
||||
// sucesivas llamadas son no-op, evitando un round-trip por percepcion.
|
||||
func (c *CDPConn) ensureAX() error {
|
||||
if c.axEnabled.Load() {
|
||||
return nil
|
||||
}
|
||||
if _, err := c.sendCDP("Accessibility.enable", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
c.axEnabled.Store(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureNetwork habilita el dominio Network una sola vez por conexion. Cacheado:
|
||||
// no lo deshabilitamos al terminar una espera (eso borraria el estado y forzaria
|
||||
// el enable de nuevo); los handlers de eventos se desregistran por su cancel().
|
||||
func (c *CDPConn) ensureNetwork() error {
|
||||
if c.netEnabled.Load() {
|
||||
return nil
|
||||
}
|
||||
if _, err := c.sendCDP("Network.enable", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
c.netEnabled.Store(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensurePage habilita el dominio Page una sola vez por conexion (necesario para
|
||||
// recibir Page.loadEventFired y demas eventos de ciclo de vida de la pagina).
|
||||
func (c *CDPConn) ensurePage() error {
|
||||
if c.pageEnabled.Load() {
|
||||
return nil
|
||||
}
|
||||
if _, err := c.sendCDP("Page.enable", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
c.pageEnabled.Store(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// readLoop lee mensajes del WebSocket y los enruta a los canales pendientes
|
||||
|
||||
Reference in New Issue
Block a user