test_backgroundparser.cpp 15.1 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
 * This file is part of KDevelop
 *
 * Copyright 2012 by Sven Brauch <svenbrauch@googlemail.com>
 * Copyright 2012 by Milian Wolff <mail@milianw.de>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Library General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program; if not, write to the
 * Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "test_backgroundparser.h"

#include <QTest>
#include <QElapsedTimer>
Milian Wolff's avatar
Milian Wolff committed
27
#include <QTemporaryFile>
28
#include <QApplication>
29
#include <QSemaphore>
30

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
31
#include <KTextEditor/Editor>
32
#include <KTextEditor/View>
33
34
35
36
37
38

#include <tests/autotestshell.h>
#include <tests/testcore.h>
#include <tests/testlanguagecontroller.h>

#include <language/duchain/duchain.h>
39
#include <language/duchain/duchainlock.h>
40
41
42
43
44
45
46
#include <language/backgroundparser/backgroundparser.h>

#include <interfaces/ilanguagecontroller.h>

#include "testlanguagesupport.h"
#include "testparsejob.h"

Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
47
QTEST_MAIN(TestBackgroundparser)
48
49

#define QVERIFY_RETURN(statement, retval) \
50
51
    do { if (!QTest::qVerify((statement), # statement, "", __FILE__, __LINE__)) \
             return retval; } while (0)
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

using namespace KDevelop;

JobPlan::JobPlan()
{
}

void JobPlan::addJob(const JobPrototype& job)
{
    m_jobs << job;
}

void JobPlan::clear()
{
    m_jobs.clear();
    m_finishedJobs.clear();
68
    m_createdJobs.clear();
69
70
71
72
}

void JobPlan::parseJobCreated(ParseJob* job)
{
73
74
75
76
77
    // e.g. for the benchmark
    if (m_jobs.isEmpty()) {
        return;
    }

78
    auto* testJob = qobject_cast<TestParseJob*>(job);
79
80
    Q_ASSERT(testJob);

Dāvis Mosāns's avatar
Dāvis Mosāns committed
81
    qDebug() << "assigning propierties for created job" << testJob->document().toUrl();
82
    testJob->duration_ms = jobForUrl(testJob->document()).m_duration;
83

84
    m_createdJobs.append(testJob->document());
85
86
}

87
void JobPlan::addJobsToParser()
88
89
{
    // add parse jobs
90
    for (const JobPrototype& job : qAsConst(m_jobs)) {
91
92
93
94
        ICore::self()->languageController()->backgroundParser()->addDocument(
            job.m_url, TopDUContext::Empty, job.m_priority, this, job.m_flags
        );
    }
95
96
97
98
99
}

bool JobPlan::runJobs(int timeoutMS)
{
    addJobsToParser();
100
101
102
103
104
105

    ICore::self()->languageController()->backgroundParser()->parseDocuments();

    QElapsedTimer t;
    t.start();

106
    while (!t.hasExpired(timeoutMS) && m_jobs.size() != m_finishedJobs.size()) {
107
108
109
        QTest::qWait(50);
    }

110
    QVERIFY_RETURN(m_jobs.size() == m_createdJobs.size(), false);
111
112
113
114
115

    QVERIFY_RETURN(m_finishedJobs.size() == m_jobs.size(), false);

    // verify they're started in the right order
    int currentBestPriority = BackgroundParser::BestPriority;
116
    for (const IndexedString& url : qAsConst(m_createdJobs)) {
117
118
119
120
121
122
123
124
        const JobPrototype p = jobForUrl(url);
        QVERIFY_RETURN(p.m_priority >= currentBestPriority, false);
        currentBestPriority = p.m_priority;
    }

    return true;
}

125
JobPrototype JobPlan::jobForUrl(const IndexedString& url) const
126
{
127
128
129
    auto it = std::find_if(m_jobs.begin(), m_jobs.end(), [&](const JobPrototype& job) {
        return (job.m_url == url);
    });
130

131
    return (it != m_jobs.end()) ? *it: JobPrototype();
132
133
}

134
void JobPlan::updateReady(const IndexedString& url, const ReferencedTopDUContext& /*context*/)
135
{
136
137
138
139
140
    if (!ICore::self() || ICore::self()->shuttingDown()) {
        // core was shutdown before we get to handle the delayed signal, cf. testShutdownWithRunningJobs
        return;
    }

Dāvis Mosāns's avatar
Dāvis Mosāns committed
141
    qDebug() << "update ready on " << url.toUrl();
142

143
144
    const JobPrototype job = jobForUrl(url);
    QVERIFY(job.m_url.toUrl().isValid());
145
146
147
148

    if (job.m_flags & ParseJob::RequiresSequentialProcessing) {
        // ensure that all jobs that respect sequential processing
        // with lower priority have been run
149
        for (const JobPrototype& otherJob : qAsConst(m_jobs)) {
150
151
152
153
            if (otherJob.m_url == job.m_url) {
                continue;
            }
            if (otherJob.m_flags & ParseJob::RespectsSequentialProcessing &&
154
                otherJob.m_priority < job.m_priority) {
155
156
157
158
159
160
161
162
163
                QVERIFY(m_finishedJobs.contains(otherJob.m_url));
            }
        }
    }

    QVERIFY(!m_finishedJobs.contains(job.m_url));
    m_finishedJobs << job.m_url;
}

164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
int JobPlan::numJobs() const
{
    return m_jobs.size();
}

int JobPlan::numCreatedJobs() const
{
    return m_createdJobs.size();
}

int JobPlan::numFinishedJobs() const
{
    return m_finishedJobs.size();
}

179
180
void TestBackgroundparser::initTestCase()
{
181
182
    AutoTestShell::init();
    TestCore* core = TestCore::initialize(Core::NoUi);
183

184
    DUChain::self()->disablePersistentStorage();
185

186
    auto* langController = new TestLanguageController(core);
187
188
189
    core->setLanguageController(langController);
    langController->backgroundParser()->setThreadCount(4);
    langController->backgroundParser()->abortAllJobs();
190

191
192
193
194
    m_langSupport = new TestLanguageSupport(this);
    connect(m_langSupport, &TestLanguageSupport::parseJobCreated,
            &m_jobPlan, &JobPlan::parseJobCreated);
    langController->addTestLanguage(m_langSupport, QStringList() << QStringLiteral("text/plain"));
Milian Wolff's avatar
Milian Wolff committed
195

196
197
198
    const auto languages = langController->languagesForUrl(QUrl::fromLocalFile(QStringLiteral("/foo.txt")));
    QCOMPARE(languages.size(), 1);
    QCOMPARE(languages.first(), m_langSupport);
199
200
201
202
}

void TestBackgroundparser::cleanupTestCase()
{
203
204
    TestCore::shutdown();
    m_langSupport = nullptr;
205
206
207
208
209
210
211
}

void TestBackgroundparser::init()
{
    m_jobPlan.clear();
}

212
213
214
215
216
217
218
219
220
221
void TestBackgroundparser::testShutdownWithRunningJobs()
{
    m_jobPlan.clear();
    // prove that background parsing happens with sequential flags although there is a high-priority
    // foreground thread (active document being edited, ...) running all the time.

    // the long-running high-prio job
    m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile(QStringLiteral("/test_fgt_hp.txt")),
                                  -500, ParseJob::IgnoresSequentialProcessing, 1000));

222
    m_jobPlan.addJobsToParser();
223
224
225
226
227
228
229
230
231
232
233

    ICore::self()->languageController()->backgroundParser()->parseDocuments();
    QTest::qWait(50);

    // shut down with running jobs, make sure we don't crash
    cleanupTestCase();

    // restart again to restore invariant (core always running in test functions)
    initTestCase();
}

