From b4696f72368f5cd9b149db529f76fe9a53bb6f92 Mon Sep 17 00:00:00 2001 From: ScrubN <72096833+ScrubN@users.noreply.github.com> Date: Fri, 19 Jan 2024 02:42:09 -0500 Subject: [PATCH] Use binary search for emotes/badges/cheermotes and reduce GetKeyName allocations --- TwitchDownloaderCore/ChatRenderer.cs | 132 +++++++++++++++++++-------- 1 file changed, 94 insertions(+), 38 deletions(-) diff --git a/TwitchDownloaderCore/ChatRenderer.cs b/TwitchDownloaderCore/ChatRenderer.cs index c3858b49..850aec93 100644 --- a/TwitchDownloaderCore/ChatRenderer.cs +++ b/TwitchDownloaderCore/ChatRenderer.cs @@ -652,17 +652,9 @@ private SKBitmap CombineImages(List<(SKImageInfo info, SKBitmap bitmap)> section private static string GetKeyName(IEnumerable codepoints) { - List codepointList = new List(); - foreach (Codepoint codepoint in codepoints) - { - if (codepoint.Value != 0xFE0F) - { - codepointList.Add(codepoint.Value.ToString("X")); - } - } + var codepointList = from codepoint in codepoints where codepoint.Value != 0xFE0F select codepoint.Value.ToString("X"); - string emojiKey = string.Join(' ', codepointList); - return emojiKey; + return string.Join(' ', codepointList); } private void DrawNonAccentedMessage(Comment comment, List<(SKImageInfo info, SKBitmap bitmap)> sectionImages, List<(Point, TwitchEmote)> emotePositionList, bool highlightWords, ref Point drawPos, ref Point defaultPos) @@ -917,15 +909,28 @@ private void DrawFragmentPart(List<(SKImageInfo info, SKBitmap bitmap)> sectionI static bool TryGetTwitchEmote(List twitchEmoteList, ReadOnlySpan emoteName, [NotNullWhen(true)] out TwitchEmote twitchEmote) { - // Enumerating over a span is faster than a list var emoteListSpan = CollectionsMarshal.AsSpan(twitchEmoteList); - foreach (var emote1 in emoteListSpan) + var lo = 0; + var hi = emoteListSpan.Length - 1; + while (lo <= hi) { - if (emote1.Name.AsSpan().SequenceEqual(emoteName)) + var i = lo + ((hi - lo) >> 1); + var order = emoteListSpan[i].Name.AsSpan().CompareTo(emoteName, StringComparison.Ordinal); + + if (order == 0) { - twitchEmote = emote1; + twitchEmote = emoteListSpan[i]; return true; } + + if (order < 0) + { + lo = i + 1; + } + else + { + hi = i - 1; + } } twitchEmote = null; @@ -1179,15 +1184,28 @@ private void DrawRegularMessage(List<(SKImageInfo info, SKBitmap bitmap)> sectio static bool TryGetCheerEmote(List cheerEmoteList, ReadOnlySpan prefix, [NotNullWhen(true)] out CheerEmote cheerEmote) { - // Enumerating over a span is faster than a list - var cheerEmoteListSpan = CollectionsMarshal.AsSpan(cheerEmoteList); - foreach (var emote1 in cheerEmoteListSpan) + var emoteListSpan = CollectionsMarshal.AsSpan(cheerEmoteList); + var lo = 0; + var hi = emoteListSpan.Length - 1; + while (lo <= hi) { - if (emote1.prefix.AsSpan().Equals(prefix, StringComparison.OrdinalIgnoreCase)) + var i = lo + ((hi - lo) >> 1); + var order = emoteListSpan[i].prefix.AsSpan().CompareTo(prefix, StringComparison.Ordinal); + + if (order == 0) { - cheerEmote = emote1; + cheerEmote = emoteListSpan[i]; return true; } + + if (order < 0) + { + lo = i + 1; + } + else + { + hi = i - 1; + } } cheerEmote = null; @@ -1228,15 +1246,28 @@ private void DrawFirstPartyEmote(List<(SKImageInfo info, SKBitmap bitmap)> secti static bool TryGetTwitchEmote(List twitchEmoteList, ReadOnlySpan emoteId, [NotNullWhen(true)] out TwitchEmote twitchEmote) { - // Enumerating over a span is faster than a list var emoteListSpan = CollectionsMarshal.AsSpan(twitchEmoteList); - foreach (var emote1 in emoteListSpan) + var lo = 0; + var hi = emoteListSpan.Length - 1; + while (lo <= hi) { - if (emote1.Id.AsSpan().SequenceEqual(emoteId)) + var i = lo + ((hi - lo) >> 1); + var order = emoteListSpan[i].Id.AsSpan().CompareTo(emoteId, StringComparison.Ordinal); + + if (order == 0) { - twitchEmote = emote1; + twitchEmote = emoteListSpan[i]; return true; } + + if (order < 0) + { + lo = i + 1; + } + else + { + hi = i - 1; + } } twitchEmote = null; @@ -1460,29 +1491,49 @@ private void DrawBadges(Comment comment, List<(SKImageInfo info, SKBitmap bitmap foreach (var badge in comment.message.user_badges) { - string id = badge._id; - string version = badge.version; + var id = badge._id; + var version = badge.version; - foreach (var cachedBadge in badgeList) + if (!TryGetBadge(badgeList, id, out var cachedBadge)) + continue; + + if (!cachedBadge.Versions.TryGetValue(version, out var badgeBitmap)) + continue; + + returnList.Add((badgeBitmap, cachedBadge.Type)); + } + + return returnList; + + static bool TryGetBadge(List badgeList, ReadOnlySpan badgeName, [NotNullWhen(true)] out ChatBadge badge) + { + var badgeSpan = CollectionsMarshal.AsSpan(badgeList); + var lo = 0; + var hi = badgeSpan.Length - 1; + while (lo <= hi) { - if (cachedBadge.Name != id) - continue; + var i = lo + ((hi - lo) >> 1); + var order = badgeSpan[i].Name.AsSpan().CompareTo(badgeName, StringComparison.Ordinal); - foreach (var cachedVersion in cachedBadge.Versions) + if (order == 0) { - if (cachedVersion.Key == version) - { - returnList.Add((cachedVersion.Value, cachedBadge.Type)); - goto NextUserBadge; - } + badge = badgeSpan[i]; + return true; + } + + if (order < 0) + { + lo = i + 1; + } + else + { + hi = i - 1; } } - // goto is cheaper and more readable than using a boolean + branch check after each operation - NextUserBadge: ; + badge = null; + return false; } - - return returnList; } private void DrawTimestamp(Comment comment, List<(SKImageInfo info, SKBitmap bitmap)> sectionImages, ref Point drawPos, ref Point defaultPos) @@ -1557,6 +1608,11 @@ private async Task FetchScaledImages(CancellationToken cancellationToken) emoteThirdList = emoteThirdTask.Result; cheermotesList = cheerTask.Result; emojiCache = emojiTask.Result; + + badgeList.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + emoteList.Sort((a, b) => string.Compare(a.Id, b.Id, StringComparison.Ordinal)); + emoteThirdList.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + cheermotesList.Sort((a, b) => string.Compare(a.prefix, b.prefix, StringComparison.Ordinal)); } private async Task> GetScaledBadges(CancellationToken cancellationToken)