Skip to content

Commit d97d534

Browse files
authored
Merge branch 'develop' into dependabot/github_actions/actions/cache-2.1.7
2 parents 3d5552a + 78d20d9 commit d97d534

File tree

16 files changed

+192
-122
lines changed

16 files changed

+192
-122
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ lib-profile:
139139
--plugin proxy.plugin.WebServerPlugin \
140140
--local-executor \
141141
--backlog 65536 \
142-
--open-file-limit 65536
142+
--open-file-limit 65536 \
143143
--log-file /dev/null
144144

145145
devtools:

README.md

+28-28
Original file line numberDiff line numberDiff line change
@@ -128,47 +128,47 @@
128128
```console
129129
# On Macbook Pro 2019 / 2.4 GHz 8-Core Intel Core i9 / 32 GB RAM
130130
❯ ./helper/benchmark.sh
131-
CONCURRENCY: 100 workers, TOTAL REQUESTS: 100000 req, QPS: 5000 req/sec, TIMEOUT: 1 sec
131+
CONCURRENCY: 100 workers, TOTAL REQUESTS: 100000 req, QPS: 8000 req/sec, TIMEOUT: 1 sec
132132

133133
Summary:
134-
Total: 3.1560 secs
135-
Slowest: 0.0375 secs
136-
Fastest: 0.0006 secs
137-
Average: 0.0031 secs
138-
Requests/sec: 31685.9140
134+
Total: 3.1217 secs
135+
Slowest: 0.0499 secs
136+
Fastest: 0.0004 secs
137+
Average: 0.0030 secs
138+
Requests/sec: 32033.7261
139139

140140
Total data: 1900000 bytes
141141
Size/request: 19 bytes
142142

143143
Response time histogram:
144-
0.001 [1] |
145-
0.004 [91680] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
146-
0.008 [7929] |■■■
147-
0.012 [263] |
148-
0.015 [29] |
149-
0.019 [8] |
150-
0.023 [23] |
151-
0.026 [15] |
152-
0.030 [27] |
153-
0.034 [16] |
154-
0.037 [9] |
144+
0.000 [1] |
145+
0.005 [92268] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
146+
0.010 [7264] |■■■
147+
0.015 [318] |
148+
0.020 [102] |
149+
0.025 [32] |
150+
0.030 [6] |
151+
0.035 [4] |
152+
0.040 [1] |
153+
0.045 [2] |
154+
0.050 [2] |
155155

156156

157157
Latency distribution:
158-
10% in 0.0022 secs
159-
25% in 0.0025 secs
160-
50% in 0.0029 secs
161-
75% in 0.0034 secs
162-
90% in 0.0041 secs
163-
95% in 0.0048 secs
164-
99% in 0.0066 secs
158+
10% in 0.0017 secs
159+
25% in 0.0020 secs
160+
50% in 0.0025 secs
161+
75% in 0.0036 secs
162+
90% in 0.0050 secs
163+
95% in 0.0060 secs
164+
99% in 0.0087 secs
165165

166166
Details (average, fastest, slowest):
167-
DNS+dialup: 0.0000 secs, 0.0006 secs, 0.0375 secs
167+
DNS+dialup: 0.0000 secs, 0.0004 secs, 0.0499 secs
168168
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
169-
req write: 0.0000 secs, 0.0000 secs, 0.0046 secs
170-
resp wait: 0.0030 secs, 0.0006 secs, 0.0320 secs
171-
resp read: 0.0000 secs, 0.0000 secs, 0.0029 secs
169+
req write: 0.0000 secs, 0.0000 secs, 0.0020 secs
170+
resp wait: 0.0030 secs, 0.0004 secs, 0.0462 secs
171+
resp read: 0.0000 secs, 0.0000 secs, 0.0027 secs
172172

173173
Status code distribution:
174174
[200] 100000 responses

codecov.yml

+13-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,21 @@ codecov:
66
coverage:
77
status:
88
project:
9-
default:
9+
default: false # disable the default status that measures entire project
10+
# examples: # declare a new status context "examples"
11+
tests: # declare a new status context "tests"
12+
# target: 100% # we always want 100% coverage here
13+
paths:
14+
- "tests/" # only include coverage in "tests/" folder
15+
lib: # declare a new status context "lib"
16+
paths:
17+
- "!tests/" # remove all files in "tests/"
1018
threshold: 1%
1119
patch:
1220
default:
21+
target: auto
22+
base: auto
1323
threshold: 1%
24+
comment:
25+
require_changes: true
1426
...

docs/conf.py

+1
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@
267267
(_py_class_role, 'HttpWebServerBasePlugin'),
268268
(_py_class_role, 'multiprocessing.context.Process'),
269269
(_py_class_role, 'multiprocessing.synchronize.Lock'),
270+
(_py_class_role, 'NonBlockingQueue'),
270271
(_py_class_role, 'paramiko.channel.Channel'),
271272
(_py_class_role, 'proxy.http.parser.parser.T'),
272273
(_py_class_role, 'proxy.plugin.cache.store.base.CacheStore'),

helper/benchmark.sh

+1-10
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ if [ $(basename $PWD) != "proxy.py" ]; then
3232
fi
3333

3434
TIMEOUT=1
35-
QPS=20000
35+
QPS=8000
3636
CONCURRENCY=100
3737
TOTAL_REQUESTS=100000
3838
OPEN_FILE_LIMIT=65536
@@ -41,15 +41,6 @@ PID_FILE=/tmp/proxy.pid
4141

4242
ulimit -n $OPEN_FILE_LIMIT
4343

44-
# time python -m \
45-
# proxy \
46-
# --enable-web-server \
47-
# --plugin proxy.plugin.WebServerPlugin \
48-
# --backlog $BACKLOG \
49-
# --open-file-limit $OPEN_FILE_LIMIT \
50-
# --pid-file $PID_FILE \
51-
# --log-file /dev/null
52-
5344
PID=$(cat $PID_FILE)
5445
if [[ -z "$PID" ]]; then
5546
echo "Either pid file doesn't exist or no pid found in the pid file"

proxy/common/backports.py

+37-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99
:license: BSD, see LICENSE for more details.
1010
"""
1111
import time
12+
import threading
1213

13-
from typing import Any
14+
from typing import Any, Deque
15+
from queue import Empty
16+
from collections import deque
1417

1518

1619
class cached_property:
@@ -80,3 +83,36 @@ def __get__(self, inst: Any, owner: Any) -> Any:
8083
finally:
8184
cache[self.__name__] = (value, now)
8285
return value
86+
87+
88+
class NonBlockingQueue:
89+
'''Simple, unbounded, non-blocking FIFO queue.
90+
91+
Supports only a single consumer.
92+
93+
NOTE: This is available in Python since 3.7 as SimpleQueue.
94+
Here because proxy.py still supports 3.6
95+
'''
96+
97+
def __init__(self) -> None:
98+
self._queue: Deque[Any] = deque()
99+
self._count: threading.Semaphore = threading.Semaphore(0)
100+
101+
def put(self, item: Any) -> None:
102+
'''Put the item on the queue.'''
103+
self._queue.append(item)
104+
self._count.release()
105+
106+
def get(self) -> Any:
107+
'''Remove and return an item from the queue.'''
108+
if not self._count.acquire(False, None):
109+
raise Empty
110+
return self._queue.popleft()
111+
112+
def empty(self) -> bool:
113+
'''Return True if the queue is empty, False otherwise (not reliable!).'''
114+
return len(self._queue) == 0
115+
116+
def qsize(self) -> int:
117+
'''Return the approximate size of the queue (not reliable!).'''
118+
return len(self._queue)

proxy/common/constants.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,10 @@ def _env_threadless_compliant() -> bool:
108108
DEFAULT_MAX_SEND_SIZE = 16 * 1024
109109
DEFAULT_WORK_KLASS = 'proxy.http.HttpProtocolHandler'
110110
DEFAULT_ENABLE_PROXY_PROTOCOL = False
111-
DEFAULT_SELECTOR_SELECT_TIMEOUT = 0.1
111+
# 25 milliseconds to keep the loops hot
112+
# Will consume ~0.3-0.6% CPU when idle.
113+
DEFAULT_SELECTOR_SELECT_TIMEOUT = 25 / 1000
114+
DEFAULT_INACTIVE_CONN_CLEANUP_TIMEOUT = 1 # in seconds
112115

113116
DEFAULT_DEVTOOLS_DOC_URL = 'http://proxy'
114117
DEFAULT_DEVTOOLS_FRAME_ID = secrets.token_hex(8)

proxy/common/utils.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,10 @@ def find_http_line(raw: bytes) -> Tuple[Optional[bytes], bytes]:
180180
"""Find and returns first line ending in CRLF along with following buffer.
181181
182182
If no ending CRLF is found, line is None."""
183-
parts = raw.split(CRLF)
184-
if len(parts) == 1:
185-
return None, raw
186-
return parts[0], CRLF.join(parts[1:])
183+
parts = raw.split(CRLF, 1)
184+
return (None, raw) \
185+
if len(parts) == 1 \
186+
else (parts[0], parts[1])
187187

188188

189189
def wrap_socket(

proxy/core/acceptor/acceptor.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
acceptor
1414
pre
1515
"""
16-
import queue
1716
import socket
1817
import logging
1918
import argparse
@@ -26,11 +25,11 @@
2625
from multiprocessing.reduction import recv_handle
2726

2827
from typing import List, Optional, Tuple
29-
from typing import Any # noqa: W0611 pylint: disable=unused-import
3028

3129
from ...common.flag import flags
3230
from ...common.utils import is_threadless
3331
from ...common.logger import Logger
32+
from ...common.backports import NonBlockingQueue
3433
from ...common.constants import DEFAULT_LOCAL_EXECUTOR
3534

3635
from ..event import EventQueue
@@ -103,7 +102,7 @@ def __init__(
103102
self.sock: Optional[socket.socket] = None
104103
# Internals
105104
self._total: Optional[int] = None
106-
self._local_work_queue: Optional['queue.Queue[Any]'] = None
105+
self._local_work_queue: Optional['NonBlockingQueue'] = None
107106
self._local: Optional[LocalExecutor] = None
108107
self._lthread: Optional[threading.Thread] = None
109108

@@ -118,7 +117,7 @@ def accept(self, events: List[Tuple[selectors.SelectorKey, int]]) -> None:
118117
work = (conn, addr or None)
119118
if self.flags.local_executor:
120119
assert self._local_work_queue
121-
self._local_work_queue.put_nowait(work)
120+
self._local_work_queue.put(work)
122121
else:
123122
self._work(*work)
124123

@@ -171,7 +170,7 @@ def run(self) -> None:
171170

172171
def _start_local(self) -> None:
173172
assert self.sock
174-
self._local_work_queue = queue.Queue()
173+
self._local_work_queue = NonBlockingQueue()
175174
self._local = LocalExecutor(
176175
work_queue=self._local_work_queue,
177176
flags=self.flags,

proxy/core/acceptor/listener.py

+2
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,15 @@ def shutdown(self) -> None:
9999
def _listen_unix_socket(self) -> None:
100100
self._socket = socket.socket(self.flags.family, socket.SOCK_STREAM)
101101
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
102+
self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
102103
self._socket.bind(self.flags.unix_socket_path)
103104
self._socket.listen(self.flags.backlog)
104105
self._socket.setblocking(False)
105106

106107
def _listen_server_port(self) -> None:
107108
self._socket = socket.socket(self.flags.family, socket.SOCK_STREAM)
108109
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
110+
self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
109111
self._socket.bind((str(self.flags.hostname), self.flags.port))
110112
self._socket.listen(self.flags.backlog)
111113
self._socket.setblocking(False)

proxy/core/acceptor/local.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@
1919
import contextlib
2020

2121
from typing import Optional
22-
from typing import Any # noqa: W0611 pylint: disable=unused-import
22+
from typing import Any
23+
24+
from ...common.backports import NonBlockingQueue # noqa: W0611, F401 pylint: disable=unused-import
2325

2426
from .threadless import Threadless
2527

2628
logger = logging.getLogger(__name__)
2729

2830

29-
class LocalExecutor(Threadless['queue.Queue[Any]']):
31+
class LocalExecutor(Threadless['NonBlockingQueue']):
3032
"""A threadless executor implementation which uses a queue to receive new work."""
3133

3234
def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -44,7 +46,7 @@ def work_queue_fileno(self) -> Optional[int]:
4446

4547
def receive_from_work_queue(self) -> bool:
4648
with contextlib.suppress(queue.Empty):
47-
work = self.work_queue.get(block=False)
49+
work = self.work_queue.get()
4850
if isinstance(work, bool) and work is False:
4951
return True
5052
assert isinstance(work, tuple)

0 commit comments

Comments
 (0)