Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c67a479

Browse files
authored
Make IPC framing more efficient (#20793)
This is not important now, but it will be important when we will start sending serialized ASTs over IPC, using stdlib base64 can easily add 0.5-1ms per file round-trip overhead, which is a lot. Initially I waned to use `librt.base64`, but ultimately simply switched to framing with a fixed size header. I am still using `librt.base64` in few places where performance doesn't really matter, because why not. Btw I noticed we have very few mypyc primitives for `bytearray`, probably we should add more, added comment in mypyc/mypyc#644
1 parent 92a7858 commit c67a479

8 files changed

Lines changed: 39 additions & 30 deletions

File tree

mypy-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ typing_extensions>=4.6.0
44
mypy_extensions>=1.0.0
55
pathspec>=1.0.0
66
tomli>=1.1.0; python_version<'3.11'
7-
librt>=0.6.2; platform_python_implementation != 'PyPy'
7+
librt>=0.8.0; platform_python_implementation != 'PyPy'

mypy/build.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
from __future__ import annotations
1515

16-
import base64
1716
import collections
1817
import contextlib
1918
import gc
@@ -40,6 +39,7 @@
4039
TypedDict,
4140
)
4241

42+
from librt.base64 import b64encode
4343
from librt.internal import (
4444
cache_version,
4545
read_bool,
@@ -353,7 +353,7 @@ def default_flush_errors(
353353
if options.num_workers > 0:
354354
# TODO: switch to something more efficient than pickle (also in the daemon).
355355
pickled_options = pickle.dumps(options.snapshot())
356-
options_data = base64.b64encode(pickled_options).decode()
356+
options_data = b64encode(pickled_options).decode()
357357
workers = [
358358
WorkerClient(f".mypy_worker.{idx}.json", options_data, worker_env or os.environ)
359359
for idx in range(options.num_workers)

mypy/build_worker/worker.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from __future__ import annotations
1515

1616
import argparse
17-
import base64
1817
import gc
1918
import json
2019
import os
@@ -24,6 +23,8 @@
2423
import time
2524
from typing import NamedTuple
2625

26+
from librt.base64 import b64decode
27+
2728
from mypy import util
2829
from mypy.build import (
2930
AckMessage,
@@ -72,7 +73,7 @@ def main(argv: list[str]) -> None:
7273
# This mimics how daemon receives the options. Note we need to postpone
7374
# processing error codes after plugins are loaded, because plugins can add
7475
# custom error codes.
75-
options_dict = pickle.loads(base64.b64decode(args.options_data))
76+
options_dict = pickle.loads(b64decode(args.options_data))
7677
options_obj = Options()
7778
disable_error_code = options_dict.pop("disable_error_code", [])
7879
enable_error_code = options_dict.pop("enable_error_code", [])

mypy/dmypy/client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from __future__ import annotations
88

99
import argparse
10-
import base64
1110
import json
1211
import os
1312
import pickle
@@ -17,6 +16,8 @@
1716
from collections.abc import Callable, Mapping
1817
from typing import Any, NoReturn
1918

19+
from librt.base64 import b64decode
20+
2021
from mypy.defaults import RECURSION_LIMIT
2122
from mypy.dmypy_os import alive, kill
2223
from mypy.dmypy_util import DEFAULT_STATUS_FILE, receive, send
@@ -620,7 +621,7 @@ def do_daemon(args: argparse.Namespace) -> None:
620621
if args.options_data:
621622
from mypy.options import Options
622623

623-
options_dict = pickle.loads(base64.b64decode(args.options_data))
624+
options_dict = pickle.loads(b64decode(args.options_data))
624625
options_obj = Options()
625626
options = options_obj.apply_changes(options_dict)
626627
else:

mypy/dmypy_server.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from __future__ import annotations
88

99
import argparse
10-
import base64
1110
import io
1211
import json
1312
import os
@@ -20,6 +19,8 @@
2019
from contextlib import redirect_stderr, redirect_stdout
2120
from typing import Any, Final, TypeAlias as _TypeAlias
2221

22+
from librt.base64 import b64encode
23+
2324
import mypy.build
2425
import mypy.errors
2526
import mypy.main
@@ -57,7 +58,7 @@ def daemonize(
5758
"""
5859
command = [sys.executable, "-m", "mypy.dmypy", "--status-file", status_file, "daemon"]
5960
pickled_options = pickle.dumps(options.snapshot())
60-
command.append(f'--options-data="{base64.b64encode(pickled_options).decode()}"')
61+
command.append(f'--options-data="{b64encode(pickled_options).decode()}"')
6162
if timeout:
6263
command.append(f"--timeout={timeout}")
6364
if log_file:

mypy/ipc.py

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
"""Cross platform abstractions for inter-process communication
1+
"""Cross-platform abstractions for inter-process communication
22
33
On Unix, this uses AF_UNIX sockets.
44
On Windows, this uses NamedPipes.
55
"""
66

77
from __future__ import annotations
88

9-
import base64
10-
import codecs
119
import json
1210
import os
1311
import shutil
12+
import struct
1413
import sys
1514
import tempfile
1615
from abc import abstractmethod
@@ -20,6 +19,7 @@
2019
from typing import Final
2120
from typing_extensions import Self
2221

22+
from librt.base64 import urlsafe_b64encode
2323
from librt.internal import ReadBuffer, WriteBuffer
2424

2525
if sys.platform == "win32":
@@ -37,6 +37,9 @@
3737

3838
_IPCHandle = socket.socket
3939

40+
# Size of the message packed as !L, i.e. 4 bytes in network order (big-endian).
41+
HEADER_SIZE = 4
42+
4043

4144
class IPCException(Exception):
4245
"""Exception for IPC issues."""
@@ -58,24 +61,29 @@ class IPCBase:
5861
def __init__(self, name: str, timeout: float | None) -> None:
5962
self.name = name
6063
self.timeout = timeout
64+
self.message_size: int | None = None
6165
self.buffer = bytearray()
6266

63-
def frame_from_buffer(self) -> bytearray | None:
67+
def frame_from_buffer(self) -> bytes | None:
6468
"""Return a full frame from the bytes we have in the buffer."""
65-
space_pos = self.buffer.find(b" ")
66-
if space_pos == -1:
69+
size = len(self.buffer)
70+
if size < HEADER_SIZE:
6771
return None
68-
# We have a full frame
69-
bdata = self.buffer[:space_pos]
70-
self.buffer = self.buffer[space_pos + 1 :]
71-
return bdata
72+
if self.message_size is None:
73+
self.message_size = struct.unpack("!L", self.buffer[:HEADER_SIZE])[0]
74+
if size < self.message_size + HEADER_SIZE:
75+
return None
76+
# We have a full frame, avoid extra copy in case we get a large frame.
77+
bdata = memoryview(self.buffer)[HEADER_SIZE : HEADER_SIZE + self.message_size]
78+
self.buffer = self.buffer[HEADER_SIZE + self.message_size :]
79+
self.message_size = None
80+
return bytes(bdata)
7281

7382
def read(self, size: int = 100000) -> str:
7483
return self.read_bytes(size).decode("utf-8")
7584

7685
def read_bytes(self, size: int = 100000) -> bytes:
7786
"""Read bytes from an IPC connection until we have a full frame."""
78-
bdata: bytearray | None = bytearray()
7987
if sys.platform == "win32":
8088
while True:
8189
# Check if we already have a message in the buffer before
@@ -126,19 +134,19 @@ def read_bytes(self, size: int = 100000) -> bytes:
126134
self.buffer.extend(more)
127135

128136
if not bdata:
129-
# Socket was empty and we didn't get any frame.
137+
# Socket was empty, and we didn't get any frame.
130138
# This should only happen if the socket was closed.
131139
return b""
132-
return codecs.decode(bdata, "base64")
140+
return bdata
133141

134142
def write(self, data: str) -> None:
135143
self.write_bytes(data.encode("utf-8"))
136144

137145
def write_bytes(self, data: bytes) -> None:
138146
"""Write to an IPC connection."""
139147

140-
# Frame the data by urlencoding it and separating by space.
141-
encoded_data = codecs.encode(data, "base64") + b" "
148+
# Frame the data by adding fixed size header.
149+
encoded_data = struct.pack("!L", len(data)) + data
142150

143151
if sys.platform == "win32":
144152
try:
@@ -226,9 +234,7 @@ class IPCServer(IPCBase):
226234

227235
def __init__(self, name: str, timeout: float | None = None) -> None:
228236
if sys.platform == "win32":
229-
name = r"\\.\pipe\{}-{}.pipe".format(
230-
name, base64.urlsafe_b64encode(os.urandom(6)).decode()
231-
)
237+
name = r"\\.\pipe\{}-{}.pipe".format(name, urlsafe_b64encode(os.urandom(6)).decode())
232238
else:
233239
name = f"{name}.sock"
234240
super().__init__(name, timeout)

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ requires = [
99
"mypy_extensions>=1.0.0",
1010
"pathspec>=1.0.0",
1111
"tomli>=1.1.0; python_version<'3.11'",
12-
"librt>=0.6.2; platform_python_implementation != 'PyPy'",
12+
"librt>=0.8.0; platform_python_implementation != 'PyPy'",
1313
# the following is from build-requirements.txt
1414
"types-psutil",
1515
"types-setuptools",
@@ -53,7 +53,7 @@ dependencies = [
5353
"mypy_extensions>=1.0.0",
5454
"pathspec>=1.0.0",
5555
"tomli>=1.1.0; python_version<'3.11'",
56-
"librt>=0.6.2; platform_python_implementation != 'PyPy'",
56+
"librt>=0.8.0; platform_python_implementation != 'PyPy'",
5757
]
5858
dynamic = ["version"]
5959

test-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ identify==2.6.15
2222
# via pre-commit
2323
iniconfig==2.1.0
2424
# via pytest
25-
librt==0.7.8 ; platform_python_implementation != "PyPy"
25+
librt==0.8.0 ; platform_python_implementation != "PyPy"
2626
# via -r mypy-requirements.txt
2727
lxml==6.0.2 ; python_version < "3.15"
2828
# via -r test-requirements.in

0 commit comments

Comments
 (0)