[wptrunner] Speed up `wptserve` startup (#41897)
`serve` constructs a list of routes and their handlers and passes them
to each server, which runs in a subprocess. However, when `start()`ing
each subprocess [0], `multiprocessing` implicitly pickles and writes all
handlers to an underlying OS pipe for transport [1]. This can block each
`start()` for a long time (~0.5s) because `StringHandler`s hold file
contents that, collectively, can easily exceed the pipe's initial size
[2].
The worst offenders are:
* /_pdf_js/pdf.worker.js: 1.7 MB
* /_pdf_js/pdf.js: 0.7 MB
* /resources/testdriver.js: 53 kB
Change the static handlers to lazily read their files so that only the
filename needs to be transported over the pipe. Unfortunately,
`/resources/testdriver.js` needs a bespoke handler because it's composed
of two files concatenated together that are not templates. Empirically,
this reduces the time until the tests start running from ~10s to ~5s.
See also: https://crbug.com/1479850
[0]: https://github.com/web-platform-tests/wpt/blob/a4d75218/tools/serve/serve.py#L629
[1]: https://github.com/python/cpython/blob/3.11/Lib/multiprocessing/popen_spawn_posix.py#L62
[2]: https://man7.org/linux/man-pages/man7/pipe.7.html says 64 KiB for Linux.
diff --git a/tools/wptrunner/wptrunner/environment.py b/tools/wptrunner/wptrunner/environment.py
index 9e217ed..24c88aa 100644
--- a/tools/wptrunner/wptrunner/environment.py
+++ b/tools/wptrunner/wptrunner/environment.py
@@ -247,13 +247,7 @@
route_builder.add_static(path, format_args, content_type, route,
headers=headers)
- data = b""
- with open(os.path.join(repo_root, "resources", "testdriver.js"), "rb") as fp:
- data += fp.read()
- with open(os.path.join(here, "testdriver-extra.js"), "rb") as fp:
- data += fp.read()
- route_builder.add_handler("GET", "/resources/testdriver.js",
- StringHandler(data, "text/javascript"))
+ route_builder.add_handler("GET", "/resources/testdriver.js", TestdriverLoader())
for url_base, paths in self.test_paths.items():
if url_base == "/":
@@ -311,6 +305,27 @@
return failed, pending
+class TestdriverLoader:
+ """A special static handler for serving `/resources/testdriver.js`.
+
+ This handler lazily reads `testdriver{,-extra}.js` so that wptrunner doesn't
+ need to pass the entire file contents to child `wptserve` processes, which
+ can slow `wptserve` startup by several seconds (crbug.com/1479850).
+ """
+ def __init__(self):
+ self._handler = None
+
+ def __call__(self, request, response):
+ if not self._handler:
+ data = b""
+ with open(os.path.join(repo_root, "resources", "testdriver.js"), "rb") as fp:
+ data += fp.read()
+ with open(os.path.join(here, "testdriver-extra.js"), "rb") as fp:
+ data += fp.read()
+ self._handler = StringHandler(data, "text/javascript")
+ return self._handler(request, response)
+
+
def wait_for_service(logger, host, port, timeout=60, server_process=None):
"""Waits until network service given as a tuple of (host, port) becomes
available, `timeout` duration is reached, or the `server_process` exits at
diff --git a/tools/wptserve/wptserve/handlers.py b/tools/wptserve/wptserve/handlers.py
index 00ba067..6d79230 100644
--- a/tools/wptserve/wptserve/handlers.py
+++ b/tools/wptserve/wptserve/handlers.py
@@ -491,7 +491,7 @@
return rv
-class StaticHandler(StringHandler):
+class StaticHandler:
def __init__(self, path, format_args, content_type, **headers):
"""Handler that reads a file from a path and substitutes some fixed data
@@ -501,10 +501,26 @@
:param format_args: Dictionary of values to substitute into the template file
:param content_type: Content type header to server the response with
:param headers: List of headers to send with responses"""
+ self._path = path
+ self._format_args = format_args
+ self._content_type = content_type
+ self._headers = headers
+ self._handler = None
- with open(path) as f:
- data = f.read()
- if format_args:
- data = data % format_args
+ def __getnewargs_ex__(self):
+ # Do not pickle `self._handler`, which can be arbitrarily large.
+ args = self._path, self._format_args, self._content_type
+ return args, self._headers
- return super().__init__(data, content_type, **headers)
+ def __call__(self, request, response):
+ # Load the static file contents lazily so that this handler can be
+ # pickled and sent to child processes efficiently. Transporting file
+ # contents across processes can slow `wptserve` startup by several
+ # seconds (crbug.com/1479850).
+ if not self._handler:
+ with open(self._path) as f:
+ data = f.read()
+ if self._format_args:
+ data = data % self._format_args
+ self._handler = StringHandler(data, self._content_type, **self._headers)
+ return self._handler(request, response)