Reduce allocation when checking WordFilter

Nick/OT/HT were ToString()'d each legality analysis as the wordfilter couldn't check ReadOnlySpan<char>
Now with .NET 9, we can use the AlternateLookup feature and now all calls are zero allocation for previously-seen strings.
This commit is contained in:
Kurt 2024-11-12 16:19:47 -06:00
parent 6be99f6ad7
commit 0c210fdd11
3 changed files with 17 additions and 24 deletions

View file

@ -1,5 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
@ -52,7 +52,8 @@ public static class WordFilter
/// <summary>
/// Due to some messages repeating (Trainer names), keep a list of repeated values for faster lookup.
/// </summary>
private static readonly Dictionary<string, string?> Lookup = new(INIT_COUNT);
private static readonly ConcurrentDictionary<string, string?>.AlternateLookup<ReadOnlySpan<char>> Lookup =
new ConcurrentDictionary<string, string?>().GetAlternateLookup<ReadOnlySpan<char>>();
/// <summary>
/// Checks to see if a phrase contains filtered content.
@ -60,45 +61,35 @@ public static class WordFilter
/// <param name="message">Phrase to check for</param>
/// <param name="regMatch">Matching regex that filters the phrase.</param>
/// <returns>Boolean result if the message is filtered or not.</returns>
public static bool IsFiltered(string message, [NotNullWhen(true)] out string? regMatch)
public static bool IsFiltered(ReadOnlySpan<char> message, [NotNullWhen(true)] out string? regMatch)
{
if (string.IsNullOrWhiteSpace(message) || message.Length <= 1)
if (message.IsWhiteSpace() || message.Length <= 1)
{
regMatch = null;
return false;
}
// Check dictionary
lock (dictLock)
{
if (Lookup.TryGetValue(message, out regMatch))
return regMatch != null;
}
if (Lookup.TryGetValue(message, out regMatch))
return regMatch != null;
// Make the string lowercase invariant
Span<char> lowercase = stackalloc char[message.Length];
for (int i = 0; i < lowercase.Length; i++)
lowercase[i] = char.ToLowerInvariant(message[i]);
message.ToLowerInvariant(lowercase);
// not in dictionary, check patterns
if (TryMatch(lowercase, out regMatch))
{
lock (dictLock)
Lookup[message] = regMatch;
Lookup.TryAdd(message, regMatch);
return true;
}
// didn't match any pattern, cache result
lock (dictLock)
{
if ((Lookup.Count & ~MAX_COUNT) != 0)
Lookup.Clear(); // reset
Lookup[message] = regMatch = null;
}
if ((Lookup.Dictionary.Count & ~MAX_COUNT) != 0)
Lookup.Dictionary.Clear(); // reset
Lookup.TryAdd(message, regMatch = null);
return false;
}
private static readonly object dictLock = new();
private const int MAX_COUNT = (1 << 17) - 1; // arbitrary cap for max dictionary size
private const int INIT_COUNT = 1 << 10; // arbitrary init size to limit future doublings
}

View file

@ -67,7 +67,7 @@ public sealed class NicknameVerifier : Verifier
// Non-nicknamed strings have already been checked.
if (ParseSettings.Settings.WordFilter.IsEnabled(pk.Format) && pk.IsNicknamed)
{
if (WordFilter.IsFiltered(nickname.ToString(), out var badPattern))
if (WordFilter.IsFiltered(nickname, out var badPattern))
data.AddLine(GetInvalid($"Word Filter: {badPattern}"));
if (TrainerNameVerifier.ContainsTooManyNumbers(nickname, data.Info.Generation))
data.AddLine(GetInvalid("Word Filter: Too many numbers."));

View file

@ -54,12 +54,14 @@ public sealed class TrainerNameVerifier : Verifier
if (ParseSettings.Settings.WordFilter.IsEnabled(pk.Format))
{
if (WordFilter.IsFiltered(trainer.ToString(), out var badPattern))
if (WordFilter.IsFiltered(trainer, out var badPattern))
data.AddLine(GetInvalid($"Word Filter: {badPattern}"));
if (ContainsTooManyNumbers(trainer, data.Info.Generation))
data.AddLine(GetInvalid("Word Filter: Too many numbers."));
if (WordFilter.IsFiltered(pk.HandlingTrainerName, out badPattern))
Span<char> ht = stackalloc char[pk.TrashCharCountTrainer];
int nameLen = pk.LoadString(pk.HandlingTrainerTrash, ht);
if (WordFilter.IsFiltered(ht[..nameLen], out badPattern))
data.AddLine(GetInvalid($"Word Filter: {badPattern}"));
}
}