Skip to content

Commit 8e34e76

Browse files
small performance improvements
1 parent 4ca6dde commit 8e34e76

7 files changed

Lines changed: 77 additions & 17 deletions

File tree

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,24 @@ by extending the base message class defined in the package.
1515

1616
More documentation can be found in the [github-pages](https://nasdaq.github.io/nasdaq-protocols/)
1717

18+
19+
Performance
20+
-----------
21+
22+
For improved throughput, install the optional performance dependencies:
23+
24+
```bash
25+
pip install nasdaq-protocols[performance]
26+
```
27+
28+
This installs [uvloop](https://github.com/MagicStack/uvloop) (Linux/macOS only), which provides ~4-5x faster event loop scheduling.
29+
30+
To activate it, call early in your application before any asyncio work:
31+
32+
```python
33+
from nasdaq_protocols.common import use_fast_loop
34+
use_fast_loop()
35+
```
36+
37+
If uvloop is not installed, `use_fast_loop()` is a no-op.
38+

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ dependencies = [
2323
]
2424
dynamic = ["version"]
2525

26+
[project.optional-dependencies]
27+
performance = ["uvloop>=0.19; sys_platform != 'win32'"]
28+
2629
[project.scripts]
2730
nasdaq-ouch-codegen = "nasdaq_protocols.ouch.codegen:generate"
2831
nasdaq-itch-codegen = "nasdaq_protocols.itch.codegen:generate"

src/nasdaq_protocols/common/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
from .session import *
55
from .message import *
66
from .sync_executor import *
7+
from ._loop import use_fast_loop
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Optional fast event loop integration."""
2+
3+
4+
def use_fast_loop():
5+
"""Install uvloop as the default event loop policy if available.
6+
7+
Call this early in your application entry point before creating any
8+
asyncio event loops. If uvloop is not installed, this is a no-op.
9+
10+
Install with: pip install nasdaq-protocols[performance]
11+
"""
12+
try:
13+
import uvloop
14+
uvloop.install()
15+
except ImportError:
16+
pass

src/nasdaq_protocols/common/session.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ class Reader(Stoppable):
109109
on_msg_coro: OnMsgCoro = attrs.field(validator=Validators.not_none())
110110
on_close_coro: OnCloseCoro = attrs.field(validator=Validators.not_none())
111111
_buffer: bytearray = attrs.field(init=False, factory=bytearray)
112+
_read_pos: int = attrs.field(init=False, default=0)
112113
_drain_buffer: bytearray | None = attrs.field(init=False, default=None)
113114
_task: asyncio.Task = attrs.field(init=False, default=None)
114115
_stopped: bool = attrs.field(init=False, default=False)
@@ -130,6 +131,10 @@ async def buffer_until_drained(self, discard_buffer: bool = False):
130131
finally:
131132
self._drain_mode = None
132133
if not discard_buffer:
134+
# Compact before extending with drain buffer
135+
if self._read_pos > 0:
136+
del self._buffer[:self._read_pos]
137+
self._read_pos = 0
133138
self._buffer.extend(self._drain_buffer)
134139
self._drain_buffer = None
135140

@@ -153,11 +158,11 @@ def is_stopped(self):
153158

154159
async def _process(self):
155160
while not self._stopped:
156-
len_before = len_after = len(self._buffer)
157-
if len_before > 0:
161+
available_before = available_after = len(self._buffer) - self._read_pos
162+
if available_before > 0:
158163
await self._process_1()
159-
len_after = len(self._buffer)
160-
if self._drain_mode and not self._drain_mode.is_set() and len_after == len_before:
164+
available_after = len(self._buffer) - self._read_pos
165+
if self._drain_mode and not self._drain_mode.is_set() and available_after == available_before:
161166
self._drain_mode.set()
162167

163168
await asyncio.sleep(0.0001)

src/nasdaq_protocols/fix/_reader.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,28 @@
1616
class FixMessageReader(common.Reader):
1717
def deserialize(self):
1818
empty_response = (None, False, False)
19-
if self._buffer.find(MSG_TYPE_TAG) != -1:
20-
start = self._buffer.find(b'=', SKIP_FIRST_EQ_POS)
19+
view = bytes(self._buffer[self._read_pos:])
20+
21+
if view.find(MSG_TYPE_TAG) != -1:
22+
start = view.find(b'=', SKIP_FIRST_EQ_POS)
2123
if start == -1:
2224
return empty_response
23-
end = self._buffer.find(SOH, start)
25+
end = view.find(SOH, start)
2426
if end == -1:
2527
return empty_response
26-
body_length = int(self._buffer[start+1:end])
28+
body_length = int(view[start+1:end])
2729
msg_len = calc_msg_len(end+1, body_length)
28-
if len(self._buffer) < msg_len:
30+
if len(view) < msg_len:
2931
return empty_response
3032

31-
_len, msg = Message.from_bytes(self._buffer[:msg_len])
32-
self._buffer = self._buffer[msg_len:]
33+
_len, msg = Message.from_bytes(view[:msg_len])
34+
self._read_pos += msg_len
35+
36+
# Compact when more than half the buffer is consumed
37+
if self._read_pos > len(self._buffer) // 2:
38+
del self._buffer[:self._read_pos]
39+
self._read_pos = 0
40+
3341
return msg, msg.is_logout(), msg.is_heartbeat()
3442
return empty_response
3543

src/nasdaq_protocols/soup/_reader.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,22 @@
1010
class SoupMessageReader(common.Reader):
1111
def deserialize(self) -> Any:
1212
empty_response = (None, False, False)
13-
buff_len = len(self._buffer)
13+
available = len(self._buffer) - self._read_pos
1414

15-
if buff_len < 2:
15+
if available < 2:
1616
return empty_response
1717

18-
siz = int.from_bytes(self._buffer[:2], 'big')
19-
if (siz+2) > buff_len:
18+
siz = int.from_bytes(self._buffer[self._read_pos:self._read_pos + 2], 'big')
19+
if (siz + 2) > available:
2020
return empty_response
2121

22-
_, msg = SoupMessage.from_bytes(self._buffer[:siz + 2])
23-
self._buffer = self._buffer[siz + 2:]
22+
frame = bytes(memoryview(self._buffer)[self._read_pos:self._read_pos + siz + 2])
23+
_, msg = SoupMessage.from_bytes(frame)
24+
self._read_pos += siz + 2
25+
26+
# Compact when more than half the buffer is consumed
27+
if self._read_pos > len(self._buffer) // 2:
28+
del self._buffer[:self._read_pos]
29+
self._read_pos = 0
2430

2531
return msg, msg.is_logout(), msg.is_heartbeat()

0 commit comments

Comments
 (0)