Skip to content

Commit

Permalink
Improve filtering by using per event bitmasks
Browse files Browse the repository at this point in the history
  • Loading branch information
davfsa committed Mar 22, 2022
1 parent c224ee2 commit f615a54
Show file tree
Hide file tree
Showing 12 changed files with 1,490 additions and 1,041 deletions.
1 change: 1 addition & 0 deletions changes/idk.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`EventManager.get_listeners` now correctly defines polymorphic and returns accordingly.
1 change: 1 addition & 0 deletions changes/idk.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Optimize event dispatching by only deserializing events when they are needed.
199 changes: 150 additions & 49 deletions hikari/api/entity_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from hikari import invites as invite_models
from hikari import messages as message_models
from hikari import presences as presence_models
from hikari import scheduled_events as scheduled_events_models
from hikari import sessions as gateway_models
from hikari import snowflakes
from hikari import stickers as sticker_models
Expand Down Expand Up @@ -958,6 +959,72 @@ def deserialize_gateway_guild(self, payload: data_binding.JSONObject) -> Gateway
# INTERACTION MODELS #
######################

@abc.abstractmethod
def deserialize_slash_command(
self,
payload: data_binding.JSONObject,
*,
guild_id: undefined.UndefinedNoneOr[snowflakes.Snowflake] = undefined.UNDEFINED,
) -> commands.SlashCommand:
"""Parse a raw payload from Discord into a slash command object.
Parameters
----------
payload : hikari.internal.data_binding.JSONObject
The JSON payload to deserialize.
Other Parameters
----------------
guild_id : hikari.undefined.UndefinedNoneOr[hikari.snowflakes.Snowflake]
The ID of the guild this command belongs to. If this is specified
then this will be prioritised over `"guild_id"` in the payload.
Returns
-------
hikari.commands.SlashCommand
The deserialized slash command object.
Raises
------
builtins.KeyError
If `guild_id` is left as `hikari.undefined.UNDEFINED` when
`"guild_id"` is not present in the passed payload for the payload of
the integration.
"""

@abc.abstractmethod
def deserialize_context_menu_command(
self,
payload: data_binding.JSONObject,
*,
guild_id: undefined.UndefinedNoneOr[snowflakes.Snowflake] = undefined.UNDEFINED,
) -> commands.ContextMenuCommand:
"""Parse a raw payload from Discord into a context menu command object.
Parameters
----------
payload : hikari.internal.data_binding.JSONObject
The JSON payload to deserialize.
Other Parameters
----------------
guild_id : hikari.undefined.UndefinedNoneOr[hikari.snowflakes.Snowflake]
The ID of the guild this command belongs to. If this is specified
then this will be prioritised over `"guild_id"` in the payload.
Returns
-------
hikari.commands.ContextMenuCommand
The deserialized context menu command object.
Raises
------
builtins.KeyError
If `guild_id` is left as `hikari.undefined.UNDEFINED` when
`"guild_id"` is not present in the passed payload for the payload of
the integration.
"""

