diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bce2526..6b24b485 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Minor: Added moderation buttons to search popup when searching in a split with moderation mode enabled. (#2148, #2803) - Minor: Made "#channel" in `/mentions` tab show in usercards and in the search popup. (#2802) - Minor: Added settings to disable custom FrankerFaceZ VIP/mod badges. (#2693, #2759) +- Minor: Limit the number of recent chatters to improve memory usage and reduce freezes. (#2796, #2814) - Minor: Added `/popout` command. Usage: `/popout [channel]`. It opens browser chat for the provided channel. Can also be used without arguments to open current channels browser chat. (#2556, #2812) - Bugfix: Fixed FFZ emote links for global emotes (#2807, #2808) diff --git a/chatterino.pro b/chatterino.pro index 2de1bda4..2dfee10d 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -127,6 +127,7 @@ SOURCES += \ src/common/Args.cpp \ src/common/Channel.cpp \ src/common/ChannelChatters.cpp \ + src/common/ChatterSet.cpp \ src/common/ChatterinoSetting.cpp \ src/common/CompletionModel.cpp \ src/common/Credentials.cpp \ @@ -139,7 +140,6 @@ SOURCES += \ src/common/NetworkPrivate.cpp \ src/common/NetworkRequest.cpp \ src/common/NetworkResult.cpp \ - src/common/UsernameSet.cpp \ src/common/Version.cpp \ src/common/WindowDescriptors.cpp \ src/common/QLogging.cpp \ @@ -340,6 +340,7 @@ HEADERS += \ src/common/Atomic.hpp \ src/common/Channel.hpp \ src/common/ChannelChatters.hpp \ + src/common/ChatterSet.hpp \ src/common/ChatterinoSetting.hpp \ src/common/Common.hpp \ src/common/CompletionModel.hpp \ @@ -363,7 +364,6 @@ HEADERS += \ src/common/SignalVectorModel.hpp \ src/common/Singleton.hpp \ src/common/UniqueAccess.hpp \ - src/common/UsernameSet.hpp \ src/common/Version.hpp \ src/common/QLogging.hpp \ src/controllers/accounts/Account.hpp \ diff --git a/lib/lrucache/.clang-format b/lib/lrucache/.clang-format new file mode 100644 index 00000000..f34c1465 --- /dev/null +++ b/lib/lrucache/.clang-format @@ -0,0 +1,35 @@ +Language: Cpp + +AccessModifierOffset: -4 +AlignEscapedNewlinesLeft: true +AllowShortFunctionsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: false +AllowShortLambdasOnASingleLine: Empty +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: false +AlwaysBreakBeforeMultilineStrings: false +BasedOnStyle: Google +BraceWrapping: { + AfterClass: 'true' + AfterControlStatement: 'true' + AfterFunction: 'true' + AfterNamespace: 'false' + BeforeCatch: 'true' + BeforeElse: 'true' +} +BreakBeforeBraces: Custom +BreakConstructorInitializersBeforeComma: true +ColumnLimit: 80 +ConstructorInitializerAllOnOneLineOrOnePerLine: false +DerivePointerBinding: false +FixNamespaceComments: true +IndentCaseLabels: true +IndentWidth: 4 +IndentWrappedFunctionNames: true +IndentPPDirectives: AfterHash +IncludeBlocks: Preserve +NamespaceIndentation: Inner +PointerBindsToType: false +SpacesBeforeTrailingComments: 2 +Standard: Auto +ReflowComments: false diff --git a/lib/lrucache/lrucache/lrucache.hpp b/lib/lrucache/lrucache/lrucache.hpp index 9e5e2850..aa2c6c51 100644 --- a/lib/lrucache/lrucache/lrucache.hpp +++ b/lib/lrucache/lrucache/lrucache.hpp @@ -1,72 +1,115 @@ -/* +/* * File: lrucache.hpp - * Author: Alexander Ponomarev + * Original Author: Alexander Ponomarev * * Created on June 20, 2013, 5:09 PM */ #ifndef _LRUCACHE_HPP_INCLUDED_ -#define _LRUCACHE_HPP_INCLUDED_ +#define _LRUCACHE_HPP_INCLUDED_ -#include -#include #include +#include #include +#include namespace cache { -template -class lru_cache { +template +class lru_cache +{ public: - typedef typename std::pair key_value_pair_t; - typedef typename std::list::iterator list_iterator_t; + typedef typename std::pair key_value_pair_t; + typedef typename std::list::iterator list_iterator_t; + + lru_cache(size_t max_size) + : _max_size(max_size) + { + } + + // copy doesn't make sense since we reference the linked list elements directly + lru_cache(lru_cache &) = delete; + lru_cache &operator=(lru_cache &) = delete; + + // move + lru_cache(lru_cache &&other) + : _cache_items_list(std::move(other._cache_items_list)) + , _cache_items_map(std::move(other._cache_items_map)) + { + other._cache_items_list.clear(); + other._cache_items_map.clear(); + } + + lru_cache &operator=(lru_cache &&other) + { + _cache_items_list = std::move(other._cache_items_list); + _cache_items_map = std::move(other._cache_items_map); + other._cache_items_list.clear(); + other._cache_items_map.clear(); + return *this; + } + + void put(const key_t &key, const value_t &value) + { + auto it = _cache_items_map.find(key); + _cache_items_list.push_front(key_value_pair_t(key, value)); + if (it != _cache_items_map.end()) + { + _cache_items_list.erase(it->second); + _cache_items_map.erase(it); + } + _cache_items_map[key] = _cache_items_list.begin(); + + if (_cache_items_map.size() > _max_size) + { + auto last = _cache_items_list.end(); + last--; + _cache_items_map.erase(last->first); + _cache_items_list.pop_back(); + } + } + + const value_t &get(const key_t &key) + { + auto it = _cache_items_map.find(key); + if (it == _cache_items_map.end()) + { + throw std::range_error("There is no such key in cache"); + } + else + { + _cache_items_list.splice(_cache_items_list.begin(), + _cache_items_list, it->second); + return it->second->second; + } + } + + bool exists(const key_t &key) const + { + return _cache_items_map.find(key) != _cache_items_map.end(); + } + + size_t size() const + { + return _cache_items_map.size(); + } + + auto begin() const + { + return _cache_items_list.begin(); + } + + auto end() const + { + return _cache_items_list.end(); + } - lru_cache(size_t max_size) : - _max_size(max_size) { - } - - void put(const key_t& key, const value_t& value) { - auto it = _cache_items_map.find(key); - _cache_items_list.push_front(key_value_pair_t(key, value)); - if (it != _cache_items_map.end()) { - _cache_items_list.erase(it->second); - _cache_items_map.erase(it); - } - _cache_items_map[key] = _cache_items_list.begin(); - - if (_cache_items_map.size() > _max_size) { - auto last = _cache_items_list.end(); - last--; - _cache_items_map.erase(last->first); - _cache_items_list.pop_back(); - } - } - - const value_t& get(const key_t& key) { - auto it = _cache_items_map.find(key); - if (it == _cache_items_map.end()) { - throw std::range_error("There is no such key in cache"); - } else { - _cache_items_list.splice(_cache_items_list.begin(), _cache_items_list, it->second); - return it->second->second; - } - } - - bool exists(const key_t& key) const { - return _cache_items_map.find(key) != _cache_items_map.end(); - } - - size_t size() const { - return _cache_items_map.size(); - } - private: - std::list _cache_items_list; - std::unordered_map _cache_items_map; - size_t _max_size; + std::list _cache_items_list; + std::unordered_map _cache_items_map; + size_t _max_size; }; -} // namespace cache - -#endif /* _LRUCACHE_HPP_INCLUDED_ */ +} // namespace cache +#endif /* _LRUCACHE_HPP_INCLUDED_ */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 04cbc19b..e71eceec 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,8 @@ set(SOURCE_FILES main.cpp common/ChannelChatters.hpp common/ChatterinoSetting.cpp common/ChatterinoSetting.hpp + common/ChatterSet.cpp + common/ChatterSet.hpp common/CompletionModel.cpp common/CompletionModel.hpp common/Credentials.cpp @@ -42,8 +44,6 @@ set(SOURCE_FILES main.cpp common/NetworkResult.hpp common/QLogging.cpp common/QLogging.hpp - common/UsernameSet.cpp - common/UsernameSet.hpp common/Version.cpp common/Version.hpp common/WindowDescriptors.cpp diff --git a/src/common/ChannelChatters.cpp b/src/common/ChannelChatters.cpp index c3ae0723..8b72f7b1 100644 --- a/src/common/ChannelChatters.cpp +++ b/src/common/ChannelChatters.cpp @@ -11,14 +11,15 @@ ChannelChatters::ChannelChatters(Channel &channel) { } -SharedAccessGuard ChannelChatters::accessChatters() const +SharedAccessGuard ChannelChatters::accessChatters() const { return this->chatters_.accessConst(); } void ChannelChatters::addRecentChatter(const QString &user) { - this->chatters_.access()->insert(user); + auto chatters = this->chatters_.access(); + chatters->addRecentChatter(user); } void ChannelChatters::addJoinedUser(const QString &user) @@ -66,9 +67,11 @@ void ChannelChatters::addPartedUser(const QString &user) } } -void ChannelChatters::setChatters(UsernameSet &&set) +void ChannelChatters::updateOnlineChatters( + const std::unordered_set &chatters) { - this->chatters_.access()->merge(std::move(set)); + auto chatters_ = this->chatters_.access(); + chatters_->updateOnlineChatters(chatters); } const QColor ChannelChatters::getUserColor(const QString &user) diff --git a/src/common/ChannelChatters.hpp b/src/common/ChannelChatters.hpp index ed135b2a..3a2c4e49 100644 --- a/src/common/ChannelChatters.hpp +++ b/src/common/ChannelChatters.hpp @@ -1,11 +1,10 @@ #pragma once #include "common/Channel.hpp" +#include "common/ChatterSet.hpp" #include "common/UniqueAccess.hpp" -#include "common/UsernameSet.hpp" -#include "util/QStringHash.hpp" - #include "lrucache/lrucache.hpp" +#include "util/QStringHash.hpp" #include @@ -17,14 +16,14 @@ public: ChannelChatters(Channel &channel); virtual ~ChannelChatters() = default; // add vtable - SharedAccessGuard accessChatters() const; + SharedAccessGuard accessChatters() const; void addRecentChatter(const QString &user); void addJoinedUser(const QString &user); void addPartedUser(const QString &user); - void setChatters(UsernameSet &&set); const QColor getUserColor(const QString &user); void setUserColor(const QString &user, const QColor &color); + void updateOnlineChatters(const std::unordered_set &chatters); private: static constexpr int maxChatterColorCount = 5000; @@ -32,7 +31,7 @@ private: Channel &channel_; // maps 2 char prefix to set of names - UniqueAccess chatters_; + UniqueAccess chatters_; UniqueAccess> chatterColors_; // combines multiple joins/parts into one message diff --git a/src/common/ChatterSet.cpp b/src/common/ChatterSet.cpp new file mode 100644 index 00000000..b11b1dc7 --- /dev/null +++ b/src/common/ChatterSet.cpp @@ -0,0 +1,58 @@ +#include "common/ChatterSet.hpp" + +#include +#include "debug/Benchmark.hpp" + +namespace chatterino { + +ChatterSet::ChatterSet() + : items(chatterLimit) +{ +} + +void ChatterSet::addRecentChatter(const QString &userName) +{ + this->items.put(userName.toLower(), userName); +} + +void ChatterSet::updateOnlineChatters( + const std::unordered_set &lowerCaseUsernames) +{ + BenchmarkGuard bench("update online chatters"); + + // Create a new lru cache without the users that are not present anymore. + cache::lru_cache tmp(chatterLimit); + + for (auto &&chatter : lowerCaseUsernames) + { + if (this->items.exists(chatter)) + tmp.put(chatter, this->items.get(chatter)); + + // Less chatters than the limit => try to preserve as many as possible. + else if (lowerCaseUsernames.size() < chatterLimit) + tmp.put(chatter, chatter); + } + + this->items = std::move(tmp); +} + +bool ChatterSet::contains(const QString &userName) const +{ + return this->items.exists(userName.toLower()); +} + +std::vector ChatterSet::filterByPrefix(const QString &prefix) const +{ + QString lowerPrefix = prefix.toLower(); + std::vector result; + + for (auto &&item : this->items) + { + if (item.first.startsWith(lowerPrefix)) + result.push_back(item.second); + } + + return result; +} + +} // namespace chatterino diff --git a/src/common/ChatterSet.hpp b/src/common/ChatterSet.hpp new file mode 100644 index 00000000..bf21c4f0 --- /dev/null +++ b/src/common/ChatterSet.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "lrucache/lrucache.hpp" +#include "util/QStringHash.hpp" + +namespace chatterino { + +/// ChatterSet is a limited container that contains a list of recent chatters +/// that can be referenced by name. +class ChatterSet +{ +public: + /// The limit of how many chatters can be saved for a channel. + static constexpr size_t chatterLimit = 2000; + + ChatterSet(); + + /// Inserts a user name if it isn't contained. Doesn't replace the original + /// if the casing hasn't changed. + void addRecentChatter(const QString &userName); + + /// Removes chatters that aren't online anymore. Adds chatters that aren't + /// in the list yet. + void updateOnlineChatters( + const std::unordered_set &lowerCaseUsernames); + + /// Checks if a username is in the list. + bool contains(const QString &userName) const; + + /// Get filtered usernames by a prefix for autocompletion. Contained items + /// are in mixed case if available. + std::vector filterByPrefix(const QString &prefix) const; + +private: + // user name in lower case -> user name in normal case + cache::lru_cache items; +}; + +using ChatterSet = ChatterSet; + +} // namespace chatterino diff --git a/src/common/CompletionModel.cpp b/src/common/CompletionModel.cpp index f3d368da..61e61d35 100644 --- a/src/common/CompletionModel.cpp +++ b/src/common/CompletionModel.cpp @@ -1,8 +1,8 @@ #include "common/CompletionModel.hpp" #include "Application.hpp" +#include "common/ChatterSet.hpp" #include "common/Common.hpp" -#include "common/UsernameSet.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/commands/CommandController.hpp" #include "debug/Benchmark.hpp" @@ -108,33 +108,31 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) } // Usernames - if (prefix.length() >= UsernameSet::PrefixLength) + QString usernamePostfix = + isFirstWord && getSettings()->mentionUsersWithComma ? "," + : QString(); + + if (prefix.startsWith("@")) { - auto usernames = channel->accessChatters(); - QString usernamePrefix = prefix; - QString usernamePostfix = - isFirstWord && getSettings()->mentionUsersWithComma ? "," - : QString(); + usernamePrefix.remove(0, 1); - if (usernamePrefix.startsWith("@")) + auto chatters = + channel->accessChatters()->filterByPrefix(usernamePrefix); + + for (const auto &name : chatters) { - usernamePrefix.remove(0, 1); - for (const auto &name : - usernames->subrange(Prefix(usernamePrefix))) - { - addString("@" + name + usernamePostfix, - TaggedString::Type::Username); - } + addString("@" + name + usernamePostfix, + TaggedString::Type::Username); } - else if (!getSettings()->userCompletionOnlyWithAt) + } + else if (!getSettings()->userCompletionOnlyWithAt) + { + auto chatters = channel->accessChatters()->filterByPrefix(prefix); + + for (const auto &name : chatters) { - for (const auto &name : - usernames->subrange(Prefix(usernamePrefix))) - { - addString(name + usernamePostfix, - TaggedString::Type::Username); - } + addString(name + usernamePostfix, TaggedString::Type::Username); } } diff --git a/src/common/UniqueAccess.hpp b/src/common/UniqueAccess.hpp index 71d2525a..d7595599 100644 --- a/src/common/UniqueAccess.hpp +++ b/src/common/UniqueAccess.hpp @@ -64,7 +64,7 @@ public: } UniqueAccess(T &&element) - : element_(element) + : element_(std::move(element)) { } @@ -76,7 +76,7 @@ public: UniqueAccess &operator=(T &&element) { - this->element_ = element; + this->element_ = std::move(element); return *this; } diff --git a/src/common/UsernameSet.cpp b/src/common/UsernameSet.cpp deleted file mode 100644 index 1c0474e9..00000000 --- a/src/common/UsernameSet.cpp +++ /dev/null @@ -1,182 +0,0 @@ -#include "UsernameSet.hpp" - -#include - -namespace chatterino { - -namespace { - - std::pair findOrErase( - std::set &set, const QString &value) - { - if (!value.isLower()) - { - auto iter = set.find(value); - if (iter != set.end()) - { - if (QString::compare(*iter, value, Qt::CaseSensitive) != 0) - { - set.erase(iter); - } - else - { - return {iter, false}; - } - } - } - return {set.end(), true}; - } -} // namespace - -// -// UsernameSet -// - -UsernameSet::ConstIterator UsernameSet::begin() const -{ - return this->items.begin(); -} - -UsernameSet::ConstIterator UsernameSet::end() const -{ - return this->items.end(); -} - -UsernameSet::Range UsernameSet::subrange(const Prefix &prefix) const -{ - auto it = this->firstKeyForPrefix.find(prefix); - if (it != this->firstKeyForPrefix.end()) - { - auto start = this->items.find(it->second); - auto end = start; - - while (end != this->items.end() && prefix.isStartOf(*end)) - { - end++; - } - return {start, end}; - } - - return {this->items.end(), this->items.end()}; -} - -std::set::size_type UsernameSet::size() const -{ - return this->items.size(); -} - -std::pair UsernameSet::insert(const QString &value) -{ - auto pair = findOrErase(this->items, value); - if (!pair.second) - { - return pair; - } - - this->insertPrefix(value); - return this->items.insert(value); -} - -std::pair UsernameSet::insert(QString &&value) -{ - auto pair = findOrErase(this->items, value); - if (!pair.second) - { - return pair; - } - - this->insertPrefix(value); - return this->items.insert(std::move(value)); -} - -void UsernameSet::insertPrefix(const QString &value) -{ - auto &string = this->firstKeyForPrefix[Prefix(value)]; - - if (string.isNull() || value.compare(string, Qt::CaseInsensitive) < 0) - string = value; -} - -bool UsernameSet::contains(const QString &value) const -{ - return this->items.count(value) == 1; -} - -void UsernameSet::merge(UsernameSet &&set) -{ - for (auto it = this->items.begin(); it != this->items.end();) - { - auto iter = set.items.find(*it); - if (iter == set.items.end()) - { - it = this->items.erase(it); - } - else - { - ++it; - } - } - this->items.merge(set.items); - this->firstKeyForPrefix = set.firstKeyForPrefix; -} - -// -// Range -// - -UsernameSet::Range::Range(ConstIterator start, ConstIterator end) - : start_(start) - , end_(end) -{ -} - -UsernameSet::ConstIterator UsernameSet::Range::begin() -{ - return this->start_; -} - -UsernameSet::ConstIterator UsernameSet::Range::end() -{ - return this->end_; -} - -// -// Prefix -// - -Prefix::Prefix(const QString &string) - : first(string.size() >= 1 ? string[0].toLower() : '\0') - , second(string.size() >= 2 ? string[1].toLower() : '\0') -{ -} - -bool Prefix::operator==(const Prefix &other) const -{ - return std::tie(this->first, this->second) == - std::tie(other.first, other.second); -} - -bool Prefix::operator!=(const Prefix &other) const -{ - return !(*this == other); -} - -bool Prefix::isStartOf(const QString &string) const -{ - if (string.size() == 0) - { - return this->first == QChar('\0') && this->second == QChar('\0'); - } - else if (string.size() == 1) - { - return this->first == string[0].toLower() && - this->second == QChar('\0'); - } - else - { - return this->first == string[0].toLower() && - this->second == string[1].toLower(); - } -} - -} // namespace chatterino diff --git a/src/common/UsernameSet.hpp b/src/common/UsernameSet.hpp deleted file mode 100644 index 1268695e..00000000 --- a/src/common/UsernameSet.hpp +++ /dev/null @@ -1,89 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace chatterino { - -class Prefix -{ -public: - Prefix(const QString &string); - bool operator==(const Prefix &other) const; - bool operator!=(const Prefix &other) const; - bool isStartOf(const QString &string) const; - -private: - QChar first; - QChar second; - - friend struct std::hash; -}; - -} // namespace chatterino - -namespace std { - -template <> -struct hash { - size_t operator()(const chatterino::Prefix &prefix) const - { - return (size_t(prefix.first.unicode()) << 16) | - size_t(prefix.second.unicode()); - } -}; - -} // namespace std - -namespace chatterino { - -struct CaseInsensitiveLess { - bool operator()(const QString &lhs, const QString &rhs) const - { - return lhs.compare(rhs, Qt::CaseInsensitive) < 0; - } -}; - -class UsernameSet -{ -public: - static constexpr int PrefixLength = 2; - - using Iterator = std::set::iterator; - using ConstIterator = std::set::const_iterator; - - class Range - { - public: - Range(ConstIterator start, ConstIterator end); - - ConstIterator begin(); - ConstIterator end(); - - private: - ConstIterator start_; - ConstIterator end_; - }; - - ConstIterator begin() const; - ConstIterator end() const; - Range subrange(const Prefix &prefix) const; - - std::set::size_type size() const; - - std::pair insert(const QString &value); - std::pair insert(QString &&value); - - bool contains(const QString &value) const; - void merge(UsernameSet &&set); - -private: - void insertPrefix(const QString &string); - - std::set items; - std::unordered_map firstKeyForPrefix; -}; - -} // namespace chatterino diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index c15b5e5f..de6a1b5e 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -119,13 +119,14 @@ namespace { return messages; } - std::pair parseChatters(const QJsonObject &jsonRoot) + std::pair> parseChatters( + const QJsonObject &jsonRoot) { static QStringList categories = {"broadcaster", "vips", "moderators", "staff", "admins", "global_mods", "viewers"}; - auto usernames = UsernameSet(); + auto usernames = std::unordered_set(); // parse json QJsonObject jsonCategories = jsonRoot.value("chatters").toObject(); @@ -823,7 +824,7 @@ void TwitchChannel::refreshChatters() auto pair = parseChatters(std::move(data)); if (pair.first) { - this->setChatters(std::move(pair.second)); + this->updateOnlineChatters(pair.second); } return pair.first; diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index df3290ea..3268984b 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -4,9 +4,9 @@ #include "common/Atomic.hpp" #include "common/Channel.hpp" #include "common/ChannelChatters.hpp" +#include "common/ChatterSet.hpp" #include "common/Outcome.hpp" #include "common/UniqueAccess.hpp" -#include "common/UsernameSet.hpp" #include "providers/twitch/ChannelPointReward.hpp" #include "providers/twitch/TwitchEmotes.hpp" #include "providers/twitch/api/Helix.hpp" diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index a339f2ce..923df4db 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -519,11 +519,11 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) if (this->twitchChannel != nullptr && getSettings()->findAllUsernames) { - auto chatters = this->twitchChannel->accessChatters(); auto match = allUsernamesMentionRegex.match(string); QString username = match.captured(1); - if (match.hasMatch() && chatters->contains(username)) + if (match.hasMatch() && + this->twitchChannel->accessChatters()->contains(username)) { auto originalTextColor = textColor; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 22580fcc..dad801ed 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,7 +15,9 @@ set(chatterino_SOURCES ${CMAKE_SOURCE_DIR}/src/common/NetworkRequest.cpp ${CMAKE_SOURCE_DIR}/src/common/NetworkResult.cpp ${CMAKE_SOURCE_DIR}/src/common/QLogging.cpp - ${CMAKE_SOURCE_DIR}/src/common/UsernameSet.cpp + ${CMAKE_SOURCE_DIR}/src/common/ChatterSet.cpp + + ${CMAKE_SOURCE_DIR}/src/debug/Benchmark.cpp ${CMAKE_SOURCE_DIR}/src/controllers/highlights/HighlightPhrase.cpp @@ -39,7 +41,7 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/AccessGuard.cpp ${CMAKE_CURRENT_LIST_DIR}/src/NetworkCommon.cpp ${CMAKE_CURRENT_LIST_DIR}/src/NetworkRequest.cpp - ${CMAKE_CURRENT_LIST_DIR}/src/UsernameSet.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/ChatterSet.cpp ${CMAKE_CURRENT_LIST_DIR}/src/HighlightPhrase.cpp ${CMAKE_CURRENT_LIST_DIR}/src/Emojis.cpp ) @@ -68,6 +70,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE Pajlada::Settings Pajlada::Signals Threads::Threads + LRUCache ) target_compile_definitions(${PROJECT_NAME} PRIVATE diff --git a/tests/src/ChatterSet.cpp b/tests/src/ChatterSet.cpp new file mode 100644 index 00000000..ac5b81ee --- /dev/null +++ b/tests/src/ChatterSet.cpp @@ -0,0 +1,84 @@ +#include "common/ChatterSet.hpp" + +#include +#include + +TEST(ChatterSet, insert) +{ + chatterino::ChatterSet set; + + EXPECT_FALSE(set.contains("pajlada")); + EXPECT_FALSE(set.contains("Pajlada")); + + set.addRecentChatter("pajlada"); + EXPECT_TRUE(set.contains("pajlada")); + EXPECT_TRUE(set.contains("Pajlada")); + + set.addRecentChatter("pajlada"); + EXPECT_TRUE(set.contains("pajlada")); + EXPECT_TRUE(set.contains("Pajlada")); + + set.addRecentChatter("PAJLADA"); + EXPECT_TRUE(set.contains("pajlada")); + EXPECT_TRUE(set.contains("Pajlada")); +} + +TEST(ChatterSet, MaxSize) +{ + chatterino::ChatterSet set; + + EXPECT_FALSE(set.contains("pajlada")); + EXPECT_FALSE(set.contains("Pajlada")); + + set.addRecentChatter("pajlada"); + EXPECT_TRUE(set.contains("pajlada")); + EXPECT_TRUE(set.contains("Pajlada")); + + // After adding CHATTER_LIMIT-1 additional chatters, pajlada should still be in the set + for (auto i = 0; i < chatterino::ChatterSet::chatterLimit - 1; ++i) + { + set.addRecentChatter(QString("%1").arg(i)); + } + + EXPECT_TRUE(set.contains("pajlada")); + EXPECT_TRUE(set.contains("Pajlada")); + + // But adding one more chatter should bump pajlada out of the set + set.addRecentChatter("notpajlada"); + + EXPECT_FALSE(set.contains("pajlada")); + EXPECT_FALSE(set.contains("Pajlada")); +} + +TEST(ChatterSet, MaxSizeLastUsed) +{ + chatterino::ChatterSet set; + + EXPECT_FALSE(set.contains("pajlada")); + EXPECT_FALSE(set.contains("Pajlada")); + + set.addRecentChatter("pajlada"); + EXPECT_TRUE(set.contains("pajlada")); + EXPECT_TRUE(set.contains("Pajlada")); + + // After adding CHATTER_LIMIT-1 additional chatters, pajlada should still be in the set + for (auto i = 0; i < chatterino::ChatterSet::chatterLimit - 1; ++i) + { + set.addRecentChatter(QString("%1").arg(i)); + } + + EXPECT_TRUE(set.contains("pajlada")); + EXPECT_TRUE(set.contains("Pajlada")); + + // Bump pajlada as a recent chatter + set.addRecentChatter("pajlada"); + + // After another CHATTER_LIMIT-1 additional chatters, pajlada should still be there + for (auto i = 0; i < chatterino::ChatterSet::chatterLimit - 1; ++i) + { + set.addRecentChatter(QString("new-%1").arg(i)); + } + + EXPECT_TRUE(set.contains("pajlada")); + EXPECT_TRUE(set.contains("Pajlada")); +} diff --git a/tests/src/UsernameSet.cpp b/tests/src/UsernameSet.cpp deleted file mode 100644 index fad21052..00000000 --- a/tests/src/UsernameSet.cpp +++ /dev/null @@ -1,161 +0,0 @@ -#include "common/UsernameSet.hpp" - -#include -#include - -chatterino::Prefix prefix_pajlada(QString("pajlada")); -chatterino::Prefix prefix_Pajlada(QString("Pajlada")); -chatterino::Prefix prefix_randers(QString("randers")); -chatterino::Prefix prefix_Chancu(QString("ch")); - -TEST(Prefix, isStartOf) -{ - EXPECT_TRUE(prefix_pajlada.isStartOf("pajlada")); - EXPECT_TRUE(prefix_pajlada.isStartOf("Pajlada")); - EXPECT_FALSE(prefix_pajlada.isStartOf("randers")); - - EXPECT_TRUE(prefix_Pajlada.isStartOf("pajlada")); - EXPECT_TRUE(prefix_Pajlada.isStartOf("Pajlada")); - EXPECT_TRUE(prefix_Pajlada.isStartOf("pajbot")); - EXPECT_TRUE(prefix_Pajlada.isStartOf("Pajbot")); - EXPECT_FALSE(prefix_Pajlada.isStartOf("randers")); -} - -TEST(Prefix, EqualsOperator) -{ - EXPECT_EQ(prefix_pajlada, prefix_Pajlada); - EXPECT_NE(prefix_pajlada, prefix_randers); -} - -TEST(UsernameSet, insert) -{ - std::pair p; - chatterino::UsernameSet set; - - EXPECT_EQ(set.size(), 0); - - p = set.insert("pajlada"); - EXPECT_TRUE(p.second); - - EXPECT_EQ(set.size(), 1); - - p = set.insert("pajlada"); - EXPECT_FALSE(p.second); - - EXPECT_EQ(set.size(), 1); - - // Non-lowercase variant should override full lowercase variant - p = set.insert("PAJLADA"); - EXPECT_TRUE(p.second); - - EXPECT_EQ(set.size(), 1); - - // Lowercase variant should not override non-lowercase variant - p = set.insert("pajlada"); - EXPECT_FALSE(p.second); - - EXPECT_EQ(set.size(), 1); - - p = set.insert("pajbot"); - EXPECT_TRUE(p.second); - - EXPECT_EQ(set.size(), 2); - - p = set.insert("pajbot"); - EXPECT_FALSE(p.second); - - EXPECT_EQ(set.size(), 2); - - p = set.insert("Pajbot"); - EXPECT_TRUE(p.second); - - EXPECT_EQ(set.size(), 2); - - p = set.insert("PAJBOT"); - EXPECT_TRUE(p.second); - - EXPECT_EQ(set.size(), 2); - - // Same uppercase should not result in a change - p = set.insert("PAJBOT"); - EXPECT_FALSE(p.second); - - EXPECT_EQ(set.size(), 2); -} - -TEST(UsernameSet, CollisionTest) -{ - QString s; - chatterino::UsernameSet set; - chatterino::Prefix prefix("not_"); - - set.insert("pajlada"); - set.insert("Chancu"); - set.insert("chief_tony"); - set.insert("ChodzacyKac"); - set.insert("ChatAbuser"); - set.insert("Normies_GTFO"); - set.insert("not_remzy"); - set.insert("Mullo2500"); - set.insert("muggedbyapie"); - - EXPECT_EQ(set.size(), 9); - - { - QStringList result; - QStringList expectation{"Normies_GTFO", "not_remzy"}; - auto subrange = set.subrange(QString("not_")); - std::copy(subrange.begin(), subrange.end(), std::back_inserter(result)); - EXPECT_EQ(expectation, result); - } - - { - QStringList result; - QStringList expectation{}; - auto subrange = set.subrange(QString("te")); - std::copy(subrange.begin(), subrange.end(), std::back_inserter(result)); - EXPECT_EQ(expectation, result); - } - - { - QStringList result; - QStringList expectation{"pajlada"}; - auto subrange = set.subrange(QString("PA")); - std::copy(subrange.begin(), subrange.end(), std::back_inserter(result)); - EXPECT_EQ(expectation, result); - } - - { - QStringList result; - QStringList expectation{"pajlada"}; - auto subrange = set.subrange(QString("pajlada")); - std::copy(subrange.begin(), subrange.end(), std::back_inserter(result)); - EXPECT_EQ(expectation, result); - } - - { - QStringList result; - QStringList expectation{"pajlada"}; - auto subrange = set.subrange(QString("Pajl")); - std::copy(subrange.begin(), subrange.end(), std::back_inserter(result)); - EXPECT_EQ(expectation, result); - } - - { - QStringList result; - QStringList expectation{"Chancu", "ChatAbuser", "chief_tony", - "ChodzacyKac"}; - auto subrange = set.subrange(QString("chan")); - std::copy(subrange.begin(), subrange.end(), std::back_inserter(result)); - EXPECT_EQ(expectation, result); - } - - { - QStringList result; - QStringList expectation{"muggedbyapie", "Mullo2500"}; - auto subrange = set.subrange(QString("mu")); - std::copy(subrange.begin(), subrange.end(), std::back_inserter(result)); - EXPECT_EQ(expectation, result); - } -} -