From 31229a53e902e7cdc6f5f8d6e55e333cbc1fb0e6 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 9 Oct 2016 05:37:37 -0400 Subject: [PATCH] Optimise VoiceState for memory. Instead of storing one VoiceState per Member, only store them if necessary. This should bring down the number of instances significantly. --- discord/calls.py | 3 +- discord/member.py | 80 ++++++++++++----------------------------------- discord/server.py | 51 ++++++++++++++++++++++++------ discord/state.py | 25 ++++++++------- 4 files changed, 76 insertions(+), 83 deletions(-) diff --git a/discord/calls.py b/discord/calls.py index 40df55acf..6a221e50a 100644 --- a/discord/calls.py +++ b/discord/calls.py @@ -117,8 +117,7 @@ class GroupCall: if data['channel_id'] is None: self._voice_states.pop(user_id, None) else: - data['voice_channel'] = self.channel - self._voice_states[user_id] = VoiceState(**data) + self._voice_states[user_id] = VoiceState(data=data, channel=self.channel) @property def connected(self): diff --git a/discord/member.py b/discord/member.py index 4d0f86b98..08d2600a4 100644 --- a/discord/member.py +++ b/discord/member.py @@ -31,9 +31,6 @@ from . import utils from .enums import Status, ChannelType, try_enum from .colour import Colour -import copy -import inspect - class VoiceState: """Represents a Discord user's voice state. @@ -49,32 +46,25 @@ class VoiceState: Indicates if the user is currently deafened by their own accord. is_afk: bool Indicates if the user is currently in the AFK channel in the server. - voice_channel: Optional[Union[:class:`Channel`, :class:`PrivateChannel`]] + channel: Optional[Union[:class:`Channel`, :class:`PrivateChannel`]] The voice channel that the user is currently connected to. None if the user is not currently in a voice channel. """ __slots__ = ( 'session_id', 'deaf', 'mute', 'self_mute', - 'self_deaf', 'is_afk', 'voice_channel' ) - - def __init__(self, **kwargs): - self.session_id = kwargs.get('session_id') - self._update_voice_state(**kwargs) - - def _update_voice_state(self, **kwargs): - self.self_mute = kwargs.get('self_mute', False) - self.self_deaf = kwargs.get('self_deaf', False) - self.is_afk = kwargs.get('suppress', False) - self.mute = kwargs.get('mute', False) - self.deaf = kwargs.get('deaf', False) - self.voice_channel = kwargs.get('voice_channel') - -def flatten_voice_states(cls): - for attr in VoiceState.__slots__: - def getter(self, x=attr): - return getattr(self.voice, x) - setattr(cls, attr, property(getter)) - return cls + 'self_deaf', 'is_afk', 'channel' ) + + def __init__(self, *, data, channel=None): + self.session_id = data.get('session_id') + self._update(data, channel) + + def _update(self, data, channel): + self.self_mute = data.get('self_mute', False) + self.self_deaf = data.get('self_deaf', False) + self.is_afk = data.get('suppress', False) + self.mute = data.get('mute', False) + self.deaf = data.get('deaf', False) + self.channel = channel def flatten_user(cls): for attr, value in User.__dict__.items(): @@ -107,7 +97,6 @@ def flatten_user(cls): return cls -@flatten_voice_states @flatten_user class Member: """Represents a Discord member to a :class:`Server`. @@ -130,9 +119,6 @@ class Member: Attributes ---------- - voice: :class:`VoiceState` - The member's voice state. Properties are defined to mirror access of the attributes. - e.g. ``Member.is_afk`` is equivalent to `Member.voice.is_afk``. roles A list of :class:`Role` that the member belongs to. Note that the first element of this list is always the default '@everyone' role. @@ -150,12 +136,11 @@ class Member: The server specific nickname of the user. """ - __slots__ = ('roles', 'joined_at', 'status', 'game', 'server', 'nick', 'voice', '_user', '_state') + __slots__ = ('roles', 'joined_at', 'status', 'game', 'server', 'nick', '_user', '_state') def __init__(self, *, data, server, state): self._state = state self._user = state.try_insert_user(data['user']) - self.voice = VoiceState(**data) self.joined_at = utils.parse_time(data.get('joined_at')) self.roles = data.get('roles', []) self.status = Status.offline @@ -176,36 +161,6 @@ class Member: def __hash__(self): return hash(self._user.id) - def _update_voice_state(self, **kwargs): - self.voice.self_mute = kwargs.get('self_mute', False) - self.voice.self_deaf = kwargs.get('self_deaf', False) - self.voice.is_afk = kwargs.get('suppress', False) - self.voice.mute = kwargs.get('mute', False) - self.voice.deaf = kwargs.get('deaf', False) - old_channel = getattr(self, 'voice_channel', None) - vc = kwargs.get('voice_channel') - - if old_channel is None and vc is not None: - # we joined a channel - vc.voice_members.append(self) - elif old_channel is not None: - try: - # we either left a channel or we switched channels - old_channel.voice_members.remove(self) - except ValueError: - pass - finally: - # we switched channels - if vc is not None: - vc.voice_members.append(self) - - self.voice.voice_channel = vc - - def _copy(self): - ret = copy.copy(self) - ret.voice = copy.copy(self.voice) - return ret - def _update(self, data, user): self._user.name = user['username'] self._user.discriminator = user['discriminator'] @@ -323,3 +278,8 @@ class Member: return Permissions.all() return base + + @property + def voice(self): + """Optional[:class:`VoiceState`]: Returns the member's current voice state.""" + return self.server._voice_state_for(self._user.id) diff --git a/discord/server.py b/discord/server.py index 414d32304..fb572fc5b 100644 --- a/discord/server.py +++ b/discord/server.py @@ -26,13 +26,15 @@ DEALINGS IN THE SOFTWARE. from . import utils from .role import Role -from .member import Member +from .member import Member, VoiceState from .emoji import Emoji from .game import Game from .channel import Channel from .enums import ServerRegion, Status, try_enum, VerificationLevel from .mixins import Hashable +import copy + class Server(Hashable): """Represents a Discord server. @@ -112,12 +114,13 @@ class Server(Hashable): 'name', 'id', 'owner', 'unavailable', 'name', 'region', '_default_role', '_default_channel', 'roles', '_member_count', 'large', 'owner_id', 'mfa_level', 'emojis', 'features', - 'verification_level', 'splash' ) + 'verification_level', 'splash', '_voice_states' ) def __init__(self, *, data, state): self._channels = {} self.owner = None self._members = {} + self._voice_states = {} self._state = state self._from_data(data) @@ -135,6 +138,9 @@ class Server(Hashable): def _remove_channel(self, channel): self._channels.pop(channel.id, None) + def _voice_state_for(self, user_id): + return self._voice_states.get(user_id) + @property def members(self): return self._members.values() @@ -153,15 +159,42 @@ class Server(Hashable): return self.name def _update_voice_state(self, data): - user_id = data.get('user_id') + user_id = data['user_id'] + channel = self.get_channel(data['channel_id']) + try: + # check if we should remove the voice state from cache + if channel is None: + after = self._voice_states.pop(user_id) + else: + after = self._voice_states[user_id] + + before = copy.copy(after) + after._update(data, channel) + except KeyError: + # if we're here then we're getting added into the cache + after = VoiceState(data=data, channel=channel) + before = VoiceState(data=data, channel=None) + self._voice_states[user_id] = after + member = self.get_member(user_id) - before = None if member is not None: - before = member._copy() - ch_id = data.get('channel_id') - channel = self.get_channel(ch_id) - member._update_voice_state(voice_channel=channel, **data) - return before, member + old = before.channel + # update the references pointed to by the voice channels + if old is None and channel is not None: + # we joined a channel + channel.voice_members.append(member) + elif old is not None: + try: + # we either left a channel or switched channels + old.voice_members.remove(member) + except ValueError: + pass + finally: + # we switched channels + if channel is not None: + channel.voice_members.append(self) + + return member, before, after def _add_role(self, role): # roles get added to the bottom (position 1, pos 0 is @everyone) diff --git a/discord/state.py b/discord/state.py index 1af26db8b..ff28c496e 100644 --- a/discord/state.py +++ b/discord/state.py @@ -340,7 +340,7 @@ class ConnectionState: member = self._make_member(server, data) server._add_member(member) - old_member = member._copy() + old_member = copy.copy(member) member._presence_update(data=data, user=user) self.dispatch('member_update', old_member, member) @@ -431,12 +431,14 @@ class ConnectionState: server._member_count -= 1 # remove them from the voice channel member list - vc = member.voice_channel - if vc is not None: - try: - vc.voice_members.remove(member) - except: - pass + vc = server._voice_state_for(user_id) + if vc: + voice_channel = vc.channel + if voice_channel is not None: + try: + voice_channel.voice_members.remove(member) + except ValueError: + pass self.dispatch('member_remove', member) @@ -446,7 +448,7 @@ class ConnectionState: user_id = user['id'] member = server.get_member(user_id) if member is not None: - old_member = member._copy() + old_member = copy.copy(member) member._update(data, user) self.dispatch('member_update', old_member, member) @@ -620,15 +622,14 @@ class ConnectionState: def parse_voice_state_update(self, data): server = self._get_server(data.get('guild_id')) if server is not None: - channel = server.get_channel(data.get('channel_id')) if data.get('user_id') == self.user.id: voice = self._get_voice_client(server.id) if voice is not None: - voice.channel = channel + voice.channel = server.get_channel(data.get('channel_id')) - before, after = server._update_voice_state(data) + member, before, after = server._update_voice_state(data) if after is not None: - self.dispatch('voice_state_update', before, after) + self.dispatch('voice_state_update', member, before, after) else: # in here we're either at private or group calls call = self._calls.get(data.get('channel_id'), None)