@abc.abstractmethod
def deserialize_command(
self,
Expand Down Expand Up @@ -1230,8 +1297,8 @@ def deserialize_guild_sticker(self, payload: data_binding.JSONObject) -> sticker
##################

@abc.abstractmethod
def deserialize_action_row(self, payload: data_binding.JSONObject) -> message_models.ActionRowComponent:
"""Parse a raw payload from Discord into an action row component object.
def deserialize_partial_message(self, payload: data_binding.JSONObject) -> message_models.PartialMessage:
"""Parse a raw payload from Discord into a partial message object.
Parameters
----------
Expand All @@ -1240,13 +1307,13 @@ def deserialize_action_row(self, payload: data_binding.JSONObject) -> message_mo
Returns
-------
hikari.messages.ActionRowComponent
The deserialized action row component.
hikari.messages.PartialMessage
The deserialized partial message object.
"""

@abc.abstractmethod
def deserialize_button(self, payload: data_binding.JSONObject) -> message_models.ButtonComponent:
"""Parse a raw payload from Discord into a button component object.
def deserialize_message(self, payload: data_binding.JSONObject) -> message_models.Message:
"""Parse a raw payload from Discord into a message object.
Parameters
----------
Expand All @@ -1255,28 +1322,63 @@ def deserialize_button(self, payload: data_binding.JSONObject) -> message_models
Returns
-------
hikari.messages.ButtonComponent
The deserialized button component.
hikari.messages.Message
The deserialized message object.
"""

###################
# PRESENCE MODELS #
###################

@abc.abstractmethod
def deserialize_select_menu(self, payload: data_binding.JSONObject) -> message_models.SelectMenuComponent:
"""Parse a raw payload from Discord into a select menu component object.
def deserialize_member_presence(
self,
payload: data_binding.JSONObject,
*,
guild_id: undefined.UndefinedOr[snowflakes.Snowflake] = undefined.UNDEFINED,
) -> presence_models.MemberPresence:
"""Parse a raw payload from Discord into a member presence object.
Parameters
----------
payload : hikari.internal.data_binding.JSONObject
The JSON payload to deserialize.
Other Parameters
----------------
guild_id : hikari.undefined.UndefinedOr[hikari.snowflakes.Snowflake]
The ID of the guild the presence belongs to. If this is specified
then it is prioritised over `guild_id` in the payload.
!!! note
At the time of writing, the only place where `guild_id` will be
mandatory is when parsing presences sent in a `GUILD_CREATE` event
from Discord, since the `guild_id` attribute in the payload will
have been omitted for redundancy.
Returns
-------
hikari.messages.SelectMenuComponent
The deserialized button component.
hikari.presences.MemberPresence
The deserialized member presence object.
Raises
------
builtins.KeyError
If `guild_id` is not an attribute of the `payload` dict, and no
guild ID was passed for the `guild_id` parameter.
If this is raised, no guild ID info was provided anywhere.
"""

##########################
# SCHEDULED EVENT MODELS #
##########################

@abc.abstractmethod
def deserialize_component(self, payload: data_binding.JSONObject) -> message_models.PartialComponent:
"""Parse a raw payload from Discord into a message component object.
def deserialize_scheduled_external_event(
self, payload: data_binding.JSONObject
) -> scheduled_events_models.ScheduledExternalEvent:
"""Parse a raw payload from Discord into a scheduled external event object.
Parameters
----------
Expand All @@ -1285,15 +1387,15 @@ def deserialize_component(self, payload: data_binding.JSONObject) -> message_mod
Returns
-------
hikari.messages.PartialComponent
The deserialized message component.
hikari.errors.UnrecognisedEntityError
If the message component type isn't recognised.
hikari.scheduled_events.ScheduledExternalEvent
The deserialized scheduled external event object.
"""

@abc.abstractmethod
def deserialize_partial_message(self, payload: data_binding.JSONObject) -> message_models.PartialMessage:
"""Parse a raw payload from Discord into a partial message object.
def deserialize_scheduled_stage_event(
self, payload: data_binding.JSONObject
) -> scheduled_events_models.ScheduledStageEvent:
"""Parse a raw payload from Discord into a scheduled stage event object.
Parameters
----------
Expand All @@ -1302,13 +1404,15 @@ def deserialize_partial_message(self, payload: data_binding.JSONObject) -> messa
Returns
-------
hikari.messages.PartialMessage
The deserialized partial message object.
hikari.scheduled_events.ScheduledStageEvent
The deserialized scheduled stage event object.
"""

@abc.abstractmethod
def deserialize_message(self, payload: data_binding.JSONObject) -> message_models.Message:
"""Parse a raw payload from Discord into a message object.
def deserialize_scheduled_voice_event(
self, payload: data_binding.JSONObject
) -> scheduled_events_models.ScheduledVoiceEvent:
"""Parse a raw payload from Discord into a scheduled voice event object.
Parameters
----------
Expand All @@ -1317,22 +1421,33 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model
Returns
-------
hikari.messages.Message
The deserialized message object.
hikari.scheduled_events.ScheduledVoiceEvent
The deserialized scheduled voice event object.
"""

###################
# PRESENCE MODELS #
###################
@abc.abstractmethod
def deserialize_scheduled_event(self, payload: data_binding.JSONObject) -> scheduled_events_models.ScheduledEvent:
"""Parse a raw payload from Discord into a scheduled event object.
Parameters
----------
payload : hikari.internal.data_binding.JSONObject
The JSON payload to deserialize.
Returns
-------
hikari.scheduled_events.ScheduledEvent
The deserialized scheduled event object.
"""

@abc.abstractmethod
def deserialize_member_presence(
def deserialize_scheduled_event_user(
self,
payload: data_binding.JSONObject,
*,
guild_id: undefined.UndefinedOr[snowflakes.Snowflake] = undefined.UNDEFINED,
) -> presence_models.MemberPresence:
"""Parse a raw payload from Discord into a member presence object.
) -> scheduled_events_models.ScheduledEventUser:
"""Parse a raw payload from Discord into a scheduled event user object.
Parameters
----------
Expand All @@ -1342,27 +1457,13 @@ def deserialize_member_presence(
Other Parameters
----------------
guild_id : hikari.undefined.UndefinedOr[hikari.snowflakes.Snowflake]
The ID of the guild the presence belongs to. If this is specified
The ID of the guild the user belongs to. If this is specified
then it is prioritised over `guild_id` in the payload.
!!! note
At the time of writing, the only place where `guild_id` will be
mandatory is when parsing presences sent in a `GUILD_CREATE` event
from Discord, since the `guild_id` attribute in the payload will
have been omitted for redundancy.
Returns
-------
hikari.presences.MemberPresence
The deserialized member presence object.
Raises
------
builtins.KeyError
If `guild_id` is not an attribute of the `payload` dict, and no
guild ID was passed for the `guild_id` parameter.
If this is raised, no guild ID info was provided anywhere.
hikari.scheduled_events.ScheduledEventUser
The deserialized scheduled event user object.
"""

###################
Expand Down
4 changes: 2 additions & 2 deletions hikari/api/event_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,8 @@ def get_listeners(
The event type to look for.
`T` must be a subclass of `hikari.events.base_events.Event`.
polymorphic : builtins.bool
If `builtins.True`, this will also return the listeners of the
subclasses of the given event type. If `builtins.False`, then
If `builtins.True`, this will also return the listeners for all the
event types `event_type` will dispatch. If `builtins.False`, then
only listeners for this class specifically are returned. The
default is `builtins.True`.
Expand Down
15 changes: 14 additions & 1 deletion hikari/events/base_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,16 @@
REQUIRED_INTENTS_ATTR: typing.Final[str] = "___requiresintents___"
NO_RECURSIVE_THROW_ATTR: typing.Final[str] = "___norecursivethrow___"

_id_counter = 1 # We start at 1 since Event is 0


class Event(abc.ABC):
"""Base event type that all Hikari events should subclass."""

__slots__: typing.Sequence[str] = ()

__dispatches: typing.ClassVar[typing.Tuple[typing.Type[Event], ...]]
__bitmask: typing.ClassVar[int]

def __init_subclass__(cls) -> None:
super().__init_subclass__()
Expand All @@ -68,11 +71,16 @@ def __init_subclass__(cls) -> None:
Event.__dispatches
except AttributeError:
Event.__dispatches = (Event,)
Event.__bitmask = 1 << 0

global _id_counter

mro = cls.mro()
# We don't have to explicitly include Event here as issubclass(Event, Event) returns True.
# Non-event classes should be ignored.
cls.__dispatches = tuple(cls for cls in mro if issubclass(cls, Event))
cls.__dispatches = tuple(sub_cls for sub_cls in mro if issubclass(sub_cls, Event))
cls.__bitmask = 1 << _id_counter
_id_counter += 1

@property
@abc.abstractmethod
Expand All @@ -90,6 +98,11 @@ def dispatches(cls) -> typing.Sequence[typing.Type[Event]]:
"""Sequence of the event classes this event is dispatched as."""
return cls.__dispatches

@classmethod
def bitmask(cls) -> int:
"""Bitmask for this event."""
return cls.__bitmask


def get_required_intents_for(event_type: typing.Type[Event]) -> typing.Collection[intents.Intents]:
"""Retrieve the intents that are required to listen to an event type.
Expand Down
Loading

0 comments on commit f615a54

Please sign in to comment.