Periodically free memory from unused images (#3915)

Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
Daniel Sage
2022-09-04 07:23:14 -04:00
committed by GitHub
parent bc38d696bc
commit 8ec032fc84
7 changed files with 246 additions and 35 deletions
+191 -22
View File
@@ -6,7 +6,9 @@
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QTimer>
#include <boost/functional/hash.hpp>
#include <functional>
#include <queue>
#include <thread>
#include "Application.hpp"
@@ -23,7 +25,10 @@
#include "util/DebugCount.hpp"
#include "util/PostToThread.hpp"
#include <queue>
// Duration between each check of every Image instance
const auto IMAGE_POOL_CLEANUP_INTERVAL = std::chrono::minutes(1);
// Duration since last usage of Image pixmap before expiration of frames
const auto IMAGE_POOL_IMAGE_LIFETIME = std::chrono::minutes(10);
namespace chatterino {
namespace detail {
@@ -33,11 +38,15 @@ namespace detail {
DebugCount::increase("images");
}
Frames::Frames(const QVector<Frame<QPixmap>> &frames)
: items_(frames)
Frames::Frames(QVector<Frame<QPixmap>> &&frames)
: items_(std::move(frames))
{
assertInGuiThread();
DebugCount::increase("images");
if (!this->empty())
{
DebugCount::increase("loaded images");
}
if (this->animated())
{
@@ -76,6 +85,10 @@ namespace detail {
{
assertInGuiThread();
DebugCount::decrease("images");
if (!this->empty())
{
DebugCount::decrease("loaded images");
}
if (this->animated())
{
@@ -114,6 +127,25 @@ namespace detail {
}
}
void Frames::clear()
{
assertInGuiThread();
if (!this->empty())
{
DebugCount::decrease("loaded images");
}
this->items_.clear();
this->index_ = 0;
this->durationOffset_ = 0;
this->gifTimerConnection_.disconnect();
}
bool Frames::empty() const
{
return this->items_.empty();
}
bool Frames::animated() const
{
return this->items_.size() > 1;
@@ -137,13 +169,13 @@ namespace detail {
QVector<Frame<QImage>> readFrames(QImageReader &reader, const Url &url)
{
QVector<Frame<QImage>> frames;
frames.reserve(reader.imageCount());
QImage image;
for (int index = 0; index < reader.imageCount(); ++index)
{
if (reader.read(&image))
{
QPixmap::fromImage(image);
// It seems that browsers have special logic for fast animations.
// This implements Chrome and Firefox's behavior which uses
// a duration of 100 ms for any frames that specify a duration of <= 10 ms.
@@ -153,7 +185,7 @@ namespace detail {
if (duration <= 10)
duration = 100;
duration = std::max(20, duration);
frames.push_back(Frame<QImage>{image, duration});
frames.push_back(Frame<QImage>{std::move(image), duration});
}
}
@@ -178,9 +210,12 @@ namespace detail {
while (!queued.empty())
{
queued.front().first(queued.front().second);
auto front = std::move(queued.front());
queued.pop();
// Call Assign with the vector of frames
front.first(std::move(front.second));
if (++i > 50)
{
QTimer::singleShot(3, [&] {
@@ -200,9 +235,14 @@ namespace detail {
auto makeConvertCallback(const QVector<Frame<QImage>> &parsed,
Assign assign)
{
static std::queue<std::pair<Assign, QVector<Frame<QPixmap>>>> queued;
static std::mutex mutex;
static std::atomic_bool loadedEventQueued{false};
return [parsed, assign] {
// convert to pixmap
auto frames = QVector<Frame<QPixmap>>();
QVector<Frame<QPixmap>> frames;
frames.reserve(parsed.size());
std::transform(parsed.begin(), parsed.end(),
std::back_inserter(frames), [](auto &frame) {
return Frame<QPixmap>{
@@ -211,15 +251,9 @@ namespace detail {
});
// put into stack
static std::queue<std::pair<Assign, QVector<Frame<QPixmap>>>>
queued;
static std::mutex mutex;
std::lock_guard<std::mutex> lock(mutex);
queued.emplace(assign, frames);
static std::atomic_bool loadedEventQueued{false};
if (!loadedEventQueued)
{
loadedEventQueued = true;
@@ -235,7 +269,9 @@ namespace detail {
// IMAGE2
Image::~Image()
{
if (this->empty_)
ImageExpirationPool::instance().removeImagePtr(this);
if (this->empty_ && !this->frames_)
{
// No data in this image, don't bother trying to release it
// The reason we do this check is that we keep a few (or one) static empty image around that are deconstructed at the end of the programs lifecycle, and we want to prevent the isGuiThread call to be called after the QApplication has been exited
@@ -268,13 +304,37 @@ ImagePtr Image::fromUrl(const Url &url, qreal scale)
return shared;
}
ImagePtr Image::fromPixmap(const QPixmap &pixmap, qreal scale)
ImagePtr Image::fromResourcePixmap(const QPixmap &pixmap, qreal scale)
{
auto result = ImagePtr(new Image(scale));
using key_t = std::pair<const QPixmap *, qreal>;
static std::unordered_map<key_t, std::weak_ptr<Image>, boost::hash<key_t>>
cache;
static std::mutex mutex;
result->setPixmap(pixmap);
std::lock_guard<std::mutex> lock(mutex);
return result;
auto it = cache.find({&pixmap, scale});
if (it != cache.end())
{
auto shared = it->second.lock();
if (shared)
{
return shared;
}
else
{
cache.erase(it);
}
}
auto newImage = ImagePtr(new Image(scale));
newImage->setPixmap(pixmap);
// store in cache
cache.insert({{&pixmap, scale}, std::weak_ptr<Image>(newImage)});
return newImage;
}
ImagePtr Image::getEmpty()
@@ -335,6 +395,11 @@ boost::optional<QPixmap> Image::pixmapOrLoad() const
{
assertInGuiThread();
// Mark the image as just used.
// Any time this Image is painted, this method is invoked.
// See src/messages/layouts/MessageLayoutElement.cpp ImageLayoutElement::paint, for example.
this->lastUsed_ = std::chrono::steady_clock::now();
this->load();
return this->frames_->current();
@@ -346,8 +411,10 @@ void Image::load() const
if (this->shouldLoad_)
{
const_cast<Image *>(this)->shouldLoad_ = false;
const_cast<Image *>(this)->actuallyLoad();
Image *this2 = const_cast<Image *>(this);
this2->shouldLoad_ = false;
this2->actuallyLoad();
ImageExpirationPool::instance().addImagePtr(this2->shared_from_this());
}
}
@@ -439,9 +506,12 @@ void Image::actuallyLoad()
auto parsed = detail::readFrames(reader, shared->url());
postToThread(makeConvertCallback(parsed, [weak](auto frames) {
postToThread(makeConvertCallback(parsed, [weak](auto &&frames) {
if (auto shared = weak.lock())
shared->frames_ = std::make_unique<detail::Frames>(frames);
{
shared->frames_ =
std::make_unique<detail::Frames>(std::move(frames));
}
}));
return Success;
@@ -459,6 +529,13 @@ void Image::actuallyLoad()
.execute();
}
void Image::expireFrames()
{
assertInGuiThread();
this->frames_->clear();
this->shouldLoad_ = true; // Mark as needing load again
}
bool Image::operator==(const Image &other) const
{
if (this->isEmpty() && other.isEmpty())
@@ -476,4 +553,96 @@ bool Image::operator!=(const Image &other) const
return !this->operator==(other);
}
ImageExpirationPool::ImageExpirationPool()
{
QObject::connect(&this->freeTimer_, &QTimer::timeout, [this] {
if (isGuiThread())
{
this->freeOld();
}
else
{
postToThread([this] {
this->freeOld();
});
}
});
this->freeTimer_.start(
std::chrono::duration_cast<std::chrono::milliseconds>(
IMAGE_POOL_CLEANUP_INTERVAL));
}
ImageExpirationPool &ImageExpirationPool::instance()
{
static ImageExpirationPool instance;
return instance;
}
void ImageExpirationPool::addImagePtr(ImagePtr imgPtr)
{
std::lock_guard<std::mutex> lock(this->mutex_);
this->allImages_.emplace(imgPtr.get(), std::weak_ptr<Image>(imgPtr));
}
void ImageExpirationPool::removeImagePtr(Image *rawPtr)
{
std::lock_guard<std::mutex> lock(this->mutex_);
this->allImages_.erase(rawPtr);
}
void ImageExpirationPool::freeOld()
{
std::lock_guard<std::mutex> 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();)
{
auto img = it->second.lock();
if (!img)
{
// This can only really happen from a race condition because ~Image
// should remove itself from the ImageExpirationPool automatically.
it = this->allImages_.erase(it);
continue;
}
if (img->frames_->empty())
{
// No frame data, nothing to do
++it;
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);
continue;
}
++it;
}
#ifndef NDEBUG
qCDebug(chatterinoImage) << "freed frame data for" << numExpired << "/"
<< eligible << "eligible images";
#endif
}
} // namespace chatterino