Clang: Take over jobs if document's project changes

On registerProjectPartsForEditor() we recreated the affected
DocumentProcessors, but did not take care of the jobs that were in the
queue, which effectively could led to lost jobs. Catch up on this.

Task-number: QTCREATORBUG-18856
Change-Id: I4184e5dc6480667953f2d2081ccf39a28c092186
Reviewed-by: Ivan Donchevskii <ivan.donchevskii@qt.io>
This commit is contained in:
Nikolai Kosjar
2018-01-05 09:35:00 +01:00
parent dd06a4188d
commit 2d7f1d6c8c
16 changed files with 162 additions and 19 deletions

View File

@@ -28,7 +28,6 @@
#include "clangdocuments.h"
#include "clangdocumentsuspenderresumer.h"
#include "clangfilesystemwatcher.h"
#include "clangtranslationunits.h"
#include "codecompleter.h"
#include "diagnosticset.h"
#include "tokeninfos.h"
@@ -158,12 +157,17 @@ void ClangCodeModelServer::registerProjectPartsForEditor(const RegisterProjectPa
std::vector<Document> affectedDocuments = documents.setDocumentsDirtyIfProjectPartChanged();
for (Document &document : affectedDocuments) {
document.setResponsivenessIncreaseNeeded(document.isResponsivenessIncreased());
documents.remove({document.fileContainer()});
documentProcessors().remove(document);
document.translationUnits().removeAll();
document.translationUnits().createAndAppend();
documentProcessors().create(document);
Document newDocument = *documents.create({document.fileContainer()}).begin();
newDocument.setDirtyIfDependencyIsMet(document.filePath());
newDocument.setIsUsedByCurrentEditor(document.isUsedByCurrentEditor());
newDocument.setIsVisibleInEditor(document.isVisibleInEditor(), document.visibleTimePoint());
newDocument.setResponsivenessIncreaseNeeded(document.isResponsivenessIncreased());
documentProcessors().reset(document, newDocument);
QTC_CHECK(document.useCount() == 1);
}
processJobsForDirtyAndVisibleDocuments();

View File

@@ -155,6 +155,11 @@ bool Document::isParsed() const
return d->translationUnits.areAllTranslationUnitsParsed();
}
long Document::useCount() const
{
return d.use_count();
}
Utf8String Document::filePath() const
{
checkIfNull();
@@ -175,6 +180,7 @@ FileContainer Document::fileContainer() const
return FileContainer(d->filePath,
d->projectPart.id(),
d->fileArguments,
Utf8String(),
false,
d->documentRevision);

View File

@@ -78,6 +78,7 @@ public:
bool isNull() const;
bool isIntact() const;
bool isParsed() const;
long useCount() const;
Utf8String filePath() const;
Utf8StringVector fileArguments() const;

View File

@@ -102,6 +102,12 @@ JobRequests DocumentProcessor::process()
return d->jobs.process();
}
JobRequests DocumentProcessor::stop()
{
d->supportiveTranslationUnitInitializer.abort();
return d->jobs.stop();
}
Document DocumentProcessor::document() const
{
return d->document;

View File

@@ -59,6 +59,7 @@ public:
= PreferredTranslationUnit::RecentlyParsed);
JobRequests process();
JobRequests stop();
Document document() const;

View File

