improve image caching for better performance
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 27s
JS/TS / lint (pull_request) Successful in 33s
Python / fmt (pull_request) Successful in 27s
Python / lint (pull_request) Successful in 30s
Python / test (pull_request) Successful in 59s
Python / typecheck (pull_request) Failing after 1m5s
C++ / build (pull_request) Successful in 2m26s
Rust / build (pull_request) Successful in 1m30s
Rust / fmt (pull_request) Successful in 1m12s
Python / buildcheck (pull_request) Successful in 2m19s
Rust / clippy (pull_request) Successful in 1m56s
C++ / clang-tidy (pull_request) Successful in 4m1s

This commit is contained in:
2026-08-14 19:49:48 +02:00
parent 88a2808a47
commit ec80adf0a5
3 changed files with 150 additions and 54 deletions
+14 -2
View File
@@ -233,7 +233,19 @@ Item {
} }
implicitHeight: tileHeight + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal implicitHeight: tileHeight + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal
implicitWidth: screen ? Math.max(0, screen.width - Config.bar.rounding * 50 - panels.bar.implicitWidth * 2) : 0 implicitWidth: {
if (!screen)
return 0;
const barMargins = Config.bar.border * 2;
let margins = 0;
if (visibilities.utilities || visibilities.sidebar)
margins = panels.utilities.implicitWidth;
const maxWidth = screen.width - (barMargins + margins) * 2 - Config.bar.rounding * 4;
return maxWidth;
}
width: implicitWidth
Component.onCompleted: { Component.onCompleted: {
const idx = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent); const idx = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent);
@@ -248,7 +260,7 @@ Item {
anchors.fill: parent anchors.fill: parent
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
cacheBuffer: root.width * 10 cacheBuffer: root.width
clip: false clip: false
currentIndex: root.centerBlockStart currentIndex: root.centerBlockStart
displayMarginBeginning: root.arrangement.effectiveExtraWidth displayMarginBeginning: root.arrangement.effectiveExtraWidth
+2
View File
@@ -55,6 +55,8 @@ Item {
height: root.tileHeight height: root.tileHeight
path: root.modelData ? root.modelData.path : "" path: root.modelData ? root.modelData.path : ""
smooth: root.isCurrent smooth: root.isCurrent
sourceSize.height: root.tileHeight
sourceSize.width: root.tileHeight * (16 / 9)
width: root.tileHeight * (16 / 9) width: root.tileHeight * (16 / 9)
} }
+134 -52
View File
@@ -1,5 +1,7 @@
#include "cachingimagemanager.hpp" #include "cachingimagemanager.hpp"
#include <algorithm>
#include <QtQuick/qquickwindow.h> #include <QtQuick/qquickwindow.h>
#include <qcryptographichash.h> #include <qcryptographichash.h>
#include <qdir.h> #include <qdir.h>
@@ -7,10 +9,27 @@
#include <qfuturewatcher.h> #include <qfuturewatcher.h>
#include <qimagereader.h> #include <qimagereader.h>
#include <qpainter.h> #include <qpainter.h>
#include <qthread.h>
#include <qthreadpool.h>
#include <qtconcurrentrun.h> #include <qtconcurrentrun.h>
namespace ZShell::internal { namespace ZShell::internal {
namespace {
QThreadPool* imageIoPool() {
static QThreadPool pool;
static const bool init = [] {
pool.setMaxThreadCount(std::max(2, QThread::idealThreadCount() / 2));
return true;
}();
(void)init;
return &pool;
}
} // namespace
qreal CachingImageManager::effectiveScale() const { qreal CachingImageManager::effectiveScale() const {
if (m_item && m_item->window()) { if (m_item && m_item->window()) {
return m_item->window()->devicePixelRatio(); return m_item->window()->devicePixelRatio();
@@ -101,14 +120,13 @@ void CachingImageManager::updateSource() {
void CachingImageManager::updateSource(const QString& path) { void CachingImageManager::updateSource(const QString& path) {
if (path.isEmpty() || path == m_shaPath) { if (path.isEmpty() || path == m_shaPath) {
// Path is empty or already calculating sha for path
return; return;
} }
m_shaPath = path; m_shaPath = path;
const auto future = const auto future =
QtConcurrent::run(&CachingImageManager::sha256sum, path); QtConcurrent::run(imageIoPool(), &CachingImageManager::sha256sum, path);
const auto watcher = new QFutureWatcher<QString>(this); const auto watcher = new QFutureWatcher<QString>(this);
@@ -118,7 +136,16 @@ void CachingImageManager::updateSource(const QString& path) {
this, this,
[watcher, path, this]() { [watcher, path, this]() {
if (m_path != path) { if (m_path != path) {
// Object is destroyed or path has changed, ignore watcher->deleteLater();
return;
}
const QString key = watcher->result();
if (key.isEmpty()) {
if (m_shaPath == path) {
m_shaPath = QString();
}
watcher->deleteLater(); watcher->deleteLater();
return; return;
} }
@@ -126,19 +153,29 @@ void CachingImageManager::updateSource(const QString& path) {
const QSize size = effectiveSize(); const QSize size = effectiveSize();
if (!m_item || !size.width() || !size.height()) { if (!m_item || !size.width() || !size.height()) {
if (m_shaPath == path) {
m_shaPath = QString();
}
watcher->deleteLater(); watcher->deleteLater();
return; return;
} }
const QString fillMode = m_item->property("fillMode").toString(); const QString fillMode = m_item->property("fillMode").toString();
// clang-format off const QString filename =
const QString filename = QString("%1@%2x%3-%4.png") QString("%1@%2x%3-%4.png")
.arg(watcher->result()).arg(size.width()).arg(size.height()) .arg(key)
.arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch"); .arg(size.width())
// clang-format on .arg(size.height())
.arg(
fillMode == "PreserveAspectCrop" ? "crop"
: fillMode == "PreserveAspectFit" ? "fit"
: "stretch");
const QUrl cache = m_cacheDir.resolved(QUrl(filename)); const QUrl cache = m_cacheDir.resolved(QUrl(filename));
if (m_cachePath == cache) { if (m_cachePath == cache) {
if (m_shaPath == path) {
m_shaPath = QString();
}
watcher->deleteLater(); watcher->deleteLater();
return; return;
} }
@@ -149,6 +186,9 @@ void CachingImageManager::updateSource(const QString& path) {
if (!cache.isLocalFile()) { if (!cache.isLocalFile()) {
qWarning() << "CachingImageManager::updateSource: cachePath" qWarning() << "CachingImageManager::updateSource: cachePath"
<< cache << "is not a local file"; << cache << "is not a local file";
if (m_shaPath == path) {
m_shaPath = QString();
}
watcher->deleteLater(); watcher->deleteLater();
return; return;
} }
@@ -161,7 +201,6 @@ void CachingImageManager::updateSource(const QString& path) {
createCache(path, cache.toLocalFile(), fillMode, size); createCache(path, cache.toLocalFile(), fillMode, size);
} }
// Clear current running sha if same
if (m_shaPath == path) { if (m_shaPath == path) {
m_shaPath = QString(); m_shaPath = QString();
} }
@@ -170,7 +209,6 @@ void CachingImageManager::updateSource(const QString& path) {
}); });
watcher->setFuture(future); watcher->setFuture(future);
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
QUrl CachingImageManager::cachePath() const { QUrl CachingImageManager::cachePath() const {
@@ -182,61 +220,105 @@ void CachingImageManager::createCache(
const QString& cache, const QString& cache,
const QString& fillMode, const QString& fillMode,
const QSize& size) const { const QSize& size) const {
QThreadPool::globalInstance()->start([path, cache, fillMode, size] { auto* watcher =
QImage image(path); new QFutureWatcher<bool>(const_cast<CachingImageManager*>(this));
if (image.isNull()) { connect(
qWarning() << "CachingImageManager::createCache: failed to read" watcher,
<< path; &QFutureWatcher<bool>::finished,
return; this,
} [this, watcher, cache, path]() {
if (watcher->result() && m_item && m_path == path &&
m_cachePath.toLocalFile() == cache) {
m_item->setProperty("source", QUrl::fromLocalFile(cache));
}
image.convertTo(QImage::Format_ARGB32); watcher->deleteLater();
});
if (fillMode == "PreserveAspectCrop") { const auto future =
image = image.scaled( QtConcurrent::run(
size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); imageIoPool(), [path, cache, fillMode, size]() -> bool {
} else if (fillMode == "PreserveAspectFit") { QImageReader reader(path);
image = image.scaled(
size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
} else {
image = image.scaled(
size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
if (fillMode == "PreserveAspectCrop" || if (reader.supportsOption(QImageIOHandler::ScaledSize)) {
fillMode == "PreserveAspectFit") { const QSize full = reader.size();
QImage canvas(size, QImage::Format_ARGB32); if (full.isValid() && !full.isEmpty()) {
canvas.fill(Qt::transparent); const QSize hint = size * 2;
QSize scaled = full;
scaled.scale(hint, Qt::KeepAspectRatioByExpanding);
scaled = QSize(
std::min(scaled.width(), full.width()),
std::min(scaled.height(), full.height()));
reader.setScaledSize(scaled);
}
}
QPainter painter(&canvas); QImage image = reader.read();
painter.drawImage(
(size.width() - image.width()) / 2,
(size.height() - image.height()) / 2,
image);
painter.end();
image = canvas; if (image.isNull()) {
} qWarning()
<< "CachingImageManager::createCache: failed to read"
<< path << reader.errorString();
return false;
}
const QString parent = QFileInfo(cache).absolutePath(); image.convertTo(QImage::Format_ARGB32);
if (!QDir().mkpath(parent) || !image.save(cache)) {
qWarning() << "CachingImageManager::createCache: failed to save to" if (fillMode == "PreserveAspectCrop") {
<< cache; image = image.scaled(
} size,
}); Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
} else if (fillMode == "PreserveAspectFit") {
image = image.scaled(
size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
} else {
image = image.scaled(
size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
if (fillMode == "PreserveAspectCrop" ||
fillMode == "PreserveAspectFit") {
QImage canvas(size, QImage::Format_ARGB32);
canvas.fill(Qt::transparent);
QPainter painter(&canvas);
painter.drawImage(
(size.width() - image.width()) / 2,
(size.height() - image.height()) / 2,
image);
painter.end();
image = canvas;
}
const QString parent = QFileInfo(cache).absolutePath();
if (!QDir().mkpath(parent) || !image.save(cache)) {
qWarning()
<< "CachingImageManager::createCache: failed to save to"
<< cache;
return false;
}
return true;
});
watcher->setFuture(future);
} }
QString CachingImageManager::sha256sum(const QString& path) { QString CachingImageManager::sha256sum(const QString& path) {
QFile file(path); const QFileInfo info(path);
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "CachingImageManager::sha256sum: failed to open" << path; if (!info.exists() || !info.isFile()) {
return ""; qWarning() << "CachingImageManager::sha256sum: failed to stat" << path;
return QString();
} }
QCryptographicHash hash(QCryptographicHash::Sha256); QCryptographicHash hash(QCryptographicHash::Sha256);
hash.addData(&file); hash.addData(path.toUtf8());
file.close(); hash.addData(QByteArray::number(info.size()));
hash.addData(QByteArray::number(info.lastModified().toMSecsSinceEpoch()));
return hash.result().toHex(); return hash.result().toHex();
} }