File indexing completed on 2024-05-12 04:37:42

0001 /*
0002     SPDX-FileCopyrightText: 2006 Adam Treat <treat@kde.org>
0003     SPDX-FileCopyrightText: 2007 Kris Wong <kris.p.wong@gmail.com>
0004     SPDX-FileCopyrightText: 2007-2008 David Nolden <david.nolden.kdevelop@art-master.de>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "backgroundparser.h"
0010 
0011 #include <QCoreApplication>
0012 #include <QList>
0013 #include <QMutex>
0014 #include <QMutexLocker>
0015 #include <QPointer>
0016 #include <QRecursiveMutex>
0017 #include <QTimer>
0018 #include <QThread>
0019 
0020 #include <KConfigGroup>
0021 #include <KSharedConfig>
0022 #include <KLocalizedString>
0023 
0024 #include <ThreadWeaver/State>
0025 #include <ThreadWeaver/ThreadWeaver>
0026 #include <ThreadWeaver/DebuggingAids>
0027 
0028 #include <interfaces/icore.h>
0029 #include <interfaces/idocumentcontroller.h>
0030 #include <interfaces/ilanguagecontroller.h>
0031 #include <interfaces/ilanguagesupport.h>
0032 #include <interfaces/isession.h>
0033 #include <interfaces/iproject.h>
0034 #include <interfaces/iprojectcontroller.h>
0035 
0036 #include <debug.h>
0037 
0038 #include "parsejob.h"
0039 
0040 using namespace KDevelop;
0041 
0042 namespace {
0043 const bool separateThreadForHighPriority = true;
0044 
0045 /**
0046  * Elides string in @p path, e.g. "VEEERY/LONG/PATH" -> ".../LONG/PATH"
0047  * - probably much faster than QFontMetrics::elidedText()
0048  * - we do not need a widget context
0049  * - takes path separators into account
0050  *
0051  * @p width Maximum number of characters
0052  *
0053  * TODO: Move to kdevutil?
0054  */
0055 QString elidedPathLeft(const QString& path, int width)
0056 {
0057     const QLatin1String placeholder("...");
0058 
0059     if (path.size() <= width) {
0060         return path;
0061     }
0062 
0063     int start = (path.size() - width) + placeholder.size();
0064     int pos = path.indexOf(QDir::separator(), start);
0065     if (pos == -1) {
0066         pos = start; // no separator => just cut off the path at the beginning
0067     }
0068     Q_ASSERT(path.size() - pos >= 0 && path.size() - pos <= width);
0069 
0070     QStringRef elidedText = path.rightRef(path.size() - pos);
0071     const QString result = placeholder + elidedText;
0072     return result;
0073 }
0074 
0075 /**
0076  * @return true if @p url is non-empty, valid and has a clean path, false otherwise.
0077  */
0078 inline bool isValidURL(const IndexedString& url)
0079 {
0080     if (url.isEmpty()) {
0081         return false;
0082     }
0083     QUrl original = url.toUrl();
0084     if (!original.isValid() || original.isRelative() || (original.fileName().isEmpty() && original.isLocalFile())) {
0085         qCWarning(LANGUAGE) << "INVALID URL ENCOUNTERED:" << url << original;
0086         return false;
0087     }
0088     QUrl cleaned = original.adjusted(QUrl::NormalizePathSegments);
0089     return original == cleaned;
0090 }
0091 }
0092 
0093 struct DocumentParseTarget
0094 {
0095     QPointer<QObject> notifyWhenReady;
0096     int priority;
0097     TopDUContext::Features features;
0098     ParseJob::SequentialProcessingFlags sequentialProcessingFlags;
0099 
0100     bool operator==(const DocumentParseTarget& rhs) const
0101     {
0102         return notifyWhenReady == rhs.notifyWhenReady
0103                && priority == rhs.priority
0104                && features == rhs.features;
0105     }
0106 };
0107 
0108 inline uint qHash(const DocumentParseTarget& target)
0109 {
0110     return target.features * 7 + target.priority * 13 + target.sequentialProcessingFlags * 17
0111            + static_cast<uint>(reinterpret_cast<quintptr>(target.notifyWhenReady.data()));
0112 }
0113 
0114 class DocumentParsePlan
0115 {
0116 public:
0117     ParseJob::SequentialProcessingFlags sequentialProcessingFlags() const
0118     {
0119         //Pick the strictest possible flags
0120         ParseJob::SequentialProcessingFlags ret = ParseJob::IgnoresSequentialProcessing;
0121         for (const DocumentParseTarget& target : m_targets) {
0122             ret |= target.sequentialProcessingFlags;
0123         }
0124 
0125         return ret;
0126     }
0127 
0128     int priority() const { return m_priority; }
0129 
0130     TopDUContext::Features features() const
0131     {
0132         //Pick the best features
0133         TopDUContext::Features ret{};
0134         for (const DocumentParseTarget& target : m_targets) {
0135             ret |= target.features;
0136         }
0137 
0138         return ret;
0139     }
0140 
0141     QVector<QPointer<QObject>> notifyWhenReady() const
0142     {
0143         QVector<QPointer<QObject>> ret;
0144 
0145         for (const DocumentParseTarget& target : m_targets) {
0146             if (target.notifyWhenReady)
0147                 ret << target.notifyWhenReady;
0148         }
0149 
0150         return ret;
0151     }
0152     const QSet<const DocumentParseTarget>& targets() const
0153     {
0154         return m_targets;
0155     }
0156 
0157     void addTarget(const DocumentParseTarget& target)
0158     {
0159         if (target.priority < m_priority) {
0160             m_priority = target.priority;
0161         }
0162         m_targets.insert(target);
0163     }
0164 
0165     void removeTargetsForListener(QObject* notifyWhenReady)
0166     {
0167         m_priority = BackgroundParser::WorstPriority;
0168         for (auto it = m_targets.cbegin(); it != m_targets.cend();) {
0169             if (it->notifyWhenReady.data() == notifyWhenReady) {
0170                 it = m_targets.erase(it);
0171             } else {
0172                 if (it->priority < m_priority) {
0173                    m_priority = it->priority;
0174                 }
0175                 ++it;
0176             }
0177         }
0178     }
0179 
0180 private:
0181     QSet<const DocumentParseTarget> m_targets;
0182     int m_priority = BackgroundParser::WorstPriority;
0183 
0184 };
0185 
0186 Q_DECLARE_TYPEINFO(DocumentParseTarget, Q_MOVABLE_TYPE);
0187 Q_DECLARE_TYPEINFO(DocumentParsePlan, Q_MOVABLE_TYPE);
0188 
0189 class KDevelop::BackgroundParserPrivate
0190 {
0191 public:
0192     enum class AddBehavior
0193     {
0194         AddIfMissing,
0195         OnlyUpdateExisting,
0196     };
0197 
0198 
0199     BackgroundParserPrivate(BackgroundParser* parser, ILanguageController* languageController)
0200         : m_parser(parser)
0201         , m_languageController(languageController)
0202         , m_shuttingDown(false)
0203     {
0204         parser->d_ptr = this; //Set this so we can safely call back BackgroundParser from within loadSettings()
0205 
0206         m_timer.setSingleShot(true);
0207         m_progressTimer.setSingleShot(true);
0208         m_progressTimer.setInterval(500);
0209 
0210         ThreadWeaver::setDebugLevel(true, 1);
0211 
0212         QObject::connect(&m_timer, &QTimer::timeout, m_parser, &BackgroundParser::parseDocuments);
0213         QObject::connect(&m_progressTimer, &QTimer::timeout, m_parser, &BackgroundParser::updateProgressBar);
0214     }
0215 
0216     void startTimerThreadSafe(int delay)
0217     {
0218         QMetaObject::invokeMethod(m_parser, "startTimer", Qt::QueuedConnection, Q_ARG(int, delay));
0219     }
0220 
0221     ~BackgroundParserPrivate()
0222     {
0223         m_weaver.resume();
0224         m_weaver.finish();
0225     }
0226 
0227     // Non-mutex guarded functions, only call with m_mutex acquired.
0228 
0229     int currentBestRunningPriority() const
0230     {
0231         int bestRunningPriority = BackgroundParser::WorstPriority;
0232         for (const auto* decorator : m_parseJobs) {
0233             const auto* parseJob = dynamic_cast<const ParseJob*>(decorator->job());
0234             Q_ASSERT(parseJob);
0235             if (parseJob->respectsSequentialProcessing() && parseJob->parsePriority() < bestRunningPriority) {
0236                 bestRunningPriority = parseJob->parsePriority();
0237             }
0238         }
0239 
0240         return bestRunningPriority;
0241     }
0242 
0243     IndexedString nextDocumentToParse() const
0244     {
0245         // Before starting a new job, first wait for all higher-priority ones to finish.
0246         // That way, parse job priorities can be used for dependency handling.
0247         const int bestRunningPriority = currentBestRunningPriority();
0248 
0249         for (auto it1 = m_documentsForPriority.begin();
0250              it1 != m_documentsForPriority.end(); ++it1) {
0251             const auto priority = it1.key();
0252             if (priority > m_neededPriority)
0253                 break; //The priority is not good enough to be processed right now
0254 
0255             if (m_parseJobs.count() >= m_threads && priority > BackgroundParser::NormalPriority && !specialParseJob) {
0256                 break; //The additional parsing thread is reserved for higher priority parsing
0257             }
0258 
0259             for (const auto& url : it1.value()) {
0260                 // When a document is scheduled for parsing while it is being parsed, it will be parsed
0261                 // again once the job finished, but not now.
0262                 if (m_parseJobs.contains(url)) {
0263                     continue;
0264                 }
0265 
0266                 Q_ASSERT(m_documents.contains(url));
0267                 const auto& parsePlan = m_documents[url];
0268                 // If the current job requires sequential processing, but not all jobs with a better priority have been
0269                 // completed yet, it will not be created now.
0270                 if (parsePlan.sequentialProcessingFlags() & ParseJob::RequiresSequentialProcessing
0271                     && parsePlan.priority() > bestRunningPriority) {
0272                     continue;
0273                 }
0274 
0275                 return url;
0276             }
0277         }
0278 
0279         return {};
0280     }
0281 
0282     /**
0283      * Create a single delayed parse job
0284      *
0285      * E.g. jobs for documents which have been changed by the user, but also to
0286      * handle initial startup where we parse all project files.
0287      */
0288     void parseDocumentsInternal()
0289     {
0290         if (m_shuttingDown)
0291             return;
0292 
0293         //Only create parse-jobs for up to thread-count * 2 documents, so we don't fill the memory unnecessarily
0294         if (m_parseJobs.count() >= m_threads + 1
0295             || (m_parseJobs.count() >= m_threads && !separateThreadForHighPriority)) {
0296             return;
0297         }
0298 
0299         const auto& url = nextDocumentToParse();
0300         if (!url.isEmpty()) {
0301             qCDebug(LANGUAGE) << "creating parse-job" << url << "new count of active parse-jobs:" <<
0302                 m_parseJobs.count() + 1;
0303 
0304             const QString elidedPathString = elidedPathLeft(url.str(), 70);
0305             emit m_parser->showMessage(m_parser, i18n("Parsing: %1", elidedPathString));
0306 
0307             ThreadWeaver::QObjectDecorator* decorator = nullptr;
0308             {
0309                 // copy shared data before unlocking the mutex
0310                 const auto parsePlanConstIt = m_documents.constFind(url);
0311                 const DocumentParsePlan parsePlan = *parsePlanConstIt;
0312 
0313                 // we must not lock the mutex while creating a parse job
0314                 // this could in turn lock e.g. the DUChain and then
0315                 // we have a classic lock order inversion (since, usually,
0316                 // we lock first the duchain and then our background parser
0317                 // mutex)
0318                 // see also: https://bugs.kde.org/show_bug.cgi?id=355100
0319                 m_mutex.unlock();
0320                 decorator = createParseJob(url, parsePlan);
0321                 m_mutex.lock();
0322             }
0323 
0324             // iterator might get invalid during the time we didn't have the lock
0325             // search again
0326             const auto parsePlanIt = m_documents.find(url);
0327             if (parsePlanIt != m_documents.end()) {
0328                 // Remove all mentions of this document.
0329                 for (const auto& target : parsePlanIt->targets()) {
0330                     m_documentsForPriority[target.priority].remove(url);
0331                 }
0332 
0333                 m_documents.erase(parsePlanIt);
0334             } else {
0335                 qCWarning(LANGUAGE) << "Document got removed during parse job creation:" << url;
0336             }
0337 
0338             if (decorator) {
0339                 if (m_parseJobs.count() == m_threads + 1 && !specialParseJob)
0340                     specialParseJob = decorator; //This parse-job is allocated into the reserved thread
0341 
0342                 m_parseJobs.insert(url, decorator);
0343                 m_weaver.enqueue(ThreadWeaver::JobPointer(decorator));
0344             } else {
0345                 --m_maxParseJobs;
0346             }
0347 
0348             if (!m_documents.isEmpty()) {
0349                 // Only try creating one parse-job at a time, else we might iterate through thousands of files
0350                 // without finding a language-support, and block the UI for a long time.
0351                 QMetaObject::invokeMethod(m_parser, "parseDocuments", Qt::QueuedConnection);
0352             } else {
0353                 // make sure we cleaned up properly
0354                 // TODO: also empty m_documentsForPriority when m_documents is empty? or do we want to keep capacity?
0355                 Q_ASSERT(std::none_of(m_documentsForPriority.constBegin(), m_documentsForPriority.constEnd(),
0356                                       [](const QSet<IndexedString>& docs) {
0357                     return !docs.isEmpty();
0358                 }));
0359             }
0360         }
0361 
0362         m_parser->updateProgressData();
0363     }
0364 
0365     // NOTE: you must not access any of the data structures that are protected by any of the
0366     //       background parser internal mutexes in this method
0367     //       see also: https://bugs.kde.org/show_bug.cgi?id=355100
0368     ThreadWeaver::QObjectDecorator* createParseJob(const IndexedString& url, const DocumentParsePlan& parsePlan)
0369     {
0370         ///FIXME: use IndexedString in the other APIs as well! Esp. for createParseJob!
0371         QUrl qUrl = url.toUrl();
0372         const auto languages = m_languageController->languagesForUrl(qUrl);
0373         const auto& notifyWhenReady = parsePlan.notifyWhenReady();
0374         for (const auto language : languages) {
0375             if (!language) {
0376                 qCWarning(LANGUAGE) << "got zero language for" << qUrl;
0377                 continue;
0378             }
0379 
0380             ParseJob* job = language->createParseJob(url);
0381             if (!job) {
0382                 continue; // Language part did not produce a valid ParseJob.
0383             }
0384 
0385             job->setParsePriority(parsePlan.priority());
0386             job->setMinimumFeatures(parsePlan.features());
0387             job->setNotifyWhenReady(notifyWhenReady);
0388             job->setSequentialProcessingFlags(parsePlan.sequentialProcessingFlags());
0389 
0390             auto* decorator = new ThreadWeaver::QObjectDecorator(job);
0391 
0392             QObject::connect(decorator, &ThreadWeaver::QObjectDecorator::done,
0393                              m_parser, &BackgroundParser::parseComplete);
0394             QObject::connect(job, &ParseJob::progress,
0395                              m_parser, &BackgroundParser::parseProgress, Qt::QueuedConnection);
0396 
0397             // TODO more thinking required here to support multiple parse jobs per url (where multiple language plugins want to parse)
0398             return decorator;
0399         }
0400 
0401         if (languages.isEmpty())
0402             qCDebug(LANGUAGE) << "found no languages for url" << qUrl;
0403         else
0404             qCDebug(LANGUAGE) << "could not create parse-job for url" << qUrl;
0405 
0406         //Notify that we failed
0407         for (const auto& n : notifyWhenReady) {
0408             if (!n) {
0409                 continue;
0410             }
0411 
0412             QMetaObject::invokeMethod(n.data(), "updateReady", Qt::QueuedConnection,
0413                                       Q_ARG(KDevelop::IndexedString, url),
0414                                       Q_ARG(KDevelop::ReferencedTopDUContext, ReferencedTopDUContext()));
0415         }
0416 
0417         return nullptr;
0418     }
0419 
0420     void loadSettings()
0421     {
0422         ///@todo re-load settings when they have been changed!
0423         Q_ASSERT(ICore::self()->activeSession());
0424         KConfigGroup config(ICore::self()->activeSession()->config(), "Background Parser");
0425 
0426         // stay backwards compatible
0427         KConfigGroup oldConfig(KSharedConfig::openConfig(), "Background Parser");
0428 #define BACKWARDS_COMPATIBLE_ENTRY(entry, default) \
0429     config.readEntry(entry, oldConfig.readEntry(entry, default))
0430 
0431         m_delay = BACKWARDS_COMPATIBLE_ENTRY("Delay", 500);
0432         m_timer.setInterval(m_delay);
0433         m_threads = 0;
0434 
0435         if (qEnvironmentVariableIsSet("KDEV_BACKGROUNDPARSER_MAXTHREADS")) {
0436             m_parser->setThreadCount(qEnvironmentVariableIntValue("KDEV_BACKGROUNDPARSER_MAXTHREADS"));
0437         } else {
0438             m_parser->setThreadCount(BACKWARDS_COMPATIBLE_ENTRY("Number of Threads", QThread::idealThreadCount()));
0439         }
0440 
0441         resume();
0442 
0443         if (BACKWARDS_COMPATIBLE_ENTRY("Enabled", true)) {
0444             m_parser->enableProcessing();
0445         } else {
0446             m_parser->disableProcessing();
0447         }
0448     }
0449 
0450     bool isSuspended() const
0451     {
0452         return m_weaver.state()->stateId() == ThreadWeaver::Suspended ||
0453                m_weaver.state()->stateId() == ThreadWeaver::Suspending;
0454     }
0455 
0456     void suspend()
0457     {
0458         qCDebug(LANGUAGE) << "Suspending background parser";
0459 
0460         if (isSuspended()) { // Already suspending
0461             qCWarning(LANGUAGE) << "Already suspended or suspending";
0462             return;
0463         }
0464 
0465         m_timer.stop();
0466         m_weaver.suspend();
0467     }
0468 
0469     void resume()
0470     {
0471         qCDebug(LANGUAGE) << "Resuming background parser";
0472 
0473         if (m_timer.isActive() && !isSuspended()) { // Not suspended
0474             qCWarning(LANGUAGE) << "Not suspended";
0475             return;
0476         }
0477 
0478         m_timer.start(m_delay);
0479         m_weaver.resume();
0480     }
0481 
0482 
0483     bool addDocumentListener(const IndexedString& url, TopDUContext::Features features, int priority,
0484                              QObject* notifyWhenReady, ParseJob::SequentialProcessingFlags flags, int delay,
0485                              AddBehavior addBehavior)
0486     {
0487         qCDebug(LANGUAGE) << "BackgroundParserPrivate::addDocumentListener" << url << url.toUrl();
0488         Q_ASSERT(isValidURL(url));
0489         DocumentParseTarget target;
0490         target.priority = priority;
0491         target.features = features;
0492         target.sequentialProcessingFlags = flags;
0493         target.notifyWhenReady = QPointer<QObject>(notifyWhenReady);
0494 
0495         {
0496             QMutexLocker lock(&m_mutex);
0497             auto it = m_documents.find(url);
0498 
0499             if (it != m_documents.end()) {
0500                 //Update the stored plan
0501                 auto currentPrio = it.value().priority();
0502                 it.value().addTarget(target);
0503                 if (currentPrio > target.priority) {
0504                     m_documentsForPriority[currentPrio].remove(url);
0505                     m_documentsForPriority[target.priority].insert(url);
0506                 }
0507             } else if (addBehavior == AddBehavior::AddIfMissing) {
0508     //             qCDebug(LANGUAGE) << "BackgroundParser::addDocument: queuing" << cleanedUrl;
0509                 auto& doc = m_documents[url];
0510                 doc.addTarget(target);
0511                 m_documentsForPriority[doc.priority()].insert(url);
0512                 ++m_maxParseJobs; //So the progress-bar waits for this document
0513             } else {
0514                 return false;
0515             }
0516 
0517             if (delay == ILanguageSupport::DefaultDelay) {
0518                 delay = m_delay;
0519             }
0520         }
0521         startTimerThreadSafe(delay);
0522         return true;
0523     }
0524 
0525 
0526     BackgroundParser* m_parser;
0527     ILanguageController* m_languageController;
0528 
0529     //Current parse-job that is executed in the additional thread
0530     QPointer<QObject> specialParseJob;
0531 
0532     QTimer m_timer;
0533     int m_delay = 500;
0534     int m_threads = 1;
0535 
0536     bool m_shuttingDown;
0537 
0538     // A list of documents that are planned to be parsed, and their priority
0539     QHash<IndexedString, DocumentParsePlan> m_documents;
0540     // The documents ordered by priority
0541     QMap<int, QSet<IndexedString>> m_documentsForPriority;
0542     // Currently running parse jobs
0543     QHash<IndexedString, ThreadWeaver::QObjectDecorator*> m_parseJobs;
0544     // The url for each managed document. Those may temporarily differ from the real url.
0545     QHash<KTextEditor::Document*, IndexedString> m_managedTextDocumentUrls;
0546     // Projects currently in progress of loading
0547     QSet<IProject*> m_loadingProjects;
0548 
0549     ThreadWeaver::Queue m_weaver;
0550 
0551     // generic high-level mutex
0552     mutable QRecursiveMutex m_mutex;
0553 
0554     // local mutex only protecting m_managed
0555     mutable QMutex m_managedMutex;
0556     // A change tracker for each managed document
0557     QHash<IndexedString, DocumentChangeTracker*> m_managed;
0558 
0559     int m_maxParseJobs = 0;
0560     int m_doneParseJobs = 0;
0561     QHash<KDevelop::ParseJob*, float> m_jobProgress;
0562     /// The minimum priority needed for processed jobs
0563     int m_neededPriority = BackgroundParser::WorstPriority;
0564     int m_progressMax = 0;
0565     int m_progressDone = 0;
0566     QTimer m_progressTimer;
0567 };
0568 
0569 BackgroundParser::BackgroundParser(ILanguageController* languageController)
0570     : QObject(languageController)
0571     , d_ptr(new BackgroundParserPrivate(this, languageController))
0572 {
0573     Q_ASSERT(ICore::self()->documentController());
0574     connect(
0575         ICore::self()->documentController(), &IDocumentController::documentLoaded, this,
0576         &BackgroundParser::documentLoaded);
0577     connect(
0578         ICore::self()->documentController(), &IDocumentController::documentUrlChanged, this,
0579         &BackgroundParser::documentUrlChanged);
0580     connect(
0581         ICore::self()->documentController(), &IDocumentController::documentClosed, this,
0582         &BackgroundParser::documentClosed);
0583     connect(ICore::self(), &ICore::aboutToShutdown, this, &BackgroundParser::aboutToQuit);
0584 
0585     QObject::connect(ICore::self()->projectController(),
0586                      &IProjectController::projectAboutToBeOpened,
0587                      this, &BackgroundParser::projectAboutToBeOpened);
0588     QObject::connect(ICore::self()->projectController(),
0589                      &IProjectController::projectOpened,
0590                      this, &BackgroundParser::projectOpened);
0591     QObject::connect(ICore::self()->projectController(),
0592                      &IProjectController::projectOpeningAborted,
0593                      this, &BackgroundParser::projectOpeningAborted);
0594 }
0595 
0596 void BackgroundParser::aboutToQuit()
0597 {
0598     Q_D(BackgroundParser);
0599 
0600     d->m_shuttingDown = true;
0601 }
0602 
0603 BackgroundParser::~BackgroundParser()
0604 {
0605     delete d_ptr;
0606 }
0607 
0608 QString BackgroundParser::statusName() const
0609 {
0610     return i18n("Background Parser");
0611 }
0612 
0613 void BackgroundParser::loadSettings()
0614 {
0615     Q_D(BackgroundParser);
0616 
0617     d->loadSettings();
0618 }
0619 
0620 void BackgroundParser::parseProgress(KDevelop::ParseJob* job, float value, const QString& text)
0621 {
0622     Q_UNUSED(text)
0623 
0624     Q_D(BackgroundParser);
0625 
0626     d->m_jobProgress[job] = value;
0627     updateProgressData();
0628 }
0629 
0630 void BackgroundParser::revertAllRequests(QObject* notifyWhenReady)
0631 {
0632     Q_ASSERT(notifyWhenReady != nullptr);
0633     Q_D(BackgroundParser);
0634 
0635     QMutexLocker lock(&d->m_mutex);
0636     for (auto it = d->m_documents.begin(); it != d->m_documents.end();) {
0637         d->m_documentsForPriority[it.value().priority()].remove(it.key());
0638 
0639         it->removeTargetsForListener(notifyWhenReady);
0640 
0641         if ((*it).targets().isEmpty()) {
0642             it = d->m_documents.erase(it);
0643             --d->m_maxParseJobs;
0644 
0645             continue;
0646         }
0647 
0648         d->m_documentsForPriority[it.value().priority()].insert(it.key());
0649         ++it;
0650     }
0651 }
0652 
0653 bool BackgroundParser::addListenerToDocumentIfExist(const IndexedString& url, TopDUContext::Features features, int priority,
0654                                            QObject* notifyWhenReady, ParseJob::SequentialProcessingFlags flags,
0655                                            int delay)
0656 {
0657     Q_D(BackgroundParser);
0658     return d->addDocumentListener(url, features, priority, notifyWhenReady, flags, delay,
0659                                   BackgroundParserPrivate::AddBehavior::OnlyUpdateExisting);
0660 }
0661 
0662 void BackgroundParser::addDocument(const IndexedString& url, TopDUContext::Features features, int priority,
0663                                    QObject* notifyWhenReady, ParseJob::SequentialProcessingFlags flags, int delay)
0664 {
0665     Q_D(BackgroundParser);
0666     d->addDocumentListener(url, features, priority, notifyWhenReady, flags, delay,
0667                            BackgroundParserPrivate::AddBehavior::AddIfMissing);
0668 }
0669 
0670 void BackgroundParser::removeDocument(const IndexedString& url, QObject* notifyWhenReady)
0671 {
0672     Q_D(BackgroundParser);
0673 
0674     Q_ASSERT(isValidURL(url));
0675 
0676     QMutexLocker lock(&d->m_mutex);
0677 
0678     auto documentParsePlanIt = d->m_documents.find(url);
0679     if (documentParsePlanIt != d->m_documents.end()) {
0680         auto& documentParsePlan = *documentParsePlanIt;
0681         d->m_documentsForPriority[documentParsePlan.priority()].remove(url);
0682 
0683         documentParsePlan.removeTargetsForListener(notifyWhenReady);
0684 
0685         if (documentParsePlan.targets().isEmpty()) {
0686             d->m_documents.erase(documentParsePlanIt);
0687             --d->m_maxParseJobs;
0688         } else {
0689             //Insert with an eventually different priority
0690             d->m_documentsForPriority[documentParsePlan.priority()].insert(url);
0691         }
0692     }
0693 }
0694 
0695 void BackgroundParser::parseDocuments()
0696 {
0697     Q_D(BackgroundParser);
0698 
0699     if (d->isSuspended() || !d->m_loadingProjects.empty()) {
0700         startTimer(d->m_delay);
0701         return;
0702     }
0703     QMutexLocker lock(&d->m_mutex);
0704 
0705     d->parseDocumentsInternal();
0706 }
0707 
0708 void BackgroundParser::parseComplete(const ThreadWeaver::JobPointer& job)
0709 {
0710     Q_D(BackgroundParser);
0711 
0712     auto decorator = dynamic_cast<ThreadWeaver::QObjectDecorator*>(job.data());
0713     Q_ASSERT(decorator);
0714     auto* parseJob = dynamic_cast<ParseJob*>(decorator->job());
0715     Q_ASSERT(parseJob);
0716     emit parseJobFinished(parseJob);
0717 
0718     {
0719         QMutexLocker lock(&d->m_mutex);
0720 
0721         d->m_parseJobs.remove(parseJob->document());
0722 
0723         d->m_jobProgress.remove(parseJob);
0724 
0725         ++d->m_doneParseJobs;
0726         updateProgressData();
0727     }
0728 
0729     //Continue creating more parse-jobs
0730     QMetaObject::invokeMethod(this, "parseDocuments", Qt::QueuedConnection);
0731 }
0732 
0733 void BackgroundParser::disableProcessing()
0734 {
0735     setNeededPriority(BestPriority);
0736 }
0737 
0738 void BackgroundParser::enableProcessing()
0739 {
0740     setNeededPriority(WorstPriority);
0741 }
0742 
0743 int BackgroundParser::priorityForDocument(const IndexedString& url) const
0744 {
0745     Q_D(const BackgroundParser);
0746 
0747     Q_ASSERT(isValidURL(url));
0748     QMutexLocker lock(&d->m_mutex);
0749     return d->m_documents[url].priority();
0750 }
0751 
0752 bool BackgroundParser::isQueued(const IndexedString& url) const
0753 {
0754     Q_D(const BackgroundParser);
0755 
0756     Q_ASSERT(isValidURL(url));
0757     QMutexLocker lock(&d->m_mutex);
0758     return d->m_documents.contains(url);
0759 }
0760 
0761 int BackgroundParser::queuedCount() const
0762 {
0763     Q_D(const BackgroundParser);
0764 
0765     QMutexLocker lock(&d->m_mutex);
0766     return d->m_documents.count();
0767 }
0768 
0769 bool BackgroundParser::isIdle() const
0770 {
0771     Q_D(const BackgroundParser);
0772 
0773     QMutexLocker lock(&d->m_mutex);
0774     return d->m_documents.isEmpty() && d->m_weaver.isIdle();
0775 }
0776 
0777 void BackgroundParser::setNeededPriority(int priority)
0778 {
0779     Q_D(BackgroundParser);
0780 
0781     QMutexLocker lock(&d->m_mutex);
0782     d->m_neededPriority = priority;
0783     d->startTimerThreadSafe(d->m_delay);
0784 }
0785 
0786 void BackgroundParser::abortAllJobs()
0787 {
0788     Q_D(BackgroundParser);
0789 
0790     qCDebug(LANGUAGE) << "Aborting all parse jobs";
0791 
0792     d->m_weaver.requestAbort();
0793 }
0794 
0795 void BackgroundParser::suspend()
0796 {
0797     Q_D(BackgroundParser);
0798 
0799     d->suspend();
0800 
0801     emit hideProgress(this);
0802 }
0803 
0804 void BackgroundParser::resume()
0805 {
0806     Q_D(BackgroundParser);
0807 
0808     d->resume();
0809     updateProgressData();
0810 }
0811 
0812 void BackgroundParser::updateProgressData()
0813 {
0814     Q_D(BackgroundParser);
0815 
0816     if (d->m_doneParseJobs >= d->m_maxParseJobs) {
0817         if (d->m_doneParseJobs > d->m_maxParseJobs) {
0818             qCDebug(LANGUAGE) << "m_doneParseJobs larger than m_maxParseJobs:" << d->m_doneParseJobs <<
0819                 d->m_maxParseJobs;
0820         }
0821         d->m_doneParseJobs = 0;
0822         d->m_maxParseJobs = 0;
0823     } else {
0824         float additionalProgress = 0;
0825         for (float progress : qAsConst(d->m_jobProgress)) {
0826             additionalProgress += progress;
0827         }
0828 
0829         d->m_progressMax = d->m_maxParseJobs * 1000;
0830         d->m_progressDone = (additionalProgress + d->m_doneParseJobs) * 1000;
0831 
0832         if (!d->m_progressTimer.isActive()) {
0833             d->m_progressTimer.start();
0834         }
0835     }
0836 
0837     // Cancel progress updating and hide progress-bar when parsing is done.
0838     if (d->m_doneParseJobs == d->m_maxParseJobs
0839         || (d->m_neededPriority == BackgroundParser::BestPriority && d->m_weaver.queueLength() == 0)) {
0840         if (d->m_progressTimer.isActive()) {
0841             d->m_progressTimer.stop();
0842         }
0843         emit d->m_parser->hideProgress(d->m_parser);
0844     }
0845 }
0846 
0847 ParseJob* BackgroundParser::parseJobForDocument(const IndexedString& document) const
0848 {
0849     Q_D(const BackgroundParser);
0850 
0851     Q_ASSERT(isValidURL(document));
0852 
0853     QMutexLocker lock(&d->m_mutex);
0854     auto decorator = d->m_parseJobs.value(document);
0855     return decorator ? dynamic_cast<ParseJob*>(decorator->job()) : nullptr;
0856 }
0857 
0858 void BackgroundParser::setThreadCount(int threadCount)
0859 {
0860     Q_D(BackgroundParser);
0861 
0862     if (d->m_threads != threadCount) {
0863         d->m_threads = threadCount;
0864         d->m_weaver.setMaximumNumberOfThreads(d->m_threads + 1); //1 Additional thread for high-priority parsing
0865     }
0866 }
0867 
0868 int BackgroundParser::threadCount() const
0869 {
0870     Q_D(const BackgroundParser);
0871 
0872     return d->m_threads;
0873 }
0874 
0875 void BackgroundParser::setDelay(int milliseconds)
0876 {
0877     Q_D(BackgroundParser);
0878 
0879     if (d->m_delay != milliseconds) {
0880         d->m_delay = milliseconds;
0881         d->m_timer.setInterval(d->m_delay);
0882     }
0883 }
0884 
0885 QList<IndexedString> BackgroundParser::managedDocuments()
0886 {
0887     Q_D(BackgroundParser);
0888 
0889     QMutexLocker l(&d->m_managedMutex);
0890     return d->m_managed.keys();
0891 }
0892 
0893 bool BackgroundParser::waitForIdle() const
0894 {
0895     Q_D(const BackgroundParser);
0896 
0897     QList<IndexedString> runningParseJobsUrls;
0898     while (true) {
0899         {
0900             QMutexLocker lock(&d->m_mutex);
0901             if (d->m_parseJobs.isEmpty()) {
0902                 qCDebug(LANGUAGE) << "All parse jobs done" << d->m_parseJobs.keys();
0903                 return true;
0904             }
0905 
0906             if (d->m_parseJobs.size() != runningParseJobsUrls.size()) {
0907                 runningParseJobsUrls = d->m_parseJobs.keys();
0908                 qCDebug(LANGUAGE) <<
0909                     "Waiting for background parser to get in idle state... -- the following parse jobs are still running:"
0910                                   << runningParseJobsUrls;
0911             }
0912         }
0913 
0914         QCoreApplication::processEvents();
0915         QThread::msleep(100);
0916     }
0917     return false;
0918 }
0919 
0920 DocumentChangeTracker* BackgroundParser::trackerForUrl(const KDevelop::IndexedString& url) const
0921 {
0922     Q_D(const BackgroundParser);
0923 
0924     if (url.isEmpty()) {
0925         // this happens e.g. when setting the final location of a problem that is not
0926         // yet associated with a top ctx.
0927         return nullptr;
0928     }
0929     if (!isValidURL(url)) {
0930         qCWarning(LANGUAGE) << "Tracker requested for invalid URL:" << url.toUrl();
0931     }
0932     Q_ASSERT(isValidURL(url));
0933 
0934     QMutexLocker l(&d->m_managedMutex);
0935     return d->m_managed.value(url, nullptr);
0936 }
0937 
0938 void BackgroundParser::documentClosed(IDocument* document)
0939 {
0940     Q_D(BackgroundParser);
0941 
0942     QMutexLocker l(&d->m_mutex);
0943 
0944     if (document->textDocument()) {
0945         KTextEditor::Document* textDocument = document->textDocument();
0946 
0947         auto documentUrlIt = d->m_managedTextDocumentUrls.find(textDocument);
0948         if (documentUrlIt == d->m_managedTextDocumentUrls.end())
0949             return; // Probably the document had an invalid url, and thus it wasn't added to the background parser
0950 
0951         Q_ASSERT(documentUrlIt != d->m_managedTextDocumentUrls.end());
0952 
0953         IndexedString url(*documentUrlIt);
0954 
0955         QMutexLocker l2(&d->m_managedMutex);
0956         auto urlIt = d->m_managed.find(url);
0957         Q_ASSERT(urlIt != d->m_managed.end());
0958         Q_ASSERT(*urlIt);
0959         Q_ASSERT((*urlIt)->document() == textDocument);
0960 
0961         qCDebug(LANGUAGE) << "removing" << url.str() << "from background parser";
0962         delete *urlIt;
0963         d->m_managedTextDocumentUrls.erase(documentUrlIt);
0964         d->m_managed.erase(urlIt);
0965     }
0966 }
0967 
0968 void BackgroundParser::documentLoaded(IDocument* document)
0969 {
0970     Q_D(BackgroundParser);
0971 
0972     QMutexLocker l(&d->m_mutex);
0973     if (document->textDocument() && document->textDocument()->url().isValid()) {
0974         KTextEditor::Document* textDocument = document->textDocument();
0975 
0976         IndexedString url(document->url());
0977         // Some debugging because we had issues with this
0978 
0979         QMutexLocker l2(&d->m_managedMutex);
0980         auto urlIt = d->m_managed.find(url);
0981         if (urlIt != d->m_managed.end() && (*urlIt)->document() == textDocument) {
0982             qCDebug(LANGUAGE) << "Got redundant documentLoaded from" << document->url() << textDocument;
0983             return;
0984         }
0985 
0986         qCDebug(LANGUAGE) << "Creating change tracker for " << document->url();
0987 
0988         Q_ASSERT(!d->m_managed.contains(url));
0989         Q_ASSERT(!d->m_managedTextDocumentUrls.contains(textDocument));
0990 
0991         d->m_managedTextDocumentUrls[textDocument] = url;
0992         d->m_managed.insert(url, new DocumentChangeTracker(textDocument));
0993     } else {
0994         qCDebug(LANGUAGE) << "NOT creating change tracker for" << document->url();
0995     }
0996 }
0997 
0998 void BackgroundParser::documentUrlChanged(IDocument* document)
0999 {
1000     documentClosed(document);
1001 
1002     // Only call documentLoaded if the file wasn't renamed to a filename that is already tracked.
1003     if (document->textDocument() && !trackerForUrl(IndexedString(document->textDocument()->url())))
1004         documentLoaded(document);
1005 }
1006 
1007 void BackgroundParser::startTimer(int delay)
1008 {
1009     Q_D(BackgroundParser);
1010 
1011     if (!d->isSuspended()) {
1012         d->m_timer.start(delay);
1013     }
1014 }
1015 
1016 void BackgroundParser::projectAboutToBeOpened(IProject* project)
1017 {
1018     Q_D(BackgroundParser);
1019 
1020     d->m_loadingProjects.insert(project);
1021 }
1022 
1023 void BackgroundParser::projectOpened(IProject* project)
1024 {
1025     Q_D(BackgroundParser);
1026 
1027     d->m_loadingProjects.remove(project);
1028 }
1029 
1030 void BackgroundParser::projectOpeningAborted(IProject* project)
1031 {
1032     Q_D(BackgroundParser);
1033 
1034     d->m_loadingProjects.remove(project);
1035 }
1036 
1037 void BackgroundParser::updateProgressBar()
1038 {
1039     Q_D(BackgroundParser);
1040 
1041     emit showProgress(this, 0, d->m_progressMax, d->m_progressDone);
1042 }
1043 
1044 #include "moc_backgroundparser.cpp"