234
235
236
237
238
239
240
void TestBackgroundparser::testParseOrdering_foregroundThread()
{
    m_jobPlan.clear();
    // prove that background parsing happens with sequential flags although there is a high-priority
    // foreground thread (active document being edited, ...) running all the time.

    // the long-running high-prio job
241
242
    m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile(QStringLiteral("/test_fgt_hp.txt")), -500,
                                  ParseJob::IgnoresSequentialProcessing, 630));
243
244

    // several small background jobs
245
246
247
    for (int i = 0; i < 10; i++) {
        m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile("/test_fgt_lp__" + QString::number(i) + ".txt"), i,
                                      ParseJob::FullSequentialProcessing, 40));
248
249
250
251
252
253
254
255
256
    }

    // not enough time if the small jobs run after the large one
    QVERIFY(m_jobPlan.runJobs(700));
}

void TestBackgroundparser::testParseOrdering_noSequentialProcessing()
{
    m_jobPlan.clear();
257
    for (int i = 0; i < 20; i++) {
258
        // create jobs with no sequential processing, and different priorities
259
260
        m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile("/test_nsp1__" + QString::number(i) + ".txt"), i,
                                      ParseJob::IgnoresSequentialProcessing, i));
261
    }
262
263

    for (int i = 0; i < 8; i++) {
264
        // create a few more jobs with the same priority
265
266
        m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile("/test_nsp2__" + QString::number(i) + ".txt"), 10,
                                      ParseJob::IgnoresSequentialProcessing, i));
