1
0
forked from BRT/arc

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.

This commit is contained in:
slava 2026-06-05 14:39:29 +05:00
parent cfc562eae6
commit e760f2bb5f
5 changed files with 148 additions and 44 deletions

View File

@ -70,6 +70,23 @@ bool AdbUtils::startProcess(const QString &program, const QStringList &args,
return true; 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 // adb shell dumpsys package ru.rshb.dbo | grep permission
// TODO доделать обработку пермишинов // 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 // 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(); qWarning() << "[AdbUtils] Failed to start adb pidof:" << process.errorString();
return {}; return {};
} }
process.waitForFinished(); waitOrKill(process, 10000, "tryToGetRunningAppPid");
QString pid = process.readAllStandardOutput(); QString pid = process.readAllStandardOutput();
return pid.replace("\n", ""); 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(); qWarning() << "[AdbUtils] Failed to start adb monkey:" << process.errorString();
return false; return false;
} }
process.waitForFinished(); waitOrKill(process, 15000, "tryToStartApplication(monkey)");
const QString stdOut = QString::fromUtf8(process.readAllStandardOutput()); const QString stdOut = QString::fromUtf8(process.readAllStandardOutput());
const QString stdErr = QString::fromUtf8(process.readAllStandardError()); const QString stdErr = QString::fromUtf8(process.readAllStandardError());
const int exitCode = process.exitCode(); 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(); qWarning() << "[AdbUtils] Failed to start adb force-stop:" << process.errorString();
return false; return false;
} }
process.waitForFinished(); waitOrKill(process, 15000, "tryToKillApplication");
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; 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(); qWarning() << "[AdbUtils] Failed to start adb tap:" << process.errorString();
return false; return false;
} }
process.waitForFinished(); waitOrKill(process, 10000, "makeTap");
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; 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(); qWarning() << "[AdbUtils] Failed to start adb broadcast:" << process.errorString();
return false; return false;
} }
process.waitForFinished(); waitOrKill(process, 10000, "inputAdbIMEText");
qDebug() << "[AdbUtils] inputAdbIMEText (b64) result:" << process.exitCode() qDebug() << "[AdbUtils] inputAdbIMEText (b64) result:" << process.exitCode()
<< "text:" << text << "text:" << text
<< "output:" << process.readAllStandardOutput().trimmed() << "output:" << process.readAllStandardOutput().trimmed()
@ -612,7 +629,7 @@ bool AdbUtils::disableAdbIMEKeyboard(const QString &deviceId) {
qWarning() << "[AdbUtils] Failed to start adb ime disable:" << process.errorString(); qWarning() << "[AdbUtils] Failed to start adb ime disable:" << process.errorString();
return false; return false;
} }
process.waitForFinished(); waitOrKill(process, 10000, "disableAdbIMEKeyboard");
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; 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(); qWarning() << "[AdbUtils] Failed to start adb swipe:" << process.errorString();
return false; return false;
} }
process.waitForFinished(); waitOrKill(process, 10000, "makeSwipe");
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; 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(); qWarning() << "[AdbUtils] Failed to start adb input text:" << process.errorString();
return false; return false;
} }
process.waitForFinished(); waitOrKill(process, 10000, "inputText");
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; 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(); qWarning() << "[AdbUtils] Failed to start adb keyevent:" << process.errorString();
return false; return false;
} }
process.waitForFinished(); waitOrKill(process, 10000, "goBack");
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
} }
@ -722,7 +739,7 @@ QSet<QString> AdbUtils::getListApps(const QString &deviceId) {
qWarning() << "[AdbUtils] Failed to start adb pm list:" << process.errorString(); qWarning() << "[AdbUtils] Failed to start adb pm list:" << process.errorString();
return {}; return {};
} }
process.waitForFinished(); waitOrKill(process, 15000, "getListApps");
const QString output = process.readAllStandardOutput(); const QString output = process.readAllStandardOutput();
QSet<QString> apps; QSet<QString> apps;

View File

