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.
This commit is contained in:
parent
3d03dbc451
commit
95d8bb2e85
@ -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
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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."""
|
||||||
|
@ -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:
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user