Browser Evaluate CDP Refactor Plan
Kontext
act:evaluate führt vom Benutzer bereitgestelltes JavaScript auf der Seite aus. Heute läuft es über Playwright (page.evaluate oder locator.evaluate). Playwright serialisiert CDP-Befehle pro Seite, sodass ein hängendes oder lang laufendes Evaluate die Seiten-Befehlswarteschlange blockieren und jede spätere Aktion auf diesem Tab als „hängend” erscheinen lassen kann.
PR #13498 fügt ein pragmatisches Sicherheitsnetz hinzu (begrenztes Evaluate, Abort-Propagation und Best-Effort-Recovery). Dieses Dokument beschreibt einen größeren Refactor, der act:evaluate von Playwright inhärent isoliert, damit ein hängendes Evaluate normale Playwright-Operationen nicht blockieren kann.
Ziele
act:evaluatekann spätere Browser-Aktionen auf demselben Tab nicht dauerhaft blockieren.- Timeouts sind durchgängig Single Source of Truth, sodass sich ein Aufrufer auf ein Budget verlassen kann.
- Abort und Timeout werden über HTTP und In-Process-Dispatch gleich behandelt.
- Element-Targeting für Evaluate wird unterstützt, ohne alles von Playwright abzulösen.
- Abwärtskompatibilität für bestehende Aufrufer und Payloads beibehalten.
Nicht-Ziele
- Alle Browser-Aktionen (click, type, wait usw.) durch CDP-Implementierungen ersetzen.
- Das bestehende, in PR #13498 eingeführte Sicherheitsnetz entfernen (es bleibt ein nützlicher Fallback).
- Neue unsichere Fähigkeiten jenseits des bestehenden
browser.evaluateEnabled-Gates einführen. - Prozessisolierung (Worker-Prozess/-Thread) für Evaluate hinzufügen. Falls nach diesem Refactor weiterhin schwer zu behebende Hänger auftreten, ist das ein Follow-up.
Aktuelle Architektur (Warum es hängt)
Auf hoher Ebene:
- Aufrufer senden
act:evaluatean den Browser-Control-Service. - Der Route-Handler ruft Playwright auf, um das JavaScript auszuführen.
- Playwright serialisiert Seitenbefehle, sodass ein nie beendetes Evaluate die Queue blockiert.
- Eine blockierte Queue bedeutet, dass spätere Click/Type/Wait-Operationen auf dem Tab hängen können.
Vorgeschlagene Architektur
1. Deadline-Propagation
Ein einzelnes Budget-Konzept einführen und alles daraus ableiten:
- Aufrufer setzt
timeoutMs(oder eine Deadline in der Zukunft). - Das äußere Request-Timeout, die Route-Handler-Logik und das Ausführungsbudget innerhalb der Seite nutzen alle dasselbe Budget, mit kleinem Headroom für Serialisierungs-Overhead wo nötig.
- Abort wird überall als
AbortSignalpropagiert, damit Abbrüche konsistent sind.
Implementierungsrichtung:
- Einen kleinen Helper hinzufügen (z. B.
createBudget({ timeoutMs, signal })), der zurückgibt:signal: das verknüpfte AbortSignaldeadlineAtMs: absolute DeadlineremainingMs(): verbleibendes Budget für Unteroperationen
- Diesen Helper verwenden in:
src/browser/client-fetch.ts(HTTP und In-Process-Dispatch)src/node-host/runner.ts(Proxy-Pfad)- Browser-Action-Implementierungen (Playwright und CDP)
2. Separate Evaluate-Engine (CDP-Pfad)
Eine CDP-basierte Evaluate-Implementierung hinzufügen, die Playwrights Per-Page-Befehlswarteschlange nicht teilt. Die Schlüsseleigenschaft ist, dass der Evaluate-Transport eine separate WebSocket-Verbindung und eine separate CDP-Session ist, die an das Ziel angehängt ist.
Implementierungsrichtung:
- Neues Modul, z. B.
src/browser/cdp-evaluate.ts, das:- Sich mit dem konfigurierten CDP-Endpunkt verbindet (Browser-Level-Socket).
Target.attachToTarget({ targetId, flatten: true })nutzt, um einesessionIdzu erhalten.- Entweder ausführt:
Runtime.evaluatefür seitenweites Evaluate, oderDOM.resolveNodeplusRuntime.callFunctionOnfür Element-Evaluate.
- Bei Timeout oder Abort:
- Best-Effort
Runtime.terminateExecutionfür die Session sendet. - Den WebSocket schließt und einen klaren Fehler zurückgibt.
- Best-Effort
Hinweise:
- Dies führt weiterhin JavaScript auf der Seite aus, daher kann die Terminierung Seiteneffekte haben. Der Gewinn ist, dass es die Playwright-Queue nicht blockiert und auf Transportebene durch Beenden der CDP-Session abbrechbar ist.
3. Ref-Geschichte (Element-Targeting ohne komplettes Rewrite)
Der schwierige Teil ist Element-Targeting. CDP benötigt ein DOM-Handle oder eine backendDOMNodeId, während die meisten Browser-Aktionen heute Playwright-Locators basierend auf Refs aus Snapshots verwenden.
Empfohlener Ansatz: bestehende Refs beibehalten, aber optional eine CDP-auflösbare ID anhängen.
3.1 Gespeicherte Ref-Info erweitern
Die gespeicherten Role-Ref-Metadaten optional um eine CDP-ID erweitern:
- Heute:
{ role, name, nth } - Vorgeschlagen:
{ role, name, nth, backendDOMNodeId?: number }
Dies lässt alle bestehenden Playwright-basierten Aktionen funktionieren und erlaubt CDP-Evaluate, denselben ref-Wert zu akzeptieren, wenn die backendDOMNodeId verfügbar ist.
3.2 backendDOMNodeId zum Snapshot-Zeitpunkt befüllen
Beim Erzeugen eines Role-Snapshots:
- Die bestehende Role-Ref-Map wie heute generieren (role, name, nth).
- Den AX-Tree über CDP holen (
Accessibility.getFullAXTree) und eine parallele Map von(role, name, nth) -> backendDOMNodeIdmit denselben Duplikat-Behandlungsregeln berechnen. - Die ID zurück in die gespeicherten Ref-Info für den aktuellen Tab mergen.
Wenn das Mapping für einen Ref fehlschlägt, backendDOMNodeId undefiniert lassen. Das macht das Feature best-effort und sicher zum Ausrollen.
3.3 Evaluate-Verhalten mit Ref
In act:evaluate:
- Wenn
refvorhanden ist und einebackendDOMNodeIdhat, Element-Evaluate über CDP ausführen. - Wenn
refvorhanden ist, aber keinebackendDOMNodeIdhat, auf den Playwright-Pfad zurückfallen (mit dem Sicherheitsnetz).
Optionaler Notausgang:
- Das Request-Shape erweitern, um
backendDOMNodeIddirekt für fortgeschrittene Aufrufer (und zum Debuggen) zu akzeptieren, währendrefdie primäre Schnittstelle bleibt.
4. Last-Resort-Recovery-Pfad beibehalten
Auch mit CDP-Evaluate gibt es andere Wege, einen Tab oder eine Verbindung zu blockieren. Die bestehenden Recovery-Mechanismen (Execution terminieren + Playwright disconnecten) als letzten Ausweg beibehalten für:
- Legacy-Aufrufer
- Umgebungen, in denen CDP-Attach blockiert ist
- Unerwartete Playwright-Randfälle
Implementierungsplan (einzelne Iteration)
Liefergegenstände
- Eine CDP-basierte Evaluate-Engine, die außerhalb der Playwright-Per-Page-Befehlswarteschlange läuft.
- Ein einzelnes durchgängiges Timeout-/Abort-Budget, konsistent von Aufrufern und Handlern genutzt.
- Ref-Metadaten, die optional
backendDOMNodeIdfür Element-Evaluate tragen können. act:evaluatebevorzugt die CDP-Engine wenn möglich und fällt auf Playwright zurück wenn nicht.- Tests, die beweisen, dass ein hängendes Evaluate spätere Aktionen nicht blockiert.
- Logs/Metriken, die Fehler und Fallbacks sichtbar machen.
Implementierungs-Checkliste
- Einen gemeinsamen „Budget”-Helper hinzufügen, der
timeoutMs+ upstreamAbortSignalverknüpft zu:- einem einzelnen
AbortSignal - einer absoluten Deadline
- einem
remainingMs()-Helper für nachgelagerte Operationen
- einem einzelnen
- Alle Aufruferpfade aktualisieren, damit
timeoutMsüberall dasselbe bedeutet:src/browser/client-fetch.ts(HTTP und In-Process-Dispatch)src/node-host/runner.ts(Node-Proxy-Pfad)- CLI-Wrapper, die
/actaufrufen (--timeout-mszubrowser evaluatehinzufügen)
src/browser/cdp-evaluate.tsimplementieren:- Zum Browser-Level-CDP-Socket verbinden
Target.attachToTargetfür einesessionIdRuntime.evaluatefür Seiten-Evaluate ausführenDOM.resolveNode+Runtime.callFunctionOnfür Element-Evaluate ausführen- Bei Timeout/Abort: Best-Effort
Runtime.terminateExecution, dann Socket schließen
- Gespeicherte Role-Ref-Metadaten optional um
backendDOMNodeIderweitern:- Bestehendes
{ role, name, nth }-Verhalten für Playwright-Aktionen beibehalten backendDOMNodeId?: numberfür CDP-Element-Targeting hinzufügen
- Bestehendes
backendDOMNodeIdwährend der Snapshot-Erstellung befüllen (best-effort):- AX-Tree über CDP holen (
Accessibility.getFullAXTree) (role, name, nth) -> backendDOMNodeIdberechnen und in die gespeicherte Ref-Map mergen- Wenn Mapping mehrdeutig oder fehlend ist, ID undefiniert lassen
- AX-Tree über CDP holen (
act:evaluate-Routing aktualisieren:- Wenn kein
ref: immer CDP-Evaluate verwenden - Wenn
refzu einerbackendDOMNodeIdaufgelöst wird: CDP-Element-Evaluate verwenden - Sonst: auf Playwright-Evaluate zurückfallen (weiterhin begrenzt und abbrechbar)
- Wenn kein
- Den bestehenden „Last Resort”-Recovery-Pfad als Fallback beibehalten, nicht als Standardpfad.
- Tests hinzufügen:
- Hängendes Evaluate hat Timeout innerhalb des Budgets und der nächste Click/Type funktioniert
- Abort bricht Evaluate ab (Client-Disconnect oder Timeout) und entsperrt nachfolgende Aktionen
- Mapping-Fehler fallen sauber auf Playwright zurück
- Beobachtbarkeit hinzufügen:
- Evaluate-Dauer und Timeout-Zähler
- terminateExecution-Nutzung
- Fallback-Rate (CDP -> Playwright) und Gründe
Akzeptanzkriterien
- Ein absichtlich hängendes
act:evaluatekehrt innerhalb des Aufrufer-Budgets zurück und blockiert den Tab nicht für spätere Aktionen. timeoutMsverhält sich konsistent über CLI, Agent-Tool, Node-Proxy und In-Process-Aufrufe.- Wenn
refaufbackendDOMNodeIdgemappt werden kann, nutzt Element-Evaluate CDP; andernfalls ist der Fallback-Pfad weiterhin begrenzt und wiederherstellbar.
Testplan
- Unit-Tests:
(role, name, nth)-Matching-Logik zwischen Role-Refs und AX-Tree-Knoten.- Budget-Helper-Verhalten (Headroom, Restzeit-Berechnung).
- Integrationstests:
- CDP-Evaluate-Timeout kehrt innerhalb des Budgets zurück und blockiert die nächste Aktion nicht.
- Abort bricht Evaluate ab und löst Best-Effort-Terminierung aus.
- Vertragstests:
- Sicherstellen, dass
BrowserActRequestundBrowserActResponsekompatibel bleiben.
- Sicherstellen, dass
Risiken und Gegenmaßnahmen
- Mapping ist unvollkommen:
- Gegenmaßnahme: Best-Effort-Mapping, Fallback auf Playwright-Evaluate und Debug-Tooling hinzufügen.
Runtime.terminateExecutionhat Seiteneffekte:- Gegenmaßnahme: Nur bei Timeout/Abort verwenden und das Verhalten in Fehlermeldungen dokumentieren.
- Zusätzlicher Overhead:
- Gegenmaßnahme: AX-Tree nur bei angefragten Snapshots holen, pro Ziel cachen und CDP-Session kurzlebig halten.
- Extension-Relay-Einschränkungen:
- Gegenmaßnahme: Browser-Level-Attach-APIs verwenden wenn Per-Page-Sockets nicht verfügbar sind, und den aktuellen Playwright-Pfad als Fallback behalten.
Offene Fragen
- Sollte die neue Engine als
playwright,cdpoderautokonfigurierbar sein? - Wollen wir ein neues „nodeRef”-Format für fortgeschrittene Benutzer einführen oder nur
refbehalten? - Wie sollten Frame-Snapshots und Selector-Scoped-Snapshots am AX-Mapping teilnehmen?