Commit 5b1c175b authored by Milian Wolff's avatar Milian Wolff
Browse files

Skip cmake configure when importing projects with up2date project data

When we open a cmake project, try to import the file data directly.
If that works and the project data was generated after the last
modification of any cmake files that are used by the project, use
the data as-is. This way, we don't need to wait for CMake configure
when opening a project in KDevelop in many cases, greatly improving
the perceived performance.

When we manually reload an item, we still force cmake to run. This
is mostly a safety-net for now. I don't think performance matters
a lot in this case, as we either need to reconfigure anyways because
we are reacting to a file change event, or because the user explicitly
triggered a reload which might indicate something was broken.
parent 5201fb42
Pipeline #194433 passed with stage
in 25 minutes and 49 seconds
......@@ -92,13 +92,13 @@ static QDir toReplyDir(const QString& buildDirectory)
return replyDir;
}
QJsonObject findReplyIndexFile(const QString& buildDirectory)
ReplyIndex findReplyIndexFile(const QString& buildDirectory)
{
const auto replyDir = toReplyDir(buildDirectory);
for (const auto& entry : replyDir.entryInfoList({QStringLiteral("index-*.json")}, QDir::Files, QDir::Name | QDir::Reversed)) {
const auto object = parseFile(entry.absoluteFilePath());
if (isKDevelopClientResponse(object)) {
return object;
return {entry.lastModified(), object};
}
}
qCWarning(CMAKE) << "no cmake-file-api reply index file found in" << replyDir.absolutePath();
......@@ -214,8 +214,8 @@ static CMakeProjectData parseCodeModel(const QJsonObject& codeModel, const QDir&
return ret;
}
static QHash<Path, CMakeProjectData::CMakeFileFlags> parseCMakeFiles(const QJsonObject& cmakeFiles,
PathInterner& sourcePathInterner)
static QHash<Path, CMakeProjectData::CMakeFileFlags>
parseCMakeFiles(const QJsonObject& cmakeFiles, PathInterner& sourcePathInterner, QDateTime* lastModifiedCMakeFile)
{
QHash<Path, CMakeProjectData::CMakeFileFlags> ret;
for (const auto& jsonInput : cmakeFiles.value(QLatin1String("inputs")).toArray()) {
......@@ -226,15 +226,19 @@ static QHash<Path, CMakeProjectData::CMakeFileFlags> parseCMakeFiles(const QJson
flags.isExternal = input.value(QLatin1String("isExternal")).toBool();
flags.isCMake = input.value(QLatin1String("isCMake")).toBool();
ret[path] = flags;
if (path.isLocalFile()) {
const auto info = QFileInfo(path.toLocalFile());
*lastModifiedCMakeFile = std::max(info.lastModified(), *lastModifiedCMakeFile);
}
}
return ret;
}
CMakeProjectData parseReplyIndexFile(const QJsonObject& replyIndex,
const Path& sourceDirectory,
CMakeProjectData parseReplyIndexFile(const ReplyIndex& replyIndex, const Path& sourceDirectory,
const Path& buildDirectory)
{
const auto reply = replyIndex.value(QLatin1String("reply")).toObject();
const auto reply = replyIndex.data.value(QLatin1String("reply")).toObject();
const auto clientKDevelop = reply.value(QLatin1String("client-kdevelop")).toObject();
const auto query = clientKDevelop.value(QLatin1String("query.json")).toObject();
const auto responses = query.value(QLatin1String("responses")).toArray();
......@@ -256,15 +260,17 @@ CMakeProjectData parseReplyIndexFile(const QJsonObject& replyIndex,
codeModel = parseCodeModel(parseFile(jsonFilePath), replyDir,
stringInterner, sourcePathInterner, buildPathInterner);
} else if (kind == QLatin1String("cmakeFiles")) {
cmakeFiles = parseCMakeFiles(parseFile(jsonFilePath), sourcePathInterner);
cmakeFiles = parseCMakeFiles(parseFile(jsonFilePath), sourcePathInterner, &codeModel.lastModifiedCMakeFile);
}
}
if (!codeModel.compilationData.isValid) {
qCWarning(CMAKE) << "failed to find code model in reply index" << sourceDirectory << buildDirectory << replyIndex;
qCWarning(CMAKE) << "failed to find code model in reply index" << sourceDirectory << buildDirectory
<< replyIndex.data;
return {};
}
codeModel.lastModifiedProjectData = replyIndex.lastModified;
codeModel.cmakeFiles = cmakeFiles;
return codeModel;
}
......
......@@ -6,7 +6,9 @@
#pragma once
class QJsonObject;
#include <QJsonObject>
#include <QDateTime>
class QString;
#include <cmakecommonexport.h>
......@@ -33,19 +35,24 @@ KDEVCMAKECOMMON_EXPORT bool supported(const QString &cmakeExecutable);
*/
KDEVCMAKECOMMON_EXPORT void writeClientQueryFile(const QString &buildDirectory);
struct ReplyIndex {
QDateTime lastModified;
QJsonObject data;
};
/**
* Read and parse latest available reply index file that corresponds to our query in @p buildDirectory.
*
* See also: https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html#v1-reply-index-file
*/
KDEVCMAKECOMMON_EXPORT QJsonObject findReplyIndexFile(const QString &buildDirectory);
KDEVCMAKECOMMON_EXPORT ReplyIndex findReplyIndexFile(const QString& buildDirectory);
/**
* Read and parse the code model referenced by the given @p replyIndex
*
* See also: https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html#id11
*/
KDEVCMAKECOMMON_EXPORT CMakeProjectData parseReplyIndexFile(const QJsonObject& replyIndex,
KDEVCMAKECOMMON_EXPORT CMakeProjectData parseReplyIndexFile(const ReplyIndex& replyIndex,
const KDevelop::Path& sourceDirectory,
const KDevelop::Path& buildDirectory);
}
......
......@@ -42,7 +42,7 @@ void ImportJob::start()
const auto buildDirectory = bsm->buildDirectory(m_project->projectItem());
auto future = QtConcurrent::run([sourceDirectory, buildDirectory]() -> CMakeProjectData {
const auto replyIndex = findReplyIndexFile(buildDirectory.toLocalFile());
if (replyIndex.isEmpty()) {
if (replyIndex.data.isEmpty()) {
return {};
}
auto ret = parseReplyIndexFile(replyIndex, sourceDirectory, buildDirectory);
......
......@@ -119,32 +119,24 @@ class ChooseCMakeInterfaceJob : public ExecuteCompositeJob
{
Q_OBJECT
public:
ChooseCMakeInterfaceJob(IProject* project, CMakeManager* manager)
ChooseCMakeInterfaceJob(IProject* project, CMakeManager* manager, bool forceConfigure)
: ExecuteCompositeJob(manager, {})
, project(project)
, manager(manager)
, forceConfigure(forceConfigure)
{
}
void start() override {
auto tryCMakeServer = [this]() {
qCDebug(CMAKE) << "try cmake server for import";
server.reset(new CMakeServer(project));
connect(server.data(), &CMakeServer::connected, this, &ChooseCMakeInterfaceJob::successfulConnection);
connect(server.data(), &CMakeServer::finished, this, &ChooseCMakeInterfaceJob::failedConnection);
};
if (CMake::FileApi::supported(CMake::currentCMakeExecutable(project).toLocalFile())) {
qCDebug(CMAKE) << "Using cmake-file-api for import of" << project->path();
addSubjob(manager->builder()->configure(project));
auto* importJob = new CMake::FileApi::ImportJob(project, this);
connect(importJob, &CMake::FileApi::ImportJob::dataAvailable, this, [this, tryCMakeServer](const CMakeProjectData& data) {
if (!data.compilationData.isValid) {
tryCMakeServer();
} else {
manager->integrateData(data, project);
}
});
addSubjob(importJob);
// try to import the data directly, if possible and not outdated
if (forceConfigure) {
reconfigureThenImport();
} else {
tryDirectImport();
}
ExecuteCompositeJob::start();
} else {
tryCMakeServer();
......@@ -152,6 +144,14 @@ public:
}
private:
void tryCMakeServer()
{
qCDebug(CMAKE) << "try cmake server for import";
server.reset(new CMakeServer(project));
connect(server.data(), &CMakeServer::connected, this, &ChooseCMakeInterfaceJob::successfulConnection);
connect(server.data(), &CMakeServer::finished, this, &ChooseCMakeInterfaceJob::failedConnection);
}
void successfulConnection() {
auto job = new CMakeServerImportJob(project, server, this);
connect(job, &CMakeServerImportJob::result, this, [this, job](){
......@@ -188,19 +188,57 @@ private:
ExecuteCompositeJob::start();
}
void reconfigureThenImport()
{
addSubjob(manager->builder()->configure(project));
auto* importJob = new CMake::FileApi::ImportJob(project, this);
connect(importJob, &CMake::FileApi::ImportJob::dataAvailable, this, &ChooseCMakeInterfaceJob::fileImportDone);
addSubjob(importJob);
}
void tryDirectImport()
{
auto* importJob = new CMake::FileApi::ImportJob(project, this);
connect(importJob, &CMake::FileApi::ImportJob::dataAvailable, this, [this](const CMakeProjectData& data) {
if (!data.compilationData.isValid || data.lastModifiedCMakeFile > data.lastModifiedProjectData) {
qCDebug(CMAKE) << "reconfigure, project data is outdated" << data.lastModifiedCMakeFile
<< data.lastModifiedProjectData;
reconfigureThenImport();
} else {
qCDebug(CMAKE) << "skip configure, project data is up to date" << data.lastModifiedCMakeFile
<< data.lastModifiedProjectData;
fileImportDone(data);
}
});
addSubjob(importJob);
}
void fileImportDone(const CMakeProjectData& data)
{
if (!data.compilationData.isValid) {
tryCMakeServer();
} else {
manager->integrateData(data, project);
}
}
QSharedPointer<CMakeServer> server;
IProject* const project;
CMakeManager* const manager;
const bool forceConfigure;
};
KJob* CMakeManager::createImportJob(ProjectFolderItem* item)
KJob* CMakeManager::createImportJob(ProjectFolderItem* item, bool forceConfigure)
{
auto project = item->project();
auto job = new ChooseCMakeInterfaceJob(project, this);
connect(job, &KJob::result, this, [this, job, project](){
auto job = new ChooseCMakeInterfaceJob(project, this, forceConfigure);
connect(job, &KJob::result, this, [this, job, project]() {
if (job->error() != 0) {
qCWarning(CMAKE) << "couldn't load project successfully" << project->name() << job->error() << job->errorText();
qCWarning(CMAKE) << "couldn't load project successfully" << project->name() << job->error()
<< job->errorText();
showConfigureErrorMessage(project->name(), job->errorText());
}
});
......@@ -212,11 +250,16 @@ KJob* CMakeManager::createImportJob(ProjectFolderItem* item)
Q_ASSERT(!jobs.contains(nullptr));
auto* composite = new ExecuteCompositeJob(this, jobs);
// even if the cmake call failed, we want to load the project so that the project can be worked on
// even if the cmake call failed, we want to load the project so that the project can be worked on
composite->setAbortOnError(false);
return composite;
}
KJob* CMakeManager::createImportJob(ProjectFolderItem* item)
{
return createImportJob(item, false);
}
QList<KDevelop::ProjectTargetItem*> CMakeManager::targets() const
{
QList<KDevelop::ProjectTargetItem*> ret;
......@@ -318,7 +361,7 @@ bool CMakeManager::reload(KDevelop::ProjectFolderItem* folder)
if (!project->isReady())
return false;
KJob *job = createImportJob(folder);
KJob* job = createImportJob(folder, true);
project->setReloadJob(job);
ICore::self()->runController()->registerJob( job );
if (folder == project->projectItem()) {
......
......@@ -127,6 +127,8 @@ private:
void showConfigureErrorMessage(const QString& projectName, const QString& errorMessage) const;
KJob* createImportJob(KDevelop::ProjectFolderItem* item, bool forceConfigure);
private:
struct PerProjectData
{
......
......@@ -10,8 +10,10 @@
#include <QSharedPointer>
#include <QStringList>
#include <QHash>
#include <util/path.h>
#include <QDebug>
#include <QDateTime>
#include <util/path.h>
#include <cmakecommonexport.h>
......@@ -108,6 +110,8 @@ struct KDEVCMAKECOMMON_EXPORT CMakeProjectData
bool isCMake = false;
};
QHash<KDevelop::Path, CMakeFileFlags> cmakeFiles;
QDateTime lastModifiedCMakeFile;
QDateTime lastModifiedProjectData;
};
#endif
......@@ -69,7 +69,8 @@ private Q_SLOTS:
QVERIFY(configureJob->exec());
const auto index = CMake::FileApi::findReplyIndexFile(buildDir);
QVERIFY(!index.isEmpty());
QVERIFY(index.lastModified.isValid());
QVERIFY(!index.data.isEmpty());
const auto projectData = CMake::FileApi::parseReplyIndexFile(index, project->path(), buildPath);
QVERIFY(projectData.compilationData.isValid);
......@@ -97,6 +98,10 @@ private Q_SLOTS:
QVERIFY(projectData.cmakeFiles.contains(Path(project->path(), "CMakeLists.txt")));
QVERIFY(projectData.cmakeFiles.contains(Path(subDirPath, "CMakeLists.txt")));
QCOMPARE(projectData.lastModifiedProjectData, index.lastModified);
QVERIFY(projectData.lastModifiedCMakeFile.isValid());
QVERIFY(projectData.lastModifiedCMakeFile < projectData.lastModifiedProjectData);
}
};
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment