Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/DeviceManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ DeviceManager::DeviceManager(bool daemon)
enableBluetooth(true); // Enables adapter // ONLY if off and permission given
connect(this, &DeviceManager::bluetoothChanged, this, &DeviceManager::bluetoothStatusChanged);

// GUI-thread responsiveness watchdog (BLE perf instrumentation). Fires on
// this (the GUI/qtMainLoopThread) event loop; tick lateness == how long the
// loop was blocked. Cheap (4 Hz); always on so it catches stalls anytime.
m_loop_watch.start();
m_loop_watchdog.setInterval(BLE_LOOP_WATCHDOG_MS);
m_loop_watchdog.setTimerType(Qt::CoarseTimer);
connect(&m_loop_watchdog, &QTimer::timeout, this, &DeviceManager::bleLoopWatchdogTick);
m_loop_watchdog.start();

// Database
DatabaseManager *db = DatabaseManager::getInstance();
if (db)
Expand Down
46 changes: 46 additions & 0 deletions src/DeviceManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
#include <QVariant>
#include <QList>
#include <QTimer>
#include <QHash>
#include <QElapsedTimer>

#include <QBluetoothLocalDevice>
#include <QBluetoothDeviceDiscoveryAgent>
Expand Down Expand Up @@ -113,6 +115,46 @@ class DeviceManager: public QObject

QList <QString> m_devices_blacklist;

//! Per-device advertisement throttle.
//!
//! ``bleDevice_updated()`` runs the full decode + model-update path
//! synchronously on the GUI thread for *every* advertisement the OS
//! delivers. In a dense BLE environment a single fast-beaconing device
//! (or hundreds of ambient ones) floods that thread with queued
//! ``deviceUpdated`` events; when the backlog keeps the GUI thread out
//! of its event loop past Android's 5 s input-dispatch deadline the app
//! ANRs (the IME's blocking ``getExtractedText`` round-trip can't be
//! serviced). We coalesce adverts to at most one decode per device per
//! ``BLE_ADV_THROTTLE_MS`` so the per-device processing rate — and thus
//! the GUI-thread backlog — is bounded at the source. The first advert
//! from a device is never dropped (no prior timestamp), so new devices
//! still appear immediately. The nearby/RSSI-finder feature uses a
//! separate path (``updateNearbyBleDevice``) and is unaffected.
static constexpr qint64 BLE_ADV_THROTTLE_MS = 1000;
QHash <QString, qint64> m_advert_throttle; //!< device identifier -> last-processed monotonic ms
QElapsedTimer m_advert_clock; //!< monotonic clock backing the throttle

//! BLE throttle / GUI-thread responsiveness instrumentation.
//!
//! Makes the per-device throttle's effect and the GUI-thread
//! (qtMainLoopThread) responsiveness readable on any device — the only
//! way to observe the throttle's *benefit*, which manifests under field
//! BLE density a single-radio bench can't synthesise. ``m_advert_recv`` /
//! ``m_advert_throttled`` give the drop rate (how often the throttle
//! actually binds); the watchdog measures how late its 250 ms tick fires,
//! i.e. how long the event loop was blocked — the ANR-relevant stall (a
//! blocked loop can't service the IME's ``getExtractedText`` round-trip).
//! Snapshot via ``bleThrottleStats()``; also logged ~every 60 s.
quint64 m_advert_recv = 0; //!< adverts past the rssi/blacklist guards
quint64 m_advert_throttled = 0; //!< adverts dropped by the per-device throttle
quint64 m_advert_recv_logged = 0; //!< recv count at last periodic log
qint64 m_loop_gap_max_ms = 0; //!< worst GUI-thread event-loop stall seen (ms)
int m_loop_watch_ticks = 0; //!< watchdog tick counter (periodic-log cadence)
QElapsedTimer m_loop_watch; //!< time of last watchdog tick
QTimer m_loop_watchdog; //!< fires on the GUI thread; measures tick lateness
static constexpr int BLE_LOOP_WATCHDOG_MS = 250;
void bleLoopWatchdogTick();

DeviceModel *m_devices_nearby_model = nullptr;
DeviceFilter *m_devices_nearby_filter = nullptr;

Expand Down Expand Up @@ -234,6 +276,10 @@ private slots:

bool isDaemon() const { return m_daemonMode; }

// BLE throttle / responsiveness instrumentation (see members above)
Q_INVOKABLE QString bleThrottleStats() const; //!< one-line snapshot for the debug log / a debug screen
Q_INVOKABLE void resetBleStats(); //!< zero the counters + max stall to measure a fresh window

