Make mention lookup way faster with a lot of members

This commit is contained in:
Vankka 2024-08-10 14:40:46 +03:00
parent 189d8375cd
commit c4eb9b56e8
No known key found for this signature in database
GPG Key ID: 62E48025ED4E7EBB
3 changed files with 54 additions and 56 deletions

View File

@ -43,29 +43,30 @@ import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameE
import net.dv8tion.jda.api.events.role.RoleCreateEvent; import net.dv8tion.jda.api.events.role.RoleCreateEvent;
import net.dv8tion.jda.api.events.role.RoleDeleteEvent; import net.dv8tion.jda.api.events.role.RoleDeleteEvent;
import net.dv8tion.jda.api.events.role.update.RoleUpdateNameEvent; import net.dv8tion.jda.api.events.role.update.RoleUpdateNameEvent;
import net.kyori.adventure.text.Component; import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class MentionCachingModule extends AbstractModule<DiscordSRV> { public class MentionCachingModule extends AbstractModule<DiscordSRV> {
private static final Pattern USER_MENTION_PATTERN = Pattern.compile("(?<!<)@[a-z0-9_.]{2,32}"); private static final Pattern USER_MENTION_PATTERN = Pattern.compile("(?<!<)@([a-z0-9_.]{2,32})");
private final Map<Long, Map<Long, CachedMention>> memberMentions = new ConcurrentHashMap<>();
private final Map<Long, Cache<String, CachedMention>> memberMentionsCache = new ConcurrentHashMap<>();
private final Cache<Pair<Long, Long>, CachedMention> memberMentionsCache;
private final Map<Long, Map<Long, CachedMention>> roleMentions = new ConcurrentHashMap<>(); private final Map<Long, Map<Long, CachedMention>> roleMentions = new ConcurrentHashMap<>();
private final Map<Long, Map<Long, CachedMention>> channelMentions = new ConcurrentHashMap<>(); private final Map<Long, Map<Long, CachedMention>> channelMentions = new ConcurrentHashMap<>();
public MentionCachingModule(DiscordSRV discordSRV) { public MentionCachingModule(DiscordSRV discordSRV) {
super(discordSRV); super(discordSRV);
this.memberMentionsCache = discordSRV.caffeineBuilder()
.expireAfterWrite(Duration.ofMinutes(5))
.build();
} }
@Override @Override
@ -108,7 +109,7 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
@Override @Override
public void disable() { public void disable() {
memberMentions.clear(); memberMentionsCache.invalidateAll();
roleMentions.clear(); roleMentions.clear();
channelMentions.clear(); channelMentions.clear();
} }
@ -117,32 +118,31 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
MinecraftToDiscordChatConfig.Mentions config, MinecraftToDiscordChatConfig.Mentions config,
Guild guild, Guild guild,
IPlayer player, IPlayer player,
Component message String messageContent
) { ) {
List<CachedMention> mentions = new ArrayList<>();
if (config.users) {
mentions.addAll(getMemberMentions(guild).values());
}
List<CompletableFuture<List<CachedMention>>> futures = new ArrayList<>(); List<CompletableFuture<List<CachedMention>>> futures = new ArrayList<>();
if (config.users && config.uncachedUsers && player.hasPermission(Permission.MENTION_USER_LOOKUP)) { List<CachedMention> mentions = new ArrayList<>();
String messageContent = discordSRV.componentFactory().plainSerializer().serialize(message);
if (config.users) {
boolean uncached = config.uncachedUsers && player.hasPermission(Permission.MENTION_USER_LOOKUP);
Matcher matcher = USER_MENTION_PATTERN.matcher(messageContent); Matcher matcher = USER_MENTION_PATTERN.matcher(messageContent);
while (matcher.find()) { while (matcher.find()) {
String mention = matcher.group(); String username = matcher.group(1);
boolean perfectMatch = false;
for (CachedMention cachedMention : mentions) { List<Member> members = guild.getMembersByName(username, false);
if (cachedMention.search().matcher(mention).matches()) { boolean any = false;
perfectMatch = true; for (Member member : members) {
break; mentions.add(getMemberMention(member));
} any = true;
break;
} }
if (!perfectMatch) {
futures.add(lookupMemberMentions(guild, mention)); if (!any && uncached) {
futures.add(lookupMemberMentions(guild, username));
} }
} }
} }
if (config.roles) { if (config.roles) {
mentions.addAll(getRoleMentions(guild).values()); mentions.addAll(getRoleMentions(guild).values());
} }
@ -163,8 +163,7 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
@Subscribe @Subscribe
public void onGuildDelete(GuildLeaveEvent event) { public void onGuildDelete(GuildLeaveEvent event) {
long guildId = event.getGuild().getIdLong(); long guildId = event.getGuild().getIdLong();
memberMentions.remove(guildId); memberMentionsCache.asMap().keySet().removeIf(pair -> pair.getKey() == guildId);
memberMentionsCache.remove(guildId);
roleMentions.remove(guildId); roleMentions.remove(guildId);
channelMentions.remove(guildId); channelMentions.remove(guildId);
} }
@ -173,38 +172,29 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
// Member // Member
// //
private CompletableFuture<List<CachedMention>> lookupMemberMentions(Guild guild, String mention) { private CompletableFuture<List<CachedMention>> lookupMemberMentions(Guild guild, String username) {
Cache<String, CachedMention> cache = memberMentionsCache.computeIfAbsent(guild.getIdLong(), key -> discordSRV.caffeineBuilder()
.expireAfterAccess(10, TimeUnit.MINUTES)
.build()
);
CachedMention cached = cache.getIfPresent(mention);
if (cached != null) {
return CompletableFuture.completedFuture(Collections.singletonList(cached));
}
CompletableFuture<List<Member>> memberFuture = new CompletableFuture<>(); CompletableFuture<List<Member>> memberFuture = new CompletableFuture<>();
guild.retrieveMembersByPrefix(mention.substring(1), 100) guild.retrieveMembersByPrefix(username, 100)
.onSuccess(memberFuture::complete).onError(memberFuture::completeExceptionally); .onSuccess(memberFuture::complete).onError(memberFuture::completeExceptionally);
return memberFuture.thenApply(members -> { return memberFuture.thenApply(members -> {
List<CachedMention> cachedMentions = new ArrayList<>(); List<CachedMention> cachedMentions = new ArrayList<>();
for (Member member : members) { for (Member member : members) {
CachedMention cachedMention = cache.get(member.getUser().getName(), k -> convertMember(member)); cachedMentions.add(getMemberMention(member));
cachedMentions.add(cachedMention);
} }
return cachedMentions; return cachedMentions;
}); });
} }
private Map<Long, CachedMention> getMemberMentions(Guild guild) { private Pair<Long, Long> getMemberKey(Member member) {
return memberMentions.computeIfAbsent(guild.getIdLong(), key -> { return Pair.of(member.getGuild().getIdLong(), member.getIdLong());
Map<Long, CachedMention> mentions = new LinkedHashMap<>(); }
for (Member member : guild.getMembers()) {
mentions.put(member.getIdLong(), convertMember(member)); private CachedMention getMemberMention(Member member) {
} return memberMentionsCache.get(
return mentions; getMemberKey(member),
}); key -> convertMember(member)
);
} }
private CachedMention convertMember(Member member) { private CachedMention convertMember(Member member) {
@ -224,13 +214,18 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
return; return;
} }
getMemberMentions(event.getGuild()).put(member.getIdLong(), convertMember(member)); memberMentionsCache.put(getMemberKey(member), convertMember(member));
} }
@Subscribe @Subscribe
public void onMemberUpdate(GuildMemberUpdateNicknameEvent event) { public void onMemberUpdate(GuildMemberUpdateNicknameEvent event) {
Member member = event.getMember(); Member member = event.getMember();
getMemberMentions(event.getGuild()).replace(member.getIdLong(), convertMember(member)); if (member.getGuild().getMemberCache().getElementById(member.getIdLong()) == null) {
// Member is not cached
return;
}
memberMentionsCache.put(getMemberKey(member), convertMember(member));
} }
@Subscribe @Subscribe
@ -240,7 +235,7 @@ public class MentionCachingModule extends AbstractModule<DiscordSRV> {
return; return;
} }
getMemberMentions(event.getGuild()).remove(member.getIdLong()); memberMentionsCache.invalidate(getMemberKey(member));
} }
// //

