diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index bd08153797..2d42991bda 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -666,6 +666,7 @@ SET(Editor_CPP_SRCS PythonConsole.cpp PythonConsolePy.cpp PythonDebugger.cpp + PythonTracing.cpp PythonEditor.cpp SyntaxHighlighter.cpp TextEdit.cpp @@ -677,6 +678,7 @@ SET(Editor_HPP_SRCS PythonConsole.h PythonConsolePy.h PythonDebugger.h + PythonTracing.h PythonEditor.h SyntaxHighlighter.h TextEdit.h diff --git a/src/Gui/PreferencePages/DlgSettingsPythonConsole.cpp b/src/Gui/PreferencePages/DlgSettingsPythonConsole.cpp index 6e59fad4ca..3619463805 100644 --- a/src/Gui/PreferencePages/DlgSettingsPythonConsole.cpp +++ b/src/Gui/PreferencePages/DlgSettingsPythonConsole.cpp @@ -44,6 +44,7 @@ void DlgSettingsPythonConsole::saveSettings() ui->PythonWordWrap->onSave(); ui->PythonBlockCursor->onSave(); ui->PythonSaveHistory->onSave(); + ui->ProfilerInterval->onSave(); } void DlgSettingsPythonConsole::loadSettings() @@ -51,6 +52,7 @@ void DlgSettingsPythonConsole::loadSettings() ui->PythonWordWrap->onRestore(); ui->PythonBlockCursor->onRestore(); ui->PythonSaveHistory->onRestore(); + ui->ProfilerInterval->onRestore(); } void DlgSettingsPythonConsole::changeEvent(QEvent *e) diff --git a/src/Gui/PreferencePages/DlgSettingsPythonConsole.ui b/src/Gui/PreferencePages/DlgSettingsPythonConsole.ui index e72f80f3cf..b7f2ac20b6 100644 --- a/src/Gui/PreferencePages/DlgSettingsPythonConsole.ui +++ b/src/Gui/PreferencePages/DlgSettingsPythonConsole.ui @@ -78,6 +78,35 @@ horizontal space in Python console + + + + Python profiler interval (milliseconds): + + + + + + + The interval at which the profiler runs when there's Python code running (to keep the GUI responding). Set to 0 to disable. + + + 0 + + + 5000 + + + 200 + + + ProfilerInterval + + + PythonConsole + + + @@ -102,6 +131,11 @@ horizontal space in Python console QCheckBox
Gui/PrefWidgets.h
+ + Gui::PrefSpinBox + QSpinBox +
Gui/PrefWidgets.h
+
diff --git a/src/Gui/PythonConsole.cpp b/src/Gui/PythonConsole.cpp index f48ff4a530..718e79cf73 100644 --- a/src/Gui/PythonConsole.cpp +++ b/src/Gui/PythonConsole.cpp @@ -32,6 +32,7 @@ # include # include # include +# include # include #endif @@ -40,6 +41,7 @@ #include "PythonConsole.h" #include "PythonConsolePy.h" +#include "PythonTracing.h" #include "Application.h" #include "CallTips.h" #include "FileDialog.h" @@ -116,12 +118,15 @@ struct PythonConsoleP colormap[QLatin1String("Python error")] = Qt::red; } }; + struct InteractiveInterpreterP { - PyObject* interpreter; - PyObject* sysmodule; + PyObject* interpreter{nullptr}; + PyObject* sysmodule{nullptr}; QStringList buffer; + PythonTracing trace; }; + } // namespace Gui InteractiveInterpreter::InteractiveInterpreter() @@ -295,6 +300,16 @@ bool InteractiveInterpreter::runSource(const char* source) const return false; } +bool InteractiveInterpreter::isOccupied() const +{ + return d->trace.isActive(); +} + +bool InteractiveInterpreter::interrupt() const +{ + return d->trace.interrupt(); +} + /* Execute a code object. * * When an exception occurs, a traceback is displayed. @@ -302,6 +317,13 @@ bool InteractiveInterpreter::runSource(const char* source) const */ void InteractiveInterpreter::runCode(PyCodeObject* code) const { + if (isOccupied()) { + return; + } + + d->trace.fetchFromSettings(); + PythonTracingLocker tracelock(d->trace); + Base::PyGILStateLocker lock; PyObject *module, *dict, *presult; /* "exec code in d, d" */ module = PyImport_AddModule("__main__"); /* get module, init python */ @@ -478,6 +500,10 @@ PythonConsole::PythonConsole(QWidget *parent) d->output = d->info; printPrompt(PythonConsole::Complete); loadHistory(); + + flusher = new QTimer(this); + connect(flusher, &QTimer::timeout, this, &PythonConsole::flushOutput); + flusher->start(100); } /** Destroys the object and frees any allocated resources */ @@ -558,6 +584,12 @@ void PythonConsole::keyPressEvent(QKeyEvent * e) QTextCursor cursor = this->textCursor(); QTextCursor inputLineBegin = this->inputBegin(); + if (e->key() == Qt::Key_C && e->modifiers() == Qt::ControlModifier) { + if (d->interpreter->interrupt()) { + return; + } + } + if (!cursorBeyond( cursor, inputLineBegin )) { /** @@ -733,6 +765,15 @@ void PythonConsole::onFlush() printPrompt(PythonConsole::Flush); } +void PythonConsole::flushOutput() +{ + if (d->interpreter->isOccupied()) { + if (d->output.length() > 0 || d->error.length() > 0) { + printPrompt(PythonConsole::Complete); + } + } +} + /** Prints the ps1 prompt (>>> ) for complete and ps2 prompt (... ) for * incomplete commands to the console window. */ @@ -823,6 +864,11 @@ void PythonConsole::runSource(const QString& line) return; } + if (d->interpreter->isOccupied()) { + insertPythonError(QString::fromLatin1("Previous command still running!")); + return; + } + bool incomplete = false; Base::PyGILStateLocker lock; PyObject* default_stdout = PySys_GetObject("stdout"); diff --git a/src/Gui/PythonConsole.h b/src/Gui/PythonConsole.h index 1db269840e..1375334cd5 100644 --- a/src/Gui/PythonConsole.h +++ b/src/Gui/PythonConsole.h @@ -24,6 +24,7 @@ #define GUI_PYTHONCONSOLE_H #include +#include #include "PythonEditor.h" @@ -42,6 +43,9 @@ public: InteractiveInterpreter(); ~InteractiveInterpreter(); + bool isOccupied() const; + bool interrupt() const; + bool push(const char*); int compileCommand(const char*) const; bool hasPendingInput( ) const; @@ -151,6 +155,7 @@ private: void appendOutput(const QString&, int); void loadHistory() const; void saveHistory() const; + void flushOutput(); Q_SIGNALS: void pendingSource( ); @@ -158,13 +163,13 @@ Q_SIGNALS: private: struct PythonConsoleP* d; + PythonConsoleHighlighter* pythonSyntax{nullptr}; + QString *_sourceDrain{nullptr}; + QString _historyFile; + QTimer *flusher{nullptr}; + friend class PythonStdout; friend class PythonStderr; - -private: - PythonConsoleHighlighter* pythonSyntax; - QString *_sourceDrain; - QString _historyFile; }; /** diff --git a/src/Gui/PythonTracing.cpp b/src/Gui/PythonTracing.cpp new file mode 100644 index 0000000000..d128f1d308 --- /dev/null +++ b/src/Gui/PythonTracing.cpp @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +/*************************************************************************** + * Copyright (c) 2023 Werner Mayer * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + **************************************************************************/ + +#include "PreCompiled.h" +#ifndef _PreComp_ +#include +#include +#endif + +#include "PythonTracing.h" +#include + +using namespace Gui; + +struct PythonTracing::Private +{ + bool active{false}; + int timeout{200}; //NOLINT + + // NOLINTBEGIN + static int profilerInterval; + static bool profilerDisabled; + // NOLINTEND +}; + +// NOLINTBEGIN +int PythonTracing::Private::profilerInterval = 200; +bool PythonTracing::Private::profilerDisabled = false; +// NOLINTEND + +PythonTracing::PythonTracing() + : d{std::make_unique()} +{ +} + +PythonTracing::~PythonTracing() = default; + +bool PythonTracing::isActive() const +{ + return d->active; +} + +void PythonTracing::activate() +{ + d->active = true; + setPythonTraceEnabled(true); +} + +void PythonTracing::deactivate() +{ + d->active = false; + setPythonTraceEnabled(false); +} + +void PythonTracing::fetchFromSettings() +{ + const long defaultTimeout = 200; + + auto parameterGroup = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/PythonConsole"); + int interval = static_cast(parameterGroup->GetInt("ProfilerInterval", defaultTimeout)); + setTimeout(interval); +} + +bool PythonTracing::interrupt() const +{ + if (isActive()) { + PyErr_SetInterrupt(); + return true; + } + + return false; +} + +void PythonTracing::setTimeout(int ms) +{ + d->timeout = ms; +} + +int PythonTracing::timeout() const +{ + return d->timeout; +} + +void PythonTracing::setPythonTraceEnabled(bool enabled) const +{ + Py_tracefunc trace = nullptr; + if (enabled && timeout() > 0) { + Private::profilerInterval = timeout(); + trace = &tracer_callback; + } + else { + Private::profilerDisabled = true; + } + + PyEval_SetTrace(trace, nullptr); +} + +/* + * This callback ensures that Qt runs its event loop (i.e. updates the GUI, processes keyboard and + * mouse events, etc.) at least every 200 ms, even when there is long-running Python code on the + * main thread. It is registered as the global trace function of the Python environment. + */ +int PythonTracing::tracer_callback(PyObject *obj, PyFrameObject *frame, int what, PyObject *arg) +{ + Q_UNUSED(obj) + Q_UNUSED(frame) + Q_UNUSED(what) + Q_UNUSED(arg) + + static QTime lastCalledTime = QTime::currentTime(); + QTime currTime = QTime::currentTime(); + + // if previous code object was executed + if (Private::profilerDisabled) { + Private::profilerDisabled = false; + lastCalledTime = currTime; + } + + int ms = lastCalledTime.msecsTo(currTime); + + if (ms >= Private::profilerInterval) { + lastCalledTime = currTime; + QGuiApplication::processEvents(); + } + + return 0; +} + +// ------------------------------------------------------------------------------------------------ + +PythonTracingLocker::PythonTracingLocker(PythonTracing& trace) + : trace{trace} +{ + trace.activate(); +} + +PythonTracingLocker::~PythonTracingLocker() +{ + trace.deactivate(); +} diff --git a/src/Gui/PythonTracing.h b/src/Gui/PythonTracing.h new file mode 100644 index 0000000000..8f09cef301 --- /dev/null +++ b/src/Gui/PythonTracing.h @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +/*************************************************************************** + * Copyright (c) 2023 Werner Mayer * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + **************************************************************************/ + +#ifndef GUI_PYTHONTRACING_H +#define GUI_PYTHONTRACING_H + +#include +#include +#include +#include + + +namespace Gui { + +class GuiExport PythonTracing +{ +public: + PythonTracing(); + ~PythonTracing(); + + PythonTracing(const PythonTracing&) = delete; + PythonTracing(PythonTracing&&) = delete; + PythonTracing& operator = (const PythonTracing&) = delete; + PythonTracing& operator = (PythonTracing&&) = delete; + + /*! + * \brief isActive + * Returns true if the tracing is active, false otherwise. + * \return + */ + bool isActive() const; + /*! + * \brief activate + * Activates the Python tracing. + */ + void activate(); + /*! + * \brief deactivate + * Deactivates the Python tracing. + */ + void deactivate(); + /*! + * \brief fetchFromSettings + * Fetch parameters from user settings. + */ + void fetchFromSettings(); + /*! + * \brief interrupt + * If the tracing is enabled it interrupts the Python interpreter. + * True is returned if the tracing is active, false otherwise. + */ + bool interrupt() const; + /*! + * \brief setTimeout + * Sets the interval after which the Qt event loop will be processed. + */ + void setTimeout(int ms); + /*! + * \brief timeout + * \return the timeout of processing the event loop. + */ + int timeout() const; + +private: + void setPythonTraceEnabled(bool enabled) const; + static int tracer_callback(PyObject *obj, PyFrameObject *frame, int what, PyObject *arg); + +private: + struct Private; + std::unique_ptr d; +}; + +class GuiExport PythonTracingLocker +{ +public: + /*! + * \brief PythonTracingLocker + * Activates the passed tracing object. + * \param trace + */ + explicit PythonTracingLocker(PythonTracing& trace); + /*! + * Deactivates the tracing object. + */ + ~PythonTracingLocker(); + + PythonTracingLocker(const PythonTracingLocker&) = delete; + PythonTracingLocker(PythonTracingLocker&&) = delete; + PythonTracingLocker& operator = (const PythonTracingLocker&) = delete; + PythonTracingLocker& operator = (PythonTracingLocker&&) = delete; + +private: + PythonTracing& trace; +}; + +} // namespace Gui + +#endif // GUI_PYTHONTRACING_H