Skip to content

Commit

Permalink
Change internal representation of roles in Member and Emoji.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Rapptz committed Sep 25, 2018
1 parent 3d03dbc commit 95d8bb2
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 21 deletions.
11 changes: 9 additions & 2 deletions discord/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,9 +415,10 @@ def permissions_for(self, member):

default = self.guild.default_role
base = Permissions(default.permissions.value)
roles = member.roles

# Apply guild roles that the member has.
for role in member.roles:
for role in roles:
base.value |= role.permissions.value

# Guild-wide Administrator -> True for everything
Expand All @@ -436,7 +437,13 @@ def permissions_for(self, member):
except IndexError:
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
allows = 0

Expand Down
6 changes: 3 additions & 3 deletions discord/emoji.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ def _from_data(self, emoji):
self.id = int(emoji['id'])
self.name = emoji['name']
self.animated = emoji.get('animated', False)
self._roles = set(emoji.get('roles', []))
self._roles = utils.SnowflakeList(map(int, emoji.get('roles', [])))

def _iterator(self):
for attr in self.__slots__:
Expand Down Expand Up @@ -187,15 +187,15 @@ def url(self):

@property
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.
"""
guild = self.guild
if guild is None:
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
def guild(self):
Expand Down
34 changes: 20 additions & 14 deletions discord/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,6 @@ class Member(discord.abc.Messageable, _BaseUser):
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`
A datetime object that specifies the date and time in UTC that the member joined the guild for
the first time.
Expand All @@ -154,7 +150,7 @@ class Member(discord.abc.Messageable, _BaseUser):
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):
self._state = state
Expand Down Expand Up @@ -187,15 +183,7 @@ async def _get_channel(self):
return ch

def _update_roles(self, data):
# update the 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()
self._roles = utils.SnowflakeList(map(int, data['roles']))

def _update(self, data, user=None):
if user:
Expand Down Expand Up @@ -248,6 +236,24 @@ def colour(self):

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
def mention(self):
"""Returns a string that mentions the member."""
Expand Down
3 changes: 2 additions & 1 deletion discord/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ def members(self):
if self.is_default():
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):
if position <= 0:
Expand Down
34 changes: 33 additions & 1 deletion discord/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@

from re import split as re_split
from .errors import InvalidArgument
import datetime
from base64 import b64encode
from email.utils import parsedate_to_datetime
from inspect import isawaitable as _isawaitable
from bisect import bisect_left

import datetime
import asyncio
import json
import warnings, functools
import array

DISCORD_EPOCH = 1420070400000

Expand Down Expand Up @@ -289,3 +292,32 @@ async def sane_wait_for(futures, *, timeout, loop):
def valid_icon_size(size):
"""Icons must be power of 2 within [16, 1024]."""
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

0 comments on commit 95d8bb2

Please sign in to comment.