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"