Skip to content

Commit b372755

Browse files
committed
Load plugins in separate thread
This puts the loading of python plugins in a different thread as they may take some time to load and we want the app to open as fast as possible
1 parent 5d2183f commit b372755

5 files changed

Lines changed: 114 additions & 37 deletions

File tree

src/CodeEditor/PythonEditor.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ PythonEditor::PythonEditor(PythonInterpreter *interpreter)
4545
readSettings();
4646
QCoreApplication::instance()->installEventFilter(this);
4747

48+
actionRun->setEnabled(!interpreter->isExecuting());
49+
4850
connect(this, &PythonEditor::executionCalled, interpreter, &PythonInterpreter::executeCode);
4951
connect(
5052
interpreter, &PythonInterpreter::executionStarted, this, &PythonEditor::executionStarted);

src/PythonInterpreter.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ void PythonInterpreter::executeStatementWithState(const std::string &code,
150150

151151
void PythonInterpreter::executeCode(const std::string &code, QListWidget *output)
152152
{
153+
if (m_isExecuting)
154+
{
155+
return;
156+
}
153157
State tmpState;
154158
executeCodeWithState(code, output, tmpState);
155159
}

src/PythonPlugin.cpp

Lines changed: 65 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,53 @@
2727
#include "Runtime/Runtime.h"
2828
#include "Utilities.h"
2929

30-
#include <QDesktopServices>
31-
#include <QUrl>
32-
3330
#include <pybind11/pytypes.h>
3431

3532
#define slots Q_SLOTS
3633
#define signals Q_SIGNALS
3734
#include <QCoreApplication>
35+
#include <QDesktopServices>
3836
#include <QDialog>
3937
#include <QFile>
4038
#include <QFileDialog>
4139
#include <QMenu>
40+
#include <QRunnable>
4241
#include <QSettings>
42+
#include <QThreadPool>
43+
#include <QUrl>
44+
4345
#include <algorithm>
4446
#include <ccCommandLineInterface.h>
4547

48+
/// Loading Python plugins may be slow,
49+
/// we want cloudcompare to open as fast as possible
50+
/// so we load plugins in a separate thread
51+
class LoadPluginsTask : public QObject, public QRunnable
52+
{
53+
Q_OBJECT
54+
55+
public:
56+
explicit LoadPluginsTask(PythonPluginManager &pluginManager, const QStringList &pluginsPaths)
57+
: m_pluginManager(pluginManager), m_pluginsPaths(pluginsPaths)
58+
{
59+
}
60+
61+
void run() override
62+
{
63+
py::gil_scoped_acquire acquire;
64+
m_pluginManager.loadPlugins(m_pluginsPaths);
65+
Q_EMIT finished();
66+
}
67+
68+
Q_SIGNALS:
69+
void finished();
70+
71+
private:
72+
// References are fine here as this object is short-lived
73+
PythonPluginManager &m_pluginManager;
74+
const QStringList m_pluginsPaths;
75+
};
76+
4677
// Useful link:
4778
// https://docs.python.org/3/c-api/init.html#initialization-finalization-and-threads
4879
PythonPlugin::PythonPlugin(QObject *parent)
@@ -110,6 +141,10 @@ PythonPlugin::PythonPlugin(QObject *parent)
110141
this,
111142
&PythonPlugin::finalizeInterpreter);
112143
}
144+
145+
// The REPL action is created here, as we need it to exist in
146+
// order to enable / disable it depending on circumstances
147+
m_showRepl = new QAction("Show REPL", this);
113148
}
114149

115150
static std::unique_ptr<QSettings> LoadSettings()
@@ -140,14 +175,9 @@ QList<QAction *> PythonPlugin::getActions()
140175
m_showEditor->setEnabled(isPythonProperlyInitialized);
141176
}
142177

143-
if (!m_showRepl)
144-
{
145-
m_showRepl = new QAction("Show REPL", this);
146-
m_showRepl->setToolTip("Show the Python REPL");
147-
m_showRepl->setIcon(QIcon(REPL_ICON_PATH));
148-
connect(m_showRepl, &QAction::triggered, this, &PythonPlugin::showRepl);
149-
m_showRepl->setEnabled(isPythonProperlyInitialized);
150-
}
178+
m_showRepl->setToolTip("Show the Python REPL");
179+
m_showRepl->setIcon(QIcon(REPL_ICON_PATH));
180+
connect(m_showRepl, &QAction::triggered, this, &PythonPlugin::showRepl);
151181

