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,26 +21,23 @@
#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()
{
DebugCount::increase("images");
}
Frames::Frames(QVector<Frame<QPixmap>> &&frames) Frames::Frames()
{
DebugCount::increase("images");
}
Frames::Frames(QList<Frame> &&frames)
: items_(std::move(frames)) : items_(std::move(frames))
{ {
assertInGuiThread(); assertInGuiThread();
DebugCount::increase("images"); DebugCount::increase("images");
if (!this->empty()) if (!this->empty())
@ -58,9 +55,8 @@ namespace detail {
}); });
} }
auto totalLength = auto totalLength = std::accumulate(this->items_.begin(), this->items_.end(),
std::accumulate(this->items_.begin(), this->items_.end(), 0UL, 0UL, [](auto init, auto &&frame) {
[](auto init, auto &&frame) {
return init + frame.duration; return init + frame.duration;
}); });
@ -71,17 +67,16 @@ namespace detail {
else else
{ {
this->durationOffset_ = std::min<int>( this->durationOffset_ = std::min<int>(
int(getIApp()->getEmotes()->getGIFTimer().position() % int(getIApp()->getEmotes()->getGIFTimer().position() % totalLength),
totalLength),
60000); 60000);
} }
this->processOffset(); this->processOffset();
DebugCount::increase("image bytes", this->memoryUsage()); DebugCount::increase("image bytes", this->memoryUsage());
DebugCount::increase("image bytes (ever loaded)", this->memoryUsage()); DebugCount::increase("image bytes (ever loaded)", this->memoryUsage());
} }
Frames::~Frames() Frames::~Frames()
{ {
assertInGuiThread(); assertInGuiThread();
DebugCount::decrease("images"); DebugCount::decrease("images");
if (!this->empty()) if (!this->empty())
@ -94,14 +89,13 @@ namespace detail {
DebugCount::decrease("animated images"); DebugCount::decrease("animated images");
} }
DebugCount::decrease("image bytes", this->memoryUsage()); DebugCount::decrease("image bytes", this->memoryUsage());
DebugCount::increase("image bytes (ever unloaded)", DebugCount::increase("image bytes (ever unloaded)", this->memoryUsage());
this->memoryUsage());
this->gifTimerConnection_.disconnect(); this->gifTimerConnection_.disconnect();
} }
int64_t Frames::memoryUsage() const int64_t Frames::memoryUsage() const
{ {
int64_t usage = 0; int64_t usage = 0;
for (const auto &frame : this->items_) for (const auto &frame : this->items_)
{ {
@ -112,16 +106,16 @@ namespace detail {
usage += memory; usage += memory;
} }
return usage; return usage;
} }
void Frames::advance() void Frames::advance()
{ {
this->durationOffset_ += GIF_FRAME_LENGTH; this->durationOffset_ += GIF_FRAME_LENGTH;
this->processOffset(); this->processOffset();
} }
void Frames::processOffset() void Frames::processOffset()
{ {
if (this->items_.isEmpty()) if (this->items_.isEmpty())
{ {
return; return;
@ -141,65 +135,63 @@ namespace detail {
break; break;
} }
} }
} }
void Frames::clear() void Frames::clear()
{ {
assertInGuiThread(); assertInGuiThread();
if (!this->empty()) if (!this->empty())
{ {
DebugCount::decrease("loaded images"); DebugCount::decrease("loaded images");
} }
DebugCount::decrease("image bytes", this->memoryUsage()); DebugCount::decrease("image bytes", this->memoryUsage());
DebugCount::increase("image bytes (ever unloaded)", DebugCount::increase("image bytes (ever unloaded)", this->memoryUsage());
this->memoryUsage());
this->items_.clear(); this->items_.clear();
this->index_ = 0; this->index_ = 0;
this->durationOffset_ = 0; this->durationOffset_ = 0;
this->gifTimerConnection_.disconnect(); this->gifTimerConnection_.disconnect();
} }
bool Frames::empty() const bool Frames::empty() const
{ {
return this->items_.empty(); return this->items_.empty();
} }
bool Frames::animated() const bool Frames::animated() const
{ {
return this->items_.size() > 1; return this->items_.size() > 1;
} }
std::optional<QPixmap> Frames::current() const std::optional<QPixmap> Frames::current() const
{ {
if (this->items_.empty()) if (this->items_.empty())
{ {
return std::nullopt; return std::nullopt;
} }
return this->items_[this->index_].image; return this->items_[this->index_].image;
} }
std::optional<QPixmap> Frames::first() const std::optional<QPixmap> Frames::first() const
{ {
if (this->items_.empty()) if (this->items_.empty())
{ {
return std::nullopt; return std::nullopt;
} }
return this->items_.front().image; return this->items_.front().image;
} }
// functions QList<Frame> readFrames(QImageReader &reader, const Url &url)
QVector<Frame<QImage>> readFrames(QImageReader &reader, const Url &url) {
{ QList<Frame> frames;
QVector<Frame<QImage>> frames;
frames.reserve(reader.imageCount()); frames.reserve(reader.imageCount());
QImage image;
for (int index = 0; index < reader.imageCount(); ++index) for (int index = 0; index < reader.imageCount(); ++index)
{ {
if (reader.read(&image)) auto pixmap = QPixmap::fromImageReader(&reader);
if (!pixmap.isNull())
{ {
// It seems that browsers have special logic for fast animations. // It seems that browsers have special logic for fast animations.
// This implements Chrome and Firefox's behavior which uses // This implements Chrome and Firefox's behavior which uses
@ -212,85 +204,55 @@ namespace detail {
duration = 100; duration = 100;
} }
duration = std::max(20, duration); duration = std::max(20, duration);
frames.push_back(Frame<QImage>{std::move(image), duration}); frames.append(Frame{
.image = std::move(pixmap),
.duration = duration,
});
} }
} }
if (frames.empty()) if (frames.empty())
{ {
qCDebug(chatterinoImage) qCDebug(chatterinoImage) << "Error while reading image" << url.string
<< "Error while reading image" << url.string << ": '" << ": '" << reader.errorString() << "'";
<< reader.errorString() << "'";
} }
return frames; return frames;
} }
// parsed void assignFrames(std::weak_ptr<Image> weak, QList<Frame> parsed)
template <typename Assign> {
void assignDelayed( static bool isPushQueued;
std::queue<std::pair<Assign, QVector<Frame<QPixmap>>>> &queued,
std::mutex &mutex, std::atomic_bool &loadedEventQueued) auto cb = [parsed = std::move(parsed), weak = std::move(weak)]() mutable {
auto shared = weak.lock();
if (!shared)
{ {
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; return;
} }
} shared->frames_ = std::make_unique<detail::Frames>(std::move(parsed));
// 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)
{
isPushQueued = true;
postToThread([] {
isPushQueued = false;
getIApp()->getWindows()->forceLayoutChannelViews(); 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 postToGuiThread(cb);
}
} // namespace chatterino::detail
namespace chatterino {
// 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,17 +18,23 @@
#include <optional> #include <optional>
namespace chatterino { namespace chatterino {
namespace detail {
template <typename Image> class Image;
struct Frame {
Image image; } // namespace chatterino
namespace chatterino::detail {
struct Frame {
QPixmap image;
int duration; int duration;
}; };
class Frames
{ class Frames
public: {
public:
Frames(); Frames();
Frames(QVector<Frame<QPixmap>> &&frames); Frames(QList<Frame> &&frames);
~Frames(); ~Frames();
Frames(const Frames &) = delete; Frames(const Frames &) = delete;
@ -45,15 +50,21 @@ namespace detail {
std::optional<QPixmap> current() const; std::optional<QPixmap> current() const;
std::optional<QPixmap> first() const; std::optional<QPixmap> first() const;
private: private:
int64_t memoryUsage() const; int64_t memoryUsage() const;
void processOffset(); void processOffset();
QVector<Frame<QPixmap>> items_; QList<Frame> items_;
int index_{0}; QList<Frame>::size_type index_{0};
int durationOffset_{0}; int durationOffset_{0};
pajlada::Signals::Connection gifTimerConnection_; pajlada::Signals::Connection gifTimerConnection_;
}; };
} // namespace detail
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