267
    }
268

269
270
271
272
273
274
    QVERIFY(m_jobPlan.runJobs(1000));
}

void TestBackgroundparser::testParseOrdering_lockup()
{
    m_jobPlan.clear();
275
    for (int i = 3; i > 0; i--) {
276
        // add 3 jobs which do not care about sequential processing, at 4 threads it should take no more than 1s to process them
277
278
        m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile("/test" + QString::number(i) + ".txt"), i,
                                      ParseJob::IgnoresSequentialProcessing, 200));
279
    }
280

281
    // add one job which requires sequential processing with high priority
282
283
    m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile(QStringLiteral("/test_hp.txt")), -200,
                                  ParseJob::FullSequentialProcessing, 200));
284
285
286
287
288
289
290
    // verify that the low-priority nonsequential jobs are run simultaneously with the other one.
    QVERIFY(m_jobPlan.runJobs(700));
}

void TestBackgroundparser::testParseOrdering_simple()
{
    m_jobPlan.clear();
291
    for (int i = 20; i > 0; i--) {
292
293
294
        // the job with priority i should be at place i in the finished list
        // (lower priority value -> should be parsed first)
        ParseJob::SequentialProcessingFlags flags = ParseJob::FullSequentialProcessing;
Milian Wolff's avatar
Milian Wolff committed
295
        m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile("/test" + QString::number(i) + ".txt"),
296
297
                                      i, flags));
    }
298

299
    // also add a few jobs which ignore the processing
300
    for (int i = 0; i < 5; ++i) {
Milian Wolff's avatar
Milian Wolff committed
301
        m_jobPlan.addJob(JobPrototype(QUrl::fromLocalFile("/test2-" + QString::number(i) + ".txt"),
302
303
                                      BackgroundParser::NormalPriority,
                                      ParseJob::IgnoresSequentialProcessing));
304
305
306
307
308
    }

    QVERIFY(m_jobPlan.runJobs(1000));
}

309
310
void TestBackgroundparser::benchmark()
{
Milian Wolff's avatar
Milian Wolff committed
311
    const int jobs = 10000;
312

313
    QVector<IndexedString> jobUrls;
314
    jobUrls.reserve(jobs);
315
    for (int i = 0; i < jobs; ++i) {
Milian Wolff's avatar
Milian Wolff committed
316
        jobUrls << IndexedString("/test" + QString::number(i) + ".txt");
317
318
319
    }

    QBENCHMARK {
320
        for (const IndexedString& url : qAsConst(jobUrls)) {
321
322
323
324
325
            ICore::self()->languageController()->backgroundParser()->addDocument(url);
        }

        ICore::self()->languageController()->backgroundParser()->parseDocuments();

326
        while (ICore::self()->languageController()->backgroundParser()->queuedCount()) {
327
328
329
330
331
            QTest::qWait(50);
        }
    }
}

332
333
void TestBackgroundparser::benchmarkDocumentChanges()
{
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
334
    KTextEditor::Editor* editor = KTextEditor::Editor::instance();
335
336
337
    QVERIFY(editor);
    KTextEditor::Document* doc = editor->createDocument(this);
    QVERIFY(doc);
338
339
340
341
342
343
344
345
346

    QString tmpFileName;
    {
        QTemporaryFile file;
        QVERIFY(file.open());
        tmpFileName = file.fileName();
    }

    doc->saveAs(QUrl::fromLocalFile(tmpFileName));
Milian Wolff's avatar
Milian Wolff committed
347

348
    DocumentChangeTracker tracker(doc);
Milian Wolff's avatar
Milian Wolff committed
349

350
    doc->setText(QStringLiteral("hello world"));
Milian Wolff's avatar
Milian Wolff committed
351
    // required for proper benchmark results
352
    doc->createView(nullptr);
353
    QBENCHMARK {
354
        for (int i = 0; i < 5000; i++) {
355
356
            {
                KTextEditor::Document::EditingTransaction t(doc);
357
                doc->insertText(KTextEditor::Cursor(0, 0), QStringLiteral("This is a test line.\n"));
358
            }
359
360
361
362
363
364
365
            QApplication::processEvents();
        }
    }
    doc->clear();
    doc->save();
}

