diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 014273dec..4c1ea84db 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -118,6 +118,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/cli_export.h ${QET_DIR}/sources/logging/crashhandler.cpp ${QET_DIR}/sources/logging/crashhandler.h + ${QET_DIR}/sources/logging/eventloopwatchdog.cpp + ${QET_DIR}/sources/logging/eventloopwatchdog.h ${QET_DIR}/sources/logging/logring.cpp ${QET_DIR}/sources/logging/logring.h ${QET_DIR}/sources/logging/qetlogger.cpp diff --git a/sources/logging/eventloopwatchdog.cpp b/sources/logging/eventloopwatchdog.cpp new file mode 100644 index 000000000..8d75c09a4 --- /dev/null +++ b/sources/logging/eventloopwatchdog.cpp @@ -0,0 +1,58 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech 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 QElectroTech. If not, see . +*/ +#include "eventloopwatchdog.h" + +#include +#include + +EventLoopWatchdog::EventLoopWatchdog(QObject *parent) : + QObject(parent) +{ + m_disabled = QProcessEnvironment::systemEnvironment() + .value(QStringLiteral("QET_WATCHDOG_DISABLE")) == QStringLiteral("1"); + + // Precise, not the default Coarse: Coarse explicitly trades timing + // accuracy for power/scheduling efficiency (platform-dependent, but + // commonly +/- a double-digit percentage), which would show up as + // noise indistinguishable from a real stall in exactly the + // measurement this class exists to make trustworthy. + m_timer.setTimerType(Qt::PreciseTimer); + connect(&m_timer, &QTimer::timeout, this, &EventLoopWatchdog::tick); +} + +void EventLoopWatchdog::start() +{ + if (m_disabled) + return; + + m_elapsed.start(); + m_timer.start(kTickIntervalMs); +} + +void EventLoopWatchdog::tick() +{ + // restart() returns the elapsed time and resets the clock in one + // call, so this tick's own cost is never counted against the next. + const qint64 actual_ms = m_elapsed.restart(); + + if (actual_ms > kStallThresholdMs) { + qWarning() << "EventLoopWatchdog: main thread stalled for" + << actual_ms << "ms (expected a tick every" + << kTickIntervalMs << "ms)"; + } +} diff --git a/sources/logging/eventloopwatchdog.h b/sources/logging/eventloopwatchdog.h new file mode 100644 index 000000000..4a0b43b47 --- /dev/null +++ b/sources/logging/eventloopwatchdog.h @@ -0,0 +1,92 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech 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 QElectroTech. If not, see . +*/ +#ifndef EVENTLOOPWATCHDOG_H +#define EVENTLOOPWATCHDOG_H + +#include +#include +#include + +/** + @brief The EventLoopWatchdog class + Detects when the main (GUI) thread's event loop goes unresponsive -- + QetLogger (discussion #644) can only see what an explicit qDebug()/ + qInfo()/qWarning() call already decided to report, and most of a + session (painting, dragging, a slow synchronous operation) produces + no log output at all, so a silent multi-second gap in the log is + indistinguishable from the user simply not doing anything. + + This closes that gap the direct way: a repeating QTimer::PreciseTimer + ticks on a short, fixed interval; each tick measures the *actual* + wall-clock time elapsed since the previous one via QElapsedTimer + (monotonic -- unaffected by system clock/NTP adjustments, unlike + QDateTime). Qt does not queue up missed fires for a normal repeating + timer, so if the event loop is blocked for 600ms, the timer fires + once as soon as the loop frees up, with ~600ms measured since the + last tick -- that gap *is* the stall, measured at its source rather + than inferred from log silence. + + Only fires a qWarning() (and so only touches the log at all) when a + tick is late by more than kStallThresholdMs, to stay within the + spirit of QetLogger's bounded-log design (see its class comment) -- + a healthy session should produce zero output from this class. This + tells you *that* a stall happened and *how long* it was, not what + caused it; pair a reported timestamp with `docker exec`+gdb the way + the CLI hang (PR #661) was diagnosed to go from "it lagged" to a + root cause. + + Escape hatch: if QET_WATCHDOG_DISABLE=1 is set in the environment at + construction time, start() does nothing. +*/ +class EventLoopWatchdog : public QObject +{ + Q_OBJECT + + public: + /// How often the watchdog checks in. Small enough to bound the + /// measurement's own granularity, large enough that the tick + /// itself is negligible overhead on the event loop it's watching. + static constexpr int kTickIntervalMs = 50; + + /// A tick arriving later than this many ms after the previous one + /// is logged as a stall. Comfortably above kTickIntervalMs so + /// ordinary OS scheduling noise doesn't produce a warning on every + /// tick, and in the range a user would actually notice as lag. + static constexpr int kStallThresholdMs = 200; + + explicit EventLoopWatchdog(QObject *parent = nullptr); + + /// Starts ticking. Must be called from the main thread, after the + /// event loop it watches is about to run (i.e. immediately before + /// QApplication::exec()) -- constructing this class earlier is + /// harmless, but start() before there is an event loop to tick + /// against would just measure the time until app.exec() is + /// reached. No-op if QET_WATCHDOG_DISABLE=1 was set at + /// construction time. + void start(); + + private slots: + void tick(); + + private: + QTimer m_timer; + QElapsedTimer m_elapsed; + bool m_disabled = false; +}; + +#endif // EVENTLOOPWATCHDOG_H diff --git a/sources/main.cpp b/sources/main.cpp index 1cce54e8e..4932b17b9 100644 --- a/sources/main.cpp +++ b/sources/main.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #include "cli_export.h" +#include "logging/eventloopwatchdog.h" #include "logging/qetlogger.h" #include "machine_info.h" #include "qet.h" @@ -209,6 +210,13 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto QetLogger::instance().pruneOldLogFiles(7); MachineInfo::instance()->send_info_to_debug(); }); + + // Constructed here rather than earlier: start() measures ticks against + // the event loop app.exec() is about to run, so there is no point + // (and no accurate baseline) before this line. + EventLoopWatchdog watchdog; + watchdog.start(); + return app.exec(); }