From e760f2bb5f5abd33939a29f60820e087f022a2e9 Mon Sep 17 00:00:00 2001 From: slava Date: Fri, 5 Jun 2026 14:39:29 +0500 Subject: [PATCH] Optimize database logging, improve thread performance, and implement periodic maintenance to avoid GUI blocking. Replace infinite waits in `AdbUtils` with safe timeouts to prevent thread starvation. --- android/adb/AdbUtils.cpp | 37 +++++++++++++----- database/DatabaseManager.cpp | 48 +++++++++++++++++++++++ database/DatabaseManager.h | 5 +++ main.cpp | 76 +++++++++++++++++++++++++----------- views/MainWindow.cpp | 26 ++++++------ 5 files changed, 148 insertions(+), 44 deletions(-) diff --git a/android/adb/AdbUtils.cpp b/android/adb/AdbUtils.cpp index fe69cc0..3b697bd 100644 --- a/android/adb/AdbUtils.cpp +++ b/android/adb/AdbUtils.cpp @@ -70,6 +70,23 @@ bool AdbUtils::startProcess(const QString &program, const QStringList &args, return true; } +// Ждёт завершения процесса с таймаутом. Раньше здесь стоял бесконечный +// waitForFinished() — если adb-клиент зависал на неотвечающем устройстве/USB, +// рабочий поток блокировался навсегда (накопление зависших потоков → фриз +// приложения через несколько часов). При таймауте убиваем зависший процесс и +// реапим его, чтобы и поток освободился, и деструктор QProcess не блокировался. +// Возвращает true, если процесс завершился сам в отведённое время. +static bool waitOrKill(QProcess &process, int timeoutMs, const char *tag) { + if (process.waitForFinished(timeoutMs)) { + return true; + } + qWarning() << "[AdbUtils]" << tag << "timed out after" << timeoutMs + << "ms — killing wedged adb process"; + process.kill(); + process.waitForFinished(2000); + return false; +} + // adb shell dumpsys package ru.rshb.dbo | grep permission // TODO доделать обработку пермишинов // adb shell pm revoke ru.rshb.dbo android.permission.WRITE_CONTACTS; adb shell pm revoke ru.rshb.dbo android.permission.READ_CONTACTS; adb shell pm revoke ru.rshb.dbo android.permission.ACCESS_COARSE_LOCATION; adb shell pm revoke ru.rshb.dbo android.permission.ACCESS_FINE_LOCATION @@ -147,7 +164,7 @@ QString AdbUtils::tryToGetRunningAppPid(const QString &deviceId, const QString & qWarning() << "[AdbUtils] Failed to start adb pidof:" << process.errorString(); return {}; } - process.waitForFinished(); + waitOrKill(process, 10000, "tryToGetRunningAppPid"); QString pid = process.readAllStandardOutput(); return pid.replace("\n", ""); } @@ -230,7 +247,7 @@ bool AdbUtils::tryToStartApplication(const QString &deviceId, const QString &pac qWarning() << "[AdbUtils] Failed to start adb monkey:" << process.errorString(); return false; } - process.waitForFinished(); + waitOrKill(process, 15000, "tryToStartApplication(monkey)"); const QString stdOut = QString::fromUtf8(process.readAllStandardOutput()); const QString stdErr = QString::fromUtf8(process.readAllStandardError()); const int exitCode = process.exitCode(); @@ -294,7 +311,7 @@ bool AdbUtils::tryToKillApplication(const QString &deviceId, const QString &pack qWarning() << "[AdbUtils] Failed to start adb force-stop:" << process.errorString(); return false; } - process.waitForFinished(); + waitOrKill(process, 15000, "tryToKillApplication"); return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; } @@ -480,7 +497,7 @@ bool AdbUtils::makeTap(const QString &deviceId, const int x, const int y) { qWarning() << "[AdbUtils] Failed to start adb tap:" << process.errorString(); return false; } - process.waitForFinished(); + waitOrKill(process, 10000, "makeTap"); return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; } @@ -595,7 +612,7 @@ bool AdbUtils::inputAdbIMEText(const QString &deviceId, const QString &text) { qWarning() << "[AdbUtils] Failed to start adb broadcast:" << process.errorString(); return false; } - process.waitForFinished(); + waitOrKill(process, 10000, "inputAdbIMEText"); qDebug() << "[AdbUtils] inputAdbIMEText (b64) result:" << process.exitCode() << "text:" << text << "output:" << process.readAllStandardOutput().trimmed() @@ -612,7 +629,7 @@ bool AdbUtils::disableAdbIMEKeyboard(const QString &deviceId) { qWarning() << "[AdbUtils] Failed to start adb ime disable:" << process.errorString(); return false; } - process.waitForFinished(); + waitOrKill(process, 10000, "disableAdbIMEKeyboard"); return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; } @@ -637,7 +654,7 @@ bool AdbUtils::makeSwipe(const QString &deviceId, const int x1, const int y1, co qWarning() << "[AdbUtils] Failed to start adb swipe:" << process.errorString(); return false; } - process.waitForFinished(); + waitOrKill(process, 10000, "makeSwipe"); return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; } @@ -653,7 +670,7 @@ bool AdbUtils::inputText(const QString &deviceId, const QString &text) { qWarning() << "[AdbUtils] Failed to start adb input text:" << process.errorString(); return false; } - process.waitForFinished(); + waitOrKill(process, 10000, "inputText"); return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; } @@ -706,7 +723,7 @@ bool AdbUtils::goBack(const QString &deviceId) { qWarning() << "[AdbUtils] Failed to start adb keyevent:" << process.errorString(); return false; } - process.waitForFinished(); + waitOrKill(process, 10000, "goBack"); return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; } @@ -722,7 +739,7 @@ QSet AdbUtils::getListApps(const QString &deviceId) { qWarning() << "[AdbUtils] Failed to start adb pm list:" << process.errorString(); return {}; } - process.waitForFinished(); + waitOrKill(process, 15000, "getListApps"); const QString output = process.readAllStandardOutput(); QSet apps; diff --git a/database/DatabaseManager.cpp b/database/DatabaseManager.cpp index c4a2e50..3782704 100644 --- a/database/DatabaseManager.cpp +++ b/database/DatabaseManager.cpp @@ -80,6 +80,17 @@ QSqlDatabase DatabaseManager::database() { QSqlQuery pragmaQuery(db); pragmaQuery.exec("PRAGMA foreign_keys = ON"); + // WAL: читатели (например, посекундный refresh GUI через getAllDevices) + // больше не блокируются писателями (screencap-BLOB каждые 4с/устройство, + // события, sync). В прежнем режиме (rollback journal = delete) писатель + // брал эксклюзивную блокировку всего файла и стопорил читателей — под + // нагрузкой 5-10 устройств это давало нарастающие подвисания GUI. + // journal_mode персистентен в файле; busy_timeout/synchronous — пер-конекшн, + // поэтому ставим их на каждом новом соединении. + pragmaQuery.exec("PRAGMA journal_mode = WAL"); + pragmaQuery.exec("PRAGMA busy_timeout = 5000"); + pragmaQuery.exec("PRAGMA synchronous = NORMAL"); + connectionPool[name] = db; // Автоматическое удаление соединения при завершении потока @@ -287,6 +298,43 @@ void DatabaseManager::cleanupOldData(int days) { qDebug() << "[cleanup] VACUUM done"; } +void DatabaseManager::performMaintenance() { + // Оставляем последние N строк general_logs — защита от роста при всплесках + // логирования. Кап по id (а не по timestamp) формат-независим и надёжен. + constexpr int kMaxGeneralLogRows = 5000; + // Старые завершённые события/транзакции держим максимум столько дней. + constexpr int kKeepDays = 2; + + QSqlDatabase db = database(); + QSqlQuery q(db); + + const QString cutoff = QDateTime::currentDateTimeUtc().addDays(-kKeepDays).toString(Qt::ISODate); + + db.transaction(); + + q.prepare("DELETE FROM general_logs WHERE id < " + "(SELECT MAX(id) FROM general_logs) - :keep"); + q.bindValue(":keep", kMaxGeneralLogRows); + if (q.exec() && q.numRowsAffected() > 0) + qDebug() << "[maintenance] general_logs: deleted" << q.numRowsAffected() << "rows (cap" << kMaxGeneralLogRows << ")"; + + q.prepare("DELETE FROM events WHERE timestamp < :cutoff AND status IN ('complete', 'error')"); + q.bindValue(":cutoff", cutoff); + if (q.exec() && q.numRowsAffected() > 0) + qDebug() << "[maintenance] events: deleted" << q.numRowsAffected() << "rows older than" << kKeepDays << "days"; + + q.prepare("DELETE FROM transactions WHERE timestamp < :cutoff"); + q.bindValue(":cutoff", cutoff); + if (q.exec() && q.numRowsAffected() > 0) + qDebug() << "[maintenance] transactions: deleted" << q.numRowsAffected() << "rows older than" << kKeepDays << "days"; + + db.commit(); + + // Подрезаем WAL, чтобы он не рос неограниченно за сессию (TRUNCATE сбрасывает + // файл к нулю после переноса кадров в основную БД). Вне транзакции. + q.exec("PRAGMA wal_checkpoint(TRUNCATE)"); +} + void DatabaseManager::closeConnection() { QMutexLocker locker(&mutex); if (const QString name = connectionName(); connectionPool.contains(name)) { diff --git a/database/DatabaseManager.h b/database/DatabaseManager.h index 2ea86d9..9648486 100644 --- a/database/DatabaseManager.h +++ b/database/DatabaseManager.h @@ -14,6 +14,11 @@ public: QSqlDatabase database(); void initSchema(); void cleanupOldData(int days = 5); + // Лёгкое периодическое обслуживание в течение сессии (вызывать с фонового + // потока по таймеру): кап general_logs по количеству, удаление старых + // завершённых событий/транзакций и подрезка WAL-файла. Без VACUUM — + // он берёт эксклюзивную блокировку и слишком тяжёл для частого вызова. + void performMaintenance(); void closeConnection(); // Путь к файлу БД из config.ini (с раскрытием ~). Единая точка вычисления. diff --git a/main.cpp b/main.cpp index d173348..8211901 100644 --- a/main.cpp +++ b/main.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include "dao/MaterialDAO.h" #include "views/MainWindow.h" @@ -253,7 +254,8 @@ void messageHandler( break; } - const QString source = context.function ? QString("%1:%2").arg(context.function).arg(context.line) : QString(); + // source использовался только для записи в general_logs (отключено ниже). + // const QString source = context.function ? QString("%1:%2").arg(context.function).arg(context.line) : QString(); const char *func = context.function ? context.function : "unknown"; const QString line = QString("%1 [%2] [%3:%4] %5\n") @@ -267,28 +269,35 @@ void messageHandler( } } - // Дублируем в БД (с защитой от рекурсии), пропуская внутренние Qt-сообщения - static const QStringList qtNoisePatterns = { - "killTimer", - "setGeometry", - "QSqlDatabase requires", - "Cannot be used without QCoreApplication", - "QWindowsWindow", - "Timers cannot be stopped from another thread", - }; - bool isQtNoise = false; - for (const QString &pattern : qtNoisePatterns) { - if (msg.contains(pattern)) { isQtNoise = true; break; } - } - - if (dbReady.load() && !isQtNoise) { - static thread_local bool writing = false; - if (!writing) { - writing = true; - GeneralLogDAO::insert(level, source, msg); - writing = false; - } - } + // ОТКЛЮЧЕНО: каждое сообщение лога (DEBUG/INFO/WARNING со всех потоков) писалось + // синхронно в general_logs. Таблица чистится только при старте (cleanupOldData), + // поэтому за сессию росла без ограничений; все вставки/чтения сериализуются на + // одном мьютексе DatabaseManager (общий с EventHandler/DeviceScreener/NetworkService), + // и GUI каждую секунду читает getLogs(). Рост таблицы → растёт время удержания + // мьютекса → GUI-поток голодает → зависание через 5–6 часов работы. + // Логи по-прежнему пишутся в application.log и stderr; явные ошибки идут в БД + // через AppLogger::log (WARNING, низкий объём) для панели ошибок и мониторинга. + // static const QStringList qtNoisePatterns = { + // "killTimer", + // "setGeometry", + // "QSqlDatabase requires", + // "Cannot be used without QCoreApplication", + // "QWindowsWindow", + // "Timers cannot be stopped from another thread", + // }; + // bool isQtNoise = false; + // for (const QString &pattern : qtNoisePatterns) { + // if (msg.contains(pattern)) { isQtNoise = true; break; } + // } + // + // if (dbReady.load() && !isQtNoise) { + // static thread_local bool writing = false; + // if (!writing) { + // writing = true; + // GeneralLogDAO::insert(level, source, msg); + // writing = false; + // } + // } // По желанию — дублировать в консоль const QByteArray localMsg = line.toLocal8Bit(); @@ -385,6 +394,27 @@ int main(int argc, char *argv[]) { DatabaseManager::instance().cleanupOldData(30); dbReady.store(true); + // Периодическое обслуживание БД на фоновом потоке (раз в 10 минут): кап + // general_logs по количеству, удаление старых событий/транзакций и подрезка + // WAL-файла. Держит размер БД/WAL ограниченным в течение долгой сессии и не + // трогает GUI-поток. cleanupOldData() выше — разовая чистка по возрасту при старте. + { + auto *maintenanceThread = new QThread; + auto *maintenanceTimer = new QTimer; + maintenanceTimer->setInterval(10 * 60 * 1000); + maintenanceTimer->moveToThread(maintenanceThread); + QObject::connect(maintenanceThread, &QThread::started, + maintenanceTimer, qOverload<>(&QTimer::start)); + QObject::connect(maintenanceTimer, &QTimer::timeout, maintenanceTimer, []() { + DatabaseManager::instance().performMaintenance(); + }); + QObject::connect(&app, &QCoreApplication::aboutToQuit, maintenanceThread, [maintenanceThread]() { + maintenanceThread->quit(); + maintenanceThread->wait(3000); + }); + maintenanceThread->start(); + } + // 1. Если есть api_key — логинимся и проверяем/создаём desktop // - ошибка сети или сервера [code -1] // - неверный api_key [code 0] — показываем окно ввода diff --git a/views/MainWindow.cpp b/views/MainWindow.cpp index 9e4c557..adee150 100644 --- a/views/MainWindow.cpp +++ b/views/MainWindow.cpp @@ -298,17 +298,21 @@ void MainWindow::createDevicePage() { m_noDevicesLabel->setVisible(devices.isEmpty()); m_scrollArea->setVisible(!devices.isEmpty()); - // Панель логов внизу - m_logList = new QListWidget(container); - m_logList->setMaximumHeight(120); - m_logList->setStyleSheet( - "QListWidget { background-color: #1e1e1e; color: #cccccc; font-family: monospace; font-size: 11px; border-top: 1px solid #444; }" - "QListWidget::item { padding: 1px 4px; }" - ); - m_logList->setSelectionMode(QAbstractItemView::NoSelection); - m_logList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - containerLayout->addWidget(m_logList); - updateLogPanel(); + // ОТКЛЮЧЕНО: панель вывода ошибок на главном экране. Она заполнялась через + // updateLogPanel() каждую секунду (timer 1000ms) запросом getLogs() к general_logs + // + множеством вложенных DAO-запросов на GUI-потоке. m_logList остаётся nullptr, + // поэтому updateLogPanel() сразу выходит (см. `if (!m_logList) return;`) — панель + // не показывается и посекундный запрос к БД больше не выполняется. + // m_logList = new QListWidget(container); + // m_logList->setMaximumHeight(120); + // m_logList->setStyleSheet( + // "QListWidget { background-color: #1e1e1e; color: #cccccc; font-family: monospace; font-size: 11px; border-top: 1px solid #444; }" + // "QListWidget::item { padding: 1px 4px; }" + // ); + // m_logList->setSelectionMode(QAbstractItemView::NoSelection); + // m_logList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + // containerLayout->addWidget(m_logList); + // updateLogPanel(); m_devicesWindow = container; stackedWidget->addWidget(m_devicesWindow);