152182
if (!m_showDoc)
153183
{
@@ -497,28 +527,28 @@ void PythonPlugin::setMainAppInterface(ccMainAppInterface *app)
497527
// python plugins, if we did this earlier, `pycc.GetInstance()`
498528
// in python would return `None` and that's bad.
499529

500-
// Start by autodiscovering plugins from metadata
501-
try
502-
{
503-
m_pluginManager.loadPluginsFromEntryPoints();
504-
}
505-
catch (const std::exception &e)
506-
{
507-
ccLog::Warning("[PythonRuntime] Failed to load autodiscovered custom python plugins: %s",
508-
e.what());
509-
}
510-
511-
// In the end, we add plugins from custom paths
512-
try
513-
{
514-
m_pluginManager.loadPluginsFrom(m_settings->pluginsPaths());
515-
}
516-
catch (const std::exception &e)
517-
{
518-
ccLog::Warning("[PythonRuntime] Failed to load custom python plugins : %s", e.what());
519-
}
520-
521-
populatePluginSubMenu();
530+
// We load the plugins in a separate thread to avoid the app from being too slow to start
531+
// However, the thread that will load plugins will need to acquire the GIL,
532+
// which the interpreter on the main thread currently owns.
533+
// So we release it first, and re-acquire it as soon as plugin loading is done
534+
auto *gilReleaser = new py::gil_scoped_release();
535+
// While plugins are loading it's not possible to run any python code
536+
Q_EMIT m_interp.executionStarted();
537+
m_showRepl->setEnabled(false); // repl is slight harder to handle
538+
auto *task = new LoadPluginsTask(m_pluginManager, m_settings->pluginsPaths());
539+
QObject::connect(task,
540+
&LoadPluginsTask::finished,
541+
this,
542+
[gilReleaser, this]()
543+
{
544+
delete gilReleaser;
545+
Q_EMIT m_interp.executionFinished();
546+
this->populatePluginSubMenu();
547+
m_showRepl->setEnabled(true);
548+
plgPrint() << "Plugins loaded, running code is enabled";
549+
});
550+
plgPrint() << "Loading plugins, running code is disabled";
551+
QThreadPool::globalInstance()->start(task);
522552

523553
m_fileRunner->setParent(m_app->getMainWindow(), Qt::Window);
524554
m_actionLauncher->setParent(m_app->getMainWindow(), Qt::Window);
@@ -671,3 +701,5 @@ void PythonPlugin::finalizeInterpreter()
671701
m_pluginManager.unloadPlugins();
672702
m_interp.finalize();
673703
}
704+
705+
#include "PythonPlugin.moc"

src/PythonPluginManager.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,37 @@ const std::vector<Runtime::RegisteredPlugin> &PythonPluginManager::plugins() con
2727
return m_plugins;
2828
}
2929

30+
void PythonPluginManager::loadPlugins(const QStringList &pluginsPaths) noexcept
31+
{
32+
// Start by autodiscovering plugins from metadata
33+
try
34+
{
35+
loadPluginsFromEntryPoints();
36+
}
37+
catch (const std::exception &e)
38+
{
39+
plgWarning() << "Failed to load autodiscovered custom python plugins: " << e.what();
40+
}
41+
catch (...)
42+
{
43+
plgWarning() << "Failed to load autodiscovered custom python plugins";
44+
}
45+
46+
// In the end, we add plugins from custom paths
47+
try
48+
{
49+
loadPluginsFrom(pluginsPaths);
50+
}
51+
catch (const std::exception &e)
52+
{
53+
plgWarning() << "Failed to load custom python plugins: " << e.what();
54+
}
55+
catch (...)
56+
{
57+
plgWarning() << "Failed to load custom python plugins";
58+
}
59+
}
60+
3061
void PythonPluginManager::loadPluginsFromEntryPoints()
3162
{
3263
plgPrint() << "Searching for custom plugins (checking metadata in site-package)";

src/PythonPluginManager.h

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ class PythonPluginManager final
3131
/// Returns the currently loaded plugins
3232
const std::vector<Runtime::RegisteredPlugin> &plugins() const;
3333

34+
/// load plugins
35+
///
36+
/// * First loads plugins from the `cloudcompare.plugins` entry point
37+
/// * Then loads plugins from the given paths
38+
///
39+
/// Errors are logged.
40+
void loadPlugins(const QStringList &pluginsPaths) noexcept;
41+
42+
/// This MUST be called before finalizing the interpreter
43+
void unloadPlugins();
44+
45+
private:
3446
/// Autodiscover plugins based on the package metadata.
3547
/// it uses the entry point mechanism described
3648
/// - In PyPa docs:
@@ -50,10 +62,6 @@ class PythonPluginManager final
5062
/// \param paths where we will look for plugins to load
5163
void loadPluginsFrom(const QStringList &paths);
5264

53-
/// This MUST be called before finalizing the interpreter
54-
void unloadPlugins();
55-
56-
private:
5765
std::vector<Runtime::RegisteredPlugin> m_plugins;
5866
};
5967

0 commit comments

Comments
 (0)