From fb02d59b483f9c5c7a9aa8bf55d4cba87b55141d Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 27 May 2023 12:18:08 +0000 Subject: [PATCH] Add tools to help debug image GC (#4578) `/debug-force-image-gc` will force garbage collection on all unused images `/debug-force-image-unload` will force unload all images Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + .../commands/CommandController.cpp | 24 ++++++++++ src/messages/Image.cpp | 45 ++++++++++++++++--- src/messages/Image.hpp | 8 ++++ src/util/DebugCount.hpp | 16 +++++++ 5 files changed, 88 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 568ae0ee4..45e57cddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Dev: Added test cases for emote and tab completion. (#4644) - Dev: Fixed `clang-tidy-review` action not picking up dependencies. (#4648) - Dev: Expanded upon `$$$` test channels. (#4655) +- Dev: Added tools to help debug image GC. (#4578) ## 2.4.4 diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 7be61286c..3956ec9c8 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -16,6 +16,7 @@ #include "controllers/commands/CommandModel.hpp" #include "controllers/plugins/PluginController.hpp" #include "controllers/userdata/UserDataController.hpp" +#include "messages/Image.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" #include "messages/MessageElement.hpp" @@ -38,6 +39,7 @@ #include "util/FormatTime.hpp" #include "util/Helpers.hpp" #include "util/IncognitoBrowser.hpp" +#include "util/PostToThread.hpp" #include "util/Qt.hpp" #include "util/StreamerMode.hpp" #include "util/StreamLink.hpp" @@ -3211,6 +3213,28 @@ void CommandController::initialize(Settings &, Paths &paths) return ""; }); + this->registerCommand( + "/debug-force-image-gc", + [](const QStringList & /*words*/, auto /*channel*/) -> QString { + runInGuiThread([] { + using namespace chatterino::detail; + auto &iep = ImageExpirationPool::instance(); + iep.freeOld(); + }); + return ""; + }); + + this->registerCommand( + "/debug-force-image-unload", + [](const QStringList & /*words*/, auto /*channel*/) -> QString { + runInGuiThread([] { + using namespace chatterino::detail; + auto &iep = ImageExpirationPool::instance(); + iep.freeAll(); + }); + return ""; + }); + this->registerCommand("/shield", &commands::shieldModeOn); this->registerCommand("/shieldoff", &commands::shieldModeOff); diff --git a/src/messages/Image.cpp b/src/messages/Image.cpp index 2e8616b19..71daf3425 100644 --- a/src/messages/Image.cpp +++ b/src/messages/Image.cpp @@ -76,6 +76,8 @@ namespace detail { 60000); } this->processOffset(); + DebugCount::increase("image bytes", this->memoryUsage()); + DebugCount::increase("image bytes (ever loaded)", this->memoryUsage()); } Frames::~Frames() @@ -91,10 +93,27 @@ namespace detail { { DebugCount::decrease("animated images"); } + DebugCount::decrease("image bytes", this->memoryUsage()); + DebugCount::increase("image bytes (ever unloaded)", + this->memoryUsage()); this->gifTimerConnection_.disconnect(); } + int64_t Frames::memoryUsage() const + { + int64_t usage = 0; + for (const auto &frame : this->items_) + { + auto sz = frame.image.size(); + auto area = sz.width() * sz.height(); + auto memory = area * frame.image.depth(); + + usage += memory; + } + return usage; + } + void Frames::advance() { this->durationOffset_ += GIF_FRAME_LENGTH; @@ -131,6 +150,9 @@ namespace detail { { DebugCount::decrease("loaded images"); } + DebugCount::decrease("image bytes", this->memoryUsage()); + DebugCount::increase("image bytes (ever unloaded)", + this->memoryUsage()); this->items_.clear(); this->index_ = 0; @@ -589,14 +611,26 @@ void ImageExpirationPool::removeImagePtr(Image *rawPtr) this->allImages_.erase(rawPtr); } +void ImageExpirationPool::freeAll() +{ + { + std::lock_guard lock(this->mutex_); + for (auto it = this->allImages_.begin(); it != this->allImages_.end();) + { + auto img = it->second.lock(); + img->expireFrames(); + it = this->allImages_.erase(it); + } + } + this->freeOld(); +} + void ImageExpirationPool::freeOld() { std::lock_guard lock(this->mutex_); -# ifndef NDEBUG size_t numExpired = 0; size_t eligible = 0; -# endif auto now = std::chrono::steady_clock::now(); for (auto it = this->allImages_.begin(); it != this->allImages_.end();) @@ -617,17 +651,13 @@ void ImageExpirationPool::freeOld() continue; } -# ifndef NDEBUG ++eligible; -# endif // Check if image has expired and, if so, expire its frame data auto diff = now - img->lastUsed_; if (diff > IMAGE_POOL_IMAGE_LIFETIME) { -# ifndef NDEBUG ++numExpired; -# endif img->expireFrames(); // erase without mutex locking issue it = this->allImages_.erase(it); @@ -641,6 +671,9 @@ void ImageExpirationPool::freeOld() qCDebug(chatterinoImage) << "freed frame data for" << numExpired << "/" << eligible << "eligible images"; # endif + DebugCount::set("last image gc: expired", numExpired); + DebugCount::set("last image gc: eligible", eligible); + DebugCount::set("last image gc: left after gc", this->allImages_.size()); } #endif diff --git a/src/messages/Image.hpp b/src/messages/Image.hpp index 90159a442..98c964eac 100644 --- a/src/messages/Image.hpp +++ b/src/messages/Image.hpp @@ -41,6 +41,7 @@ namespace detail { boost::optional first() const; private: + int64_t memoryUsage() const; void processOffset(); QVector> items_; int index_{0}; @@ -111,6 +112,7 @@ class ImageExpirationPool { private: friend class Image; + friend class CommandController; ImageExpirationPool(); static ImageExpirationPool &instance(); @@ -126,6 +128,12 @@ private: */ void freeOld(); + /* + * Debug function that unloads all images in the pool. This is intended to + * test for possible memory leaks from tracked images. + */ + void freeAll(); + private: // Timer to periodically run freeOld() QTimer *freeTimer_; diff --git a/src/util/DebugCount.hpp b/src/util/DebugCount.hpp index 629cb5fbe..35f9fc621 100644 --- a/src/util/DebugCount.hpp +++ b/src/util/DebugCount.hpp @@ -27,6 +27,22 @@ public: reinterpret_cast(it.value())++; } } + + static void set(const QString &name, const int64_t &amount) + { + auto counts = counts_.access(); + + auto it = counts->find(name); + if (it == counts->end()) + { + counts->insert(name, amount); + } + else + { + reinterpret_cast(it.value()) = amount; + } + } + static void increase(const QString &name, const int64_t &amount) { auto counts = counts_.access();