Compare commits

..

2 Commits

Author SHA1 Message Date
1f98e6627d add __slots__ to most classes 2023-10-02 09:18:32 +02:00
b8d9c6f2a1 fix json stream decode performance issue
string slicing is expensive as python copies the (immutable) string...
2023-10-02 09:18:25 +02:00

64
pqm
View File

@ -33,7 +33,7 @@ import yaml
T = typing.TypeVar('T') T = typing.TypeVar('T')
@dataclasses.dataclass @dataclasses.dataclass(slots=True)
class Config: class Config:
# if not remote_sources are configured -> use local queue # if not remote_sources are configured -> use local queue
remote_sources: dict[str, str] = dataclasses.field(default_factory=dict) remote_sources: dict[str, str] = dataclasses.field(default_factory=dict)
@ -157,7 +157,7 @@ async def trio_parallel_ordered(
await tpo.close() await tpo.close()
@dataclasses.dataclass @dataclasses.dataclass(slots=True)
class Recipient: class Recipient:
"""Recipient in a postfix mail""" """Recipient in a postfix mail"""
address: str address: str
@ -184,7 +184,22 @@ class QueueName(enum.Enum):
ALL_QUEUE_NAMES: set[QueueName] = set(QueueName) ALL_QUEUE_NAMES: set[QueueName] = set(QueueName)
@dataclasses.dataclass def json_decode_stream(data: str):
decoder = json.JSONDecoder()
data_len = len(data)
data_pos = 0
while data_len > data_pos and data[data_len-1].isspace():
data_len -= 1
while True:
while data_pos < data_len and data[data_pos].isspace():
data_pos += 1
if data_pos >= data_len:
return
obj, data_pos = decoder.raw_decode(data, data_pos)
yield obj
@dataclasses.dataclass(slots=True)
class Mail: class Mail:
"""Metadata for mail in postfix queue""" """Metadata for mail in postfix queue"""
queue_name: QueueName queue_name: QueueName
@ -209,11 +224,7 @@ class Mail:
@staticmethod @staticmethod
def read_postqueue_json(data: str, id_prefix: str = '') -> list[Mail]: def read_postqueue_json(data: str, id_prefix: str = '') -> list[Mail]:
queue = [] queue = []
decoder = json.JSONDecoder() for obj in json_decode_stream(data):
data = data.strip()
while data:
obj, end = decoder.raw_decode(data, 0)
data = data[end:].lstrip()
mail = Mail.from_json(obj) mail = Mail.from_json(obj)
mail.queue_id = id_prefix + mail.queue_id mail.queue_id = id_prefix + mail.queue_id
queue.append(mail) queue.append(mail)
@ -222,6 +233,8 @@ class Mail:
# abstract collection/cluster of postfix nodes (or just a single one) # abstract collection/cluster of postfix nodes (or just a single one)
class Source(abc.ABC): class Source(abc.ABC):
__slots__ = ()
# list of server names in collection # list of server names in collection
@abc.abstractmethod @abc.abstractmethod
def server_list(self) -> list[str]: def server_list(self) -> list[str]:
@ -320,6 +333,8 @@ class Source(abc.ABC):
class LocalSource(Source): class LocalSource(Source):
__slots__ = ()
def server_list(self) -> list[str]: def server_list(self) -> list[str]:
return [socket.gethostname()] return [socket.gethostname()]
@ -377,6 +392,8 @@ class LocalSource(Source):
class RemoteSource(Source): class RemoteSource(Source):
__slots__ = ('remotes',)
def __init__(self, remotes: dict[str, str]) -> None: def __init__(self, remotes: dict[str, str]) -> None:
super().__init__() super().__init__()
self.remotes = remotes self.remotes = remotes
@ -509,6 +526,8 @@ class RemoteSource(Source):
class UserActivityStats: class UserActivityStats:
__slots__ = ('count_mails', 'related_mails', 'cut_off')
# find most active address for `health` command # find most active address for `health` command
def __init__(self, cut_off: int = 10) -> None: def __init__(self, cut_off: int = 10) -> None:
@ -577,6 +596,8 @@ class UserActivityStats:
class Filter(abc.ABC): class Filter(abc.ABC):
"""abstract base class for filter expressions""" """abstract base class for filter expressions"""
__slots__ = ()
def __init__(self) -> None: def __init__(self) -> None:
pass pass
@ -639,11 +660,14 @@ class Filter(abc.ABC):
# abstract base class for filters that don't need (...) around in representations of members in combined filters # abstract base class for filters that don't need (...) around in representations of members in combined filters
class SingleExprFilter(Filter): class SingleExprFilter(Filter):
pass __slots__ = ()
class FalseFilter(SingleExprFilter): class FalseFilter(SingleExprFilter):
"""constant false - matches no mail""" """constant false - matches no mail"""
__slots__ = ()
def __repr__(self) -> str: def __repr__(self) -> str:
return '0' return '0'
@ -669,6 +693,9 @@ class FalseFilter(SingleExprFilter):
class TrueFilter(SingleExprFilter): class TrueFilter(SingleExprFilter):
"""constant true - matches all mail""" """constant true - matches all mail"""
__slots__ = ()
def __repr__(self) -> str: def __repr__(self) -> str:
return '1' return '1'
@ -697,6 +724,8 @@ TRUE_FILTER = TrueFilter()
class AndFilter(Filter): class AndFilter(Filter):
__slots__ = ('expressions',)
def __init__(self, expressions: list[Filter] | tuple[()] = ()) -> None: def __init__(self, expressions: list[Filter] | tuple[()] = ()) -> None:
super().__init__() super().__init__()
self.expressions: list[Filter] = expressions or [] self.expressions: list[Filter] = expressions or []
@ -750,6 +779,8 @@ class AndFilter(Filter):
class OrFilter(Filter): class OrFilter(Filter):
__slots__ = ('expressions',)
def __init__(self, expressions: list[Filter] | tuple[()] = ()) -> None: def __init__(self, expressions: list[Filter] | tuple[()] = ()) -> None:
super().__init__() super().__init__()
self.expressions: list[Filter] = expressions or [] self.expressions: list[Filter] = expressions or []
@ -803,6 +834,8 @@ class OrFilter(Filter):
class NotFilter(SingleExprFilter): class NotFilter(SingleExprFilter):
__slots__ = ('expression',)
def __init__(self, expression: Filter) -> None: def __init__(self, expression: Filter) -> None:
super().__init__() super().__init__()
self.expression = expression self.expression = expression
@ -840,6 +873,9 @@ class NotFilter(SingleExprFilter):
class QueueFilter(SingleExprFilter): class QueueFilter(SingleExprFilter):
"""match mails based on queue they are in""" """match mails based on queue they are in"""
__slots__ = ('select',)
def __init__(self, select: list[QueueName]) -> None: def __init__(self, select: list[QueueName]) -> None:
self.select = set(select) self.select = set(select)
@ -898,6 +934,8 @@ class AddressSelector(enum.Enum):
class BaseAddressPattern(abc.ABC): class BaseAddressPattern(abc.ABC):
__slots__ = ()
# abstract base class of patterns to use to match a mail address # abstract base class of patterns to use to match a mail address
# subclasses match either: full address, domain part, regex # subclasses match either: full address, domain part, regex
def __init__(self) -> None: def __init__(self) -> None:
@ -930,6 +968,8 @@ class BaseAddressPattern(abc.ABC):
class AddressPattern(BaseAddressPattern): class AddressPattern(BaseAddressPattern):
__slots__ = ('address',)
# match full address exactly # match full address exactly
def __init__(self, address: str) -> None: def __init__(self, address: str) -> None:
super().__init__() super().__init__()
@ -952,6 +992,8 @@ class AddressPattern(BaseAddressPattern):
class AddressDomainPattern(BaseAddressPattern): class AddressDomainPattern(BaseAddressPattern):
__slots__ = ('domain',)
# match address by domain # match address by domain
def __init__(self, domain: str) -> None: def __init__(self, domain: str) -> None:
super().__init__() super().__init__()
@ -969,6 +1011,8 @@ class AddressDomainPattern(BaseAddressPattern):
class AddressRegexMatch(BaseAddressPattern): class AddressRegexMatch(BaseAddressPattern):
__slots__ = ('address',)
# match address by regex # match address by regex
def __init__(self, address: str | re.Pattern[str]) -> None: def __init__(self, address: str | re.Pattern[str]) -> None:
super().__init__() super().__init__()
@ -990,6 +1034,8 @@ class AddressRegexMatch(BaseAddressPattern):
class AddressFilter(SingleExprFilter): class AddressFilter(SingleExprFilter):
__slots__ = ('selector', 'patterns')
# match mails by address # match mails by address
def __init__(self, selector: AddressSelector, patterns: list[BaseAddressPattern]) -> None: def __init__(self, selector: AddressSelector, patterns: list[BaseAddressPattern]) -> None:
self.selector = selector self.selector = selector