Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0283402

Browse files
davfsaFasterSpeeding
andcommittedMar 25, 2022
Improve filtering by using per event bitmasks (#903)
* Start filtering events before unmarshalling (#636) * Switch to iterating over mro during listener/waiter registration (rather than during event dispatch) * Add in a system to avoid unmarshalling data for events which aren't being used * Add settings property to cache interface to allow for introspection * Also add "ME" resource to cache config * Specialise guild create and update handlers to avoid unmarshalling data which isn't being cached when no listeners are registered for the event * For this to work gateway guild definition handling had to be refactored to switch to explicitly specifying which mappings it should include when calling it * Logic fixes around event checks * Register listeners by subclasses not parents (mro) (For this the subclasses need to be cached on the Event classes) * Add voodoo on new event cls callback to Event class * This is meant to be a mock way to handle the edge case of new subclassing Event types being added after the event manage has been initialised which might be unorthodox but probably has some wack use case * Switch over to mro based approach * Switch over to mro based approach * Cache whether a consumer can be dispatched or not * Slight logic cleanup * Prefer internal granularity on guild create and update methods * rename event_manager_base.as_listener to "filtered" and remove from on_guild_create and update * Also clear the dispatches for cache when waiters are depleted * Only deserialize guild object on guild create and update if necessary * Add check to shard payload dispatch and refactor consumer check logic * Internal refactors and naming scheme changes * Plus fix CacheImpl.update_me not copying the stored member entry before returning it * Add internal _FilteredMethod proto to event manager base * Move filtering to _handle_dispatch * Add internal _FilteredMethod proto to event manager base * Move filtering to _handle_dispatch * Add trace logging calls to on_guild_create and on_guild_update * Small logic fix + add code/logic comments and docs * As an artifact of this addition, on_guild_integrations_update acn raise NotImplementedError now since it should always be skipped * Some test fixes * cache_components shouldn't ever be undefined if event_types isn't * Try the builder pattern for GatewayGuildDefinition * Switch GatewayGuildDefinition to using getter style methods for delaying deserialization * test fixes and additions * bug fixes + tests * Post-rebase fixes * Have EventManagerBase take components rather than the cache settings * remove _dispatches_for_cache + add in missing filtered decorator calls * Post-rebase fix * post-rebase fixes * Change i forgot to commit * formatting fixes * Mypy and flake8 fixes * Improve filtering by using per event bitmasks Co-authored-by: Lucina <luke@lmbyrne.dev>
1 parent b6a540b commit 0283402

File tree

11 files changed

+792
-991
lines changed

11 files changed

+792
-991
lines changed
 

‎changes/idk.bugfix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`EventManager.get_listeners` now correctly defines polymorphic and returns accordingly.

‎changes/idk.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Optimize event dispatching by only deserializing events when they are needed.

‎hikari/api/event_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,8 @@ def get_listeners(
372372
The event type to look for.
373373
`T` must be a subclass of `hikari.events.base_events.Event`.
374374
polymorphic : builtins.bool
375-
If `builtins.True`, this will also return the listeners of the
376-
subclasses of the given event type. If `builtins.False`, then
375+
If `builtins.True`, this will also return the listeners for all the
376+
event types `event_type` will dispatch. If `builtins.False`, then
377377
only listeners for this class specifically are returned. The
378378
default is `builtins.True`.
379379

‎hikari/events/base_events.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,16 @@
5151
REQUIRED_INTENTS_ATTR: typing.Final[str] = "___requiresintents___"
5252
NO_RECURSIVE_THROW_ATTR: typing.Final[str] = "___norecursivethrow___"
5353

54+
_id_counter = 1 # We start at 1 since Event is 0
55+
5456

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

5860
__slots__: typing.Sequence[str] = ()
5961

6062
__dispatches: typing.ClassVar[typing.Tuple[typing.Type[Event], ...]]
63+
__bitmask: typing.ClassVar[int]
6164

6265
def __init_subclass__(cls) -> None:
6366
super().__init_subclass__()
@@ -68,11 +71,16 @@ def __init_subclass__(cls) -> None:
6871
Event.__dispatches
6972
except AttributeError:
7073
Event.__dispatches = (Event,)
74+
Event.__bitmask = 1 << 0
75+
76+
global _id_counter
7177

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

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

101+
@classmethod
102+
def bitmask(cls) -> int:
103+
"""Bitmask for this event."""
104+
return cls.__bitmask
105+
93106

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

‎hikari/impl/entity_factory.py

Lines changed: 53 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
if typing.TYPE_CHECKING:
6969
ValueT = typing.TypeVar("ValueT")
7070
EntityT = typing.TypeVar("EntityT")
71-
UndefinedNoneSnowflakeMapping = undefined.UndefinedNoneOr[typing.Mapping[snowflakes.Snowflake, EntityT]]
71+
UndefinedSnowflakeMapping = undefined.UndefinedOr[typing.Mapping[snowflakes.Snowflake, EntityT]]
7272

7373

7474
_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.entity_factory")
@@ -190,47 +190,42 @@ class _UserFields:
190190
@attr_extensions.with_copy
191191
@attr.define(weakref_slot=False)
192192
class _GatewayGuildDefinition(entity_factory.GatewayGuildDefinition):
193-
"""A structure for handling entities within guild create and update events."""
194-
195193
id: snowflakes.Snowflake = attr.field()
196194
_payload: data_binding.JSONObject = attr.field()
197195
_entity_factory: EntityFactoryImpl = attr.field()
198196
# These will get deserialized as needed
199-
_channels: UndefinedNoneSnowflakeMapping[channel_models.GuildChannel] = attr.field(
197+
_channels: UndefinedSnowflakeMapping[channel_models.GuildChannel] = attr.field(
200198
init=False, default=undefined.UNDEFINED
201199
)
202200
_guild: undefined.UndefinedOr[guild_models.GatewayGuild] = attr.field(init=False, default=undefined.UNDEFINED)
203-
_emojis: undefined.UndefinedOr[typing.Mapping[emoji_models.KnownCustomEmoji]] = attr.field(
204-
init=False, default=undefined.UNDEFINED
205-
)
206-
_members: UndefinedNoneSnowflakeMapping[guild_models.Member] = attr.field(init=False, default=undefined.UNDEFINED)
207-
_presences: UndefinedNoneSnowflakeMapping[presence_models.MemberPresence] = attr.field(
201+
_emojis: UndefinedSnowflakeMapping[emoji_models.KnownCustomEmoji] = attr.field(
208202
init=False, default=undefined.UNDEFINED
209203
)
210-
_roles: undefined.UndefinedOr[typing.Mapping[snowflakes.Snowflake, guild_models.Role]] = attr.field(
204+
_members: UndefinedSnowflakeMapping[guild_models.Member] = attr.field(init=False, default=undefined.UNDEFINED)
205+
_presences: UndefinedSnowflakeMapping[presence_models.MemberPresence] = attr.field(
211206
init=False, default=undefined.UNDEFINED
212207
)
213-
_voice_states: UndefinedNoneSnowflakeMapping[voice_models.VoiceState] = attr.field(
208+
_roles: UndefinedSnowflakeMapping[guild_models.Role] = attr.field(init=False, default=undefined.UNDEFINED)
209+
_voice_states: UndefinedSnowflakeMapping[voice_models.VoiceState] = attr.field(
214210
init=False, default=undefined.UNDEFINED
215211
)
216212

217-
def channels(self) -> typing.Optional[typing.Mapping[snowflakes.Snowflake, channel_models.GuildChannel]]:
213+
def channels(self) -> typing.Mapping[snowflakes.Snowflake, channel_models.GuildChannel]:
218214
if self._channels is undefined.UNDEFINED:
219-
if "channels" in self._payload:
220-
self._channels = {}
215+
if "channels" not in self._payload:
216+
raise LookupError("'channels' not in payload")
221217

222-
for channel_payload in self._payload["channels"]:
223-
try:
224-
channel = self._entity_factory.deserialize_channel(channel_payload, guild_id=self.id)
225-
except errors.UnrecognisedEntityError:
226-
# Ignore the channel, this has already been logged
227-
continue
218+
self._channels = {}
228219

229-
assert isinstance(channel, channel_models.GuildChannel)
230-
self._channels[channel.id] = channel
220+
for channel_payload in self._payload["channels"]:
221+
try:
222+
channel = self._entity_factory.deserialize_channel(channel_payload, guild_id=self.id)
223+
except errors.UnrecognisedEntityError:
224+
# Ignore the channel, this has already been logged
225+
continue
231226

232-
else:
233-
self._channels = None
227+
assert isinstance(channel, channel_models.GuildChannel)
228+
self._channels[channel.id] = channel
234229

235230
return self._channels
236231

@@ -286,66 +281,55 @@ def guild(self) -> guild_models.GatewayGuild:
286281

287282
return self._guild
288283

289-
def members(self) -> typing.Optional[typing.Mapping[snowflakes.Snowflake, guild_models.Member]]:
284+
def members(self) -> typing.Mapping[snowflakes.Snowflake, guild_models.Member]:
290285
if self._members is undefined.UNDEFINED:
291-
if "members" in self._payload:
292-
self._members = {
293-
snowflakes.Snowflake(m["user"]["id"]): self._entity_factory.deserialize_member(m, guild_id=self.id)
294-
for m in self._payload["members"]
295-
}
296-
297-
for member_payload in self._payload["members"]:
298-
member = self._entity_factory.deserialize_member(member_payload, guild_id=self.id)
299-
self._members[member.user.id] = member
300-
else:
301-
self._members = None
286+
if "members" not in self._payload:
287+
raise LookupError("'members' not in payload")
288+
289+
self._members = {
290+
snowflakes.Snowflake(m["user"]["id"]): self._entity_factory.deserialize_member(m, guild_id=self.id)
291+
for m in self._payload["members"]
292+
}
302293

303294
return self._members
304295

305-
def presences(self) -> typing.Optional[typing.Mapping[snowflakes.Snowflake, presence_models.MemberPresence]]:
296+
def presences(self) -> typing.Mapping[snowflakes.Snowflake, presence_models.MemberPresence]:
306297
if self._presences is undefined.UNDEFINED:
307-
if "presences" in self._payload:
308-
self._presences = {
309-
snowflakes.Snowflake(p["user"]["id"]): self._entity_factory.deserialize_member_presence(
310-
p, guild_id=self.id
311-
)
312-
for p in self._payload["presences"]
313-
}
298+
if "presences" not in self._payload:
299+
raise LookupError("'presences' not in payload")
314300

315-
for presence_payload in self._payload["presences"]:
316-
presence = self._entity_factory.deserialize_member_presence(presence_payload, guild_id=self.id)
317-
self._presences[presence.user_id] = presence
318-
else:
319-
self._presences = None
301+
self._presences = {
302+
snowflakes.Snowflake(p["user"]["id"]): self._entity_factory.deserialize_member_presence(
303+
p, guild_id=self.id
304+
)
305+
for p in self._payload["presences"]
306+
}
320307

321308
return self._presences
322309

323310
def roles(self) -> typing.Mapping[snowflakes.Snowflake, guild_models.Role]:
324-
if self._roles is None:
325-
if self._roles is undefined.UNDEFINED:
326-
self._roles = {
327-
snowflakes.Snowflake(r["id"]): self._entity_factory.deserialize_role(r, guild_id=self.id)
328-
for r in self._payload["roles"]
329-
}
311+
if self._roles is undefined.UNDEFINED:
312+
self._roles = {
313+
snowflakes.Snowflake(r["id"]): self._entity_factory.deserialize_role(r, guild_id=self.id)
314+
for r in self._payload["roles"]
315+
}
330316

331317
return self._roles
332318

333-
def voice_states(self) -> typing.Optional[typing.Mapping[snowflakes.Snowflake, voice_models.VoiceState]]:
319+
def voice_states(self) -> typing.Mapping[snowflakes.Snowflake, voice_models.VoiceState]:
334320
if self._voice_states is undefined.UNDEFINED:
335-
if "voice_states" in self._payload:
336-
members = self.members()
337-
assert members is not None, "voice_states present but not members?"
338-
self._voice_states = {}
339-
340-
for voice_state_payload in self._payload["voice_states"]:
341-
member = members[snowflakes.Snowflake(voice_state_payload["user_id"])]
342-
voice_state = self._entity_factory.deserialize_voice_state(
343-
voice_state_payload, guild_id=self.id, member=member
344-
)
345-
self._voice_states[voice_state.user_id] = voice_state
321+
if "voice_states" not in self._payload:
322+
raise LookupError("'voice_states' not in payload")
346323

347-
else:
348-
self._voice_states = None
324+
members = self.members()
325+
self._voice_states = {}
326+
327+
for voice_state_payload in self._payload["voice_states"]:
328+
member = members[snowflakes.Snowflake(voice_state_payload["user_id"])]
329+
voice_state = self._entity_factory.deserialize_voice_state(
330+
voice_state_payload, guild_id=self.id, member=member
331+
)
332+
self._voice_states[voice_state.user_id] = voice_state
349333

350334
return self._voice_states
351335

‎hikari/impl/event_manager.py

Lines changed: 52 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,33 @@ async def on_channel_pins_update(self, shard: gateway_shard.GatewayShard, payloa
162162
# TODO: we need a method for this specifically
163163
await self.dispatch(self._event_factory.deserialize_channel_pins_update_event(shard, payload))
164164

165-
# Internal granularity is preferred for GUILD_CREATE over decorator based filtering due to its large cache scope.
166-
async def on_guild_create(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
165+
# Internal granularity is preferred for GUILD_CREATE over decorator based filtering due to its large scope.
166+
async def on_guild_create( # noqa: C901 - Function too complex
167+
self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject
168+
) -> None:
167169
"""See https://discord.com/developers/docs/topics/gateway#guild-create for more info."""
168-
enabled_for_event = self._enabled_for_event(guild_events.GuildAvailableEvent)
169-
if not enabled_for_event and self._cache:
170+
event: typing.Union[guild_events.GuildAvailableEvent, guild_events.GuildJoinEvent, None]
171+
172+
if "unavailable" in payload and self._enabled_for_event(guild_events.GuildAvailableEvent):
173+
event = self._event_factory.deserialize_guild_available_event(shard, payload)
174+
elif "unavailable" not in payload and self._enabled_for_event(guild_events.GuildJoinEvent):
175+
event = self._event_factory.deserialize_guild_join_event(shard, payload)
176+
else:
177+
event = None
178+
179+
if event:
180+
# We also filter here to prevent iterating over them and calling a function that won't do anything
181+
channels = event.channels if self._cache_enabled_for(config.CacheComponents.GUILD_CHANNELS) else None
182+
emojis = event.emojis if self._cache_enabled_for(config.CacheComponents.EMOJIS) else None
183+
guild = event.guild if self._cache_enabled_for(config.CacheComponents.GUILDS) else None
184+
guild_id = event.guild.id
185+
members = event.members if self._cache_enabled_for(config.CacheComponents.MEMBERS) else None
186+
presences = event.presences if self._cache_enabled_for(config.CacheComponents.PRESENCES) else None
187+
roles = event.roles if self._cache_enabled_for(config.CacheComponents.ROLES) else None
188+
voice_states = event.voice_states if self._cache_enabled_for(config.CacheComponents.VOICE_STATES) else None
189+
190+
elif self._cache:
170191
_LOGGER.log(ux.TRACE, "Skipping on_guild_create dispatch due to lack of any registered listeners")
171-
event: typing.Union[guild_events.GuildAvailableEvent, guild_events.GuildJoinEvent, None] = None
172192
gd = self._entity_factory.deserialize_gateway_guild(payload)
173193

174194
channels = gd.channels() if self._cache_enabled_for(config.CacheComponents.GUILD_CHANNELS) else None
@@ -180,23 +200,11 @@ async def on_guild_create(self, shard: gateway_shard.GatewayShard, payload: data
180200
roles = gd.roles() if self._cache_enabled_for(config.CacheComponents.ROLES) else None
181201
voice_states = gd.voice_states() if self._cache_enabled_for(config.CacheComponents.VOICE_STATES) else None
182202

183-
elif enabled_for_event:
184-
if "unavailable" in payload:
185-
event = self._event_factory.deserialize_guild_available_event(shard, payload)
186-
else:
187-
event = self._event_factory.deserialize_guild_join_event(shard, payload)
188-
189-
channels = event.channels
190-
emojis = event.emojis
191-
guild = event.guild
192-
guild_id = guild.id
193-
members = event.members
194-
presences = event.presences
195-
roles = event.roles
196-
voice_states = event.voice_states
197-
198203
else:
199-
event = None
204+
_LOGGER.log(
205+
ux.TRACE, "Skipping on_guild_create raw dispatch due to lack of any registered listeners or cache need"
206+
)
207+
200208
channels = None
201209
emojis = None
202210
guild = None
@@ -241,16 +249,19 @@ async def on_guild_create(self, shard: gateway_shard.GatewayShard, payload: data
241249
for voice_state in voice_states.values():
242250
self._cache.set_voice_state(voice_state)
243251

244-
recv_chunks = self._enabled_for_event(shard_events.MemberChunkEvent) or self._cache_enabled_for(
245-
config.CacheComponents.MEMBERS
246-
)
247-
members_declared = self._intents & intents_.Intents.GUILD_MEMBERS
248252
presences_declared = self._intents & intents_.Intents.GUILD_PRESENCES
249253

250-
# When intents are enabled discord will only send other member objects on the guild create
254+
# When intents are enabled Discord will only send other member objects on the guild create
251255
# payload if presence intents are also declared, so if this isn't the case then we also want
252256
# to chunk small guilds.
253-
if recv_chunks and members_declared and (payload.get("large") or not presences_declared):
257+
if (
258+
self._intents & intents_.Intents.GUILD_MEMBERS
259+
and (payload.get("large") or not presences_declared)
260+
and (
261+
self._cache_enabled_for(config.CacheComponents.MEMBERS)
262+
or self._enabled_for_event(shard_events.MemberChunkEvent)
263+
)
264+
):
254265
# We create a task here instead of awaiting the result to avoid any rate-limits from delaying dispatch.
255266
nonce = f"{shard.id}.{_fixed_size_nonce()}"
256267

@@ -263,28 +274,30 @@ async def on_guild_create(self, shard: gateway_shard.GatewayShard, payload: data
263274
if event:
264275
await self.dispatch(event)
265276

266-
# Internal granularity is preferred for GUILD_UPDATE over decorator based filtering due to its large cache scope.
277+
# Internal granularity is preferred for GUILD_UPDATE over decorator based filtering due to its large scope.
267278
async def on_guild_update(self, shard: gateway_shard.GatewayShard, payload: data_binding.JSONObject) -> None:
268279
"""See https://discord.com/developers/docs/topics/gateway#guild-update for more info."""
269-
enabled_for_event = self._enabled_for_event(guild_events.GuildUpdateEvent)
280+
event: typing.Optional[guild_events.GuildUpdateEvent]
281+
if self._enabled_for_event(guild_events.GuildUpdateEvent):
282+
guild_id = snowflakes.Snowflake(payload["id"])
283+
old = self._cache.get_guild(guild_id) if self._cache else None
284+
event = self._event_factory.deserialize_guild_update_event(shard, payload, old_guild=old)
285+
286+
# We also filter here to prevent iterating over them and calling a function that won't do anything
287+
emojis = event.emojis if self._cache_enabled_for(config.CacheComponents.EMOJIS) else None
288+
guild = event.guild if self._cache_enabled_for(config.CacheComponents.GUILDS) else None
289+
roles = event.roles if self._cache_enabled_for(config.CacheComponents.ROLES) else None
270290

271-
if not enabled_for_event and self._cache:
291+
elif self._cache:
272292
_LOGGER.log(ux.TRACE, "Skipping on_guild_update raw dispatch due to lack of any registered listeners")
273-
event: typing.Optional[guild_events.GuildUpdateEvent] = None
293+
event = None
294+
274295
gd = self._entity_factory.deserialize_gateway_guild(payload)
275296
emojis = gd.emojis() if self._cache_enabled_for(config.CacheComponents.EMOJIS) else None
276297
guild = gd.guild() if self._cache_enabled_for(config.CacheComponents.GUILDS) else None
277298
guild_id = gd.id
278299
roles = gd.roles() if self._cache_enabled_for(config.CacheComponents.ROLES) else None
279300

280-
elif enabled_for_event:
281-
guild_id = snowflakes.Snowflake(payload["id"])
282-
old = self._cache.get_guild(guild_id) if self._cache else None
283-
event = self._event_factory.deserialize_guild_update_event(shard, payload, old_guild=old)
284-
emojis = event.emojis
285-
guild = event.guild
286-
roles = event.roles
287-
288301
else:
289302
_LOGGER.log(
290303
ux.TRACE, "Skipping on_guild_update raw dispatch due to lack of any registered listeners or cache need"

0 commit comments

Comments
 (0)
Please sign in to comment.