From a2250dd85dbba8ae58f129a51c121c4f84b85c2c Mon Sep 17 00:00:00 2001 From: tetektoza Date: Fri, 22 Aug 2025 05:06:07 +0200 Subject: [PATCH] Start: Add caching for performance for thumbnails on start page (#23186) * Start: Add caching for performance for thumbnails on start page So currently we can have a problem where we are trying to load whole image as a thumbnail, which can result in over 256MB~ of internal buffer memory. Also, even if we load smaller size - every now and then start page gets refreshed, so to check if any file got modified and refresh it in recent files. This is okay, but with large files it loads them over and over, resulting in start page lagging. Solution for that is first - load image thumbnails as scaled, small images instead of full image. Second - for performance, use caching by using `path:modtime:size` key. If the item fits this key, it means it didn't change, so just proceed further and get this item from the cache. If the key is different, it means it has been changed on the disk, so reload it. * Start: Deactivate Start page if it loses focus to stop receiving events As the title says. This prevents Start page from processing unnecessary events (mostly paint ones that were causing extreme lag previously) if it is not opened. * Start: Preserve aspect ratio of the image for the thumbnail * Start: use brace initialization when returning QString * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- src/Mod/Start/Gui/FileCardDelegate.cpp | 114 +++++++++++++++++++------ src/Mod/Start/Gui/FileCardDelegate.h | 9 ++ src/Mod/Start/Gui/StartView.cpp | 38 +++++++++ src/Mod/Start/Gui/StartView.h | 7 ++ 4 files changed, 142 insertions(+), 26 deletions(-) diff --git a/src/Mod/Start/Gui/FileCardDelegate.cpp b/src/Mod/Start/Gui/FileCardDelegate.cpp index 5a260753f6..7162a40710 100644 --- a/src/Mod/Start/Gui/FileCardDelegate.cpp +++ b/src/Mod/Start/Gui/FileCardDelegate.cpp @@ -35,21 +35,39 @@ #include #include #include +#include #endif #include "FileCardDelegate.h" #include "../App/DisplayedFilesModel.h" #include "App/Application.h" #include +#include +#include +#include using namespace Start; +QCache FileCardDelegate::_thumbnailCache; + FileCardDelegate::FileCardDelegate(QObject* parent) : QStyledItemDelegate(parent) { _parameterGroup = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Mod/Start"); setObjectName(QStringLiteral("thumbnailWidget")); + + // Initialize cache size based on thumbnail size (only once) + if (_thumbnailCache.maxCost() == 0) { + int thumbnailSize = + static_cast(_parameterGroup->GetInt("FileThumbnailIconsSize", 128)); + int thumbnailMemory = thumbnailSize * thumbnailSize * 4; // rgba + int maxCacheItems = (CACHE_SIZE_MB * 1024 * 1024) / thumbnailMemory; + _thumbnailCache.setMaxCost(maxCacheItems); + Base::Console().log("FileCardDelegate: Initialized thumbnail cache for %d items (%d MB)\n", + maxCacheItems, + CACHE_SIZE_MB); + } } void FileCardDelegate::paint(QPainter* painter, @@ -135,49 +153,93 @@ QSize FileCardDelegate::sizeHint(const QStyleOptionViewItem& option, const QMode return {cardWidth, cardHeight}; } -namespace -{ -QPixmap pixmapToSizedQImage(const QImage& pixmap, int size) -{ - return QPixmap::fromImage(pixmap).scaled(size, - size, - Qt::AspectRatioMode::KeepAspectRatio, - Qt::TransformationMode::SmoothTransformation); -} -} // namespace - QPixmap FileCardDelegate::generateThumbnail(const QString& path) const { auto thumbnailSize = static_cast(_parameterGroup->GetInt("FileThumbnailIconsSize", 128)); // NOLINT + + // check if we have this thumbnail already inside cache, don't load it once again + QString cacheKey = getCacheKey(path, thumbnailSize); + if (!cacheKey.isEmpty()) { + if (QPixmap* cachedThumbnail = _thumbnailCache.object(cacheKey)) { + return *cachedThumbnail; // cache hit - we bail out + } + } + + // cache miss - go and load the thumbnail as it could be changed + return loadAndCacheThumbnail(path, thumbnailSize); +} + +QString FileCardDelegate::getCacheKey(const QString& path, int thumbnailSize) const +{ + QFileInfo fileInfo(path); + if (!fileInfo.exists()) { + return {}; + } + + // create cache key: path:modtime:size + QString modTime = QString::number(fileInfo.lastModified().toSecsSinceEpoch()); + return QStringLiteral("%1:%2:%3").arg(path, modTime, QString::number(thumbnailSize)); +} + +QPixmap FileCardDelegate::loadAndCacheThumbnail(const QString& path, int thumbnailSize) const +{ + QPixmap thumbnail; + if (path.endsWith(QLatin1String(".fcstd"), Qt::CaseSensitivity::CaseInsensitive)) { // This is a fallback, the model will have pulled the thumbnail out of the FCStd file if it // existed. QImageReader reader(QLatin1String(":/icons/freecad-doc.svg")); - reader.setScaledSize({thumbnailSize, thumbnailSize}); - return QPixmap::fromImage(reader.read()); + reader.setScaledSize(QSize(thumbnailSize, thumbnailSize)); + thumbnail = QPixmap::fromImage(reader.read()); } - if (path.endsWith(QLatin1String(".fcmacro"), Qt::CaseSensitivity::CaseInsensitive)) { + else if (path.endsWith(QLatin1String(".fcmacro"), Qt::CaseSensitivity::CaseInsensitive)) { QImageReader reader(QLatin1String(":/icons/MacroEditor.svg")); - reader.setScaledSize({thumbnailSize, thumbnailSize}); - return QPixmap::fromImage(reader.read()); + reader.setScaledSize(QSize(thumbnailSize, thumbnailSize)); + thumbnail = QPixmap::fromImage(reader.read()); } - if (!QImageReader::imageFormat(path).isEmpty()) { + else if (!QImageReader::imageFormat(path).isEmpty()) { // It is an image: it can be its own thumbnail QImageReader reader(path); + + // get original size to calculate proper aspect-preserving scaled size + QSize originalSize = reader.size(); + if (originalSize.isValid()) { + QSize scaledSize = + originalSize.scaled(thumbnailSize, thumbnailSize, Qt::KeepAspectRatio); + reader.setScaledSize(scaledSize); + } + auto image = reader.read(); if (!image.isNull()) { - return pixmapToSizedQImage(image, thumbnailSize); + thumbnail = QPixmap::fromImage(image); + } + else { + Base::Console().log("FileCardDelegate: Failed to load image %s: %s\n", + path.toStdString().c_str(), + reader.errorString().toStdString().c_str()); } } - QIcon icon = QFileIconProvider().icon(QFileInfo(path)); - if (!icon.isNull()) { - QPixmap pixmap = icon.pixmap(thumbnailSize); - if (!pixmap.isNull()) { - return pixmap; + + // fallback to system icon if no thumbnail was generated + if (thumbnail.isNull()) { + QIcon icon = QFileIconProvider().icon(QFileInfo(path)); + if (!icon.isNull()) { + thumbnail = icon.pixmap(thumbnailSize); + } + else { + thumbnail = QPixmap(thumbnailSize, thumbnailSize); + thumbnail.fill(); } } - QPixmap pixmap = QPixmap(thumbnailSize, thumbnailSize); - pixmap.fill(); - return pixmap; + + // cache the thumbnail if valid + if (!thumbnail.isNull()) { + QString cacheKey = getCacheKey(path, thumbnailSize); + if (!cacheKey.isEmpty()) { + _thumbnailCache.insert(cacheKey, new QPixmap(thumbnail), 1); + } + } + + return thumbnail; } diff --git a/src/Mod/Start/Gui/FileCardDelegate.h b/src/Mod/Start/Gui/FileCardDelegate.h index 3da54f3c53..a072c074a4 100644 --- a/src/Mod/Start/Gui/FileCardDelegate.h +++ b/src/Mod/Start/Gui/FileCardDelegate.h @@ -25,6 +25,9 @@ #define FREECAD_START_FILECARDDELEGATE_H #include +#include +#include +#include #include #include #include @@ -45,10 +48,16 @@ protected: QPixmap generateThumbnail(const QString& path) const; private: + QString getCacheKey(const QString& path, int thumbnailSize) const; + QPixmap loadAndCacheThumbnail(const QString& path, int thumbnailSize) const; + Base::Reference _parameterGroup; const int margin = 11; const int textspacing = 2; QPushButton styleButton; + + static QCache _thumbnailCache; // cache key structure: "path:modtime:size" + static constexpr const int CACHE_SIZE_MB = 50; // 50MB cache limit }; diff --git a/src/Mod/Start/Gui/StartView.cpp b/src/Mod/Start/Gui/StartView.cpp index 923fabf563..5b039357e4 100644 --- a/src/Mod/Start/Gui/StartView.cpp +++ b/src/Mod/Start/Gui/StartView.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #endif #include "StartView.h" @@ -248,6 +249,7 @@ void StartView::configureFileCardWidget(QListView* fileCardWidget) { auto delegate = gsl::owner(new FileCardDelegate(fileCardWidget)); fileCardWidget->setItemDelegate(delegate); + fileCardWidget->setMinimumWidth(fileCardWidget->parentWidget()->width()); // fileCardWidget->setGridSize( // fileCardWidget->itemDelegate()->sizeHint(QStyleOptionViewItem(), @@ -442,12 +444,48 @@ void StartView::changeEvent(QEvent* event) } } } + if (event->type() == QEvent::LanguageChange) { this->retranslateUi(); } + Gui::MDIView::changeEvent(event); } +void StartView::showEvent(QShowEvent* event) +{ + if (auto mainWindow = Gui::getMainWindow()) { + if (auto mdiArea = mainWindow->findChild()) { + connect(mdiArea, + &QMdiArea::subWindowActivated, + this, + &StartView::onMdiSubWindowActivated, + Qt::UniqueConnection); + } + } + Gui::MDIView::showEvent(event); +} + +void StartView::onMdiSubWindowActivated(QMdiSubWindow* subWindow) +{ + // check if start view is activated subwindow if yes, then enable updates + // so we can once again receive paint events + bool isOurWindow = subWindow && subWindow->isAncestorOf(this); + setListViewUpdatesEnabled(isOurWindow); +} + +void StartView::setListViewUpdatesEnabled(bool enabled) +{ + // disable updates on all QListView widgets when inactive to prevent unnecessary paint events + QList listViews = findChildren(); + for (QListView* listView : listViews) { + listView->setUpdatesEnabled(enabled); + if (listView->viewport()) { + listView->viewport()->setUpdatesEnabled(enabled); + } + } +} + void StartView::retranslateUi() { QString title = QCoreApplication::translate("Workbench", "Start"); diff --git a/src/Mod/Start/Gui/StartView.h b/src/Mod/Start/Gui/StartView.h index f550f6700b..31397cf5f2 100644 --- a/src/Mod/Start/Gui/StartView.h +++ b/src/Mod/Start/Gui/StartView.h @@ -38,6 +38,7 @@ class QEvent; class QGridLayout; class QLabel; class QListView; +class QMdiSubWindow; class QScrollArea; class QStackedWidget; class QPushButton; @@ -82,6 +83,7 @@ public: protected: void changeEvent(QEvent* e) override; + void showEvent(QShowEvent* event) override; void configureNewFileButtons(QLayout* layout) const; static void configureFileCardWidget(QListView* fileCardWidget); @@ -98,8 +100,13 @@ protected: QString fileCardStyle() const; +private Q_SLOTS: + void onMdiSubWindowActivated(QMdiSubWindow* subWindow); + private: void retranslateUi(); + void setListViewUpdatesEnabled(bool enabled); + QStackedWidget* _contents = nullptr; Start::RecentFilesModel _recentFilesModel; Start::ExamplesModel _examplesModel;