@ -80,6 +80,17 @@ QSqlDatabase DatabaseManager::database() {
QSqlQuery pragmaQuery(db); QSqlQuery pragmaQuery(db);
pragmaQuery.exec("PRAGMA foreign_keys = ON"); 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; connectionPool[name] = db;
// Автоматическое удаление соединения при завершении потока // Автоматическое удаление соединения при завершении потока
@ -287,6 +298,43 @@ void DatabaseManager::cleanupOldData(int days) {
qDebug() << "[cleanup] VACUUM done"; 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() { void DatabaseManager::closeConnection() {
QMutexLocker<QRecursiveMutex> locker(&mutex); QMutexLocker<QRecursiveMutex> locker(&mutex);
if (const QString name = connectionName(); connectionPool.contains(name)) { if (const QString name = connectionName(); connectionPool.contains(name)) {

View File

@ -14,6 +14,11 @@ public:
QSqlDatabase database(); QSqlDatabase database();
void initSchema(); void initSchema();
void cleanupOldData(int days = 5); void cleanupOldData(int days = 5);
// Лёгкое периодическое обслуживание в течение сессии (вызывать с фонового
// потока по таймеру): кап general_logs по количеству, удаление старых
// завершённых событий/транзакций и подрезка WAL-файла. Без VACUUM —
// он берёт эксклюзивную блокировку и слишком тяжёл для частого вызова.
void performMaintenance();
void closeConnection(); void closeConnection();
// Путь к файлу БД из config.ini (с раскрытием ~). Единая точка вычисления. // Путь к файлу БД из config.ini (с раскрытием ~). Единая точка вычисления.

View File

@ -1,6 +1,7 @@
#include <QApplication> #include <QApplication>
#include <QSslSocket> #include <QSslSocket>
#include <QThread> #include <QThread>
#include <QTimer>
#include "dao/MaterialDAO.h" #include "dao/MaterialDAO.h"
#include "views/MainWindow.h" #include "views/MainWindow.h"
@ -253,7 +254,8 @@ void messageHandler(
break; 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 char *func = context.function ? context.function : "unknown";
const QString line = QString("%1 [%2] [%3:%4] %5\n") const QString line = QString("%1 [%2] [%3:%4] %5\n")
@ -267,28 +269,35 @@ void messageHandler(
} }
} }
// Дублируем в БД (с защитой от рекурсии), пропуская внутренние Qt-сообщения // ОТКЛЮЧЕНО: каждое сообщение лога (DEBUG/INFO/WARNING со всех потоков) писалось
static const QStringList qtNoisePatterns = { // синхронно в general_logs. Таблица чистится только при старте (cleanupOldData),
"killTimer", // поэтому за сессию росла без ограничений; все вставки/чтения сериализуются на
"setGeometry", // одном мьютексе DatabaseManager (общий с EventHandler/DeviceScreener/NetworkService),
"QSqlDatabase requires", // и GUI каждую секунду читает getLogs(). Рост таблицы → растёт время удержания
"Cannot be used without QCoreApplication", // мьютекса → GUI-поток голодает → зависание через 56 часов работы.
"QWindowsWindow", // Логи по-прежнему пишутся в application.log и stderr; явные ошибки идут в БД
"Timers cannot be stopped from another thread", // через AppLogger::log (WARNING, низкий объём) для панели ошибок и мониторинга.
}; // static const QStringList qtNoisePatterns = {
bool isQtNoise = false; // "killTimer",
for (const QString &pattern : qtNoisePatterns) { // "setGeometry",
if (msg.contains(pattern)) { isQtNoise = true; break; } // "QSqlDatabase requires",
} // "Cannot be used without QCoreApplication",
// "QWindowsWindow",
if (dbReady.load() && !isQtNoise) { // "Timers cannot be stopped from another thread",
static thread_local bool writing = false; // };
if (!writing) { // bool isQtNoise = false;
writing = true; // for (const QString &pattern : qtNoisePatterns) {
GeneralLogDAO::insert(level, source, msg); // if (msg.contains(pattern)) { isQtNoise = true; break; }
writing = false; // }
} //
} // 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(); const QByteArray localMsg = line.toLocal8Bit();
@ -385,6 +394,27 @@ int main(int argc, char *argv[]) {
DatabaseManager::instance().cleanupOldData(30); DatabaseManager::instance().cleanupOldData(30);
dbReady.store(true); 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 // 1. Если есть api_key — логинимся и проверяем/создаём desktop
// - ошибка сети или сервера [code -1] // - ошибка сети или сервера [code -1]
// - неверный api_key [code 0] — показываем окно ввода // - неверный api_key [code 0] — показываем окно ввода

View File

@ -298,17 +298,21 @@ void MainWindow::createDevicePage() {
m_noDevicesLabel->setVisible(devices.isEmpty()); m_noDevicesLabel->setVisible(devices.isEmpty());
m_scrollArea->setVisible(!devices.isEmpty()); m_scrollArea->setVisible(!devices.isEmpty());
// Панель логов внизу // ОТКЛЮЧЕНО: панель вывода ошибок на главном экране. Она заполнялась через
m_logList = new QListWidget(container); // updateLogPanel() каждую секунду (timer 1000ms) запросом getLogs() к general_logs
m_logList->setMaximumHeight(120); // + множеством вложенных DAO-запросов на GUI-потоке. m_logList остаётся nullptr,
m_logList->setStyleSheet( // поэтому updateLogPanel() сразу выходит (см. `if (!m_logList) return;`) — панель
"QListWidget { background-color: #1e1e1e; color: #cccccc; font-family: monospace; font-size: 11px; border-top: 1px solid #444; }" // не показывается и посекундный запрос к БД больше не выполняется.
"QListWidget::item { padding: 1px 4px; }" // m_logList = new QListWidget(container);
); // m_logList->setMaximumHeight(120);
m_logList->setSelectionMode(QAbstractItemView::NoSelection); // m_logList->setStyleSheet(
m_logList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // "QListWidget { background-color: #1e1e1e; color: #cccccc; font-family: monospace; font-size: 11px; border-top: 1px solid #444; }"
containerLayout->addWidget(m_logList); // "QListWidget::item { padding: 1px 4px; }"
updateLogPanel(); // );
// m_logList->setSelectionMode(QAbstractItemView::NoSelection);
// m_logList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
// containerLayout->addWidget(m_logList);
// updateLogPanel();
m_devicesWindow = container; m_devicesWindow = container;
stackedWidget->addWidget(m_devicesWindow); stackedWidget->addWidget(m_devicesWindow);