File indexing completed on 2024-06-23 04:34:31

0001 /*
0002     SPDX-FileCopyrightText: 2012 Sven Brauch <svenbrauch@googlemail.com>
0003     SPDX-FileCopyrightText: 2012 Milian Wolff <mail@milianw.de>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "test_backgroundparser.h"
0009 
0010 #include <QTest>
0011 #include <QElapsedTimer>
0012 #include <QTemporaryFile>
0013 #include <QApplication>
0014 #include <QSemaphore>
0015 
0016 #include <KTextEditor/Editor>
0017 #include <KTextEditor/View>
0018 
0019 #include <tests/autotestshell.h>
0020 #include <tests/testcore.h>
0021 #include <tests/testlanguagecontroller.h>
0022 #include <tests/testhelpers.h>
0023 
0024 #include <language/duchain/duchain.h>
0025 #include <language/duchain/duchainlock.h>
0026 #include <language/backgroundparser/backgroundparser.h>
0027 
0028 #include <interfaces/ilanguagecontroller.h>
0029 
0030 #include "testlanguagesupport.h"
0031 #include "testparsejob.h"
0032 
0033 QTEST_MAIN(TestBackgroundparser)
0034 
0035 using namespace KDevelop;
0036 
0037 JobPlan::JobPlan()
0038 {
0039 }
0040 
0041 void JobPlan::addJob(const JobPrototype& job)
0042 {
0043     m_jobs << job;
0044 }
0045 
0046 void JobPlan::clear()
0047 {
0048     m_jobs.clear();
0049     m_finishedJobs.clear();
0050     m_createdJobs.clear();
0051 }
0052 
0053 void JobPlan::parseJobCreated(ParseJob* job)
0054 {
0055     // e.g. for the benchmark
0056     if (m_jobs.isEmpty()) {
0057         return;
0058     }
0059 
0060     auto* testJob = qobject_cast<TestParseJob*>(job);
0061     Q_ASSERT(testJob);
0062 
0063     qDebug() << "assigning propierties for created job" << testJob->document().toUrl();
0064     testJob->duration_ms = jobForUrl(testJob->document()).m_duration;
0065 
0066     m_createdJobs.append(testJob->document());
0067 }
0068 
0069 void JobPlan::addJobsToParser()
0070 {
0071     // add parse jobs
0072     for (const JobPrototype& job : qAsConst(m_jobs)) {
0073         ICore::self()->languageController()->backgroundParser()->addDocument(
0074             job.m_url, TopDUContext::Empty, job.m_priority, this, job.m_flags
0075         );
0076     }
0077 }
0078 
0079 bool JobPlan::runJobs(int timeoutMS)
0080 {
0081     addJobsToParser();
0082 
0083     ICore::self()->languageController()->backgroundParser()->parseDocuments();
0084 
0085     QElapsedTimer t;
0086     t.start();
0087 
0088     while (!t.hasExpired(timeoutMS) && m_jobs.size() != m_finishedJobs.size()) {
0089         QTest::qWait(50);
0090     }
0091 
0092     QVERIFY_RETURN(m_jobs.size() == m_createdJobs.size(), false);
0093 
0094     QVERIFY_RETURN(m_finishedJobs.size() == m_jobs.size(), false);
0095 
0096     // verify they're started in the right order
0097     int currentBestPriority = BackgroundParser::BestPriority;
0098     for (const IndexedString& url : qAsConst(m_createdJobs)) {
0099         const JobPrototype p = jobForUrl(url);
0100         QVERIFY_RETURN(p.m_priority >= currentBestPriority, false);
0101         currentBestPriority = p.m_priority;
0102     }
0103 
0104     return true;
0105 }
0106 
0107 JobPrototype JobPlan::jobForUrl(const IndexedString& url) const
0108 {
0109     auto it = std::find_if(m_jobs.begin(), m_jobs.end(), [&](const JobPrototype& job) {
0110         return (job.m_url == url);
0111     });
0112 
0113     return (it != m_jobs.end()) ? *it: JobPrototype();
0114 }
0115 
0116 void JobPlan::updateReady(const IndexedString& url, const ReferencedTopDUContext& /*context*/)
0117 {
0118     if (!ICore::self() || ICore::self()->shuttingDown()) {
0119         // core was shutdown before we get to handle the delayed signal, cf. testShutdownWithRunningJobs
0120         return;
0121     }
0122 
0123     qDebug() << "update ready on " << url.toUrl();
0124 
0125     const JobPrototype job = jobForUrl(url);
0126     QVERIFY(job.m_url.toUrl().isValid());
0127 
0128     if (job.m_flags & ParseJob::RequiresSequentialProcessing) {
0129         // ensure that all jobs that respect sequential processing
0130         // with lower priority have been run
0131         for (const JobPrototype& otherJob : qAsConst(m_jobs)) {
0132             if (otherJob.m_url == job.m_url) {
0133                 continue;
0134             }
0135             if (otherJob.m_flags & ParseJob::RespectsSequentialProcessing &&
0136                 otherJob.m_priority < job.m_priority) {
0137                 QVERIFY(m_finishedJobs.contains(otherJob.m_url));
0138             }
0139         }
0140     }
0141 
0142     QVERIFY(!m_finishedJobs.contains(job.m_url));
0143     m_finishedJobs << job.m_url;
0144 }
0145 
0146 int JobPlan::numJobs() const
0147 {
0148     return m_jobs.size();
0149 }
0150 
0151 int JobPlan::numCreatedJobs() const
0152 {
0153     return m_createdJobs.size();
0154 }
0155 
0156 int JobPlan::numFinishedJobs() const
0157 {
0158     return m_finishedJobs.size();
0159 }
0160 
0161 void TestBackgroundparser::initTestCase()
0162 {
0163     AutoTestShell::init();
0164     TestCore* core = TestCore::initialize(Core::NoUi);
0165 
0166     DUChain::self()->disablePersistentStorage();
0167 
0168     auto* langController = new TestLanguageController(core);
0169     core->setLanguageController(langController);
0170     langController->backgroundParser()->setThreadCount(4);
0171     langController->backgroundParser()->abortAllJobs();
0172 
0173     m_langSupport = new TestLanguageSupport(this);
0174     connect(m_langSupport, &TestLanguageSupport::parseJobCreated,
0175             &m_jobPlan, &JobPlan::parseJobCreated);
0176     langController->addTestLanguage(m_langSupport, QStringList() << QStringLiteral("text/plain"));
0177 
0178     const auto languages = langController->languagesForUrl(QUrl::fromLocalFile(QStringLiteral("/foo.txt")));
0179     QCOMPARE(languages.size(), 1);
0180     QCOMPARE(languages.first(), m_langSupport);
0181 }
0182 
0183 void TestBackgroundparser::cleanupTestCase()
0184 {
0185     TestCore::shutdown();
0186     m_langSupport = nullptr;
0187 }
0188 
0189 void TestBackgroundparser::init()
0190 {
0191     m_jobPlan.clear();
0192 }
0193 
0194 void TestBackgroundparser::testShutdownWithRunningJobs()
0195 {
0196     m_jobPlan.clear();
0197     // prove that background parsing happens with sequential flags although there is a high-priority
0198     // foreground thread (active document being edited, ...) running all the time.
0199 
0200     // the long-running high-prio job
0201     m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile(QStringLiteral("/test_fgt_hp.txt")),
0202                                   -500, ParseJob::IgnoresSequentialProcessing, 1000));
0203 
0204     m_jobPlan.addJobsToParser();
0205 
0206     ICore::self()->languageController()->backgroundParser()->parseDocuments();
0207     QTest::qWait(50);
0208 
0209     // shut down with running jobs, make sure we don't crash
0210     cleanupTestCase();
0211 
0212     // restart again to restore invariant (core always running in test functions)
0213     initTestCase();
0214 }
0215 
0216 void TestBackgroundparser::testParseOrdering_foregroundThread()
0217 {
0218     m_jobPlan.clear();
0219     // prove that background parsing happens with sequential flags although there is a high-priority
0220     // foreground thread (active document being edited, ...) running all the time.
0221 
0222     // the long-running high-prio job
0223     m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile(QStringLiteral("/test_fgt_hp.txt")), -500,
0224                                   ParseJob::IgnoresSequentialProcessing, 630));
0225 
0226     // several small background jobs
0227     for (int i = 0; i < 10; i++) {
0228         m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile("/test_fgt_lp__" + QString::number(i) + ".txt"), i,
0229                                       ParseJob::FullSequentialProcessing, 40));
0230     }
0231 
0232     // not enough time if the small jobs run after the large one
0233     QVERIFY(m_jobPlan.runJobs(700));
0234 }
0235 
0236 void TestBackgroundparser::testParseOrdering_noSequentialProcessing()
0237 {
0238     m_jobPlan.clear();
0239     for (int i = 0; i < 20; i++) {
0240         // create jobs with no sequential processing, and different priorities
0241         m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile("/test_nsp1__" + QString::number(i) + ".txt"), i,
0242                                       ParseJob::IgnoresSequentialProcessing, i));
0243     }
0244 
0245     for (int i = 0; i < 8; i++) {
0246         // create a few more jobs with the same priority
0247         m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile("/test_nsp2__" + QString::number(i) + ".txt"), 10,
0248                                       ParseJob::IgnoresSequentialProcessing, i));
0249     }
0250 
0251     QVERIFY(m_jobPlan.runJobs(1000));
0252 }
0253 
0254 void TestBackgroundparser::testParseOrdering_lockup()
0255 {
0256     m_jobPlan.clear();
0257     for (int i = 3; i > 0; i--) {
0258         // add 3 jobs which do not care about sequential processing, at 4 threads it should take no more than 1s to process them
0259         m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile("/test" + QString::number(i) + ".txt"), i,
0260                                       ParseJob::IgnoresSequentialProcessing, 200));
0261     }
0262 
0263     // add one job which requires sequential processing with high priority
0264     m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile(QStringLiteral("/test_hp.txt")), -200,
0265                                   ParseJob::FullSequentialProcessing, 200));
0266     // verify that the low-priority nonsequential jobs are run simultaneously with the other one.
0267     QVERIFY(m_jobPlan.runJobs(700));
0268 }
0269 
0270 void TestBackgroundparser::testParseOrdering_simple()
0271 {
0272     m_jobPlan.clear();
0273     for (int i = 20; i > 0; i--) {
0274         // the job with priority i should be at place i in the finished list
0275         // (lower priority value -> should be parsed first)
0276         ParseJob::SequentialProcessingFlags flags = ParseJob::FullSequentialProcessing;
0277         m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile("/test" + QString::number(i) + ".txt"),
0278                                       i, flags));
0279     }
0280 
0281     // also add a few jobs which ignore the processing
0282     for (int i = 0; i < 5; ++i) {
0283         m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile("/test2-" + QString::number(i) + ".txt"),
0284                                       BackgroundParser::NormalPriority,
0285                                       ParseJob::IgnoresSequentialProcessing));
0286     }
0287 
0288     QVERIFY(m_jobPlan.runJobs(1000));
0289 }
0290 
0291 void TestBackgroundparser::benchmark()
0292 {
0293     const int jobs = 10000;
0294 
0295     QVector<IndexedString> jobUrls;
0296     jobUrls.reserve(jobs);
0297     for (int i = 0; i < jobs; ++i) {
0298         jobUrls << IndexedString("/test" + QString::number(i) + ".txt");
0299     }
0300 
0301     QBENCHMARK {
0302         for (const IndexedString& url : qAsConst(jobUrls)) {
0303             ICore::self()->languageController()->backgroundParser()->addDocument(url);
0304         }
0305 
0306         ICore::self()->languageController()->backgroundParser()->parseDocuments();
0307 
0308         while (ICore::self()->languageController()->backgroundParser()->queuedCount()) {
0309             QTest::qWait(50);
0310         }
0311     }
0312 }
0313 
0314 void TestBackgroundparser::benchmarkDocumentChanges()
0315 {
0316     KTextEditor::Editor* editor = KTextEditor::Editor::instance();
0317     QVERIFY(editor);
0318     KTextEditor::Document* doc = editor->createDocument(this);
0319     QVERIFY(doc);
0320 
0321     QString tmpFileName;
0322     {
0323         QTemporaryFile file;
0324         QVERIFY(file.open());
0325         tmpFileName = file.fileName();
0326     }
0327 
0328     doc->saveAs(QUrl::fromLocalFile(tmpFileName));
0329 
0330     DocumentChangeTracker tracker(doc);
0331 
0332     doc->setText(QStringLiteral("hello world"));
0333     // required for proper benchmark results
0334     doc->createView(nullptr);
0335     QBENCHMARK {
0336         for (int i = 0; i < 5000; i++) {
0337             {
0338                 KTextEditor::Document::EditingTransaction t(doc);
0339                 doc->insertText(KTextEditor::Cursor(0, 0), QStringLiteral("This is a test line.\n"));
0340             }
0341             QApplication::processEvents();
0342         }
0343     }
0344     doc->clear();
0345     doc->save();
0346 }
0347 
0348 // see also: https://bugs.kde.org/355100
0349 void TestBackgroundparser::testNoDeadlockInJobCreation()
0350 {
0351     m_jobPlan.clear();
0352 
0353     // we need to run the background thread first (best priority)
0354     const auto runUrl = QUrl::fromLocalFile(QStringLiteral("/lockInRun.txt"));
0355     const auto run = IndexedString(runUrl);
0356     m_jobPlan.addJob(JobPrototype(runUrl, BackgroundParser::BestPriority,
0357                                   ParseJob::IgnoresSequentialProcessing, 0));
0358 
0359     // before handling the foreground code (worst priority)
0360     const auto ctorUrl = QUrl::fromLocalFile(QStringLiteral("/lockInCtor.txt"));
0361     const auto ctor = IndexedString(ctorUrl);
0362     m_jobPlan.addJob(JobPrototype(ctorUrl, BackgroundParser::WorstPriority,
0363                                   ParseJob::IgnoresSequentialProcessing, 0));
0364 
0365     // make sure that the background thread has the duchain locked for write
0366     QSemaphore semaphoreA;
0367     // make sure the foreground thread is inside the parse job ctor
0368     QSemaphore semaphoreB;
0369 
0370     QObject lifetimeControl; // used to disconnect signal at end of scope
0371 
0372     // actually distribute the complicate code across threads to trigger the
0373     // deadlock reliably
0374     QObject::connect(m_langSupport, &TestLanguageSupport::aboutToCreateParseJob,
0375                      &lifetimeControl, [&](const IndexedString& url, ParseJob** job) {
0376         if (url == run) {
0377             auto testJob = new TestParseJob(url, m_langSupport);
0378             testJob->run_callback = [&](const IndexedString& url) {
0379                                         // this is run in the background parse thread
0380                                         DUChainWriteLocker lock;
0381                                         semaphoreA.release();
0382                                         // sync with the foreground parse job ctor
0383                                         semaphoreB.acquire();
0384                                         // this is acquiring the background parse lock
0385                                         // we want to support this order - i.e. DUChain -> Background Parser
0386                                         ICore::self()->languageController()->backgroundParser()->isQueued(
0387                                             url);
0388                                     };
0389             *job = testJob;
0390         } else if (url == ctor) {
0391             // this is run in the foreground, essentially the same
0392             // as code run within the parse job ctor
0393             semaphoreA.acquire();
0394             semaphoreB.release();
0395             // Note how currently, the background parser is locked while creating a parse job
0396             // thus locking the duchain here used to trigger a lock order inversion
0397             DUChainReadLocker lock;
0398             *job = new TestParseJob(url, m_langSupport);
0399         }
0400     }, Qt::DirectConnection);
0401 
0402     // should be able to run quickly, if no deadlock occurs
0403     QVERIFY(m_jobPlan.runJobs(500));
0404 }
0405 
0406 void TestBackgroundparser::testSuspendResume()
0407 {
0408     auto parser = ICore::self()->languageController()->backgroundParser();
0409 
0410     m_jobPlan.clear();
0411 
0412     const auto runUrl = QUrl::fromLocalFile(QStringLiteral("/file.txt"));
0413     const auto job = JobPrototype(runUrl, BackgroundParser::BestPriority,
0414                                   ParseJob::IgnoresSequentialProcessing, 0);
0415     m_jobPlan.addJob(job);
0416 
0417     parser->suspend();
0418 
0419     m_jobPlan.addJobsToParser();
0420 
0421     parser->parseDocuments();
0422     QTest::qWait(250);
0423 
0424     QCOMPARE(m_jobPlan.numCreatedJobs(), 0);
0425     QCOMPARE(m_jobPlan.numFinishedJobs(), 0);
0426 
0427     parser->resume();
0428     QVERIFY(m_jobPlan.runJobs(100));
0429 
0430     // run once again, this time suspend and resume quickly after another
0431     m_jobPlan.clear();
0432     m_jobPlan.addJob(job);
0433 
0434     parser->suspend();
0435     parser->resume();
0436     QVERIFY(m_jobPlan.runJobs(100));
0437 }
0438 
0439 #include "moc_test_backgroundparser.cpp"