perf: answer /api/status with one targeted query

/api/status is polled by every open page (on load, every 60s, and on
visibility resume) and cost ~266-450ms in-container while doing no device
I/O at all — check_connection() is just an attribute read, and /health
returns in ~1ms, so all of it was SQLite.

Two calls were over-fetching:

- get_stats() ran COUNT(*) over 11 tables to answer a question about 2.
  COUNT(*) FROM echoes alone was ~200ms over 19k rows and the result was
  discarded. That table only grows, so the endpoint kept getting slower.
- get_channel_messages(limit=1) was a SELECT * fetching every column,
  raw_packet included, to read one timestamp. ORDER BY timestamp has no
  usable index (idx_cm_channel_ts leads with channel_idx), so the plan
  was a full SCAN through two temp B-trees. MAX(timestamp) is served off
  that index as a covering scan instead.

Replaced with Database.get_status_summary(): three scalar subqueries on
one connection. ~371ms -> ~46ms for the DB work; endpoint median ~57ms
in-container, ~78ms from the page. Response verified byte-identical.

This does not address the multi-tab congestion — at one poll per tab per
60s /api/status was never that cause — but it removes a recurring cost
and re-baselines the probe those measurements are taken with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-07-31 08:35:53 +02:00
parent 1e82a1ed7c
commit ab64ef72f2
3 changed files with 33 additions and 6 deletions
+29
View File
@@ -1480,6 +1480,35 @@ class Database:
# Maintenance
# ================================================================
def get_status_summary(self) -> Dict[str, Any]:
"""Message count + newest channel timestamp, for /api/status.
/api/status is polled by every open tab, so it gets a dedicated query
instead of reusing get_stats() + get_channel_messages(limit=1):
- get_stats() counts 11 tables to answer a question about 2. The
COUNT(*) over echoes alone costs ~200ms on a 20k-row table and its
result was discarded — and echoes only ever grow.
- get_channel_messages(limit=1) was a `SELECT *` whose ORDER BY
timestamp has no usable index (idx_cm_channel_ts leads with
channel_idx), so it scanned the table through two temp B-trees and
materialized every column, raw_packet included, to read one field.
MAX(timestamp) is served straight off that index instead.
Together with folding both into a single connection this is ~46ms
where the pair cost ~371ms.
"""
with self._connect() as conn:
row = conn.execute(
"""SELECT (SELECT COUNT(*) FROM channel_messages) AS channel_count,
(SELECT COUNT(*) FROM direct_messages) AS dm_count,
(SELECT MAX(timestamp) FROM channel_messages) AS latest_timestamp"""
).fetchone()
return {
'message_count': (row['channel_count'] or 0) + (row['dm_count'] or 0),
'latest_message_timestamp': row['latest_timestamp'],
}
def get_stats(self) -> Dict[str, Any]:
"""Get row counts for all tables."""
tables = ['device', 'contacts', 'channels', 'channel_messages',