File indexing completed on 2024-05-05 04:22:00

0001 // SPDX-FileCopyrightText: 2003 David Faure <faure@kde.org>
0002 // SPDX-FileCopyrightText: 2003 Simon Hausmann <hausmann@kde.org>
0003 // SPDX-FileCopyrightText: 2003-2022 Jesper K. Pedersen <jesper.pedersen@kdab.com>
0004 // SPDX-FileCopyrightText: 2005-2007 Dirk Mueller <mueller@kde.org>
0005 // SPDX-FileCopyrightText: 2007 Tuomas Suutari <tuomas@nepnep.net>
0006 // SPDX-FileCopyrightText: 2008 Henner Zeller <h.zeller@acm.org>
0007 // SPDX-FileCopyrightText: 2008-2013 Jan Kundrát <jkt@flaska.net>
0008 // SPDX-FileCopyrightText: 2013 Andreas Neustifter <andreas.neustifter@gmail.com>
0009 // SPDX-FileCopyrightText: 2013-2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0010 // SPDX-FileCopyrightText: 2018-2019 Robert Krawitz <rlk@alum.mit.edu>
0011 
0012 // SPDX-License-Identifier: GPL-2.0-or-later
0013 
0014 #include "AsyncLoader.h"
0015 
0016 #include "CancelEvent.h"
0017 #include "ImageClientInterface.h"
0018 #include "ImageEvent.h"
0019 #include "ImageLoaderThread.h"
0020 #include "ThumbnailBuilder.h"
0021 
0022 #include <BackgroundJobs/HandleVideoThumbnailRequestJob.h>
0023 #include <BackgroundTaskManager/JobManager.h>
0024 #include <MainWindow/FeatureDialog.h>
0025 #include <kpabase/FileExtensions.h>
0026 #include <kpabase/SettingsData.h>
0027 #include <kpathumbnails/ThumbnailCache.h>
0028 
0029 #include <QIcon>
0030 #include <QPixmapCache>
0031 
0032 ImageManager::AsyncLoader *ImageManager::AsyncLoader::s_instance = nullptr;
0033 
0034 // -- Manager --
0035 
0036 ImageManager::AsyncLoader *ImageManager::AsyncLoader::instance()
0037 {
0038     if (!s_instance) {
0039         s_instance = new AsyncLoader;
0040         s_instance->init();
0041     }
0042 
0043     return s_instance;
0044 }
0045 
0046 // We need this as a separate method as the s_instance variable will otherwise not be initialized
0047 // corrected before the thread starts.
0048 void ImageManager::AsyncLoader::init()
0049 {
0050 
0051     // Use up to three cores for thumbnail generation. No more than three as that
0052     // likely will make it less efficient due to three cores hitting the harddisk at the same time.
0053     // This might limit the throughput on SSD systems, but we likely have a few years before people
0054     // put all of their pictures on SSDs.
0055     // rlk 20180515: with improvements to the thumbnail generation code, I've conducted
0056     // experiments demonstrating benefit even at 2x the number of hyperthreads, even on
0057     // an HDD.  However, we need to reserve a thread for the UI or it gets very sluggish
0058     // We need one more core in the computer for the GUI thread, but we won't dedicate it to GUI,
0059     // as that'd mean that a dual-core box would only have one core decoding images, which would be
0060     // suboptimal.
0061     // In case of only one core in the computer, use one core for thumbnail generation
0062     // jzarl:  It seems that many people have their images on NFS-mounts.
0063     //         Should we somehow detect this and allocate less threads there?
0064     //         rlk 20180515: IMO no; if anything, we need more threads to hide the latency of NFS.
0065     int desiredThreads = Settings::SettingsData::instance()->getThumbnailBuilderThreadCount();
0066     if (desiredThreads == 0) {
0067         desiredThreads = qMax(1, qMin(16, QThread::idealThreadCount() - 1));
0068     }
0069 
0070     for (int i = 0; i < desiredThreads; ++i) {
0071         ImageLoaderThread *imageLoader = new ImageLoaderThread();
0072         // The thread is set to the lowest priority to ensure that it doesn't starve the GUI thread.
0073         m_threadList << imageLoader;
0074         imageLoader->start(QThread::IdlePriority);
0075     }
0076 }
0077 
0078 bool ImageManager::AsyncLoader::load(ImageRequest *request)
0079 {
0080     if (m_exitRequested)
0081         return false;
0082 
0083     // rlk 2018-05-15: Skip this check here.  Even if the check
0084     // succeeds at this point, it may fail later, and if we're suddenly
0085     // processing a lot of requests (e. g. a thumbnail build),
0086     // this may be very I/O-intensive since it actually has to
0087     // read the inode.
0088     // silently ignore images not (currently) on disk:
0089     // if ( ! request->fileSystemFileName().exists() )
0090     //    return false;
0091 
0092     if (KPABase::isVideo(request->fileSystemFileName())) {
0093         if (!loadVideo(request))
0094             return false;
0095     } else {
0096         loadImage(request);
0097     }
0098     return true;
0099 }
0100 
0101 bool ImageManager::AsyncLoader::loadVideo(ImageRequest *request)
0102 {
0103     if (m_exitRequested)
0104         return false;
0105 
0106     if (!MainWindow::FeatureDialog::hasVideoThumbnailer())
0107         return false;
0108 
0109     if (!request->fileSystemFileName().exists())
0110         return false;
0111 
0112     BackgroundTaskManager::Priority priority = (request->priority() > ThumbnailInvisible)
0113         ? BackgroundTaskManager::ForegroundThumbnailRequest
0114         : BackgroundTaskManager::BackgroundVideoThumbnailRequest;
0115 
0116     BackgroundTaskManager::JobManager::instance()->addJob(
0117         new BackgroundJobs::HandleVideoThumbnailRequestJob(request, priority, MainWindow::Window::theMainWindow()->thumbnailCache()));
0118     return true;
0119 }
0120 
0121 void ImageManager::AsyncLoader::loadImage(ImageRequest *request)
0122 {
0123     QMutexLocker dummy(&m_lock);
0124     if (m_exitRequested)
0125         return;
0126     auto req = m_currentLoading.constFind(request);
0127     if (req != m_currentLoading.cend() && m_loadList.isRequestStillValid(request)) {
0128         // The last part of the test above is needed to not fail on a race condition from AnnotationDialog::ImagePreview, where the preview
0129         // at startup request the same image numerous time (likely from resize event).
0130         Q_ASSERT(*req != request);
0131         delete request;
0132 
0133         return; // We are currently loading it, calm down and wait please ;-)
0134     }
0135 
0136     // Try harder to find a pending request.  Unfortunately, we can't simply use
0137     // m_currentLoading.contains() because that will compare pointers
0138     // when we want to compare values.
0139     for (req = m_currentLoading.cbegin(); req != m_currentLoading.cend(); req++) {
0140         ImageRequest *r = *req;
0141         if (*request == *r) {
0142             delete request;
0143             return; // We are currently loading it, calm down and wait please ;-)
0144         }
0145     }
0146 
0147     // if request is "fresh" (not yet pending):
0148     if (m_loadList.addRequest(request))
0149         m_sleepers.wakeOne();
0150 }
0151 
0152 void ImageManager::AsyncLoader::stop(ImageClientInterface *client, StopAction action)
0153 {
0154     // remove from pending map.
0155     QMutexLocker requestLocker(&m_lock);
0156     m_loadList.cancelRequests(client, action);
0157 
0158     // PENDING(blackie) Reintroduce this
0159     // VideoManager::instance().stop( client, action );
0160     // Was implemented as m_pending.cancelRequests( client, action );
0161     // Where m_pending is the RequestQueue
0162 }
0163 
0164 int ImageManager::AsyncLoader::activeCount() const
0165 {
0166     QMutexLocker dummy(&m_lock);
0167     return m_currentLoading.count();
0168 }
0169 
0170 bool ImageManager::AsyncLoader::isExiting() const
0171 {
0172     return m_exitRequested;
0173 }
0174 
0175 ImageManager::ImageRequest *ImageManager::AsyncLoader::next()
0176 {
0177     QMutexLocker dummy(&m_lock);
0178     ImageRequest *request = nullptr;
0179     while (!(request = m_loadList.popNext()))
0180         m_sleepers.wait(&m_lock);
0181     m_currentLoading.insert(request);
0182 
0183     return request;
0184 }
0185 
0186 void ImageManager::AsyncLoader::requestExit()
0187 {
0188     m_exitRequested = true;
0189     ImageManager::ThumbnailBuilder::instance()->cancelRequests();
0190     m_sleepers.wakeAll();
0191 
0192     // TODO(jzarl): check if we can just connect the finished() signal of the threads to deleteLater()
0193     //              and exit this function without waiting
0194     for (QList<ImageLoaderThread *>::iterator it = m_threadList.begin(); it != m_threadList.end(); ++it) {
0195         while (!(*it)->isFinished()) {
0196             QThread::msleep(10);
0197         }
0198         delete (*it);
0199     }
0200 }
0201 
0202 void ImageManager::AsyncLoader::customEvent(QEvent *ev)
0203 {
0204     if (ev->type() == ImageEventID) {
0205         ImageEvent *iev = dynamic_cast<ImageEvent *>(ev);
0206         if (!iev) {
0207             Q_ASSERT(iev);
0208             return;
0209         }
0210 
0211         ImageRequest *request = iev->loadInfo();
0212 
0213         QMutexLocker requestLocker(&m_lock);
0214         const bool requestStillNeeded = m_loadList.isRequestStillValid(request);
0215         m_loadList.removeRequest(request);
0216         m_currentLoading.remove(request);
0217         requestLocker.unlock();
0218 
0219         QImage image = iev->image();
0220         if (!request->loadedOK()) {
0221             if (m_brokenImage.size() != request->size()) {
0222                 // we can ignore the krazy warning here because we have a valid fallback
0223                 QIcon brokenFileIcon = QIcon::fromTheme(QLatin1String("file-broken")); // krazy:exclude=iconnames
0224                 if (brokenFileIcon.isNull()) {
0225                     brokenFileIcon = QIcon::fromTheme(QLatin1String("image-x-generic"));
0226                 }
0227                 m_brokenImage = brokenFileIcon.pixmap(request->size()).toImage();
0228             }
0229 
0230             image = m_brokenImage;
0231         }
0232 
0233         if (request->isThumbnailRequest())
0234             MainWindow::Window::theMainWindow()->thumbnailCache()->insert(request->databaseFileName(), image);
0235 
0236         if (requestStillNeeded && request->client()) {
0237             request->client()->pixmapLoaded(request, image);
0238         }
0239         delete request;
0240     } else if (ev->type() == CANCELEVENTID) {
0241         CancelEvent *cancelEvent = dynamic_cast<CancelEvent *>(ev);
0242         Q_ASSERT(cancelEvent);
0243         cancelEvent->request()->client()->requestCanceled();
0244     }
0245 }
0246 // vi:expandtab:tabstop=4 shiftwidth=4:
0247 
0248 #include "moc_AsyncLoader.cpp"