// Adapters management
Q_INVOKABLE bool areAdaptersAvailable() const { return m_bluetoothAdapters.size(); }
QVariant getAdapters() const { return QVariant::fromValue(m_bluetoothAdapters); }
Expand Down
89 changes: 87 additions & 2 deletions src/DeviceManager_advertisement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,52 @@ void DeviceManager::bleDevice_updated(const QBluetoothDeviceInfo &info, QBluetoo
if (m_devices_blacklist.contains(info.address().toString())) return; // device MAC is blacklisted
if (m_devices_blacklist.contains(info.deviceUuid().toString())) return; // device UUID is blacklisted

/// PER-DEVICE THROTTLE /////////////////////////////////////////////////////
// Bound the per-device advertisement processing rate so a flood of
// adverts can't saturate the GUI thread and build a deviceUpdated
// backlog (the root of the IME-round-trip ANR). See the member docs in
// DeviceManager.h. First advert from a device always passes.
#if !defined(DEBUG_FAKE_DEVICES)
{
#if defined(Q_OS_MACOS) || defined(Q_OS_IOS)
const QString throttle_key = info.deviceUuid().toString();
#else
const QString throttle_key = info.address().toString();
#endif
if (!throttle_key.isEmpty())
{
m_advert_recv++; // instrumentation: adverts reaching the throttle
if (!m_advert_clock.isValid()) m_advert_clock.start();
const qint64 now = m_advert_clock.elapsed();

auto it = m_advert_throttle.find(throttle_key);
if (it != m_advert_throttle.end() && (now - it.value()) < BLE_ADV_THROTTLE_MS)
{
m_advert_throttled++; // instrumentation: dropped by the throttle
return; // too soon since this device was last processed
}

m_advert_throttle.insert(throttle_key, now);

// Opportunistic prune so devices that rotate their (random) MAC
// can't grow the map without bound over a long-running scan.
if (m_advert_throttle.size() > 1024)
{
for (auto p = m_advert_throttle.begin(); p != m_advert_throttle.end(); )
{
if ((now - p.value()) > (30 * BLE_ADV_THROTTLE_MS)) p = m_advert_throttle.erase(p);
else ++p;
}
}
}
}
#endif

// One decoder for the whole call: TheengsDecoder holds only stable config
// (no per-decode state), so reusing it across the known/unknown attempts
// below avoids reconstructing it on every loop iteration.
TheengsDecoder decoder;

/// KNOWN GATEWAYS /////////////////////////////////////////////////////////

for (auto d: std::as_const(m_gateways_model->m_devices))
Expand Down Expand Up @@ -193,7 +239,6 @@ void DeviceManager::bleDevice_updated(const QBluetoothDeviceInfo &info, QBluetoo
}

// theengs decoding
TheengsDecoder decoder;
ArduinoJson::JsonObject obj = doc.as<ArduinoJson::JsonObject>();
if (decoder.decodeBLEJson(obj) >= 0)
{
Expand Down Expand Up @@ -338,7 +383,6 @@ void DeviceManager::bleDevice_updated(const QBluetoothDeviceInfo &info, QBluetoo
doc["servicedatauuid"] = svc_uuid;
}

TheengsDecoder decoder;
ArduinoJson::JsonObject obj = doc.as<ArduinoJson::JsonObject>();
if (decoder.decodeBLEJson(obj) < 0) continue;

Expand Down Expand Up @@ -418,3 +462,44 @@ void DeviceManager::bleDevice_updated(const QBluetoothDeviceInfo &info, QBluetoo
}

/* ************************************************************************** */

void DeviceManager::bleLoopWatchdogTick()
{
// How late this tick fired beyond its scheduled interval == how long the
// GUI thread (qtMainLoopThread) was blocked out of its event loop. The max
// is the ANR-relevant stall: a blocked loop can't service the IME's
// BlockingQueued getExtractedText() round-trip -> input-dispatch ANR.
const qint64 elapsed = m_loop_watch.restart();
const qint64 gap = elapsed - BLE_LOOP_WATCHDOG_MS;
if (gap > m_loop_gap_max_ms) m_loop_gap_max_ms = gap;

// Periodic trend line into the (logcat-captured) debug log, ~ every 60 s,
// but only when adverts actually flowed since the last line (no idle spam).
if (++m_loop_watch_ticks >= (60000 / BLE_LOOP_WATCHDOG_MS))
{
m_loop_watch_ticks = 0;
if (m_advert_recv != m_advert_recv_logged)
{
m_advert_recv_logged = m_advert_recv;
qInfo().noquote() << "[ble-perf]" << bleThrottleStats();
}
}
}

QString DeviceManager::bleThrottleStats() const
{
const double pct = m_advert_recv ? (100.0 * double(m_advert_throttled) / double(m_advert_recv)) : 0.0;
return QStringLiteral("adverts recv=%1 throttled=%2 (%3%) | tracked MACs=%4 | max GUI-loop stall=%5 ms")
.arg(m_advert_recv).arg(m_advert_throttled)
.arg(pct, 0, 'f', 1).arg(m_advert_throttle.size()).arg(m_loop_gap_max_ms);
}

void DeviceManager::resetBleStats()
{
m_advert_recv = 0;
m_advert_throttled = 0;
m_advert_recv_logged = 0;
m_loop_gap_max_ms = 0;
}

/* ************************************************************************** */
Loading