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"