Files
moli/moli-benchmark/moli_benchmark/sequential_navigate.py
ldm0 c461cb1799 refactor(benchmark): unify public web comparisons
Move public-web seeds into CSV fixtures and share scheduling, command construction, classification, evidence, and artifact handling across top-sites and wild-web suites.

Bind Chromium DCL and response evidence to the current navigation loader, surface navigation errors promptly, rotate target-first order across repeated runs, and normalize HTTP and timeout outcomes so cross-engine results remain comparable.
2026-08-23 04:45:50 +08:00

1397 lines
49 KiB
Python

from __future__ import annotations
import argparse
import asyncio
import json
import random
import re
import subprocess
import time
import urllib.parse
from dataclasses import asdict, dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Iterable
from .artifacts import write_json
from .config import REPO_ROOT, chrome_binary, moli_binary
from .raw_cdp import (
RawCdpCommandError,
RawCdpError,
RawCdpTimeoutError,
RecordedCdpMessage,
RoutedRawCdpClient,
connect_routed_raw_cdp,
)
from .sampling import snapshot_resources
from .target_serve import start_target_serve, stop_target_serve
from .top_sites import TOP_SITES_LIST_PATH, parse_top_sites_list
DEFAULT_SEED = 20260809
DEFAULT_COUNT = 50
DEFAULT_PINNED_DOMAINS = ("csdn.net", "zol.com.cn")
DEFAULT_SEED_FILE = TOP_SITES_LIST_PATH
TRACE_METHODS = {
"Page.frameStartedNavigating",
"Page.frameStartedLoading",
"Page.frameNavigated",
"Page.domContentEventFired",
"Page.loadEventFired",
"Page.lifecycleEvent",
"Page.frameStoppedLoading",
}
NETWORK_EVENT_METHODS = {
"Network.requestWillBeSent",
"Network.responseReceived",
"Network.loadingFinished",
"Network.loadingFailed",
}
NAVIGATION_RESOURCE_FIELDS = (
"process_count",
"pss_bytes",
"pss_process_count",
"rss_bytes",
"rss_process_count",
"thread_count",
"fd_count",
)
@dataclass(frozen=True)
class NavigationIdentity:
frame_id: str
loader_id: str
@dataclass(frozen=True)
class NavigationProgress:
kind: str
record: RecordedCdpMessage
identity: NavigationIdentity
@dataclass(frozen=True)
class NavigationTimeouts:
response: float
dcl: float
load: float
postcheck: float
def parse_top_100_domains(markdown: str) -> list[str]:
in_top_100 = False
domains: list[str] = []
for line in markdown.splitlines():
if line.strip() == "## Top 100":
in_top_100 = True
continue
if in_top_100 and line.startswith("## "):
break
if not in_top_100:
continue
match = re.match(r"^\s*\d+\.\s+`([^`]+)`", line)
if match is not None:
domains.append(match.group(1).strip())
if not domains:
raise ValueError("seed document does not contain any domains under `## Top 100`")
return domains
def normalize_public_url(value: str) -> str:
value = value.strip()
if not value:
raise ValueError("navigation URL cannot be empty")
if "://" not in value and not value.startswith("data:"):
value = f"https://{value}"
parsed = urllib.parse.urlsplit(value)
if parsed.scheme not in {"http", "https", "data"}:
raise ValueError(f"unsupported navigation URL scheme: {parsed.scheme}")
return value
def select_seed_urls(
domains: Iterable[str],
*,
seed: int,
count: int,
pinned_domains: Iterable[str] = DEFAULT_PINNED_DOMAINS,
) -> list[str]:
if count <= 0:
raise ValueError("navigation count must be positive")
normalized_domains = list(dict.fromkeys(domain.strip() for domain in domains if domain.strip()))
pinned = list(dict.fromkeys(domain.strip() for domain in pinned_domains if domain.strip()))
selected_pinned = pinned[:count]
remaining = [domain for domain in normalized_domains if domain not in set(selected_pinned)]
random.Random(seed).shuffle(remaining)
selected = (selected_pinned + remaining)[:count]
if len(selected) < count:
raise ValueError(f"requested {count} URLs but seed set contains only {len(selected)}")
return [normalize_public_url(domain) for domain in selected]
def make_recovery_url(marker: str) -> str:
html = (
"<!doctype html><meta charset=utf-8>"
f"<title>{marker}</title><body data-recovery-marker=\"{marker}\">{marker}</body>"
)
return "data:text/html;charset=utf-8," + urllib.parse.quote(html, safe="")
def _is_exact_lifecycle_event(
payload: dict[str, Any],
*,
session_id: str,
identity: NavigationIdentity,
name: str,
) -> bool:
if payload.get("sessionId") != session_id or payload.get("method") != "Page.lifecycleEvent":
return False
params = payload.get("params")
return (
isinstance(params, dict)
and params.get("name") == name
and params.get("frameId") == identity.frame_id
and params.get("loaderId") == identity.loader_id
)
def find_exact_lifecycle_record(
records: Iterable[RecordedCdpMessage],
*,
session_id: str,
identity: NavigationIdentity,
name: str,
) -> RecordedCdpMessage | None:
return next(
(
record
for record in records
if _is_exact_lifecycle_event(
record.payload,
session_id=session_id,
identity=identity,
name=name,
)
),
None,
)
def _successor_navigation_identity(
payload: dict[str, Any],
*,
session_id: str,
identity: NavigationIdentity,
) -> NavigationIdentity | None:
if payload.get("sessionId") != session_id or payload.get("method") != "Page.frameStartedNavigating":
return None
params = payload.get("params")
if not isinstance(params, dict) or params.get("frameId") != identity.frame_id:
return None
loader_id = params.get("loaderId")
if not isinstance(loader_id, str) or not loader_id or loader_id == identity.loader_id:
return None
return NavigationIdentity(frame_id=identity.frame_id, loader_id=loader_id)
def find_navigation_progress_record(
records: Iterable[RecordedCdpMessage],
*,
session_id: str,
identity: NavigationIdentity,
lifecycle_name: str,
) -> NavigationProgress | None:
for record in records:
if _is_exact_lifecycle_event(
record.payload,
session_id=session_id,
identity=identity,
name=lifecycle_name,
):
return NavigationProgress(
kind="lifecycle",
record=record,
identity=identity,
)
successor = _successor_navigation_identity(
record.payload,
session_id=session_id,
identity=identity,
)
if successor is not None:
return NavigationProgress(
kind="successor",
record=record,
identity=successor,
)
return None
def navigation_order_violations(
response: RecordedCdpMessage,
dcl: RecordedCdpMessage,
load: RecordedCdpMessage | None,
) -> list[str]:
violations = []
if response.sequence >= dcl.sequence:
violations.append("DOMContentLoaded was observable before Page.navigate response")
if load is not None:
if response.sequence >= load.sequence:
violations.append("load was observable before Page.navigate response")
if dcl.sequence >= load.sequence:
violations.append("load was observable before DOMContentLoaded")
return violations
def _record_elapsed_ms(record: RecordedCdpMessage, started: float) -> float:
return round((record.received_monotonic - started) * 1000.0, 3)
def _compact_trace_record(record: RecordedCdpMessage, started: float) -> dict[str, Any] | None:
payload = record.payload
method = payload.get("method")
if method not in TRACE_METHODS and "id" not in payload:
return None
compact: dict[str, Any] = {
"sequence": record.sequence,
"elapsed_ms": _record_elapsed_ms(record, started),
}
if isinstance(method, str):
compact["method"] = method
params = payload.get("params")
if isinstance(params, dict):
if method == "Page.lifecycleEvent":
compact.update(
{
"name": params.get("name"),
"frame_id": params.get("frameId"),
"loader_id": params.get("loaderId"),
}
)
elif method == "Page.frameNavigated":
frame = params.get("frame")
if isinstance(frame, dict):
compact.update(
{
"frame_id": frame.get("id"),
"loader_id": frame.get("loaderId"),
"url": frame.get("url"),
}
)
else:
for source, destination in (
("frameId", "frame_id"),
("loaderId", "loader_id"),
("url", "url"),
):
if source in params:
compact[destination] = params[source]
else:
compact["response_id"] = payload.get("id")
if "error" in payload:
compact["error"] = payload["error"]
return compact
def _compact_trace(
client: RoutedRawCdpClient,
*,
after_sequence: int,
started: float,
) -> list[dict[str, Any]]:
return [
compact
for record in client.messages_since(after_sequence)
if (compact := _compact_trace_record(record, started)) is not None
]
def summarize_network_activity(
records: Iterable[RecordedCdpMessage],
*,
session_id: str,
started: float,
) -> dict[str, Any]:
requests: dict[str, dict[str, Any]] = {}
event_counts = {method: 0 for method in sorted(NETWORK_EVENT_METHODS)}
for record in records:
payload = record.payload
method = payload.get("method")
if payload.get("sessionId") != session_id or method not in NETWORK_EVENT_METHODS:
continue
event_counts[str(method)] += 1
params = payload.get("params")
if not isinstance(params, dict):
continue
request_id = params.get("requestId")
if not isinstance(request_id, str) or not request_id:
continue
entry = requests.setdefault(
request_id,
{
"request_id": request_id,
"url": None,
"type": None,
"frame_id": None,
"loader_id": None,
"initiator_type": None,
"started_sequence": None,
"started_ms": None,
"redirect_count": 0,
"response_status": None,
"response_mime_type": None,
"terminal": None,
"terminal_sequence": None,
"terminal_ms": None,
"error_text": None,
"canceled": None,
},
)
if method == "Network.requestWillBeSent":
if entry["started_sequence"] is not None:
entry["redirect_count"] += 1
request = params.get("request")
initiator = params.get("initiator")
entry.update(
{
"url": request.get("url") if isinstance(request, dict) else None,
"type": params.get("type"),
"frame_id": params.get("frameId"),
"loader_id": params.get("loaderId"),
"initiator_type": (
initiator.get("type") if isinstance(initiator, dict) else None
),
"started_sequence": record.sequence,
"started_ms": _record_elapsed_ms(record, started),
"terminal": None,
"terminal_sequence": None,
"terminal_ms": None,
"error_text": None,
"canceled": None,
}
)
elif method == "Network.responseReceived":
response = params.get("response")
entry["type"] = params.get("type", entry["type"])
entry["frame_id"] = params.get("frameId", entry["frame_id"])
entry["loader_id"] = params.get("loaderId", entry["loader_id"])
if isinstance(response, dict):
entry["url"] = response.get("url", entry["url"])
entry["response_status"] = response.get("status")
entry["response_mime_type"] = response.get("mimeType")
elif method == "Network.loadingFinished":
entry.update(
{
"terminal": "finished",
"terminal_sequence": record.sequence,
"terminal_ms": _record_elapsed_ms(record, started),
}
)
elif method == "Network.loadingFailed":
entry.update(
{
"type": params.get("type", entry["type"]),
"terminal": "failed",
"terminal_sequence": record.sequence,
"terminal_ms": _record_elapsed_ms(record, started),
"error_text": params.get("errorText"),
"canceled": params.get("canceled"),
}
)
ordered = sorted(
requests.values(),
key=lambda entry: (
entry["started_sequence"] is None,
entry["started_sequence"] or entry["terminal_sequence"] or 0,
),
)
inflight = [entry for entry in ordered if entry["terminal"] is None]
failed = [entry for entry in ordered if entry["terminal"] == "failed"]
return {
"event_counts": event_counts,
"request_count": len(ordered),
"finished_count": sum(entry["terminal"] == "finished" for entry in ordered),
"failed_count": len(failed),
"inflight_count": len(inflight),
"requests": ordered,
}
def network_event_order_violations(
records: Iterable[RecordedCdpMessage],
*,
session_id: str,
) -> list[dict[str, Any]]:
timelines: dict[str, dict[str, Any]] = {}
for record in records:
payload = record.payload
method = payload.get("method")
if payload.get("sessionId") != session_id or method not in NETWORK_EVENT_METHODS:
continue
params = payload.get("params")
if not isinstance(params, dict):
continue
request_id = params.get("requestId")
if not isinstance(request_id, str) or not request_id:
continue
timeline = timelines.setdefault(
request_id,
{
"request_id": request_id,
"url": None,
"type": None,
"start_sequence": None,
"response_sequence": None,
"terminals": [],
},
)
if method == "Network.requestWillBeSent":
request = params.get("request")
if isinstance(request, dict):
timeline["url"] = request.get("url", timeline["url"])
timeline["type"] = params.get("type", timeline["type"])
if timeline["start_sequence"] is None:
timeline["start_sequence"] = record.sequence
elif method == "Network.responseReceived":
timeline["type"] = params.get("type", timeline["type"])
if timeline["response_sequence"] is None:
timeline["response_sequence"] = record.sequence
else:
timeline["terminals"].append(
{"method": method, "sequence": record.sequence}
)
violations: list[dict[str, Any]] = []
for timeline in timelines.values():
terminals = timeline.pop("terminals")
first_start = timeline["start_sequence"]
first_response = timeline["response_sequence"]
first_terminal = min(
terminals,
key=lambda terminal: terminal["sequence"],
default=None,
)
def report(kind: str, **details: Any) -> None:
violations.append({**timeline, "kind": kind, **details})
if first_response is not None and (
first_start is None or first_response < first_start
):
report("response_before_start", sequence=first_response)
if first_terminal is not None and (
first_start is None or first_terminal["sequence"] < first_start
):
report(
"terminal_before_start",
sequence=first_terminal["sequence"],
terminal_method=first_terminal["method"],
)
first_successful_terminal = min(
(
terminal
for terminal in terminals
if terminal["method"] == "Network.loadingFinished"
),
key=lambda terminal: terminal["sequence"],
default=None,
)
if first_successful_terminal is not None and (
first_response is None
or first_successful_terminal["sequence"] < first_response
):
report(
"successful_terminal_before_response",
sequence=first_successful_terminal["sequence"],
terminal_method=first_successful_terminal["method"],
)
if len(terminals) > 1:
report(
"duplicate_terminal",
terminal_sequences=[terminal["sequence"] for terminal in terminals],
terminal_methods=[terminal["method"] for terminal in terminals],
)
return violations
def _error_detail(error: BaseException) -> dict[str, Any]:
detail: dict[str, Any] = {
"type": type(error).__name__,
"message": str(error),
}
if isinstance(error, RawCdpCommandError):
detail["cdp_error"] = error.error
if isinstance(error, RawCdpTimeoutError):
detail["method"] = error.method
detail["message_id"] = error.message_id
return detail
def _command_response_record(
records: Iterable[RecordedCdpMessage], message_id: int
) -> RecordedCdpMessage | None:
return next((record for record in records if record.payload.get("id") == message_id), None)
async def _wait_for_navigation_progress(
client: RoutedRawCdpClient,
*,
after_sequence: int,
session_id: str,
identity: NavigationIdentity,
lifecycle_name: str,
timeout: float,
) -> NavigationProgress:
existing = find_navigation_progress_record(
client.messages_since(after_sequence),
session_id=session_id,
identity=identity,
lifecycle_name=lifecycle_name,
)
if existing is not None:
return existing
lifecycle_task = asyncio.create_task(
client.wait_for_event(
"Page.lifecycleEvent",
after_sequence=after_sequence,
session_id=session_id,
predicate=lambda payload: _is_exact_lifecycle_event(
payload,
session_id=session_id,
identity=identity,
name=lifecycle_name,
),
timeout=timeout + 1.0,
)
)
successor_task = asyncio.create_task(
client.wait_for_event(
"Page.frameStartedNavigating",
after_sequence=after_sequence,
session_id=session_id,
predicate=lambda payload: _successor_navigation_identity(
payload,
session_id=session_id,
identity=identity,
)
is not None,
timeout=timeout + 1.0,
)
)
tasks = {lifecycle_task, successor_task}
try:
done, _ = await asyncio.wait(
tasks,
timeout=timeout,
return_when=asyncio.FIRST_COMPLETED,
)
if not done:
raise RawCdpTimeoutError(
f"timed out waiting for {lifecycle_name} or a successor navigation",
method="Page.lifecycleEvent",
messages=[
record.json_value() for record in client.messages_since(after_sequence)
],
)
recorded = find_navigation_progress_record(
client.messages_since(after_sequence),
session_id=session_id,
identity=identity,
lifecycle_name=lifecycle_name,
)
if recorded is not None:
return recorded
progress: list[NavigationProgress] = []
for task in done:
record = task.result()
if task is lifecycle_task:
progress.append(
NavigationProgress(
kind="lifecycle",
record=record,
identity=identity,
)
)
continue
successor = _successor_navigation_identity(
record.payload,
session_id=session_id,
identity=identity,
)
if successor is not None:
progress.append(
NavigationProgress(
kind="successor",
record=record,
identity=successor,
)
)
if not progress:
raise RawCdpError("navigation progress waiter completed without a matching record")
return min(progress, key=lambda item: item.record.sequence)
finally:
for task in tasks:
if not task.done():
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
def _runtime_value(response: dict[str, Any]) -> Any:
result = response.get("result")
if not isinstance(result, dict):
return None
remote = result.get("result")
if not isinstance(remote, dict):
return None
return remote.get("value")
async def navigate_once(
client: RoutedRawCdpClient,
*,
session_id: str,
url: str,
timeouts: NavigationTimeouts,
require_load: bool = True,
network_diagnostics: bool = False,
) -> dict[str, Any]:
started = time.perf_counter()
start_sequence = client.current_sequence
row: dict[str, Any] = {
"url": url,
"ok": False,
"failure_stage": None,
"error": None,
"navigate_result": None,
"identity": None,
"effective_identity": None,
"supersessions": [],
"response_ms": None,
"dcl_ms": None,
"load_ms": None,
"order_violations": [],
"document": None,
"dom_root": None,
"content_kind": None,
"network_diagnostics": None,
}
def capture_network_diagnostics() -> None:
if network_diagnostics:
row["network_diagnostics"] = summarize_network_activity(
client.messages_since(start_sequence),
session_id=session_id,
started=started,
)
def fail(stage: str, error: BaseException | str) -> dict[str, Any]:
row["failure_stage"] = stage
row["error"] = _error_detail(error) if isinstance(error, BaseException) else {"message": error}
row["trace"] = _compact_trace(
client,
after_sequence=start_sequence,
started=started,
)
row["elapsed_ms"] = round((time.perf_counter() - started) * 1000.0, 3)
capture_network_diagnostics()
return row
try:
navigation = await client.command(
"Page.navigate",
{"url": url},
session_id=session_id,
timeout=timeouts.response,
)
except (RawCdpError, TimeoutError) as error:
return fail("navigate_response", error)
response_record = _command_response_record(navigation.messages, navigation.message_id)
if response_record is None:
return fail("navigate_response", "Page.navigate response was not retained in the CDP trace")
row["response_ms"] = _record_elapsed_ms(response_record, started)
navigate_result = navigation.response.get("result")
if not isinstance(navigate_result, dict):
return fail("navigate_response", f"Page.navigate returned no result object: {navigation.response}")
row["navigate_result"] = navigate_result
frame_id = navigate_result.get("frameId")
loader_id = navigate_result.get("loaderId")
if not isinstance(frame_id, str) or not frame_id or not isinstance(loader_id, str) or not loader_id:
return fail(
"navigate_identity",
f"cross-document Page.navigate returned no exact frameId/loaderId: {navigate_result}",
)
identity = NavigationIdentity(frame_id=frame_id, loader_id=loader_id)
row["identity"] = asdict(identity)
row["effective_identity"] = asdict(identity)
effective_identity = identity
progress_after_sequence = start_sequence
lifecycle_name = "DOMContentLoaded"
dcl_record: RecordedCdpMessage | None = None
load_record: RecordedCdpMessage | None = None
loop = asyncio.get_running_loop()
deadline = loop.time() + timeouts.dcl
load_budget_started = False
while True:
remaining = deadline - loop.time()
failure_stage = "load" if load_budget_started else "dom_content_loaded"
if remaining <= 0:
return fail(
failure_stage,
f"timed out waiting for {lifecycle_name} or a successor navigation",
)
try:
progress = await _wait_for_navigation_progress(
client,
after_sequence=progress_after_sequence,
session_id=session_id,
identity=effective_identity,
lifecycle_name=lifecycle_name,
timeout=remaining,
)
except (RawCdpError, TimeoutError) as error:
return fail(failure_stage, error)
progress_after_sequence = progress.record.sequence
if progress.kind == "successor":
params = progress.record.payload.get("params")
row["supersessions"].append(
{
"from_identity": asdict(effective_identity),
"to_identity": asdict(progress.identity),
"url": params.get("url") if isinstance(params, dict) else None,
"during": "load" if load_budget_started else "dom_content_loaded",
"sequence": progress.record.sequence,
"elapsed_ms": _record_elapsed_ms(progress.record, started),
}
)
effective_identity = progress.identity
row["effective_identity"] = asdict(effective_identity)
row["dcl_ms"] = None
dcl_record = None
lifecycle_name = "DOMContentLoaded"
continue
if lifecycle_name == "DOMContentLoaded":
dcl_record = progress.record
row["dcl_ms"] = _record_elapsed_ms(dcl_record, started)
if not require_load:
break
lifecycle_name = "load"
if not load_budget_started:
load_budget_started = True
deadline = loop.time() + timeouts.load
continue
load_record = progress.record
row["load_ms"] = _record_elapsed_ms(load_record, started)
break
if dcl_record is None:
return fail("dom_content_loaded", "navigation completed without DOMContentLoaded")
order_violations = navigation_order_violations(response_record, dcl_record, load_record)
row["order_violations"] = order_violations
if order_violations:
return fail("event_order", "; ".join(order_violations))
expression = (
"(() => ({"
"url:String(location.href),"
"title:document.title,"
"readyState:document.readyState,"
"bodyTextLength:document.body?document.body.innerText.length:0,"
"htmlLength:document.documentElement?document.documentElement.outerHTML.length:0"
"}))()"
)
try:
evaluation = await client.command(
"Runtime.evaluate",
{"expression": expression, "returnByValue": True},
session_id=session_id,
timeout=timeouts.postcheck,
)
except (RawCdpError, TimeoutError) as error:
return fail("runtime_evaluate", error)
document = _runtime_value(evaluation.response)
if not isinstance(document, dict):
return fail("runtime_evaluate", f"Runtime.evaluate returned no document value: {evaluation.response}")
row["document"] = document
expected_ready_states = {"complete"} if require_load else {"interactive", "complete"}
if document.get("readyState") not in expected_ready_states:
return fail(
"document_ready_state",
f"lifecycle completed but readyState was {document.get('readyState')!r}",
)
try:
dom = await client.command(
"DOM.getDocument",
{"depth": 1},
session_id=session_id,
timeout=timeouts.postcheck,
)
except (RawCdpError, TimeoutError) as error:
return fail("dom_get_document", error)
dom_result = dom.response.get("result")
root = dom_result.get("root") if isinstance(dom_result, dict) else None
node_id = root.get("nodeId") if isinstance(root, dict) else None
if not isinstance(node_id, int) or node_id <= 0:
return fail("dom_get_document", f"DOM.getDocument returned no root node: {dom.response}")
row["dom_root"] = {"node_id": node_id, "backend_node_id": root.get("backendNodeId")}
error_text = navigate_result.get("errorText") if not row["supersessions"] else None
final_url = document.get("url")
row["content_kind"] = (
"network_error_document"
if isinstance(error_text, str)
or (isinstance(final_url, str) and final_url.startswith("chrome-error://"))
else "document"
)
row["ok"] = True
row["trace"] = _compact_trace(
client,
after_sequence=start_sequence,
started=started,
)
row["elapsed_ms"] = round((time.perf_counter() - started) * 1000.0, 3)
capture_network_diagnostics()
return row
async def _create_page_session(
client: RoutedRawCdpClient,
timeout: float,
*,
network_diagnostics: bool,
) -> tuple[str, str]:
target = await client.command(
"Target.createTarget",
{"url": "about:blank"},
timeout=timeout,
)
target_id = target.response.get("result", {}).get("targetId")
if not isinstance(target_id, str) or not target_id:
raise RuntimeError(f"Target.createTarget returned no targetId: {target.response}")
attached = await client.command(
"Target.attachToTarget",
{"targetId": target_id, "flatten": True},
timeout=timeout,
)
session_id = attached.response.get("result", {}).get("sessionId")
if not isinstance(session_id, str) or not session_id:
raise RuntimeError(f"Target.attachToTarget returned no sessionId: {attached.response}")
for method, params in (
("Page.enable", None),
("Runtime.enable", None),
("DOM.enable", None),
("Page.setLifecycleEventsEnabled", {"enabled": True}),
):
await client.command(method, params, session_id=session_id, timeout=timeout)
if network_diagnostics:
await client.command("Network.enable", session_id=session_id, timeout=timeout)
return target_id, session_id
def _git_output(*args: str) -> str | None:
result = subprocess.run(
["git", *args],
cwd=REPO_ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
check=False,
text=True,
)
if result.returncode != 0:
return None
return result.stdout.strip()
def repository_state() -> dict[str, Any]:
status = _git_output("status", "--porcelain")
return {
"commit": _git_output("rev-parse", "HEAD"),
"dirty": None if status is None else bool(status),
}
def capture_navigation_resource_sample(
root_pid: int,
*,
index: int,
url: str | None,
) -> dict[str, Any]:
sample: dict[str, Any] = {
"index": index,
"url": url,
"captured_at": datetime.now(UTC).isoformat(),
"error": None,
}
try:
observed = snapshot_resources(root_pid, include_lifetime_cpu=False)
except Exception as error:
sample["error"] = f"{type(error).__name__}: {error}"
return sample
sample.update({field: observed.get(field) for field in NAVIGATION_RESOURCE_FIELDS})
return sample
def _average(values: Iterable[float | int]) -> float | None:
values = list(values)
return sum(values) / len(values) if values else None
def _linear_slope(points: list[tuple[int, float]]) -> float | None:
if len(points) < 2:
return None
mean_x = sum(point[0] for point in points) / len(points)
mean_y = sum(point[1] for point in points) / len(points)
denominator = sum((point[0] - mean_x) ** 2 for point in points)
if denominator == 0:
return None
return sum(
(point[0] - mean_x) * (point[1] - mean_y) for point in points
) / denominator
def _resource_metric_summary(
samples: list[dict[str, Any]],
metric: str,
) -> dict[str, Any]:
initial = next(
(
sample.get(metric)
for sample in samples
if sample.get("index") == 0
and isinstance(sample.get(metric), (int, float))
),
None,
)
points = [
(int(sample["index"]), float(sample[metric]))
for sample in samples
if isinstance(sample.get("index"), int)
and sample["index"] > 0
and isinstance(sample.get(metric), (int, float))
]
values = [value for _, value in points]
window_size = min(10, len(values))
first_window = values[:window_size]
last_window = values[-window_size:] if window_size else []
first_window_average = _average(first_window)
last_window_average = _average(last_window)
warm_start = min(len(points), max(1, len(points) // 10))
warm_slope = _linear_slope(points[warm_start:])
return {
"observed_samples": len(points),
"initial": initial,
"first": values[0] if values else None,
"final": values[-1] if values else None,
"minimum": min(values) if values else None,
"peak": max(values) if values else None,
"window_size": window_size,
"first_window_average": first_window_average,
"last_window_average": last_window_average,
"first_to_last_window_delta": (
last_window_average - first_window_average
if first_window_average is not None and last_window_average is not None
else None
),
"warm_slope_per_navigation": warm_slope,
"warm_slope_per_100_navigations": (
warm_slope * 100.0 if warm_slope is not None else None
),
}
def summarize_navigation_resource_samples(
samples: list[dict[str, Any]],
periodic_resources: dict[str, Any],
) -> dict[str, Any]:
navigation_samples = [
sample
for sample in samples
if isinstance(sample.get("index"), int) and sample["index"] > 0
]
quarters = []
for offset in range(4):
start = len(navigation_samples) * offset // 4
end = len(navigation_samples) * (offset + 1) // 4
quarter_samples = navigation_samples[start:end]
def quarter_metric(metric: str) -> dict[str, Any]:
values = [
float(sample[metric])
for sample in quarter_samples
if isinstance(sample.get(metric), (int, float))
]
return {
"observed_samples": len(values),
"average": _average(values),
"final": values[-1] if values else None,
"peak": max(values) if values else None,
}
quarters.append(
{
"quarter": offset + 1,
"start_index": quarter_samples[0]["index"] if quarter_samples else None,
"end_index": quarter_samples[-1]["index"] if quarter_samples else None,
"sample_count": len(quarter_samples),
"rss_bytes": quarter_metric("rss_bytes"),
"pss_bytes": quarter_metric("pss_bytes"),
"fd_count": quarter_metric("fd_count"),
}
)
return {
"sample_count": len(navigation_samples),
"initial_sample_present": any(sample.get("index") == 0 for sample in samples),
"sample_errors": sum(bool(sample.get("error")) for sample in samples),
"rss_bytes": _resource_metric_summary(samples, "rss_bytes"),
"pss_bytes": _resource_metric_summary(samples, "pss_bytes"),
"fd_count": _resource_metric_summary(samples, "fd_count"),
"thread_count": _resource_metric_summary(samples, "thread_count"),
"process_count": _resource_metric_summary(samples, "process_count"),
"periodic": {
"sample_count": periodic_resources.get("sample_count"),
"peak_rss_bytes": periodic_resources.get("peak_rss_bytes"),
"peak_pss_bytes": periodic_resources.get("peak_pss_bytes"),
"peak_fd_count": periodic_resources.get("peak_fd_count"),
"peak_thread_count": periodic_resources.get("peak_thread_count"),
"observer_error": periodic_resources.get("observer_error"),
"late_sample_count": periodic_resources.get("late_sample_count"),
},
"quarters": quarters,
}
def selected_engines(engine: str) -> tuple[str, ...]:
if engine == "both":
return ("moli", "chromium")
if engine in {"moli", "chromium"}:
return (engine,)
raise ValueError(f"unsupported engine selection: {engine}")
async def run_engine(
*,
target: str,
binary: Path,
urls: list[str],
timeouts: NavigationTimeouts,
startup_timeout: float,
recovery_timeout: float,
network_diagnostics: bool,
navigation_resource_samples: bool,
periodic_resource_samples: bool = False,
) -> dict[str, Any]:
started_at = datetime.now(UTC).isoformat()
serve = start_target_serve(target, binary, startup_timeout)
client: RoutedRawCdpClient | None = None
session_id: str | None = None
rows: list[dict[str, Any]] = []
network_order_violations: list[dict[str, Any]] = []
resource_samples: list[dict[str, Any]] = []
aborted_after_index: int | None = None
try:
client = await connect_routed_raw_cdp(serve.endpoint)
target_id, session_id = await _create_page_session(
client,
startup_timeout,
network_diagnostics=network_diagnostics,
)
if navigation_resource_samples:
resource_samples.append(
capture_navigation_resource_sample(
serve.process.pid,
index=0,
url=None,
)
)
for index, url in enumerate(urls, 1):
row = await navigate_once(
client,
session_id=session_id,
url=url,
timeouts=timeouts,
network_diagnostics=network_diagnostics,
)
row.update({"index": index, "target_id": target_id})
if not row["ok"]:
marker = f"moli-navigate-recovery-{index}-{time.time_ns()}"
recovery_timeouts = NavigationTimeouts(
response=recovery_timeout,
dcl=recovery_timeout,
load=recovery_timeout,
postcheck=recovery_timeout,
)
recovery = await navigate_once(
client,
session_id=session_id,
url=make_recovery_url(marker),
timeouts=recovery_timeouts,
network_diagnostics=network_diagnostics,
)
document = recovery.get("document")
marker_ok = isinstance(document, dict) and document.get("title") == marker
recovery["marker"] = marker
recovery["marker_ok"] = marker_ok
recovery["ok"] = bool(recovery["ok"] and marker_ok)
row["recovery"] = recovery
if not recovery["ok"]:
aborted_after_index = index
rows.append(row)
if navigation_resource_samples:
resource_samples.append(
capture_navigation_resource_sample(
serve.process.pid,
index=index,
url=url,
)
)
print(
json.dumps(
{
"target": target,
"index": index,
"url": url,
"ok": row["ok"],
"failure_stage": row["failure_stage"],
"response_ms": row["response_ms"],
"dcl_ms": row["dcl_ms"],
"load_ms": row["load_ms"],
"content_kind": row["content_kind"],
"supersessions": len(row["supersessions"]),
"network_inflight": (
row.get("network_diagnostics") or {}
).get("inflight_count"),
"recovery_ok": row.get("recovery", {}).get("ok"),
},
ensure_ascii=False,
),
flush=True,
)
if aborted_after_index is not None:
break
finally:
if client is not None:
if network_diagnostics and session_id is not None:
network_order_violations = network_event_order_violations(
client.messages_since(0),
session_id=session_id,
)
await client.close()
serve_result = stop_target_serve(
serve,
include_resource_samples=periodic_resource_samples,
)
summary = {
"planned": len(urls),
"attempted": len(rows),
"observable_passes": sum(bool(row["ok"]) for row in rows),
"failures": sum(not row["ok"] for row in rows),
"document_passes": sum(row.get("content_kind") == "document" for row in rows),
"network_error_documents": sum(
row.get("content_kind") == "network_error_document" for row in rows
),
"recovery_attempts": sum("recovery" in row for row in rows),
"recovery_passes": sum(row.get("recovery", {}).get("ok") is True for row in rows),
"recovery_failures": sum(row.get("recovery", {}).get("ok") is False for row in rows),
"order_violations": sum(bool(row.get("order_violations")) for row in rows),
"network_order_violations": len(network_order_violations),
"superseded_passes": sum(
bool(row["ok"] and row.get("supersessions")) for row in rows
),
"aborted_after_index": aborted_after_index,
}
return {
"target": target,
"binary": str(binary),
"started_at": started_at,
"finished_at": datetime.now(UTC).isoformat(),
"ready_ms": serve.ready_ms,
"summary": summary,
"network_order_violations": network_order_violations,
"navigation_resources": (
{
"samples": resource_samples,
"summary": summarize_navigation_resource_samples(
resource_samples,
serve_result.get("resources", {}),
),
}
if navigation_resource_samples
else None
),
"rows": rows,
"process": serve_result,
}
def _parse_args(argv: list[str] | None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Fuzz sequential Page.navigate calls on one CDP target and verify recovery.",
)
parser.add_argument("--url", action="append", help="Explicit URL; repeat to define exact order.")
parser.add_argument("--seed-file", type=Path, default=DEFAULT_SEED_FILE)
parser.add_argument("--seed", type=int, default=DEFAULT_SEED)
parser.add_argument("--count", type=int, default=DEFAULT_COUNT)
parser.add_argument("--rounds", type=int, default=1)
parser.add_argument("--pinned-domain", action="append")
parser.add_argument("--moli-bin")
parser.add_argument("--chrome-bin")
parser.add_argument(
"--engine",
choices=("moli", "chromium", "both"),
default="moli",
help="Browser engine to run; `both` runs the same URL order sequentially.",
)
parser.add_argument("--full-resources", action="store_true")
parser.add_argument(
"--network-diagnostics",
action="store_true",
help=(
"Enable Network events, record compact request terminal-state summaries, "
"and audit exact request event order; off by default to keep benchmark "
"overhead comparable."
),
)
parser.add_argument(
"--navigation-resource-samples",
action="store_true",
help=(
"Capture a process-tree RSS/PSS/FD checkpoint before the first "
"navigation and after every navigation, then report windows, "
"quarters, and a warm memory slope."
),
)
parser.add_argument(
"--periodic-resource-samples",
action="store_true",
help=(
"Retain the process-tree RSS/PSS/CPU time series in the output. "
"The sampler always runs, but raw points are omitted by default "
"because they can make long-run JSON files large."
),
)
parser.add_argument("--startup-timeout", type=float, default=20.0)
parser.add_argument("--response-timeout", type=float, default=15.0)
parser.add_argument("--dcl-timeout", type=float, default=15.0)
parser.add_argument("--load-timeout", type=float, default=20.0)
parser.add_argument("--postcheck-timeout", type=float, default=5.0)
parser.add_argument("--recovery-timeout", type=float, default=8.0)
parser.add_argument("--output", type=Path)
args = parser.parse_args(argv)
for name in (
"startup_timeout",
"response_timeout",
"dcl_timeout",
"load_timeout",
"postcheck_timeout",
"recovery_timeout",
):
if getattr(args, name) <= 0:
parser.error(f"--{name.replace('_', '-')} must be positive")
if args.rounds <= 0:
parser.error("--rounds must be positive")
if args.count <= 0:
parser.error("--count must be positive")
return args
def _selected_urls(args: argparse.Namespace) -> list[str]:
if args.url:
base = [normalize_public_url(url) for url in args.url]
else:
domains = [target for _, target in parse_top_sites_list(args.seed_file)]
base = select_seed_urls(
domains,
seed=args.seed,
count=args.count,
pinned_domains=args.pinned_domain or DEFAULT_PINNED_DOMAINS,
)
return base * args.rounds
async def _run(args: argparse.Namespace) -> tuple[dict[str, Any], int]:
urls = _selected_urls(args)
timeouts = NavigationTimeouts(
response=args.response_timeout,
dcl=args.dcl_timeout,
load=args.load_timeout,
postcheck=args.postcheck_timeout,
)
engines: list[tuple[str, Path]] = []
for engine in selected_engines(args.engine):
if engine == "moli":
engines.append(
(
"moli-full-cdp" if args.full_resources else "moli-cdp",
moli_binary(args.moli_bin),
)
)
continue
chrome = chrome_binary(args.chrome_bin)
if chrome is None:
raise RuntimeError("Chromium binary not found; pass --chrome-bin or set CHROME_BIN")
engines.append(("chrome-cdp", chrome))
results = []
for target, binary in engines:
results.append(
await run_engine(
target=target,
binary=binary,
urls=urls,
timeouts=timeouts,
startup_timeout=args.startup_timeout,
recovery_timeout=args.recovery_timeout,
network_diagnostics=args.network_diagnostics,
navigation_resource_samples=args.navigation_resource_samples,
periodic_resource_samples=args.periodic_resource_samples,
)
)
payload = {
"schema_version": 5,
"repository": repository_state(),
"engine_selection": args.engine,
"seed": args.seed,
"rounds": args.rounds,
"urls": urls,
"timeouts": asdict(timeouts),
"network_diagnostics": args.network_diagnostics,
"navigation_resource_samples": args.navigation_resource_samples,
"periodic_resource_samples": args.periodic_resource_samples,
"results": results,
}
failed = any(
result["summary"]["failures"]
or result["summary"]["network_order_violations"]
for result in results
)
return payload, 1 if failed else 0
def main(argv: list[str] | None = None) -> int:
args = _parse_args(argv)
payload, exit_code = asyncio.run(_run(args))
output = args.output
if output is None:
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
output = REPO_ROOT / "moli-benchmark" / "results" / f"sequential-navigate-{stamp}.json"
write_json(output, payload)
print(json.dumps({"output": str(output), "exit_code": exit_code}), flush=True)
return exit_code
if __name__ == "__main__":
raise SystemExit(main())