https://github.com/python/cpython/commit/29acc08c8dad664cd5713cb392e5beba65724c10
commit: 29acc08c8dad664cd5713cb392e5beba65724c10
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-02-02T20:00:12+02:00
summary:
gh-75572: Speed up test_xpickle (GH-144393)
Run a long living subprocess which handles multiple requests instead of
running a new subprocess for each request.
files:
M Lib/test/test_xpickle.py
M Lib/test/xpickle_worker.py
diff --git a/Lib/test/test_xpickle.py b/Lib/test/test_xpickle.py
index 158f27dce4fdc2..d87c671d4f5394 100644
--- a/Lib/test/test_xpickle.py
+++ b/Lib/test/test_xpickle.py
@@ -3,6 +3,7 @@
import io
import os
import pickle
+import struct
import subprocess
import sys
import unittest
@@ -83,9 +84,19 @@ def have_python_version(py_version):
return py_executable_map.get(py_version, None)
[email protected]_resource('cpu')
+def read_exact(f, n):
+ buf = b''
+ while len(buf) < n:
+ chunk = f.read(n - len(buf))
+ if not chunk:
+ raise EOFError
+ buf += chunk
+ return buf
+
+
class AbstractCompatTests(pickletester.AbstractPickleTests):
py_version = None
+ worker = None
@classmethod
def setUpClass(cls):
@@ -93,6 +104,7 @@ def setUpClass(cls):
if not have_python_version(cls.py_version):
py_version_str = ".".join(map(str, cls.py_version))
raise unittest.SkipTest(f'Python {py_version_str} not available')
+ cls.addClassCleanup(cls.finish_worker)
# Override the default pickle protocol to match what xpickle worker
# will be running.
highest_protocol = highest_proto_for_py_version(cls.py_version)
@@ -101,8 +113,32 @@ def setUpClass(cls):
cls.enterClassContext(support.swap_attr(pickle, 'HIGHEST_PROTOCOL',
highest_protocol))
- @staticmethod
- def send_to_worker(python, data):
+ @classmethod
+ def start_worker(cls, python):
+ target = os.path.join(os.path.dirname(__file__), 'xpickle_worker.py')
+ worker = subprocess.Popen([*python, target],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ # For windows bpo-17023.
+ shell=is_windows)
+ cls.worker = worker
+ return worker
+
+ @classmethod
+ def finish_worker(cls):
+ worker = cls.worker
+ if worker is None:
+ return
+ cls.worker = None
+ worker.stdin.close()
+ worker.stdout.close()
+ worker.stderr.close()
+ worker.terminate()
+ worker.wait()
+
+ @classmethod
+ def send_to_worker(cls, python, data):
"""Bounce a pickled object through another version of Python.
This will send data to a child process where it will
be unpickled, then repickled and sent back to the parent process.
@@ -112,33 +148,33 @@ def send_to_worker(python, data):
Returns:
The pickled data received from the child process.
"""
- target = os.path.join(os.path.dirname(__file__), 'xpickle_worker.py')
- worker = subprocess.Popen([*python, target],
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- # For windows bpo-17023.
- shell=is_windows)
- stdout, stderr = worker.communicate(data)
- if worker.returncode == 0:
- return stdout
- # if the worker fails, it will write the exception to stdout
+ worker = cls.worker
+ if worker is None:
+ worker = cls.start_worker(python)
+
try:
- exception = pickle.loads(stdout)
- except (pickle.UnpicklingError, EOFError):
+ worker.stdin.write(struct.pack('!i', len(data)) + data)
+ worker.stdin.flush()
+
+ size, = struct.unpack('!i', read_exact(worker.stdout, 4))
+ if size > 0:
+ return read_exact(worker.stdout, size)
+ # if the worker fails, it will write the exception to stdout
+ if size < 0:
+ stdout = read_exact(worker.stdout, -size)
+ try:
+ exception = pickle.loads(stdout)
+ except (pickle.UnpicklingError, EOFError):
+ pass
+ else:
+ if isinstance(exception, Exception):
+ # To allow for tests which test for errors.
+ raise exception
+ _, stderr = worker.communicate()
raise RuntimeError(stderr)
- else:
- if support.verbose > 1:
- print()
- print(f'{data = }')
- print(f'{stdout = }')
- print(f'{stderr = }')
- if isinstance(exception, Exception):
- # To allow for tests which test for errors.
- raise exception
- else:
- raise RuntimeError(stderr)
-
+ except:
+ cls.finish_worker()
+ raise
def dumps(self, arg, proto=0, **kwargs):
# Skip tests that require buffer_callback arguments since
@@ -148,9 +184,8 @@ def dumps(self, arg, proto=0, **kwargs):
self.skipTest('Test does not support "buffer_callback" argument.')
f = io.BytesIO()
p = self.pickler(f, proto, **kwargs)
- p.dump((proto, arg))
- f.seek(0)
- data = bytes(f.read())
+ p.dump(arg)
+ data = struct.pack('!i', proto) + f.getvalue()
python = py_executable_map[self.py_version]
return self.send_to_worker(python, data)
diff --git a/Lib/test/xpickle_worker.py b/Lib/test/xpickle_worker.py
index 3fd957f4a0b939..1b49515123c6ab 100644
--- a/Lib/test/xpickle_worker.py
+++ b/Lib/test/xpickle_worker.py
@@ -2,6 +2,7 @@
# pickles in a different Python version.
import os
import pickle
+import struct
import sys
@@ -24,16 +25,38 @@
sources = f.read()
exec(sources, vars(test_module))
+def read_exact(f, n):
+ buf = b''
+ while len(buf) < n:
+ chunk = f.read(n - len(buf))
+ if not chunk:
+ raise EOFError
+ buf += chunk
+ return buf
in_stream = getattr(sys.stdin, 'buffer', sys.stdin)
out_stream = getattr(sys.stdout, 'buffer', sys.stdout)
try:
- message = pickle.load(in_stream)
- protocol, obj = message
- pickle.dump(obj, out_stream, protocol)
-except Exception as e:
+ while True:
+ size, = struct.unpack('!i', read_exact(in_stream, 4))
+ if not size:
+ break
+ data = read_exact(in_stream, size)
+ protocol, = struct.unpack('!i', data[:4])
+ obj = pickle.loads(data[4:])
+ data = pickle.dumps(obj, protocol)
+ out_stream.write(struct.pack('!i', len(data)) + data)
+ out_stream.flush()
+except Exception as exc:
# dump the exception to stdout and write to stderr, then exit
- pickle.dump(e, out_stream)
- sys.stderr.write(repr(e))
+ try:
+ data = pickle.dumps(exc)
+ out_stream.write(struct.pack('!i', -len(data)) + data)
+ out_stream.flush()
+ except Exception:
+ out_stream.write(struct.pack('!i', 0))
+ out_stream.flush()
+ sys.stderr.write(repr(exc))
+ sys.stderr.flush()
sys.exit(1)
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]