File indexing completed on 2024-04-28 15:39:58

0001 // SPDX-FileCopyrightText: 2018-2020 The KPhotoAlbum Development Team
0002 // SPDX-FileCopyrightText: 2022 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0003 //
0004 // SPDX-License-Identifier: GPL-2.0-or-later
0005 
0006 #include "ImageScout.h"
0007 
0008 #include <kpabase/Logging.h>
0009 
0010 #include <QAtomicInt>
0011 #include <QDataStream>
0012 #include <QFile>
0013 #include <QMutexLocker>
0014 #include <QThread>
0015 
0016 extern "C" {
0017 #include <fcntl.h>
0018 #include <unistd.h>
0019 }
0020 
0021 using namespace DB;
0022 
0023 namespace
0024 {
0025 constexpr int DEFAULT_SCOUT_BUFFER_SIZE = 1048576; // *sizeof(int) bytes
0026 // We might want this to be bytes rather than images.
0027 constexpr int DEFAULT_MAX_SEEKAHEAD_IMAGES = 10;
0028 constexpr int SEEKAHEAD_WAIT_MS = 10; // 10 milliseconds, and retry
0029 constexpr int TERMINATION_WAIT_MS = 10; // 10 milliseconds, and retry
0030 }
0031 
0032 // 1048576 with a single scout thread empirically yields best performance
0033 // on a Seagate 2TB 2.5" disk, sustaining throughput in the range of
0034 // 95-100 MB/sec with 100-110 IO/sec on large files.  This is close to what
0035 // would be expected.  A SATA SSD (Crucial MX300) is much less sensitive to
0036 // I/O size and scout thread, achieving about 340 MB/sec with high CPU
0037 // utilization.
0038 
0039 class DB::ImageScoutThread : public QThread
0040 {
0041     friend class DB::ImageScout;
0042 
0043 public:
0044     ImageScoutThread(ImageScoutQueue &, QMutex *, QAtomicInt &count,
0045                      QAtomicInt &preloadCount, QAtomicInt &skippedCount);
0046 
0047 protected:
0048     void run() override;
0049     void setBufSize(int);
0050     int getBufSize();
0051     void setMaxSeekAhead(int);
0052     int getMaxSeekAhead();
0053     void setReadLimit(int);
0054     int getReadLimit();
0055     void setPreloadFunc(PreloadFunc);
0056     PreloadFunc getPreloadFunc();
0057 
0058 private:
0059     void doRun(char *);
0060     ImageScoutQueue &m_queue;
0061     QMutex *m_mutex;
0062     QAtomicInt &m_loadedCount;
0063     QAtomicInt &m_preloadedCount;
0064     QAtomicInt &m_skippedCount;
0065     int m_scoutBufSize;
0066     int m_maxSeekAhead;
0067     int m_readLimit;
0068     PreloadFunc m_preloadFunc;
0069     bool m_isStarted;
0070 };
0071 
0072 ImageScoutThread::ImageScoutThread(ImageScoutQueue &queue, QMutex *mutex,
0073                                    QAtomicInt &count,
0074                                    QAtomicInt &preloadedCount,
0075                                    QAtomicInt &skippedCount)
0076     : m_queue(queue)
0077     , m_mutex(mutex)
0078     , m_loadedCount(count)
0079     , m_preloadedCount(preloadedCount)
0080     , m_skippedCount(skippedCount)
0081     , m_scoutBufSize(DEFAULT_SCOUT_BUFFER_SIZE)
0082     , m_maxSeekAhead(DEFAULT_MAX_SEEKAHEAD_IMAGES)
0083     , m_readLimit(-1)
0084     , m_preloadFunc(nullptr)
0085     , m_isStarted(false)
0086 {
0087 }
0088 
0089 void ImageScoutThread::doRun(char *tmpBuf)
0090 {
0091     while (!isInterruptionRequested()) {
0092         QMutexLocker locker(m_mutex);
0093         if (m_queue.isEmpty()) {
0094             return;
0095         }
0096         DB::FileName fileName = m_queue.dequeue();
0097         locker.unlock();
0098         // If we're behind the reader, move along
0099         m_preloadedCount++;
0100         if (m_loadedCount.loadRelaxed() >= m_preloadedCount.loadRelaxed()) {
0101             m_skippedCount++;
0102             continue;
0103         } else {
0104             // Don't get too far ahead of the loader, or we just waste memory
0105             // TODO: wait on something rather than polling
0106             while (m_preloadedCount.loadRelaxed() >= m_loadedCount.loadRelaxed() + m_maxSeekAhead && !isInterruptionRequested()) {
0107                 QThread::msleep(SEEKAHEAD_WAIT_MS);
0108             }
0109             // qCDebug(DBImageScoutLog) << ">>>>>Scout: preload" << m_preloadedCount.loadRelaxed() << "load" << m_loadedCount.loadRelaxed() << fileName.relative();
0110         }
0111         if (m_preloadFunc) {
0112             (*m_preloadFunc)(fileName);
0113         } else {
0114             // Note(jzarl): for Windows, we'd need a functional replacement for open(), read(), close() in unistd.h
0115             int inputFD = open(QFile::encodeName(fileName.absolute()).constData(), O_RDONLY);
0116             int bytesRead = 0;
0117             if (inputFD >= 0) {
0118                 while (read(inputFD, tmpBuf, m_scoutBufSize) && (m_readLimit < 0 || ((bytesRead += m_scoutBufSize) < m_readLimit)) && !isInterruptionRequested()) {
0119                 }
0120                 (void)close(inputFD);
0121             }
0122         }
0123     }
0124 }
0125 
0126 void ImageScoutThread::setBufSize(int bufSize)
0127 {
0128     if (!m_isStarted)
0129         m_scoutBufSize = bufSize;
0130 }
0131 
0132 int ImageScoutThread::getBufSize()
0133 {
0134     return m_scoutBufSize;
0135 }
0136 
0137 void ImageScoutThread::setMaxSeekAhead(int maxSeekAhead)
0138 {
0139     if (!m_isStarted)
0140         m_maxSeekAhead = maxSeekAhead;
0141 }
0142 
0143 int ImageScoutThread::getMaxSeekAhead()
0144 {
0145     return m_maxSeekAhead;
0146 }
0147 
0148 void ImageScoutThread::setReadLimit(int readLimit)
0149 {
0150     if (!m_isStarted)
0151         m_readLimit = readLimit;
0152 }
0153 
0154 int ImageScoutThread::getReadLimit()
0155 {
0156     return m_readLimit;
0157 }
0158 
0159 void ImageScoutThread::setPreloadFunc(PreloadFunc scoutFunc)
0160 {
0161     if (!m_isStarted)
0162         m_preloadFunc = scoutFunc;
0163 }
0164 
0165 PreloadFunc ImageScoutThread::getPreloadFunc()
0166 {
0167     return m_preloadFunc;
0168 }
0169 
0170 void ImageScoutThread::run()
0171 {
0172     m_isStarted = true;
0173     char *tmpBuf = new char[m_scoutBufSize];
0174     doRun(tmpBuf);
0175     delete[] tmpBuf;
0176 }
0177 
0178 ImageScout::ImageScout(ImageScoutQueue &images,
0179                        QAtomicInt &count,
0180                        int threads)
0181     : m_preloadedCount(0)
0182     , m_skippedCount(0)
0183     , m_isStarted(false)
0184     , m_scoutBufSize(DEFAULT_SCOUT_BUFFER_SIZE)
0185     , m_maxSeekAhead(DEFAULT_MAX_SEEKAHEAD_IMAGES)
0186     , m_readLimit(-1)
0187     , m_preloadFunc(nullptr)
0188 {
0189     if (threads > 0) {
0190         for (int i = 0; i < threads; i++) {
0191             ImageScoutThread *t = new ImageScoutThread(images,
0192                                                        threads > 1 ? &m_mutex : nullptr,
0193                                                        count,
0194                                                        m_preloadedCount,
0195                                                        m_skippedCount);
0196             m_scoutList.append(t);
0197         }
0198     }
0199 }
0200 
0201 ImageScout::~ImageScout()
0202 {
0203     if (m_scoutList.count() > 0) {
0204         for (QList<ImageScoutThread *>::iterator it = m_scoutList.begin();
0205              it != m_scoutList.end(); ++it) {
0206             if (m_isStarted) {
0207                 if (!(*it)->isFinished()) {
0208                     (*it)->requestInterruption();
0209                     while (!(*it)->isFinished())
0210                         QThread::msleep(TERMINATION_WAIT_MS);
0211                 }
0212             }
0213             delete (*it);
0214         }
0215     }
0216     qCDebug(DBImageScoutLog) << "Total files:" << m_preloadedCount << "skipped" << m_skippedCount;
0217 }
0218 
0219 void ImageScout::start()
0220 {
0221     // Yes, there's a race condition here between isStartd and setting
0222     // the buf size or seek ahead...but this isn't a hot code path!
0223     if (!m_isStarted && m_scoutList.count() > 0) {
0224         m_isStarted = true;
0225         for (QList<ImageScoutThread *>::iterator it = m_scoutList.begin();
0226              it != m_scoutList.end(); ++it) {
0227             (*it)->start();
0228         }
0229     }
0230 }
0231 
0232 void ImageScout::setBufSize(int bufSize)
0233 {
0234     if (!m_isStarted && bufSize > 0) {
0235         m_scoutBufSize = bufSize;
0236         for (QList<ImageScoutThread *>::iterator it = m_scoutList.begin();
0237              it != m_scoutList.end(); ++it) {
0238             (*it)->setBufSize(m_scoutBufSize);
0239         }
0240     }
0241 }
0242 
0243 int ImageScout::getBufSize()
0244 {
0245     return m_scoutBufSize;
0246 }
0247 
0248 void ImageScout::setMaxSeekAhead(int maxSeekAhead)
0249 {
0250     if (!m_isStarted && maxSeekAhead > 0) {
0251         m_maxSeekAhead = maxSeekAhead;
0252         for (QList<ImageScoutThread *>::iterator it = m_scoutList.begin();
0253              it != m_scoutList.end(); ++it) {
0254             (*it)->setMaxSeekAhead(m_maxSeekAhead);
0255         }
0256     }
0257 }
0258 
0259 int ImageScout::getMaxSeekAhead()
0260 {
0261     return m_maxSeekAhead;
0262 }
0263 
0264 void ImageScout::setReadLimit(int readLimit)
0265 {
0266     if (!m_isStarted && readLimit > 0) {
0267         m_readLimit = readLimit;
0268         for (QList<ImageScoutThread *>::iterator it = m_scoutList.begin();
0269              it != m_scoutList.end(); ++it) {
0270             (*it)->setReadLimit(m_readLimit);
0271         }
0272     }
0273 }
0274 
0275 int ImageScout::getReadLimit()
0276 {
0277     return m_readLimit;
0278 }
0279 
0280 void ImageScout::setPreloadFunc(PreloadFunc scoutFunc)
0281 {
0282     if (!m_isStarted) {
0283         m_preloadFunc = scoutFunc;
0284         for (QList<ImageScoutThread *>::iterator it = m_scoutList.begin();
0285              it != m_scoutList.end(); ++it) {
0286             (*it)->setPreloadFunc(m_preloadFunc);
0287         }
0288     }
0289 }
0290 
0291 PreloadFunc ImageScout::getPreloadFunc()
0292 {
0293     return m_preloadFunc;
0294 }
0295 
0296 // vi:expandtab:tabstop=4 shiftwidth=4: