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 <rasmus.karlsson@pajlada.com>
This commit is contained in:
Mm2PL 2023-05-27 12:18:08 +00:00 committed by GitHub
parent 5ca7d387e4
commit fb02d59b48
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 88 additions and 6 deletions

View file

@ -9,6 +9,7 @@
- Dev: Added test cases for emote and tab completion. (#4644) - Dev: Added test cases for emote and tab completion. (#4644)
- Dev: Fixed `clang-tidy-review` action not picking up dependencies. (#4648) - Dev: Fixed `clang-tidy-review` action not picking up dependencies. (#4648)
- Dev: Expanded upon `$$$` test channels. (#4655) - Dev: Expanded upon `$$$` test channels. (#4655)
- Dev: Added tools to help debug image GC. (#4578)
## 2.4.4 ## 2.4.4

View file

@ -16,6 +16,7 @@
#include "controllers/commands/CommandModel.hpp" #include "controllers/commands/CommandModel.hpp"
#include "controllers/plugins/PluginController.hpp" #include "controllers/plugins/PluginController.hpp"
#include "controllers/userdata/UserDataController.hpp" #include "controllers/userdata/UserDataController.hpp"
#include "messages/Image.hpp"
#include "messages/Message.hpp" #include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp" #include "messages/MessageBuilder.hpp"
#include "messages/MessageElement.hpp" #include "messages/MessageElement.hpp"
@ -38,6 +39,7 @@
#include "util/FormatTime.hpp" #include "util/FormatTime.hpp"
#include "util/Helpers.hpp" #include "util/Helpers.hpp"
#include "util/IncognitoBrowser.hpp" #include "util/IncognitoBrowser.hpp"
#include "util/PostToThread.hpp"
#include "util/Qt.hpp" #include "util/Qt.hpp"
#include "util/StreamerMode.hpp" #include "util/StreamerMode.hpp"
#include "util/StreamLink.hpp" #include "util/StreamLink.hpp"
@ -3211,6 +3213,28 @@ void CommandController::initialize(Settings &, Paths &paths)
return ""; 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("/shield", &commands::shieldModeOn);
this->registerCommand("/shieldoff", &commands::shieldModeOff); this->registerCommand("/shieldoff", &commands::shieldModeOff);

View file

@ -76,6 +76,8 @@ namespace detail {
60000); 60000);
} }
this->processOffset(); this->processOffset();
DebugCount::increase("image bytes", this->memoryUsage());
DebugCount::increase("image bytes (ever loaded)", this->memoryUsage());
} }
Frames::~Frames() Frames::~Frames()
@ -91,10 +93,27 @@ namespace detail {
{ {
DebugCount::decrease("animated images"); DebugCount::decrease("animated images");
} }
DebugCount::decrease("image bytes", this->memoryUsage());
DebugCount::increase("image bytes (ever unloaded)",
this->memoryUsage());
this->gifTimerConnection_.disconnect(); 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() void Frames::advance()
{ {
this->durationOffset_ += GIF_FRAME_LENGTH; this->durationOffset_ += GIF_FRAME_LENGTH;
@ -131,6 +150,9 @@ namespace detail {
{ {
DebugCount::decrease("loaded images"); DebugCount::decrease("loaded images");
} }
DebugCount::decrease("image bytes", this->memoryUsage());
DebugCount::increase("image bytes (ever unloaded)",
this->memoryUsage());
this->items_.clear(); this->items_.clear();
this->index_ = 0; this->index_ = 0;
@ -589,14 +611,26 @@ void ImageExpirationPool::removeImagePtr(Image *rawPtr)
this->allImages_.erase(rawPtr); this->allImages_.erase(rawPtr);
} }
void ImageExpirationPool::freeAll()
{
{
std::lock_guard<std::mutex> 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() void ImageExpirationPool::freeOld()
{ {
std::lock_guard<std::mutex> lock(this->mutex_); std::lock_guard<std::mutex> lock(this->mutex_);
# ifndef NDEBUG
size_t numExpired = 0; size_t numExpired = 0;
size_t eligible = 0; size_t eligible = 0;
# endif
auto now = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now();
for (auto it = this->allImages_.begin(); it != this->allImages_.end();) for (auto it = this->allImages_.begin(); it != this->allImages_.end();)
@ -617,17 +651,13 @@ void ImageExpirationPool::freeOld()
continue; continue;
} }
# ifndef NDEBUG
++eligible; ++eligible;
# endif
// Check if image has expired and, if so, expire its frame data // Check if image has expired and, if so, expire its frame data
auto diff = now - img->lastUsed_; auto diff = now - img->lastUsed_;
if (diff > IMAGE_POOL_IMAGE_LIFETIME) if (diff > IMAGE_POOL_IMAGE_LIFETIME)
{ {
# ifndef NDEBUG
++numExpired; ++numExpired;
# endif
img->expireFrames(); img->expireFrames();
// erase without mutex locking issue // erase without mutex locking issue
it = this->allImages_.erase(it); it = this->allImages_.erase(it);
@ -641,6 +671,9 @@ void ImageExpirationPool::freeOld()
qCDebug(chatterinoImage) << "freed frame data for" << numExpired << "/" qCDebug(chatterinoImage) << "freed frame data for" << numExpired << "/"
<< eligible << "eligible images"; << eligible << "eligible images";
# endif # 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 #endif

View file

@ -41,6 +41,7 @@ namespace detail {
boost::optional<QPixmap> first() const; boost::optional<QPixmap> first() const;
private: private:
int64_t memoryUsage() const;
void processOffset(); void processOffset();
QVector<Frame<QPixmap>> items_; QVector<Frame<QPixmap>> items_;
int index_{0}; int index_{0};
@ -111,6 +112,7 @@ class ImageExpirationPool
{ {
private: private:
friend class Image; friend class Image;
friend class CommandController;
ImageExpirationPool(); ImageExpirationPool();
static ImageExpirationPool &instance(); static ImageExpirationPool &instance();
@ -126,6 +128,12 @@ private:
*/ */
void freeOld(); 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: private:
// Timer to periodically run freeOld() // Timer to periodically run freeOld()
QTimer *freeTimer_; QTimer *freeTimer_;

View file

@ -27,6 +27,22 @@ public:
reinterpret_cast<int64_t &>(it.value())++; reinterpret_cast<int64_t &>(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<int64_t &>(it.value()) = amount;
}
}
static void increase(const QString &name, const int64_t &amount) static void increase(const QString &name, const int64_t &amount)
{ {
auto counts = counts_.access(); auto counts = counts_.access();