Skip to content

Commit f4e22e5

Browse files
committed
qml: avoid QtConcurrent in debug log model
1 parent e0c41a9 commit f4e22e5

3 files changed

Lines changed: 157 additions & 52 deletions

File tree

qml/bitcoin.cpp

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
#include <vector>
8282

8383
#include <QApplication>
84+
#include <QCoreApplication>
8485
#include <QDebug>
8586
#include <QEventLoop>
8687
#include <QFontDatabase>
@@ -557,6 +558,7 @@ int QmlGuiMain(int argc, char* argv[])
557558
node_model.addStartupWarnings(startup_warnings);
558559
QmlInitExecutor init_executor{*node};
559560
bool shutdown_requested{false};
561+
DebugLogModel debug_log_model{gArgs.GetDataDirNet() / "debug.log"};
560562
#ifdef ENABLE_WALLET
561563
std::unique_ptr<WalletQmlController> wallet_controller;
562564
if (wallet_enabled) {
@@ -581,11 +583,12 @@ int QmlGuiMain(int argc, char* argv[])
581583
wallet_controller->unloadWallets();
582584
}
583585
#endif
584-
node->startShutdown();
585586
init_executor.shutdown();
586587
});
587588
QObject::connect(&init_executor, &QmlInitExecutor::initializeResult, &node_model, &NodeModel::initializeResult);
588-
QObject::connect(&init_executor, &QmlInitExecutor::shutdownResult, qGuiApp, &QGuiApplication::quit, Qt::QueuedConnection);
589+
QObject::connect(&init_executor, &QmlInitExecutor::shutdownResult, qGuiApp, [] {
590+
QCoreApplication::exit(0);
591+
}, Qt::QueuedConnection);
589592
QObject::connect(&init_executor, &QmlInitExecutor::runawayException, &node_model, &NodeModel::handleRunawayException);
590593

591594
NetworkTrafficTower network_traffic_tower{node_model};
@@ -630,28 +633,26 @@ int QmlGuiMain(int argc, char* argv[])
630633
QObject::connect(&node_model, &NodeModel::nodeInitialized,
631634
&ban_list_model, &BanListModel::refresh);
632635

633-
QQmlApplicationEngine engine;
636+
auto engine = std::make_unique<QQmlApplicationEngine>();
634637

635638
QScopedPointer<const NetworkStyle> network_style{NetworkStyle::instantiate(Params().GetChainType())};
636639
assert(!network_style.isNull());
637-
engine.addImageProvider(QStringLiteral("images"), new ImageProvider{network_style.data()});
638-
engine.addImageProvider(QStringLiteral("qr"), new QRImageProvider);
639-
640-
engine.rootContext()->setContextProperty("networkTrafficTower", &network_traffic_tower);
641-
engine.rootContext()->setContextProperty("networkStatusModel", &network_status_model);
642-
engine.rootContext()->setContextProperty("nodeModel", &node_model);
643-
engine.rootContext()->setContextProperty("chainModel", &chain_model);
644-
engine.rootContext()->setContextProperty("peerTableModel", &peer_model);
645-
engine.rootContext()->setContextProperty("peerListModelProxy", &peer_model_sort_proxy);
646-
engine.rootContext()->setContextProperty("banListModel", &ban_list_model);
647-
648-
DebugLogModel debug_log_model{gArgs.GetDataDirNet() / "debug.log"};
649-
engine.rootContext()->setContextProperty("debugLogModel", &debug_log_model);
640+
engine->addImageProvider(QStringLiteral("images"), new ImageProvider{network_style.data()});
641+
engine->addImageProvider(QStringLiteral("qr"), new QRImageProvider);
642+
643+
engine->rootContext()->setContextProperty("networkTrafficTower", &network_traffic_tower);
644+
engine->rootContext()->setContextProperty("networkStatusModel", &network_status_model);
645+
engine->rootContext()->setContextProperty("nodeModel", &node_model);
646+
engine->rootContext()->setContextProperty("chainModel", &chain_model);
647+
engine->rootContext()->setContextProperty("peerTableModel", &peer_model);
648+
engine->rootContext()->setContextProperty("peerListModelProxy", &peer_model_sort_proxy);
649+
engine->rootContext()->setContextProperty("banListModel", &ban_list_model);
650+
engine->rootContext()->setContextProperty("debugLogModel", &debug_log_model);
650651

651652
RpcConsoleModel rpc_console_model{*node};
652653
QObject::connect(&node_model, &NodeModel::nodeInitialized,
653654
&rpc_console_model, &RpcConsoleModel::onNodeInitialized);
654-
engine.rootContext()->setContextProperty("rpcConsoleModel", &rpc_console_model);
655+
engine->rootContext()->setContextProperty("rpcConsoleModel", &rpc_console_model);
655656