366
// see also: https://bugs.kde.org/355100
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
void TestBackgroundparser::testNoDeadlockInJobCreation()
{
    m_jobPlan.clear();

    // we need to run the background thread first (best priority)
    const auto runUrl = QUrl::fromLocalFile(QStringLiteral("/lockInRun.txt"));
    const auto run = IndexedString(runUrl);
    m_jobPlan.addJob(JobPrototype(runUrl, BackgroundParser::BestPriority,
                                  ParseJob::IgnoresSequentialProcessing, 0));

    // before handling the foreground code (worst priority)
    const auto ctorUrl = QUrl::fromLocalFile(QStringLiteral("/lockInCtor.txt"));
    const auto ctor = IndexedString(ctorUrl);
    m_jobPlan.addJob(JobPrototype(ctorUrl, BackgroundParser::WorstPriority,
                                  ParseJob::IgnoresSequentialProcessing, 0));

    // make sure that the background thread has the duchain locked for write
    QSemaphore semaphoreA;
    // make sure the foreground thread is inside the parse job ctor
    QSemaphore semaphoreB;

388
389
    QObject lifetimeControl; // used to disconnect signal at end of scope

390
391
392
    // actually distribute the complicate code across threads to trigger the
    // deadlock reliably
    QObject::connect(m_langSupport, &TestLanguageSupport::aboutToCreateParseJob,
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
                     &lifetimeControl, [&](const IndexedString& url, ParseJob** job) {
        if (url == run) {
            auto testJob = new TestParseJob(url, m_langSupport);
            testJob->run_callback = [&](const IndexedString& url) {
                                        // this is run in the background parse thread
                                        DUChainWriteLocker lock;
                                        semaphoreA.release();
                                        // sync with the foreground parse job ctor
                                        semaphoreB.acquire();
                                        // this is acquiring the background parse lock
                                        // we want to support this order - i.e. DUChain -> Background Parser
                                        ICore::self()->languageController()->backgroundParser()->isQueued(
                                            url);
                                    };
            *job = testJob;
        } else if (url == ctor) {
            // this is run in the foreground, essentially the same
            // as code run within the parse job ctor
            semaphoreA.acquire();
            semaphoreB.release();
            // Note how currently, the background parser is locked while creating a parse job
            // thus locking the duchain here used to trigger a lock order inversion
            DUChainReadLocker lock;
            *job = new TestParseJob(url, m_langSupport);
        }
    }, Qt::DirectConnection);
419
420
421
422

    // should be able to run quickly, if no deadlock occurs
    QVERIFY(m_jobPlan.runJobs(500));
}
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455

void TestBackgroundparser::testSuspendResume()
{
    auto parser = ICore::self()->languageController()->backgroundParser();

    m_jobPlan.clear();

    const auto runUrl = QUrl::fromLocalFile(QStringLiteral("/file.txt"));
    const auto job = JobPrototype(runUrl, BackgroundParser::BestPriority,
                                  ParseJob::IgnoresSequentialProcessing, 0);
    m_jobPlan.addJob(job);

    parser->suspend();

    m_jobPlan.addJobsToParser();

    parser->parseDocuments();
    QTest::qWait(250);

    QCOMPARE(m_jobPlan.numCreatedJobs(), 0);
    QCOMPARE(m_jobPlan.numFinishedJobs(), 0);

    parser->resume();
    QVERIFY(m_jobPlan.runJobs(100));

    // run once again, this time suspend and resume quickly after another
    m_jobPlan.clear();
    m_jobPlan.addJob(job);

    parser->suspend();
    parser->resume();
    QVERIFY(m_jobPlan.runJobs(100));
}