Browse Source

Change internal representation of roles in Member and Emoji.

Introduce a new internal type, SnowflakeList, which has better memory
footprint over a regular list or set of roles. It is suspected that
there will be a 9x reduction of memory for every Emoji instance and a
48 byte saving per Member instance. However, these savings will
probably only be evident on larger bots.

As a consequence of this change, Member.roles is now computed lazily.

Currently I am not sure if I want to do the initial sorting on the
SnowflakeList for Member, as this comes with a O(n log n) cost when
creating a Member for little purpose since SnowflakeList.has is not
overly relied on. If CPU time becomes an issue this might change.
pull/796/merge
Rapptz 7 years ago
parent
commit
95d8bb2e85
  1. 11
      discord/abc.py
  2. 6
      discord/emoji.py
  3. 34
      discord/member.py
  4. 3
      discord/role.py
  5. 34
      discord/utils.py

11
discord/abc.py

@ -415,9 +415,10 @@ class GuildChannel:
default = self.guild.default_role default = self.guild.default_role
base = Permissions(default.permissions.value) base = Permissions(default.permissions.value)
roles = member.roles
# Apply guild roles that the member has. # Apply guild roles that the member has.
for role in member.roles: for role in roles:
base.value |= role.permissions.value base.value |= role.permissions.value
# Guild-wide Administrator -> True for everything # Guild-wide Administrator -> True for everything
@ -436,7 +437,13 @@ class GuildChannel:
except IndexError: except IndexError:
remaining_overwrites = self._overwrites remaining_overwrites = self._overwrites
member_role_ids = set(map(lambda r: r.id, member.roles)) # not sure if doing member._roles.get(...) is better than the
# set approach. While this is O(N) to re-create into a set for O(1)
# the direct approach would just be O(log n) for searching with no
# extra memory overhead. For now, I'll keep the set cast
# Note that the member.roles accessor up top also creates a
# temporary list
member_role_ids = {r.id for r in roles}
denies = 0 denies = 0
allows = 0 allows = 0

6
discord/emoji.py

@ -154,7 +154,7 @@ class Emoji(Hashable):
self.id = int(emoji['id']) self.id = int(emoji['id'])
self.name = emoji['name'] self.name = emoji['name']
self.animated = emoji.get('animated', False) self.animated = emoji.get('animated', False)
self._roles = set(emoji.get('roles', [])) self._roles = utils.SnowflakeList(map(int, emoji.get('roles', [])))
def _iterator(self): def _iterator(self):
for attr in self.__slots__: for attr in self.__slots__:
@ -187,7 +187,7 @@ class Emoji(Hashable):
@property @property
def roles(self): def roles(self):
"""List[:class:`Role`]: A list of roles that is allowed to use this emoji. """List[:class:`Role`]: A :class:`list` of roles that is allowed to use this emoji.
If roles is empty, the emoji is unrestricted. If roles is empty, the emoji is unrestricted.
""" """
@ -195,7 +195,7 @@ class Emoji(Hashable):
if guild is None: if guild is None:
return [] return []
return [role for role in guild.roles if role.id in self._roles] return [role for role in guild.roles if self._roles.has(role.id)]
@property @property
def guild(self): def guild(self):

34
discord/member.py

@ -136,10 +136,6 @@ class Member(discord.abc.Messageable, _BaseUser):
Attributes Attributes
---------- ----------
roles: List[:class:`Role`]
A :class:`list` of :class:`Role` that the member belongs to. Note that the first element of this
list is always the default '@everyone' role. These roles are sorted by their position
in the role hierarchy.
joined_at: `datetime.datetime` joined_at: `datetime.datetime`
A datetime object that specifies the date and time in UTC that the member joined the guild for A datetime object that specifies the date and time in UTC that the member joined the guild for
the first time. the first time.
@ -154,7 +150,7 @@ class Member(discord.abc.Messageable, _BaseUser):
The guild specific nickname of the user. The guild specific nickname of the user.
""" """
__slots__ = ('roles', 'joined_at', 'status', 'activity', 'guild', 'nick', '_user', '_state') __slots__ = ('_roles', 'joined_at', 'status', 'activity', 'guild', 'nick', '_user', '_state')
def __init__(self, *, data, guild, state): def __init__(self, *, data, guild, state):
self._state = state self._state = state
@ -187,15 +183,7 @@ class Member(discord.abc.Messageable, _BaseUser):
return ch return ch
def _update_roles(self, data): def _update_roles(self, data):
# update the roles self._roles = utils.SnowflakeList(map(int, data['roles']))
self.roles = [self.guild.default_role]
for role_id in map(int, data['roles']):
role = self.guild.get_role(role_id)
if role is not None:
self.roles.append(role)
# sort the roles by hierarchy since they can be "randomised"
self.roles.sort()
def _update(self, data, user=None): def _update(self, data, user=None):
if user: if user:
@ -248,6 +236,24 @@ class Member(discord.abc.Messageable, _BaseUser):
color = colour color = colour
@property
def roles(self):
"""A :class:`list` of :class:`Role` that the member belongs to. Note
that the first element of this list is always the default '@everyone'
role.
These roles are sorted by their position in the role hierarchy.
"""
result = []
g = self.guild
for role_id in self._roles:
role = g.get_role(role_id)
if role:
result.append(role)
result.append(g.default_role)
result.sort()
return result
@property @property
def mention(self): def mention(self):
"""Returns a string that mentions the member.""" """Returns a string that mentions the member."""

3
discord/role.py

@ -173,7 +173,8 @@ class Role(Hashable):
if self.is_default(): if self.is_default():
return all_members return all_members
return [member for member in all_members if self in member.roles] role_id = self.id
return [member for member in all_members if member._roles.has(role_id)]
async def _move(self, position, reason): async def _move(self, position, reason):
if position <= 0: if position <= 0:

34
discord/utils.py

@ -26,13 +26,16 @@ DEALINGS IN THE SOFTWARE.
from re import split as re_split from re import split as re_split
from .errors import InvalidArgument from .errors import InvalidArgument
import datetime
from base64 import b64encode from base64 import b64encode
from email.utils import parsedate_to_datetime from email.utils import parsedate_to_datetime
from inspect import isawaitable as _isawaitable from inspect import isawaitable as _isawaitable
from bisect import bisect_left
import datetime
import asyncio import asyncio
import json import json
import warnings, functools import warnings, functools
import array
DISCORD_EPOCH = 1420070400000 DISCORD_EPOCH = 1420070400000
@ -289,3 +292,32 @@ async def sane_wait_for(futures, *, timeout, loop):
def valid_icon_size(size): def valid_icon_size(size):
"""Icons must be power of 2 within [16, 1024].""" """Icons must be power of 2 within [16, 1024]."""
return ((size != 0) and not (size & (size - 1))) and size in range(16, 1025) return ((size != 0) and not (size & (size - 1))) and size in range(16, 1025)
class SnowflakeList(array.array):
"""Internal data storage class to efficiently store a list of snowflakes.
This should have the following characteristics:
- Low memory usage
- O(n) iteration (obviously)
- O(n log n) initial creation if data is unsorted
- O(log n) search and indexing
- O(n) insertion
"""
__slots__ = ()
def __new__(cls, data, *, is_sorted=False):
return array.array.__new__(cls, 'Q', data if is_sorted else sorted(data))
def add(self, element):
i = bisect_left(self, element)
self.insert(i, element)
def get(self, element):
i = bisect_left(self, element)
return self[i] if i != len(self) and self[i] == element else None
def has(self, element):
i = bisect_left(self, element)
return i != len(self) and self[i] == element

Loading…
Cancel
Save