# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import threading
import time
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from base_python_support import BasePythonSupport
from logger.logger import RaptorLogger
LOG = RaptorLogger(component="raptor-browsertime")
# Artificial /target.html backend delay so prefetch vs. cold-nav is legible.
# 500 ms corresponds to the p75 response time from the pageload event.
TARGET_STALL_MS = 500
SPECULATION_RULES_TAG = """
"""
LANDING_HTML_TEMPLATE = """
Speculation Rules Prefetch Demo
{rules}
Speculation Rules Prefetch Demo
Inline <script type="speculationrules"> with
moderate eagerness fires prefetch after ~200 ms
of sustained hover on a matching link.
Target pages have a {stall_ms} ms server stall.
Hover a button to trigger prefetch, then click.
"""
TARGET_HTML = """
Target · Speculation Rules Demo
Arrived
Target page · 500 ms server stall.
Delivery —
Response time —
FCP —
LCP —
Load time —
"""
class _Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def _send(self, body):
body_bytes = body.encode("utf-8")
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body_bytes)))
# Give the cache entry a non-expired freshness lifetime; without it
# nsPrefetchNode::OnStartRequest aborts speculation prefetches with
# NS_BINDING_ABORTED ("document cannot be reused from cache").
self.send_header("Cache-Control", "max-age=60")
self.end_headers()
self.wfile.write(body_bytes)
def _send_404(self):
self.send_response(HTTPStatus.NOT_FOUND)
self.send_header("Content-Length", "0")
self.end_headers()
def do_GET(self):
path = self.path.split("?", 1)[0]
if path in ("/", "/landing.html"):
self._send(
LANDING_HTML_TEMPLATE.format(
rules=SPECULATION_RULES_TAG,
stall_ms=TARGET_STALL_MS,
)
)
elif path == "/target.html":
time.sleep(TARGET_STALL_MS / 1000.0)
self._send(TARGET_HTML)
else:
self._send_404()
def log_message(self, fmt, *args):
LOG.info("demo-server: " + fmt % args)
class SpeculationRules(BasePythonSupport):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.httpd = None
self.server_thread = None
self.port = None
def setup_test(self, test, args):
super().setup_test(test, args)
self.httpd = ThreadingHTTPServer(("127.0.0.1", 0), _Handler)
self.port = self.httpd.server_address[1]
self.server_thread = threading.Thread(
target=self.httpd.serve_forever, daemon=True
)
self.server_thread.start()
LOG.info(f"SpeculationRules demo server: http://127.0.0.1:{self.port}/")
def modify_command(self, cmd, test):
cmd += [
"--browsertime.server_url",
f"http://127.0.0.1:{self.port}",
# Target page renders its own Performance-API panel; this overlay
# (desktop default) otherwise obscures it in the recording.
"--videoParams.addTimer",
"false",
]
def handle_result(self, bt_result, raw_result, last_result=False, **kwargs):
extras = raw_result.get("extras", [])
if not extras or not isinstance(extras, list):
return
custom = extras[0].get("custom_data", {})
for key in ("navigation_duration", "response_start"):
val = custom.get(key)
if val is None:
continue
try:
bt_result["measurements"].setdefault(key, []).append(float(val))
except (TypeError, ValueError):
LOG.warning(f"SpeculationRules: non-numeric {key}={val!r}; skipped")
delivery_type = custom.get("delivery_type")
if delivery_type is not None:
LOG.info(f"SpeculationRules delivery_type: {delivery_type!r}")
def summarize_test(self, test, suite, **kwargs):
# Perfherder schema requires subtests to be a list, not a dict.
if suite.get("subtests") == {}:
suite["subtests"] = []
for name, replicates in test["measurements"].items():
if not replicates:
continue
suite["subtests"].append(
self._build_standard_subtest(test, replicates, name, should_alert=False)
)
suite["subtests"].sort(key=lambda st: st["name"])
def clean_up(self):
if self.httpd is not None:
try:
self.httpd.shutdown()
self.httpd.server_close()
except Exception as e:
LOG.warning(f"SpeculationRules shutdown error: {e}")
if self.server_thread is not None:
self.server_thread.join(5)