View File

@ -86,7 +86,9 @@ public class MentionGameRenderingModule extends AbstractModule<DiscordSRV> {
guilds.add(channel.getGuild()); guilds.add(channel.getGuild());
} }
Component component = ComponentUtil.fromAPI(event.getMessage()); Component message = ComponentUtil.fromAPI(event.getMessage());
String messageContent = discordSRV.componentFactory().plainSerializer().serialize(message);
List<CachedMention> cachedMentions = new ArrayList<>(); List<CachedMention> cachedMentions = new ArrayList<>();
for (DiscordGuild guild : guilds) { for (DiscordGuild guild : guilds) {
cachedMentions.addAll( cachedMentions.addAll(
@ -94,7 +96,7 @@ public class MentionGameRenderingModule extends AbstractModule<DiscordSRV> {
config.minecraftToDiscord.mentions, config.minecraftToDiscord.mentions,
guild.asJDA(), guild.asJDA(),
(IPlayer) event.getPlayer(), (IPlayer) event.getPlayer(),
component messageContent
).join() ).join()
); );
} }
@ -103,13 +105,13 @@ public class MentionGameRenderingModule extends AbstractModule<DiscordSRV> {
DiscordGuild guild = guilds.size() == 1 ? guilds.iterator().next() : null; DiscordGuild guild = guilds.size() == 1 ? guilds.iterator().next() : null;
for (CachedMention cachedMention : cachedMentions) { for (CachedMention cachedMention : cachedMentions) {
component = component.replaceText( message = message.replaceText(
TextReplacementConfig.builder().match(cachedMention.search()) TextReplacementConfig.builder().match(cachedMention.search())
.replacement(() -> replacement(cachedMention, mentionsConfig, guild)) .replacement(() -> replacement(cachedMention, mentionsConfig, guild))
.build() .build()
); );
} }
event.process(ComponentUtil.toAPI(component)); event.process(ComponentUtil.toAPI(message));
} }
private Component replacement(CachedMention mention, MentionsConfig config, DiscordGuild guild) { private Component replacement(CachedMention mention, MentionsConfig config, DiscordGuild guild) {

View File

@ -121,7 +121,8 @@ public class MinecraftToDiscordChatModule extends AbstractGameMessageModule<Mine
) { ) {
MentionCachingModule mentionCaching = discordSRV.getModule(MentionCachingModule.class); MentionCachingModule mentionCaching = discordSRV.getModule(MentionCachingModule.class);
if (mentionCaching != null) { if (mentionCaching != null) {
return mentionCaching.lookup(config.mentions, guild, player, message) String messageContent = discordSRV.componentFactory().plainSerializer().serialize(message);
return mentionCaching.lookup(config.mentions, guild, player, messageContent)
.thenApply(mentions -> getMessageForGuildWithMentions(config, format, guild, message, player, context, mentions)); .thenApply(mentions -> getMessageForGuildWithMentions(config, format, guild, message, player, context, mentions));
} }