656657
#ifdef ENABLE_WALLET
657658
std::unique_ptr<WalletListModel> wallet_list_model;
@@ -679,17 +680,17 @@ int QmlGuiMain(int argc, char* argv[])
679680
list_model->listWalletDir();
680681
}
681682
});
682-
engine.rootContext()->setContextProperty("walletController", wallet_controller.get());
683-
engine.rootContext()->setContextProperty("walletListModel", wallet_list_model.get());
683+
engine->rootContext()->setContextProperty("walletController", wallet_controller.get());
684+
engine->rootContext()->setContextProperty("walletListModel", wallet_list_model.get());
684685
}
685686
#endif
686687

687688
OptionsQmlModel options_model(*node);
688-
engine.rootContext()->setContextProperty("optionsModel", &options_model);
689+
engine->rootContext()->setContextProperty("optionsModel", &options_model);
689690
#ifdef ENABLE_TEST_AUTOMATION
690-
engine.rootContext()->setContextProperty("testAutomationEnabled", true);
691+
engine->rootContext()->setContextProperty("testAutomationEnabled", true);
691692
#else
692-
engine.rootContext()->setContextProperty("testAutomationEnabled", false);
693+
engine->rootContext()->setContextProperty("testAutomationEnabled", false);
693694
#endif
694695
// Install language before QML engine loads so that all qsTr() calls in QML
695696
// pick up the correct locale from the start.
@@ -698,7 +699,7 @@ int QmlGuiMain(int argc, char* argv[])
698699
// Retranslate the QML UI immediately when the user picks a new language.
699700
QObject::connect(&options_model, &OptionsQmlModel::languageChanged, [&]() {
700701
install_language(options_model.language());
701-
engine.retranslate();
702+
engine->retranslate();
702703
});
703704

704705
desktop_tray_icon_controller.setBasePixmap(
@@ -711,20 +712,20 @@ int QmlGuiMain(int argc, char* argv[])
711712
[&desktop_window_behavior_model](bool supported) {
712713
if (!supported) desktop_window_behavior_model.setShowTrayIcon(false);
713714
});
714-
engine.rootContext()->setContextProperty("desktopWindowBehaviorModel", &desktop_window_behavior_model);
715-
engine.rootContext()->setContextProperty("desktopTrayIconController", &desktop_tray_icon_controller);
715+
engine->rootContext()->setContextProperty("desktopWindowBehaviorModel", &desktop_window_behavior_model);
716+
engine->rootContext()->setContextProperty("desktopTrayIconController", &desktop_tray_icon_controller);
716717

717-
engine.setInitialProperties({
718+
engine->setInitialProperties({
718719
{QStringLiteral("walletAvailableForUi"), wallet_enabled},
719720
{QStringLiteral("appModeDesktopForUi"), app_mode.mode() == AppMode::DESKTOP},
720721
{QStringLiteral("preInitOnboardingRanForUi"), pre_init_onboarding_status == PreInitOnboardingStatus::COMPLETED},
721722
});
722-
engine.load(QUrl(QStringLiteral("qrc:///qml/pages/MainWindow.qml")));
723-
if (engine.rootObjects().isEmpty()) {
723+
engine->load(QUrl(QStringLiteral("qrc:///qml/pages/MainWindow.qml")));
724+
if (engine->rootObjects().isEmpty()) {
724725
return EXIT_FAILURE;
725726
}
726727

727-
auto window = qobject_cast<QQuickWindow*>(engine.rootObjects().first());
728+
auto window = qobject_cast<QQuickWindow*>(engine->rootObjects().first());
728729
if (!window) {
729730
return EXIT_FAILURE;
730731
}
@@ -743,7 +744,7 @@ int QmlGuiMain(int argc, char* argv[])
743744
socket_path = QString::fromStdString(
744745
(gArgs.GetDataDirNet() / "test_bridge.sock").utf8string());
745746
}
746-
test_bridge = std::make_unique<TestBridge>(&engine, socket_path);
747+
test_bridge = std::make_unique<TestBridge>(engine.get(), socket_path);
747748
}
748749
#endif
749750

@@ -753,5 +754,10 @@ int QmlGuiMain(int argc, char* argv[])
753754
qInfo() << "Graphics API in use:" << QmlUtil::GraphicsApi(window);
754755

755756
node_model.startShutdownPolling();
756-
return qGuiApp->exec();
757+
const int exit_code{qGuiApp->exec()};
758+
#ifdef ENABLE_TEST_AUTOMATION
759+
test_bridge.reset();
760+
#endif
761+
engine.reset();
762+
return exit_code;
757763
}

qml/models/debuglogmodel.cpp

Lines changed: 102 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,21 @@
44

55
#include <qml/models/debuglogmodel.h>
66

