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"