@@ -28,6 +28,8 @@
#include "clangexceptions.h"
#include "projectpart.h"
#include <utils/algorithm.h>
namespace ClangBackEnd {
DocumentProcessors::DocumentProcessors(Documents &documents,
@@ -84,6 +86,25 @@ void DocumentProcessors::remove(const Document &document)
throw DocumentProcessorDoesNotExist(document.filePath(), document.projectPart().id());
}
void DocumentProcessors::reset(const Document &oldDocument, const Document &newDocument)
{
// Wait until the currently running jobs finish and remember the not yet
// processed job requests for the new processor...
JobRequests jobsToTakeOver = processor(oldDocument).stop();
// ...but do not take over irrelevant ones.
jobsToTakeOver = Utils::filtered(jobsToTakeOver, [](const JobRequest &job){
return job.isTakeOverable();
});
// Remove current processor
remove(oldDocument);
// Create new processor and take over not yet processed jobs.
DocumentProcessor newProcessor = create(newDocument);
for (const JobRequest &job : jobsToTakeOver)
newProcessor.addJob(job);
}
JobRequests DocumentProcessors::process()
{
JobRequests jobsStarted;

View File

@@ -54,6 +54,7 @@ public:
DocumentProcessor create(const Document &document);
DocumentProcessor processor(const Document &document);
void remove(const Document &document);
void reset(const Document &oldDocument, const Document &newDocument);
JobRequests process();

View File

@@ -168,6 +168,42 @@ static JobRequest::RunConditions conditionsForType(JobRequest::Type type)
return conditions;
}
bool JobRequest::isTakeOverable() const
{
// When new project information comes in and there are unprocessed jobs
// in the queue, we need to decide what to do with them.
switch (type) {
// Never discard these as the client side might wait for a response.
case Type::CompleteCode:
case Type::RequestReferences:
case Type::FollowSymbol:
return true;
// Discard this one as UpdateDocumentAnnotations will have the same effect.
case Type::RequestDocumentAnnotations:
// Discard Suspend because the document will be cleared anyway.
// Discard Resume because a (re)parse will happen on demand.
case Type::SuspendDocument:
case Type::ResumeDocument:
// Discard these as they are initial jobs that will be recreated on demand
// anyway.
case Type::UpdateDocumentAnnotations:
case Type::CreateInitialDocumentPreamble:
// Discard these as they only make sense in a row. Avoid splitting them up.
case Type::ParseSupportiveTranslationUnit:
case Type::ReparseSupportiveTranslationUnit:
case Type::Invalid:
return false;
}
return false;
}
JobRequest::JobRequest(Type type)
{
static quint64 idCounter = 0;

View File

@@ -96,6 +96,7 @@ public:
IAsyncJob *createJob() const;
void cancelJob(ClangCodeModelClientInterface &client) const;
bool isTakeOverable() const;
bool operator==(const JobRequest &other) const;

View File

@@ -113,6 +113,20 @@ JobRequests Jobs::process()
return jobsStarted;
}
JobRequests Jobs::stop()
{
// Take the queued jobs to prevent processing them.
const JobRequests queuedJobs = queue();
queue().clear();
// Wait until currently running jobs finish.
QFutureSynchronizer<void> waitForFinishedJobs;
foreach (const RunningJob &runningJob, m_running.values())
waitForFinishedJobs.addFuture(runningJob.future);
return queuedJobs;
}
JobRequests Jobs::runJobs(const JobRequests &jobsRequests)
{
JobRequests jobsStarted;
@@ -166,6 +180,11 @@ void Jobs::onJobFinished(IAsyncJob *asyncJob)
process();
}
Jobs::JobFinishedCallback Jobs::jobFinishedCallback() const
{
return m_jobFinishedCallback;
}
void Jobs::setJobFinishedCallback(const JobFinishedCallback &jobFinishedCallback)
{
m_jobFinishedCallback = jobFinishedCallback;

View File

@@ -71,6 +71,7 @@ public:
= PreferredTranslationUnit::RecentlyParsed);
JobRequests process();
JobRequests stop();
void setJobFinishedCallback(const JobFinishedCallback &jobFinishedCallback);
@@ -80,6 +81,7 @@ public /*for tests*/:
const JobRequests &queue() const;
bool isJobRunningForTranslationUnit(const Utf8String &translationUnitId) const;
bool isJobRunningForJobRequest(const JobRequest &jobRequest) const;
JobFinishedCallback jobFinishedCallback() const;
private:
JobRequests runJobs(const JobRequests &jobRequest);

View File

@@ -56,8 +56,7 @@ SupportiveTranslationUnitInitializer::State SupportiveTranslationUnitInitializer
void SupportiveTranslationUnitInitializer::startInitializing()
{
QTC_CHECK(m_state == State::NotInitialized);
if (abortIfDocumentIsClosed())
if (!checkStateAndDocument(State::NotInitialized))
return;
m_document.translationUnits().createAndAppend();
@@ -71,10 +70,15 @@ void SupportiveTranslationUnitInitializer::startInitializing()
m_state = State::WaitingForParseJob;
}
void SupportiveTranslationUnitInitializer::abort()
{
m_jobs.setJobFinishedCallback(Jobs::JobFinishedCallback());
m_state = State::Aborted;
}
void SupportiveTranslationUnitInitializer::checkIfParseJobFinished(const Jobs::RunningJob &job)
{
QTC_CHECK(m_state == State::WaitingForParseJob);
if (abortIfDocumentIsClosed())
if (!checkStateAndDocument(State::WaitingForParseJob))
return;
if (job.jobRequest.type == JobRequest::Type::ParseSupportiveTranslationUnit) {
@@ -90,8 +94,7 @@ void SupportiveTranslationUnitInitializer::checkIfParseJobFinished(const Jobs::R
void SupportiveTranslationUnitInitializer::checkIfReparseJobFinished(const Jobs::RunningJob &job)
{
QTC_CHECK(m_state == State::WaitingForReparseJob);
if (abortIfDocumentIsClosed())
if (!checkStateAndDocument(State::WaitingForReparseJob))
return;
if (job.jobRequest.type == JobRequest::Type::ReparseSupportiveTranslationUnit) {
@@ -106,18 +109,22 @@ void SupportiveTranslationUnitInitializer::checkIfReparseJobFinished(const Jobs:
}
}
bool SupportiveTranslationUnitInitializer::abortIfDocumentIsClosed()
bool SupportiveTranslationUnitInitializer::checkStateAndDocument(State currentExpectedState)
{
QTC_CHECK(m_isDocumentClosedChecker);
if (m_isDocumentClosedChecker(m_document.filePath(), m_document.projectPart().id())) {
if (m_state != currentExpectedState) {
m_state = State::Aborted;
return true;
return false;
}
QTC_CHECK(m_isDocumentClosedChecker);
if (m_isDocumentClosedChecker(m_document.filePath(), m_document.projectPart().id())) {
m_state = State::Aborted;
return false;
}
return true;
}
void SupportiveTranslationUnitInitializer::addJob(JobRequest::Type jobRequestType)
{
const JobRequest jobRequest = m_jobs.createJobRequest(

View File

@@ -52,6 +52,7 @@ public:
State state() const;
void startInitializing();
void abort();
public: // for tests
void setState(const State &state);
@@ -59,7 +60,7 @@ public: // for tests
void checkIfReparseJobFinished(const Jobs::RunningJob &job);
private:
bool abortIfDocumentIsClosed();
bool checkStateAndDocument(State currentExpectedState);
void addJob(JobRequest::Type jobRequestType);
private:

View File

@@ -406,6 +406,18 @@ TEST_F(ClangCodeModelServerSlowTest, TranslationUnitAfterUpdateNeedsReparse)
ASSERT_THAT(clangServer, HasDirtyDocument(filePathA, projectPartId, 1U, true, true));
}
TEST_F(ClangCodeModelServerSlowTest, TakeOverJobsOnProjectPartChange)
{
registerProjectAndFileAndWaitForFinished(filePathC, 2);
updateVisibilty(filePathB, filePathB); // Disable processing jobs
requestReferences();
expectReferences();
changeProjectPartArguments(); // Here we do not want to loose the RequestReferences job
updateVisibilty(filePathC, filePathC); // Enable processing jobs
}
void ClangCodeModelServer::SetUp()
{
clangServer.setClient(&mockClangCodeModelClient);

View File

@@ -126,6 +126,19 @@ TEST_F(DocumentProcessors, Remove)
ASSERT_TRUE(documentProcessors.processors().empty());
}
TEST_F(DocumentProcessors, ResetTakesOverJobsInQueue)
{
documentProcessors.create(document);
documentProcessors.processor(document).addJob(JobRequest::Type::RequestReferences);
documents.remove({document.fileContainer()});
const auto newDocument = *documents.create({document.fileContainer()}).begin();
documentProcessors.reset(document, newDocument);
ASSERT_THAT(documentProcessors.processor(document).queue().first().type,
JobRequest::Type::RequestReferences);
}
TEST_F(DocumentProcessors, RemoveThrowsForNotExisting)
{
ASSERT_THROW(documentProcessors.remove(document),

View File

@@ -111,6 +111,18 @@ TEST_F(SupportiveTranslationUnitInitializerSlowTest, StartInitializingStartsJob)
ASSERT_THAT(runningJob.jobRequest.type, JobRequest::Type::ParseSupportiveTranslationUnit);
}
TEST_F(SupportiveTranslationUnitInitializerSlowTest, Abort)
{
initializer.startInitializing();
assertSingleJobRunningAndEmptyQueue();
initializer.abort();
ASSERT_THAT(initializer.state(),
Eq(ClangBackEnd::SupportiveTranslationUnitInitializer::State::Aborted));
ASSERT_FALSE(jobs.jobFinishedCallback());
}
TEST_F(SupportiveTranslationUnitInitializer, CheckIfParseJobFinishedAbortsIfDocumentIsClosed)
{
documents.remove({FileContainer(filePath, projectPartId)});