7+
#include <util/threadnames.h>
8+
79
#include <algorithm>
10+
#include <utility>
811

912
#include <QDateTime>
1013
#include <QDesktopServices>
1114
#include <QFile>
12-
#include <QFutureWatcher>
15+
#include <QMetaObject>
16+
#include <QObject>
1317
#include <QRegularExpression>
1418
#include <QTextStream>
19+
#include <QThread>
20+
#include <QTimer>
1521
#include <QUrl>
16-
#include <QtConcurrent/QtConcurrentRun>
1722

1823
static const QRegularExpression TIMESTAMP_RX(
1924
QStringLiteral(R"(^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)\s*(.*)$)"));
@@ -24,13 +29,30 @@ DebugLogModel::DebugLogModel(const fs::path& log_path, QObject* parent)
2429
: QAbstractListModel(parent)
2530
, m_log_path(log_path)
2631
{
32+
m_reader = new QObject;
33+
m_reader_thread = new QThread(this);
34+
m_reader->moveToThread(m_reader_thread);
35+
connect(m_reader_thread, &QThread::finished, m_reader, &QObject::deleteLater);
36+
m_reader_thread->start();
37+
QTimer::singleShot(0, m_reader, [] {
38+
util::ThreadRename("qml-debuglog");
39+
});
40+
2741
m_debounce.setSingleShot(true);
2842
m_debounce.setInterval(500);
2943
connect(&m_debounce, &QTimer::timeout, this, [this]() { refresh(); });
3044

3145
connectFileWatcher();
3246
}
3347

48+
DebugLogModel::~DebugLogModel()
49+
{
50+
stop();
51+
if (m_reader_thread) {
52+
m_reader_thread->wait();
53+
}
54+
}
55+
3456
int DebugLogModel::rowCount(const QModelIndex& parent) const
3557
{
3658
if (parent.isValid()) return 0;
@@ -85,9 +107,11 @@ void DebugLogModel::setFilter(const QString& filter)
85107

86108
void DebugLogModel::refresh(bool full_load)
87109
{
110+
if (m_stopping) return;
111+
88112
// Single-read-in-flight guard. If a read is already running, fold this
89113
// request into a trailing re-run rather than piling another job onto the
90-
// thread pool. A burst of watcher events on a noisy node therefore
114+
// worker thread. A burst of watcher events on a noisy node therefore
91115
// collapses to at most two reads: the one in flight, plus one trailer
92116
// that sees the final file state.
93117
if (m_read_in_flight) {
@@ -106,14 +130,32 @@ void DebugLogModel::refresh(bool full_load)
106130
const fs::path path = m_log_path;
107131
const int load_limit = m_load_limit;
108132

109-
auto* watcher = new QFutureWatcher<ReadResult>(this);
110-
connect(watcher, &QFutureWatcher<ReadResult>::finished, this,
111-
[this, watcher, prev_top_identity, full_load]() {
112-
onReadCompleted(watcher->result(), prev_top_identity, full_load);
113-
watcher->deleteLater();
114-
});
115-
watcher->setFuture(QtConcurrent::run(&DebugLogModel::ReadAndFilter,
116-
path, load_limit, full_load));
133+
if (!m_reader || !m_reader_thread || !m_reader_thread->isRunning()) {
134+
m_read_in_flight = false;
135+
return;
136+
}
137+
138+
const bool queued = QMetaObject::invokeMethod(m_reader,
139+
[this, path, load_limit, full_load, prev_top_identity]() mutable {
140+
if (m_read_cancelled.load(std::memory_order_relaxed)) return;
141+
142+
ReadResult result = ReadAndFilter(path, load_limit, full_load, m_read_cancelled);
143+
if (m_read_cancelled.load(std::memory_order_relaxed)) return;
144+
145+
QMetaObject::invokeMethod(this,
146+
[this,
147+
result = std::move(result),
148+
prev_top_identity,
149+
full_load]() mutable {
150+
if (m_stopping || m_read_cancelled.load(std::memory_order_relaxed)) return;
151+
onReadCompleted(result, prev_top_identity, full_load);
152+
},
153+
Qt::QueuedConnection);
154+
},
155+
Qt::QueuedConnection);
156+
if (!queued) {
157+
m_read_in_flight = false;
158+
}
117159
}
118160

119161
void DebugLogModel::loadMore()
@@ -180,13 +222,41 @@ void DebugLogModel::updateRelativeTimes()
180222
}
181223
}
182224

