refactor: load images in workers and push immediately (#5431)

This commit is contained in:
nerix 2024-06-02 16:31:17 +02:00 committed by GitHub
parent b6dc5d9e03
commit d00cadf4eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 260 additions and 284 deletions

View file

@ -30,7 +30,8 @@ Checks: "-*,
-readability-function-cognitive-complexity, -readability-function-cognitive-complexity,
-bugprone-easily-swappable-parameters, -bugprone-easily-swappable-parameters,
-cert-err58-cpp, -cert-err58-cpp,
-modernize-avoid-c-arrays -modernize-avoid-c-arrays,
-misc-include-cleaner
" "
CheckOptions: CheckOptions:
- key: readability-identifier-naming.ClassCase - key: readability-identifier-naming.ClassCase

View file

@ -23,6 +23,7 @@
- Dev: Reduced the amount of scale events. (#5404, #5406) - Dev: Reduced the amount of scale events. (#5404, #5406)
- Dev: Removed unused timegate settings. (#5361) - Dev: Removed unused timegate settings. (#5361)
- Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385) - Dev: All Lua globals now show in the `c2` global in the LuaLS metadata. (#5385)
- Dev: Images are now loaded in worker threads. (#5431)
## 2.5.1 ## 2.5.1

View file

@ -21,276 +21,238 @@
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QTimer> #include <QTimer>
#include <functional> #include <atomic>
#include <queue>
#include <thread>
// Duration between each check of every Image instance // Duration between each check of every Image instance
const auto IMAGE_POOL_CLEANUP_INTERVAL = std::chrono::minutes(1); const auto IMAGE_POOL_CLEANUP_INTERVAL = std::chrono::minutes(1);
// Duration since last usage of Image pixmap before expiration of frames // Duration since last usage of Image pixmap before expiration of frames
const auto IMAGE_POOL_IMAGE_LIFETIME = std::chrono::minutes(10); const auto IMAGE_POOL_IMAGE_LIFETIME = std::chrono::minutes(10);
namespace chatterino { namespace chatterino::detail {
namespace detail {
// Frames Frames::Frames()
Frames::Frames() {
DebugCount::increase("images");
}
Frames::Frames(QList<Frame> &&frames)
: items_(std::move(frames))
{
assertInGuiThread();
DebugCount::increase("images");
if (!this->empty())
{ {
DebugCount::increase("images"); DebugCount::increase("loaded images");
} }
Frames::Frames(QVector<Frame<QPixmap>> &&frames) if (this->animated())
: items_(std::move(frames))
{ {
assertInGuiThread(); DebugCount::increase("animated images");
DebugCount::increase("images");
if (!this->empty()) this->gifTimerConnection_ =
getIApp()->getEmotes()->getGIFTimer().signal.connect([this] {
this->advance();
});
}
auto totalLength = std::accumulate(this->items_.begin(), this->items_.end(),
0UL, [](auto init, auto &&frame) {
return init + frame.duration;
});
if (totalLength == 0)
{
this->durationOffset_ = 0;
}
else
{
this->durationOffset_ = std::min<int>(
int(getIApp()->getEmotes()->getGIFTimer().position() % totalLength),
60000);
}
this->processOffset();
DebugCount::increase("image bytes", this->memoryUsage());
DebugCount::increase("image bytes (ever loaded)", this->memoryUsage());
}
Frames::~Frames()
{
assertInGuiThread();
DebugCount::decrease("images");
if (!this->empty())
{
DebugCount::decrease("loaded images");
}
if (this->animated())
{
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() / 8;
usage += memory;
}
return usage;
}
void Frames::advance()
{
this->durationOffset_ += GIF_FRAME_LENGTH;
this->processOffset();
}
void Frames::processOffset()
{
if (this->items_.isEmpty())
{
return;
}
while (true)
{
this->index_ %= this->items_.size();
if (this->durationOffset_ > this->items_[this->index_].duration)
{ {
DebugCount::increase("loaded images"); this->durationOffset_ -= this->items_[this->index_].duration;
} this->index_ = (this->index_ + 1) % this->items_.size();
if (this->animated())
{
DebugCount::increase("animated images");
this->gifTimerConnection_ =
getIApp()->getEmotes()->getGIFTimer().signal.connect([this] {
this->advance();
});
}
auto totalLength =
std::accumulate(this->items_.begin(), this->items_.end(), 0UL,
[](auto init, auto &&frame) {
return init + frame.duration;
});
if (totalLength == 0)
{
this->durationOffset_ = 0;
} }
else else
{ {
this->durationOffset_ = std::min<int>( break;
int(getIApp()->getEmotes()->getGIFTimer().position() %
totalLength),
60000);
} }
this->processOffset(); }
DebugCount::increase("image bytes", this->memoryUsage()); }
DebugCount::increase("image bytes (ever loaded)", this->memoryUsage());
void Frames::clear()
{
assertInGuiThread();
if (!this->empty())
{
DebugCount::decrease("loaded images");
}
DebugCount::decrease("image bytes", this->memoryUsage());
DebugCount::increase("image bytes (ever unloaded)", this->memoryUsage());
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;
}
std::optional<QPixmap> Frames::current() const
{
if (this->items_.empty())
{
return std::nullopt;
} }
Frames::~Frames() return this->items_[this->index_].image;
}
std::optional<QPixmap> Frames::first() const
{
if (this->items_.empty())
{ {
assertInGuiThread(); return std::nullopt;
DebugCount::decrease("images"); }
if (!this->empty())
return this->items_.front().image;
}
QList<Frame> readFrames(QImageReader &reader, const Url &url)
{
QList<Frame> frames;
frames.reserve(reader.imageCount());
for (int index = 0; index < reader.imageCount(); ++index)
{
auto pixmap = QPixmap::fromImageReader(&reader);
if (!pixmap.isNull())
{ {
DebugCount::decrease("loaded images"); // 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.
// See http://webkit.org/b/36082 for more information.
// https://github.com/SevenTV/chatterino7/issues/46#issuecomment-1010595231
int duration = reader.nextImageDelay();
if (duration <= 10)
{
duration = 100;
}
duration = std::max(20, duration);
frames.append(Frame{
.image = std::move(pixmap),
.duration = duration,
});
} }
if (this->animated())
{
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 if (frames.empty())
{ {
int64_t usage = 0; qCDebug(chatterinoImage) << "Error while reading image" << url.string
for (const auto &frame : this->items_) << ": '" << reader.errorString() << "'";
{
auto sz = frame.image.size();
auto area = sz.width() * sz.height();
auto memory = area * frame.image.depth() / 8;
usage += memory;
}
return usage;
} }
void Frames::advance() return frames;
{ }
this->durationOffset_ += GIF_FRAME_LENGTH;
this->processOffset();
}
void Frames::processOffset() void assignFrames(std::weak_ptr<Image> weak, QList<Frame> parsed)
{ {
if (this->items_.isEmpty()) static bool isPushQueued;
auto cb = [parsed = std::move(parsed), weak = std::move(weak)]() mutable {
auto shared = weak.lock();
if (!shared)
{ {
return; return;
} }
shared->frames_ = std::make_unique<detail::Frames>(std::move(parsed));
while (true) // Avoid too many layouts in one event-loop iteration
//
// This callback is called for every image, so there might be multiple
// callbacks queued on the event-loop in this iteration, but we only
// want to generate one invalidation.
if (!isPushQueued)
{ {
this->index_ %= this->items_.size(); isPushQueued = true;
postToThread([] {
if (this->durationOffset_ > this->items_[this->index_].duration) isPushQueued = false;
{ getIApp()->getWindows()->forceLayoutChannelViews();
this->durationOffset_ -= this->items_[this->index_].duration; });
this->index_ = (this->index_ + 1) % this->items_.size();
}
else
{
break;
}
} }
} };
void Frames::clear() postToGuiThread(cb);
{ }
assertInGuiThread();
if (!this->empty())
{
DebugCount::decrease("loaded images");
}
DebugCount::decrease("image bytes", this->memoryUsage());
DebugCount::increase("image bytes (ever unloaded)",
this->memoryUsage());
this->items_.clear(); } // namespace chatterino::detail
this->index_ = 0;
this->durationOffset_ = 0;
this->gifTimerConnection_.disconnect();
}
bool Frames::empty() const namespace chatterino {
{
return this->items_.empty();
}
bool Frames::animated() const
{
return this->items_.size() > 1;
}
std::optional<QPixmap> Frames::current() const
{
if (this->items_.empty())
{
return std::nullopt;
}
return this->items_[this->index_].image;
}
std::optional<QPixmap> Frames::first() const
{
if (this->items_.empty())
{
return std::nullopt;
}
return this->items_.front().image;
}
// functions
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))
{
// 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.
// See http://webkit.org/b/36082 for more information.
// https://github.com/SevenTV/chatterino7/issues/46#issuecomment-1010595231
int duration = reader.nextImageDelay();
if (duration <= 10)
{
duration = 100;
}
duration = std::max(20, duration);
frames.push_back(Frame<QImage>{std::move(image), duration});
}
}
if (frames.empty())
{
qCDebug(chatterinoImage)
<< "Error while reading image" << url.string << ": '"
<< reader.errorString() << "'";
}
return frames;
}
// parsed
template <typename Assign>
void assignDelayed(
std::queue<std::pair<Assign, QVector<Frame<QPixmap>>>> &queued,
std::mutex &mutex, std::atomic_bool &loadedEventQueued)
{
std::lock_guard<std::mutex> lock(mutex);
int i = 0;
while (!queued.empty())
{
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, [&] {
assignDelayed(queued, mutex, loadedEventQueued);
});
return;
}
}
getIApp()->getWindows()->forceLayoutChannelViews();
loadedEventQueued = false;
}
template <typename Assign>
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
QVector<Frame<QPixmap>> frames;
frames.reserve(parsed.size());
std::transform(parsed.begin(), parsed.end(),
std::back_inserter(frames), [](auto &frame) {
return Frame<QPixmap>{
QPixmap::fromImage(frame.image),
frame.duration};
});
// put into stack
std::lock_guard<std::mutex> lock(mutex);
queued.emplace(assign, frames);
if (!loadedEventQueued)
{
loadedEventQueued = true;
QTimer::singleShot(100, [=] {
assignDelayed(queued, mutex, loadedEventQueued);
});
}
};
}
} // namespace detail
// IMAGE2 // IMAGE2
Image::~Image() Image::~Image()
@ -402,7 +364,7 @@ void Image::setPixmap(const QPixmap &pixmap)
{ {
auto setFrames = [shared = this->shared_from_this(), pixmap]() { auto setFrames = [shared = this->shared_from_this(), pixmap]() {
shared->frames_ = std::make_unique<detail::Frames>( shared->frames_ = std::make_unique<detail::Frames>(
QVector<detail::Frame<QPixmap>>{detail::Frame<QPixmap>{pixmap, 1}}); QList<detail::Frame>{detail::Frame{pixmap, 1}});
}; };
if (isGuiThread()) if (isGuiThread())
@ -512,11 +474,8 @@ void Image::actuallyLoad()
return; return;
} }
auto data = result.getData(); QBuffer buffer;
buffer.setData(result.getData());
// const cast since we are only reading from it
QBuffer buffer(const_cast<QByteArray *>(&data));
buffer.open(QIODevice::ReadOnly);
QImageReader reader(&buffer); QImageReader reader(&buffer);
if (!reader.canRead()) if (!reader.canRead())
@ -557,14 +516,7 @@ void Image::actuallyLoad()
auto parsed = detail::readFrames(reader, shared->url()); auto parsed = detail::readFrames(reader, shared->url());
postToThread(makeConvertCallback( assignFrames(shared, parsed);
parsed, [weak = std::weak_ptr<Image>(shared)](auto &&frames) {
if (auto shared = weak.lock())
{
shared->frames_ = std::make_unique<detail::Frames>(
std::forward<decltype(frames)>(frames));
}
}));
}) })
.onError([weak](auto /*result*/) { .onError([weak](auto /*result*/) {
auto shared = weak.lock(); auto shared = weak.lock();

View file

@ -1,15 +1,14 @@
#pragma once #pragma once
#include "common/Aliases.hpp" #include "common/Aliases.hpp"
#include "common/Common.hpp"
#include <boost/variant.hpp> #include <boost/variant.hpp>
#include <pajlada/signals/signal.hpp> #include <pajlada/signals/signal.hpp>
#include <QList>
#include <QPixmap> #include <QPixmap>
#include <QString> #include <QString>
#include <QThread> #include <QThread>
#include <QTimer> #include <QTimer>
#include <QVector>
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
@ -19,41 +18,53 @@
#include <optional> #include <optional>
namespace chatterino { namespace chatterino {
namespace detail {
template <typename Image>
struct Frame {
Image image;
int duration;
};
class Frames
{
public:
Frames();
Frames(QVector<Frame<QPixmap>> &&frames);
~Frames();
Frames(const Frames &) = delete; class Image;
Frames &operator=(const Frames &) = delete;
Frames(Frames &&) = delete; } // namespace chatterino
Frames &operator=(Frames &&) = delete;
void clear(); namespace chatterino::detail {
bool empty() const;
bool animated() const;
void advance();
std::optional<QPixmap> current() const;
std::optional<QPixmap> first() const;
private: struct Frame {
int64_t memoryUsage() const; QPixmap image;
void processOffset(); int duration;
QVector<Frame<QPixmap>> items_; };
int index_{0};
int durationOffset_{0}; class Frames
pajlada::Signals::Connection gifTimerConnection_; {
}; public:
} // namespace detail Frames();
Frames(QList<Frame> &&frames);
~Frames();
Frames(const Frames &) = delete;
Frames &operator=(const Frames &) = delete;
Frames(Frames &&) = delete;
Frames &operator=(Frames &&) = delete;
void clear();
bool empty() const;
bool animated() const;
void advance();
std::optional<QPixmap> current() const;
std::optional<QPixmap> first() const;
private:
int64_t memoryUsage() const;
void processOffset();
QList<Frame> items_;
QList<Frame>::size_type index_{0};
int durationOffset_{0};
pajlada::Signals::Connection gifTimerConnection_;
};
QList<Frame> readFrames(QImageReader &reader, const Url &url);
void assignFrames(std::weak_ptr<Image> weak, QList<Frame> parsed);
} // namespace chatterino::detail
namespace chatterino {
class Image; class Image;
using ImagePtr = std::shared_ptr<Image>; using ImagePtr = std::shared_ptr<Image>;
@ -116,9 +127,11 @@ private:
mutable std::chrono::time_point<std::chrono::steady_clock> lastUsed_; mutable std::chrono::time_point<std::chrono::steady_clock> lastUsed_;
// gui thread only // gui thread only
std::unique_ptr<detail::Frames> frames_{}; std::unique_ptr<detail::Frames> frames_;
friend class ImageExpirationPool; friend class ImageExpirationPool;
friend void detail::assignFrames(std::weak_ptr<Image>,
QList<detail::Frame>);
}; };
// forward-declarable function that calls Image::getEmpty() under the hood. // forward-declarable function that calls Image::getEmpty() under the hood.

View file

@ -70,4 +70,13 @@ static void runInGuiThread(F &&fun)
} }
} }
template <typename F>
inline void postToGuiThread(F &&fun)
{
assert(!isGuiThread() &&
"postToGuiThread must be called from a non-GUI thread");
postToThread(std::forward<F>(fun));
}
} // namespace chatterino } // namespace chatterino