From b38455f4277a512dbd473220a57e9a94ff939e01 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 21 Jun 2016 01:09:50 -0300 Subject: [PATCH] Several performance/memory improvements. Renamed CachedPublicUser -> CachedGlobalUser. --- src/Discord.Net/Data/DefaultDataStore.cs | 16 +- src/Discord.Net/Data/IDataStore.cs | 8 +- src/Discord.Net/DiscordSocketClient.cs | 177 +++++++++++------- .../Entities/Channels/DMChannel.cs | 11 +- .../Entities/Channels/GuildChannel.cs | 50 +++-- src/Discord.Net/Entities/Users/GuildUser.cs | 5 +- src/Discord.Net/Entities/Users/User.cs | 4 +- .../Entities/WebSocket/CachedDMChannel.cs | 11 +- .../Entities/WebSocket/CachedDMUser.cs | 38 ++++ .../Entities/WebSocket/CachedGlobalUser.cs | 39 ++++ .../Entities/WebSocket/CachedGuildUser.cs | 6 +- .../Entities/WebSocket/CachedPublicUser.cs | 75 -------- .../Entities/WebSocket/CachedTextChannel.cs | 7 +- .../Entities/WebSocket/MessageCache.cs | 69 +------ .../Entities/WebSocket/MessageManager.cs | 81 ++++++++ 15 files changed, 348 insertions(+), 249 deletions(-) create mode 100644 src/Discord.Net/Entities/WebSocket/CachedDMUser.cs create mode 100644 src/Discord.Net/Entities/WebSocket/CachedGlobalUser.cs delete mode 100644 src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs create mode 100644 src/Discord.Net/Entities/WebSocket/MessageManager.cs diff --git a/src/Discord.Net/Data/DefaultDataStore.cs b/src/Discord.Net/Data/DefaultDataStore.cs index 20e804a68..b267f5932 100644 --- a/src/Discord.Net/Data/DefaultDataStore.cs +++ b/src/Discord.Net/Data/DefaultDataStore.cs @@ -15,12 +15,12 @@ namespace Discord.Data private readonly ConcurrentDictionary _channels; private readonly ConcurrentDictionary _dmChannels; private readonly ConcurrentDictionary _guilds; - private readonly ConcurrentDictionary _users; + private readonly ConcurrentDictionary _users; internal override IReadOnlyCollection Channels => _channels.ToReadOnlyCollection(); internal override IReadOnlyCollection DMChannels => _dmChannels.ToReadOnlyCollection(); internal override IReadOnlyCollection Guilds => _guilds.ToReadOnlyCollection(); - internal override IReadOnlyCollection Users => _users.ToReadOnlyCollection(); + internal override IReadOnlyCollection Users => _users.ToReadOnlyCollection(); public DefaultDataStore(int guildCount, int dmChannelCount) { @@ -29,7 +29,7 @@ namespace Discord.Data _channels = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(estimatedChannelCount * CollectionMultiplier)); _dmChannels = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(dmChannelCount * CollectionMultiplier)); _guilds = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(guildCount * CollectionMultiplier)); - _users = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); + _users = new ConcurrentDictionary(CollectionConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); } internal override ICachedChannel GetChannel(ulong id) @@ -94,20 +94,20 @@ namespace Discord.Data return null; } - internal override CachedPublicUser GetUser(ulong id) + internal override CachedGlobalUser GetUser(ulong id) { - CachedPublicUser user; + CachedGlobalUser user; if (_users.TryGetValue(id, out user)) return user; return null; } - internal override CachedPublicUser GetOrAddUser(ulong id, Func userFactory) + internal override CachedGlobalUser GetOrAddUser(ulong id, Func userFactory) { return _users.GetOrAdd(id, userFactory); } - internal override CachedPublicUser RemoveUser(ulong id) + internal override CachedGlobalUser RemoveUser(ulong id) { - CachedPublicUser user; + CachedGlobalUser user; if (_users.TryRemove(id, out user)) return user; return null; diff --git a/src/Discord.Net/Data/IDataStore.cs b/src/Discord.Net/Data/IDataStore.cs index f10507f3c..26d9c6e40 100644 --- a/src/Discord.Net/Data/IDataStore.cs +++ b/src/Discord.Net/Data/IDataStore.cs @@ -8,7 +8,7 @@ namespace Discord.Data internal abstract IReadOnlyCollection Channels { get; } internal abstract IReadOnlyCollection DMChannels { get; } internal abstract IReadOnlyCollection Guilds { get; } - internal abstract IReadOnlyCollection Users { get; } + internal abstract IReadOnlyCollection Users { get; } internal abstract ICachedChannel GetChannel(ulong id); internal abstract void AddChannel(ICachedChannel channel); @@ -22,8 +22,8 @@ namespace Discord.Data internal abstract void AddGuild(CachedGuild guild); internal abstract CachedGuild RemoveGuild(ulong id); - internal abstract CachedPublicUser GetUser(ulong id); - internal abstract CachedPublicUser GetOrAddUser(ulong userId, Func userFactory); - internal abstract CachedPublicUser RemoveUser(ulong id); + internal abstract CachedGlobalUser GetUser(ulong id); + internal abstract CachedGlobalUser GetOrAddUser(ulong userId, Func userFactory); + internal abstract CachedGlobalUser RemoveUser(ulong id); } } diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs index 443ba0890..6d26614ee 100644 --- a/src/Discord.Net/DiscordSocketClient.cs +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -13,7 +13,6 @@ using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; -using System.Diagnostics; namespace Discord { @@ -51,15 +50,17 @@ namespace Discord private readonly bool _enablePreUpdateEvents; private readonly int _largeThreshold; private readonly int _totalShards; - private ConcurrentHashSet _dmChannels; + private string _sessionId; private int _lastSeq; private ImmutableDictionary _voiceRegions; private TaskCompletionSource _connectTask; - private CancellationTokenSource _heartbeatCancelToken; - private Task _heartbeatTask, _reconnectTask; + private CancellationTokenSource _cancelToken; + private Task _heartbeatTask, _guildDownloadTask, _reconnectTask; private long _heartbeatTime; private bool _isReconnecting; + private int _unavailableGuilds; + private long _lastGuildAvailableTime; /// Gets the shard if of this client. public int ShardId { get; } @@ -74,15 +75,7 @@ namespace Discord internal CachedSelfUser CurrentUser => _currentUser as CachedSelfUser; internal IReadOnlyCollection Guilds => DataStore.Guilds; - internal IReadOnlyCollection DMChannels - { - get - { - var dmChannels = _dmChannels; - var store = DataStore; - return dmChannels.Select(x => store.GetChannel(x) as CachedDMChannel).Where(x => x != null).ToReadOnlyCollection(dmChannels); - } - } + internal IReadOnlyCollection DMChannels => DataStore.DMChannels; internal IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); /// Creates a new REST/WebSocket discord client. @@ -132,7 +125,6 @@ namespace Discord _voiceRegions = ImmutableDictionary.Create(); _largeGuilds = new ConcurrentQueue(); - _dmChannels = new ConcurrentHashSet(); } protected override async Task OnLoginAsync() @@ -169,10 +161,11 @@ namespace Discord try { _connectTask = new TaskCompletionSource(); - _heartbeatCancelToken = new CancellationTokenSource(); + _cancelToken = new CancellationTokenSource(); await ApiClient.ConnectAsync().ConfigureAwait(false); await _connectTask.Task.ConfigureAwait(false); + ConnectionState = ConnectionState.Connected; await _gatewayLogger.InfoAsync("Connected"); } @@ -203,9 +196,24 @@ namespace Discord ConnectionState = ConnectionState.Disconnecting; await _gatewayLogger.InfoAsync("Disconnecting"); - try { _heartbeatCancelToken.Cancel(); } catch { } + //Signal tasks to complete + try { _cancelToken.Cancel(); } catch { } + + //Disconnect from server await ApiClient.DisconnectAsync().ConfigureAwait(false); - await _heartbeatTask.ConfigureAwait(false); + + //Wait for tasks to complete + var heartbeatTask = _heartbeatTask; + if (heartbeatTask != null) + await heartbeatTask.ConfigureAwait(false); + _heartbeatTask = null; + + var guildDownloadTask = _guildDownloadTask; + if (guildDownloadTask != null) + await guildDownloadTask.ConfigureAwait(false); + _guildDownloadTask = null; + + //Clear large guild queue while (_largeGuilds.TryDequeue(out guildId)) { } ConnectionState = ConnectionState.Disconnected; @@ -216,22 +224,21 @@ namespace Discord private async Task StartReconnectAsync() { //TODO: Is this thread-safe? - while (true) + await _log.InfoAsync("Debug", "Trying to reconnect...").ConfigureAwait(false); + if (_reconnectTask != null) return; + + await _connectionLock.WaitAsync().ConfigureAwait(false); + try { if (_reconnectTask != null) return; - - await _connectionLock.WaitAsync().ConfigureAwait(false); - try - { - if (_reconnectTask != null) return; - _isReconnecting = true; - _reconnectTask = ReconnectInternalAsync(); - } - finally { _connectionLock.Release(); } + _isReconnecting = true; + _reconnectTask = ReconnectInternalAsync(); } + finally { _connectionLock.Release(); } } private async Task ReconnectInternalAsync() { + await _log.InfoAsync("Debug", "Reconnecting...").ConfigureAwait(false); try { int nextReconnectDelay = 1000; @@ -255,13 +262,18 @@ namespace Discord catch (Exception ex) { await _gatewayLogger.WarningAsync("Reconnect failed", ex).ConfigureAwait(false); - } } + } + } } finally { - _isReconnecting = false; - _reconnectTask = null; - + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + _isReconnecting = false; + _reconnectTask = null; + } + finally { _connectionLock.Release(); } } } @@ -318,20 +330,22 @@ namespace Discord { return Task.FromResult>(DMChannels); } - internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore, ConcurrentHashSet dmChannels) + internal CachedDMChannel AddDMChannel(API.Channel model, DataStore dataStore) { var recipient = GetOrAddUser(model.Recipient.Value, dataStore); - var channel = recipient.AddDMChannel(this, model); - dataStore.AddChannel(channel); - dmChannels.TryAdd(model.Id); + var channel = new CachedDMChannel(this, new CachedDMUser(recipient), model); + recipient.AddRef(); + dataStore.AddDMChannel(channel); return channel; } internal CachedDMChannel RemoveDMChannel(ulong id) { - var dmChannel = DataStore.RemoveChannel(id) as CachedDMChannel; - var recipient = dmChannel.Recipient; - recipient.RemoveDMChannel(id); - _dmChannels.TryRemove(id); + var dmChannel = DataStore.RemoveDMChannel(id); + if (dmChannel != null) + { + var recipient = dmChannel.Recipient; + recipient.User.RemoveRef(this); + } return dmChannel; } @@ -345,13 +359,13 @@ namespace Discord { return Task.FromResult(DataStore.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault()); } - internal CachedPublicUser GetOrAddUser(API.User model, DataStore dataStore) + internal CachedGlobalUser GetOrAddUser(API.User model, DataStore dataStore) { - var user = dataStore.GetOrAddUser(model.Id, _ => new CachedPublicUser(model)); + var user = dataStore.GetOrAddUser(model.Id, _ => new CachedGlobalUser(model)); user.AddRef(); return user; } - internal CachedPublicUser RemoveUser(ulong id) + internal CachedGlobalUser RemoveUser(ulong id) { return DataStore.RemoveUser(id); } @@ -425,7 +439,7 @@ namespace Discord else await ApiClient.SendIdentifyAsync().ConfigureAwait(false); _heartbeatTime = 0; - _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _heartbeatCancelToken.Token); + _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _cancelToken.Token); } break; case GatewayOpCode.Heartbeat: @@ -439,12 +453,16 @@ namespace Discord { await _gatewayLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); - var latency = (int)(Environment.TickCount - _heartbeatTime); - _heartbeatTime = 0; - await _gatewayLogger.DebugAsync($"Latency = {latency} ms").ConfigureAwait(false); - Latency = latency; + var heartbeatTime = _heartbeatTime; + if (heartbeatTime != 0) + { + var latency = (int)(Environment.TickCount - _heartbeatTime); + _heartbeatTime = 0; + await _gatewayLogger.VerboseAsync($"Latency = {latency} ms").ConfigureAwait(false); + Latency = latency; - await LatencyUpdated.RaiseAsync(latency).ConfigureAwait(false); + await LatencyUpdated.RaiseAsync(latency).ConfigureAwait(false); + } } break; case GatewayOpCode.InvalidSession: @@ -475,21 +493,29 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); var dataStore = _dataStoreProvider(ShardId, _totalShards, data.Guilds.Length, data.PrivateChannels.Length); - var dmChannels = new ConcurrentHashSet(); var currentUser = new CachedSelfUser(this, data.User); + int unavailableGuilds = 0; //dataStore.GetOrAddUser(data.User.Id, _ => currentUser); for (int i = 0; i < data.Guilds.Length; i++) - AddGuild(data.Guilds[i], dataStore); + { + var model = data.Guilds[i]; + AddGuild(model, dataStore); + if (model.Unavailable == true) + unavailableGuilds++; + } for (int i = 0; i < data.PrivateChannels.Length; i++) - AddDMChannel(data.PrivateChannels[i], dataStore, dmChannels); + AddDMChannel(data.PrivateChannels[i], dataStore); _sessionId = data.SessionId; _currentUser = currentUser; - _dmChannels = dmChannels; + _unavailableGuilds = unavailableGuilds; + _lastGuildAvailableTime = Environment.TickCount; DataStore = dataStore; + _guildDownloadTask = WaitForGuildsAsync(_cancelToken.Token); + await Ready.RaiseAsync().ConfigureAwait(false); _connectTask.TrySetResult(true); //Signal the .Connect() call to complete @@ -503,7 +529,10 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); if (data.Unavailable == false) + { type = "GUILD_AVAILABLE"; + _lastGuildAvailableTime = Environment.TickCount; + } await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false); CachedGuild guild; @@ -511,6 +540,7 @@ namespace Discord { guild = AddGuild(data, DataStore); await JoinedGuild.RaiseAsync(guild).ConfigureAwait(false); + await _gatewayLogger.InfoAsync($"Joined {data.Name}").ConfigureAwait(false); } else { @@ -526,7 +556,7 @@ namespace Discord if (data.Unavailable != true) { - await _gatewayLogger.InfoAsync($"Connected to {data.Name}").ConfigureAwait(false); + await _gatewayLogger.VerboseAsync($"Connected to {data.Name}").ConfigureAwait(false); await GuildAvailable.RaiseAsync(guild).ConfigureAwait(false); } } @@ -564,7 +594,7 @@ namespace Discord member.User.RemoveRef(this); await GuildUnavailable.RaiseAsync(guild).ConfigureAwait(false); - await _gatewayLogger.InfoAsync($"Disconnected from {data.Name}").ConfigureAwait(false); + await _gatewayLogger.VerboseAsync($"Disconnected from {data.Name}").ConfigureAwait(false); if (data.Unavailable != true) { await LeftGuild.RaiseAsync(guild).ConfigureAwait(false); @@ -587,7 +617,7 @@ namespace Discord var data = (payload as JToken).ToObject(_serializer); ICachedChannel channel = null; - if (data.GuildId.IsSpecified) + if (!data.IsPrivate) { var guild = DataStore.GetGuild(data.GuildId.Value); if (guild != null) @@ -599,7 +629,7 @@ namespace Discord } } else - channel = AddDMChannel(data, DataStore, _dmChannels); + channel = AddDMChannel(data, DataStore); if (channel != null) await ChannelCreated.RaiseAsync(channel).ConfigureAwait(false); } @@ -629,7 +659,7 @@ namespace Discord ICachedChannel channel = null; var data = (payload as JToken).ToObject(_serializer); - if (data.GuildId.IsSpecified) + if (!data.IsPrivate) { var guild = DataStore.GetGuild(data.GuildId.Value); if (guild != null) @@ -975,9 +1005,9 @@ namespace Discord } else { - var user = DataStore.GetUser(data.User.Id); - if (user == null) - user.Update(data, UpdateSource.WebSocket); + var channel = DataStore.GetDMChannel(data.User.Id); + if (channel != null) + channel.Recipient.Update(data, UpdateSource.WebSocket); } } break; @@ -1095,22 +1125,37 @@ namespace Discord { try { - var state = ConnectionState; - while (state == ConnectionState.Connecting || state == ConnectionState.Connected) + while (!cancelToken.IsCancellationRequested) { await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); if (_heartbeatTime != 0) //Server never responded to our last heartbeat { - await _gatewayLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); - await StartReconnectAsync().ConfigureAwait(false); - return; + if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? false)) + { + await _gatewayLogger.WarningAsync("Server missed last heartbeat").ConfigureAwait(false); + await StartReconnectAsync().ConfigureAwait(false); + return; + } } - _heartbeatTime = Environment.TickCount; + else + _heartbeatTime = Environment.TickCount; await ApiClient.SendHeartbeatAsync(_lastSeq).ConfigureAwait(false); } } catch (OperationCanceledException) { } } + + private async Task WaitForGuildsAsync(CancellationToken cancelToken) + { + while ((_unavailableGuilds > 0) || (Environment.TickCount - _lastGuildAvailableTime > 2000)) + await Task.Delay(500, cancelToken).ConfigureAwait(false); + } + public async Task WaitForGuildsAsync() + { + var downloadTask = _guildDownloadTask; + if (downloadTask != null) + await _guildDownloadTask.ConfigureAwait(false); + } } } diff --git a/src/Discord.Net/Entities/Channels/DMChannel.cs b/src/Discord.Net/Entities/Channels/DMChannel.cs index 6294e3f21..7df415b49 100644 --- a/src/Discord.Net/Entities/Channels/DMChannel.cs +++ b/src/Discord.Net/Entities/Channels/DMChannel.cs @@ -14,11 +14,11 @@ namespace Discord internal class DMChannel : SnowflakeEntity, IDMChannel { public override DiscordClient Discord { get; } - public User Recipient { get; private set; } + public IUser Recipient { get; private set; } public virtual IReadOnlyCollection CachedMessages => ImmutableArray.Create(); - public DMChannel(DiscordClient discord, User recipient, Model model) + public DMChannel(DiscordClient discord, IUser recipient, Model model) : base(model.Id) { Discord = discord; @@ -30,7 +30,9 @@ namespace Discord { if (source == UpdateSource.Rest && IsAttached) return; - Recipient.Update(model.Recipient.Value, UpdateSource.Rest); + //TODO: Is this cast okay? + if (Recipient is User) + (Recipient as User).Update(model.Recipient.Value, source); } public async Task UpdateAsync() @@ -119,8 +121,7 @@ namespace Discord public override string ToString() => '@' + Recipient.ToString(); private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; - - IUser IDMChannel.Recipient => Recipient; + IMessage IMessageChannel.GetCachedMessage(ulong id) => null; } } diff --git a/src/Discord.Net/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Entities/Channels/GuildChannel.cs index 7716f897d..9898c6132 100644 --- a/src/Discord.Net/Entities/Channels/GuildChannel.cs +++ b/src/Discord.Net/Entities/Channels/GuildChannel.cs @@ -1,7 +1,5 @@ using Discord.API.Rest; -using Discord.Extensions; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -14,7 +12,7 @@ namespace Discord [DebuggerDisplay(@"{DebuggerDisplay,nq}")] internal abstract class GuildChannel : SnowflakeEntity, IGuildChannel { - private ConcurrentDictionary _overwrites; + private List _overwrites; //TODO: Is maintaining a list here too expensive? Is this threadsafe? public string Name { get; private set; } public int Position { get; private set; } @@ -38,9 +36,9 @@ namespace Discord Position = model.Position.Value; var overwrites = model.PermissionOverwrites.Value; - var newOverwrites = new ConcurrentDictionary(); + var newOverwrites = new List(overwrites.Length); for (int i = 0; i < overwrites.Length; i++) - newOverwrites[overwrites[i].TargetId] = new Overwrite(overwrites[i]); + newOverwrites.Add(new Overwrite(overwrites[i])); _overwrites = newOverwrites; } @@ -89,16 +87,20 @@ namespace Discord public OverwritePermissions? GetPermissionOverwrite(IUser user) { - Overwrite value; - if (_overwrites.TryGetValue(user.Id, out value)) - return value.Permissions; + for (int i = 0; i < _overwrites.Count; i++) + { + if (_overwrites[i].TargetId == user.Id) + return _overwrites[i].Permissions; + } return null; } public OverwritePermissions? GetPermissionOverwrite(IRole role) { - Overwrite value; - if (_overwrites.TryGetValue(role.Id, out value)) - return value.Permissions; + for (int i = 0; i < _overwrites.Count; i++) + { + if (_overwrites[i].TargetId == role.Id) + return _overwrites[i].Permissions; + } return null; } @@ -106,34 +108,46 @@ namespace Discord { var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; await Discord.ApiClient.ModifyChannelPermissionsAsync(Id, user.Id, args).ConfigureAwait(false); - _overwrites[user.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User }); + _overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = user.Id, TargetType = PermissionTarget.User })); } public async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions perms) { var args = new ModifyChannelPermissionsParams { Allow = perms.AllowValue, Deny = perms.DenyValue }; await Discord.ApiClient.ModifyChannelPermissionsAsync(Id, role.Id, args).ConfigureAwait(false); - _overwrites[role.Id] = new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role }); + _overwrites.Add(new Overwrite(new API.Overwrite { Allow = perms.AllowValue, Deny = perms.DenyValue, TargetId = role.Id, TargetType = PermissionTarget.Role })); } public async Task RemovePermissionOverwriteAsync(IUser user) { await Discord.ApiClient.DeleteChannelPermissionAsync(Id, user.Id).ConfigureAwait(false); - Overwrite value; - _overwrites.TryRemove(user.Id, out value); + for (int i = 0; i < _overwrites.Count; i++) + { + if (_overwrites[i].TargetId == user.Id) + { + _overwrites.RemoveAt(i); + return; + } + } } public async Task RemovePermissionOverwriteAsync(IRole role) { await Discord.ApiClient.DeleteChannelPermissionAsync(Id, role.Id).ConfigureAwait(false); - Overwrite value; - _overwrites.TryRemove(role.Id, out value); + for (int i = 0; i < _overwrites.Count; i++) + { + if (_overwrites[i].TargetId == role.Id) + { + _overwrites.RemoveAt(i); + return; + } + } } public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; IGuild IGuildChannel.Guild => Guild; - IReadOnlyCollection IGuildChannel.PermissionOverwrites => _overwrites.ToReadOnlyCollection(); + IReadOnlyCollection IGuildChannel.PermissionOverwrites => _overwrites.AsReadOnly(); async Task IChannel.GetUserAsync(ulong id) => await GetUserAsync(id).ConfigureAwait(false); async Task> IChannel.GetUsersAsync() => await GetUsersAsync().ConfigureAwait(false); diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs index bd5826473..3778f98b3 100644 --- a/src/Discord.Net/Entities/Users/GuildUser.cs +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -33,8 +33,9 @@ namespace Discord public bool IsBot => User.IsBot; public string Mention => User.Mention; public string Username => User.Username; - public virtual UserStatus Status => User.Status; - public virtual Game Game => User.Game; + + public virtual UserStatus Status => UserStatus.Unknown; + public virtual Game Game => null; public DiscordClient Discord => Guild.Discord; public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); diff --git a/src/Discord.Net/Entities/Users/User.cs b/src/Discord.Net/Entities/Users/User.cs index 3e32bb954..fdfc06abf 100644 --- a/src/Discord.Net/Entities/Users/User.cs +++ b/src/Discord.Net/Entities/Users/User.cs @@ -1,7 +1,5 @@ -using Discord.API.Rest; -using System; +using System; using System.Diagnostics; -using System.Threading.Tasks; using Model = Discord.API.User; namespace Discord diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs index 568cef3d9..ed3eadac9 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedDMChannel.cs @@ -9,16 +9,19 @@ namespace Discord { internal class CachedDMChannel : DMChannel, IDMChannel, ICachedChannel, ICachedMessageChannel { - private readonly MessageCache _messages; + private readonly MessageManager _messages; public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public new CachedPublicUser Recipient => base.Recipient as CachedPublicUser; + public new CachedDMUser Recipient => base.Recipient as CachedDMUser; public IReadOnlyCollection Members => ImmutableArray.Create(Discord.CurrentUser, Recipient); - public CachedDMChannel(DiscordSocketClient discord, CachedPublicUser recipient, Model model) + public CachedDMChannel(DiscordSocketClient discord, CachedDMUser recipient, Model model) : base(discord, recipient, model) { - _messages = new MessageCache(Discord, this); + if (Discord.MessageCacheSize > 0) + _messages = new MessageCache(Discord, this); + else + _messages = new MessageManager(Discord, this); } public override Task GetUserAsync(ulong id) => Task.FromResult(GetUser(id)); diff --git a/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs b/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs new file mode 100644 index 000000000..de69c7c91 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/CachedDMUser.cs @@ -0,0 +1,38 @@ +using System; +using PresenceModel = Discord.API.Presence; + +namespace Discord +{ + internal class CachedDMUser : ICachedUser + { + public CachedGlobalUser User { get; } + + public Game Game { get; private set; } + public UserStatus Status { get; private set; } + + public DiscordSocketClient Discord => User.Discord; + + public ulong Id => User.Id; + public string AvatarUrl => User.AvatarUrl; + public DateTimeOffset CreatedAt => User.CreatedAt; + public string Discriminator => User.Discriminator; + public bool IsAttached => User.IsAttached; + public bool IsBot => User.IsBot; + public string Mention => User.Mention; + public string Username => User.Username; + + public CachedDMUser(CachedGlobalUser user) + { + User = user; + } + + public void Update(PresenceModel model, UpdateSource source) + { + Status = model.Status; + Game = model.Game != null ? new Game(model.Game) : null; + } + + public CachedDMUser Clone() => MemberwiseClone() as CachedDMUser; + ICachedUser ICachedUser.Clone() => Clone(); + } +} diff --git a/src/Discord.Net/Entities/WebSocket/CachedGlobalUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGlobalUser.cs new file mode 100644 index 000000000..e07472ae8 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/CachedGlobalUser.cs @@ -0,0 +1,39 @@ +using System; +using Model = Discord.API.User; + +namespace Discord +{ + internal class CachedGlobalUser : User, ICachedUser + { + private ushort _references; + + public new DiscordSocketClient Discord { get { throw new NotSupportedException(); } } + public override UserStatus Status => UserStatus.Unknown;// _status; + public override Game Game => null; //_game; + + public CachedGlobalUser(Model model) + : base(model) + { + } + + public void AddRef() + { + checked + { + lock (this) + _references++; + } + } + public void RemoveRef(DiscordSocketClient discord) + { + lock (this) + { + if (--_references == 0) + discord.RemoveUser(Id); + } + } + + public CachedGlobalUser Clone() => MemberwiseClone() as CachedGlobalUser; + ICachedUser ICachedUser.Clone() => Clone(); + } +} diff --git a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs index 427ad6699..294752d64 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedGuildUser.cs @@ -10,7 +10,7 @@ namespace Discord public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public new CachedGuild Guild => base.Guild as CachedGuild; - public new CachedPublicUser User => base.User as CachedPublicUser; + public new CachedGlobalUser User => base.User as CachedGlobalUser; public override Game Game => _game; public override UserStatus Status => _status; @@ -21,11 +21,11 @@ namespace Discord public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; public CachedVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; - public CachedGuildUser(CachedGuild guild, CachedPublicUser user, Model model) + public CachedGuildUser(CachedGuild guild, CachedGlobalUser user, Model model) : base(guild, user, model) { } - public CachedGuildUser(CachedGuild guild, CachedPublicUser user, PresenceModel model) + public CachedGuildUser(CachedGuild guild, CachedGlobalUser user, PresenceModel model) : base(guild, user, model) { } diff --git a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs b/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs deleted file mode 100644 index 915c897a4..000000000 --- a/src/Discord.Net/Entities/WebSocket/CachedPublicUser.cs +++ /dev/null @@ -1,75 +0,0 @@ -using ChannelModel = Discord.API.Channel; -using Model = Discord.API.User; -using PresenceModel = Discord.API.Presence; - -namespace Discord -{ - internal class CachedPublicUser : User, ICachedUser - { - //TODO: Fix removed game/status (add CachedDMUser?) - private int _references; - //private Game? _game; - //private UserStatus _status; - - public CachedDMChannel DMChannel { get; private set; } - - public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; - public override UserStatus Status => UserStatus.Unknown;// _status; - public override Game Game => null; //_game; - - public CachedPublicUser(Model model) - : base(model) - { - } - - public CachedDMChannel AddDMChannel(DiscordSocketClient discord, ChannelModel model) - { - lock (this) - { - var channel = new CachedDMChannel(discord, this, model); - DMChannel = channel; - return channel; - } - } - public CachedDMChannel RemoveDMChannel(ulong id) - { - lock (this) - { - var channel = DMChannel; - if (channel.Id == id) - { - DMChannel = null; - return channel; - } - return null; - } - } - - public void Update(PresenceModel model, UpdateSource source) - { - if (source == UpdateSource.Rest) return; - - //var game = model.Game != null ? new Game(model.Game) : (Game)null; - - //_status = model.Status; - //_game = game; - } - - public void AddRef() - { - lock (this) - _references++; - } - public void RemoveRef(DiscordSocketClient discord) - { - lock (this) - { - if (--_references == 0 && DMChannel == null) - discord.RemoveUser(Id); - } - } - - public CachedPublicUser Clone() => MemberwiseClone() as CachedPublicUser; - ICachedUser ICachedUser.Clone() => Clone(); - } -} diff --git a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs index 410d36e58..7a91a8221 100644 --- a/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs +++ b/src/Discord.Net/Entities/WebSocket/CachedTextChannel.cs @@ -9,7 +9,7 @@ namespace Discord { internal class CachedTextChannel : TextChannel, ICachedGuildChannel, ICachedMessageChannel { - private readonly MessageCache _messages; + private readonly MessageManager _messages; public new DiscordSocketClient Discord => base.Discord as DiscordSocketClient; public new CachedGuild Guild => base.Guild as CachedGuild; @@ -20,7 +20,10 @@ namespace Discord public CachedTextChannel(CachedGuild guild, Model model) : base(guild, model) { - _messages = new MessageCache(Discord, this); + if (Discord.MessageCacheSize > 0) + _messages = new MessageCache(Discord, this); + else + _messages = new MessageManager(Discord, this); } public override Task GetUserAsync(ulong id) => Task.FromResult(GetUser(id)); diff --git a/src/Discord.Net/Entities/WebSocket/MessageCache.cs b/src/Discord.Net/Entities/WebSocket/MessageCache.cs index 7aa99bcd2..0eaee13c3 100644 --- a/src/Discord.Net/Entities/WebSocket/MessageCache.cs +++ b/src/Discord.Net/Entities/WebSocket/MessageCache.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using Discord.Extensions; +using Discord.Extensions; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -9,26 +8,23 @@ using System.Threading.Tasks; namespace Discord { - internal class MessageCache + internal class MessageCache : MessageManager { - private readonly DiscordSocketClient _discord; - private readonly ICachedMessageChannel _channel; private readonly ConcurrentDictionary _messages; private readonly ConcurrentQueue _orderedMessages; private readonly int _size; - public IReadOnlyCollection Messages => _messages.ToReadOnlyCollection(); + public override IReadOnlyCollection Messages => _messages.ToReadOnlyCollection(); public MessageCache(DiscordSocketClient discord, ICachedMessageChannel channel) + : base(discord, channel) { - _discord = discord; - _channel = channel; _size = discord.MessageCacheSize; _messages = new ConcurrentDictionary(1, (int)(_size * 1.05)); _orderedMessages = new ConcurrentQueue(); } - public void Add(CachedMessage message) + public override void Add(CachedMessage message) { if (_messages.TryAdd(message.Id, message)) { @@ -41,21 +37,21 @@ namespace Discord } } - public CachedMessage Remove(ulong id) + public override CachedMessage Remove(ulong id) { CachedMessage msg; _messages.TryRemove(id, out msg); return msg; } - public CachedMessage Get(ulong id) + public override CachedMessage Get(ulong id) { CachedMessage result; if (_messages.TryGetValue(id, out result)) return result; return null; } - public IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + public override IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) { if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); if (limit == 0) return ImmutableArray.Empty; @@ -81,57 +77,12 @@ namespace Discord .ToImmutableArray(); } - public async Task DownloadAsync(ulong id) + public override async Task DownloadAsync(ulong id) { var msg = Get(id); if (msg != null) return msg; - var model = await _discord.ApiClient.GetChannelMessageAsync(_channel.Id, id).ConfigureAwait(false); - if (model != null) - return new CachedMessage(_channel, new User(model.Author.Value), model); - return null; - } - public async Task> DownloadAsync(ulong? fromId, Direction dir, int limit) - { - //TODO: Test heavily, especially the ordering of messages - if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); - if (limit == 0) return ImmutableArray.Empty; - - var cachedMessages = GetMany(fromId, dir, limit); - if (cachedMessages.Count == limit) - return cachedMessages; - else if (cachedMessages.Count > limit) - return cachedMessages.Skip(cachedMessages.Count - limit).ToImmutableArray(); - else - { - Optional relativeId; - if (cachedMessages.Count == 0) - relativeId = fromId ?? new Optional(); - else - relativeId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Count - 1].Id; - var args = new GetChannelMessagesParams - { - Limit = limit - cachedMessages.Count, - RelativeDirection = dir, - RelativeMessageId = relativeId - }; - var downloadedMessages = await _discord.ApiClient.GetChannelMessagesAsync(_channel.Id, args).ConfigureAwait(false); - - var guild = (_channel as ICachedGuildChannel).Guild; - return cachedMessages.Concat(downloadedMessages.Select(x => - { - IUser user = _channel.GetUser(x.Author.Value.Id, true); - if (user == null) - { - var newUser = new User(x.Author.Value); - if (guild != null) - user = new GuildUser(guild, newUser); - else - user = newUser; - } - return new CachedMessage(_channel, user, x); - })).ToImmutableArray(); - } + return await base.DownloadAsync(id).ConfigureAwait(false); } } } diff --git a/src/Discord.Net/Entities/WebSocket/MessageManager.cs b/src/Discord.Net/Entities/WebSocket/MessageManager.cs new file mode 100644 index 000000000..98fde21b0 --- /dev/null +++ b/src/Discord.Net/Entities/WebSocket/MessageManager.cs @@ -0,0 +1,81 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord +{ + internal class MessageManager + { + private readonly DiscordSocketClient _discord; + private readonly ICachedMessageChannel _channel; + + public virtual IReadOnlyCollection Messages + => ImmutableArray.Create(); + + public MessageManager(DiscordSocketClient discord, ICachedMessageChannel channel) + { + _discord = discord; + _channel = channel; + } + + public virtual void Add(CachedMessage message) { } + public virtual CachedMessage Remove(ulong id) => null; + public virtual CachedMessage Get(ulong id) => null; + + public virtual IImmutableList GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + => ImmutableArray.Create(); + + public virtual async Task DownloadAsync(ulong id) + { + var model = await _discord.ApiClient.GetChannelMessageAsync(_channel.Id, id).ConfigureAwait(false); + if (model != null) + return new CachedMessage(_channel, new User(model.Author.Value), model); + return null; + } + public async Task> DownloadAsync(ulong? fromId, Direction dir, int limit) + { + //TODO: Test heavily, especially the ordering of messages + if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); + if (limit == 0) return ImmutableArray.Empty; + + var cachedMessages = GetMany(fromId, dir, limit); + if (cachedMessages.Count == limit) + return cachedMessages; + else if (cachedMessages.Count > limit) + return cachedMessages.Skip(cachedMessages.Count - limit).ToImmutableArray(); + else + { + Optional relativeId; + if (cachedMessages.Count == 0) + relativeId = fromId ?? new Optional(); + else + relativeId = dir == Direction.Before ? cachedMessages[0].Id : cachedMessages[cachedMessages.Count - 1].Id; + var args = new GetChannelMessagesParams + { + Limit = limit - cachedMessages.Count, + RelativeDirection = dir, + RelativeMessageId = relativeId + }; + var downloadedMessages = await _discord.ApiClient.GetChannelMessagesAsync(_channel.Id, args).ConfigureAwait(false); + + var guild = (_channel as ICachedGuildChannel).Guild; + return cachedMessages.Concat(downloadedMessages.Select(x => + { + IUser user = _channel.GetUser(x.Author.Value.Id, true); + if (user == null) + { + var newUser = new User(x.Author.Value); + if (guild != null) + user = new GuildUser(guild, newUser); + else + user = newUser; + } + return new CachedMessage(_channel, user, x); + })).ToImmutableArray(); + } + } + } +}