225+
void DebugLogModel::stop()
226+
{
227+
if (m_stopping) return;
228+
229+
m_stopping = true;
230+
m_read_cancelled.store(true, std::memory_order_relaxed);
231+
m_debounce.stop();
232+
233+
const auto watched_files = m_watcher.files();
234+
if (!watched_files.isEmpty()) {
235+
m_watcher.removePaths(watched_files);
236+
}
237+
238+
m_refresh_pending = false;
239+
m_pending_full_load = false;
240+
m_read_in_flight = false;
241+
242+
if (m_reader_thread) {
243+
m_reader_thread->quit();
244+
if (QThread::currentThread() != m_reader_thread) {
245+
m_reader_thread->wait();
246+
}
247+
}
248+
}
249+
183250
// ── Private ──────────────────────────────────────────────────────────────────
184251

185252
DebugLogModel::ReadResult DebugLogModel::ReadAndFilter(const fs::path& log_path,
186253
int load_limit,
187-
bool full_load)
254+
bool full_load,
255+
const std::atomic_bool& cancelled)
188256
{
189257
ReadResult result;
258+
if (cancelled.load(std::memory_order_relaxed)) return result;
259+
190260
const QString path_str = QString::fromStdString(log_path.utf8string());
191261
QFile probe(path_str);
192262
if (!probe.open(QIODevice::ReadOnly | QIODevice::Text)) {
@@ -204,9 +274,14 @@ DebugLogModel::ReadResult DebugLogModel::ReadAndFilter(const fs::path& log_path,
204274
QList<LogLine> filtered;
205275
int fetch_size = load_limit;
206276
while (true) {
207-
const QList<LogLine> raw = ReadRawLines(log_path, fetch_size);
277+
if (cancelled.load(std::memory_order_relaxed)) return {};
278+
279+
const QList<LogLine> raw = ReadRawLines(log_path, fetch_size, cancelled);
280+
if (cancelled.load(std::memory_order_relaxed)) return {};
281+
208282
filtered.clear();
209283
for (const LogLine& l : raw) {
284+
if (cancelled.load(std::memory_order_relaxed)) return {};
210285
if (!l.content.trimmed().isEmpty() || l.timestamp_ms >= 0)
211286
filtered.append(l);
212287
}
@@ -219,8 +294,11 @@ DebugLogModel::ReadResult DebugLogModel::ReadAndFilter(const fs::path& log_path,
219294
}
220295

221296
QList<DebugLogModel::LogLine> DebugLogModel::ReadRawLines(const fs::path& log_path,
222-
int max_lines)
297+
int max_lines,
298+
const std::atomic_bool& cancelled)
223299
{
300+
if (cancelled.load(std::memory_order_relaxed)) return {};
301+
224302
const QString path_str = QString::fromStdString(log_path.utf8string());
225303
QFile file(path_str);
226304
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return {};
@@ -236,14 +314,19 @@ QList<DebugLogModel::LogLine> DebugLogModel::ReadRawLines(const fs::path& log_pa
236314

237315
QStringList raw;
238316
raw.reserve(max_lines);
239-
while (!in.atEnd()) raw.append(in.readLine());
317+
while (!in.atEnd()) {
318+
if (cancelled.load(std::memory_order_relaxed)) return {};
319+
raw.append(in.readLine());
320+
}
240321
if (raw.size() > max_lines)
241322
raw = raw.mid(raw.size() - max_lines);
242323

243324
const qint64 now_ms = QDateTime::currentMSecsSinceEpoch();
244325
QList<LogLine> result;
245326
result.reserve(raw.size());
246327
for (const QString& line : raw) {
328+
if (cancelled.load(std::memory_order_relaxed)) return {};
329+
247330
LogLine entry;
248331
QString raw_message;
249332
const QRegularExpressionMatch m = TIMESTAMP_RX.match(line);
@@ -268,6 +351,8 @@ void DebugLogModel::onReadCompleted(const ReadResult& result,
268351
const QString& prev_top_identity,
269352
bool full_load)
270353
{
354+
if (m_stopping) return;
355+
271356
// Propagate open-error state from the background read.
272357
if (!result.file_opened) {
273358
if (m_open_error != result.error_message) {
@@ -362,7 +447,7 @@ void DebugLogModel::onReadCompleted(const ReadResult& result,
362447

363448
m_read_in_flight = false;
364449
// If changes arrived while we were reading, run one trailing refresh.
365-
if (m_refresh_pending) {
450+
if (!m_stopping && m_refresh_pending) {
366451
m_refresh_pending = false;
367452
const bool do_full = m_pending_full_load;
368453
m_pending_full_load = false;
@@ -377,6 +462,7 @@ void DebugLogModel::connectFileWatcher()
377462
m_watcher.addPath(path_str);
378463
connect(&m_watcher, &QFileSystemWatcher::fileChanged,
379464
this, [this](const QString& path) {
465+
if (m_stopping) return;
380466
m_watcher.addPath(path); // re-add in case of log rotation
381467
m_debounce.start();
382468
});

0 commit comments

Comments
 (0)