Skip to content

perf: per-IATA activity tables + analytics query/matview tuning - #92

Merged
446564 merged 10 commits into
MeshCore-Beacon:devfrom
MrAlders0n:perf/analytics-and-query-tuning
Jul 24, 2026
Merged

perf: per-IATA activity tables + analytics query/matview tuning#92
446564 merged 10 commits into
MeshCore-Beacon:devfrom
MrAlders0n:perf/analytics-and-query-tuning

Conversation

@MrAlders0n

Copy link
Copy Markdown
Member

This rolls the work from #87 and #88 into one branch, plus a couple of follow-up fixes I found while testing. They were stacked on each other anyway, so reviewing them together is easier. Supersedes #87 and #88.

The common thread: the stats/analytics side was doing big live scans and cross-joins over packet_observations (tens of millions of rows on a busy instance). This moves that work onto per-IATA activity tables and precomputed matviews so the read path stays cheap.

Per-IATA activity tables

  • track channel activity per IATA in its own table — channel activity was derived by joining through observations every time. Now there's a dedicated channel_iatas table kept current on ingest.
  • review fixes for channel_iatas — cleanup from self-reviewing the above.
  • track trace activity per IATA in its own table — same idea for traces (trace_iatas); the old query spilled the hash join on large data.
  • review fixes for trace_iatas — follow-up fixes.

Query + index fixes

  • stop cross-joining packets in scope stats — the scope stats query cross-joined packets and ballooned to millions of rows before the COUNT(DISTINCT) (~10s). Each table is now counted on its own.
  • index known_routes(last_seen) — adds the index the routes list actually orders by.

Analytics matviews

  • track payload_type on observations and serve the breakdown from a matview — denormalizes payload_type onto observations and serves the payload breakdown from a precomputed view instead of scanning a week of data per request.
  • serve top observers from a matview instead of scanning per request — top observers was a ~2s scan; now a matview.

Follow-ups found while testing

  • bucket payload & observer matviews by hour for windowed queries — the two matviews above stored a single fixed 7-day total, so the 24h/7d/30d selector on the analytics tab did nothing. Re-bucketed them hourly over 30 days (matching the existing mv_hourly_iata_stats) so the read just sums the buckets in range. The windows work now and the read stays small.
  • matview-back top talkers and advertisers — the last two live-scan endpoints. Top talkers joined channel_messages to packet_observations (each message heard by ~15 observers), exploding to ~5.9M rows and taking ~29s at 30d on our data. Both now read hourly-bucketed matviews like the rest, refreshed in the background.

Testing

Deployed and running on a large instance (the meshmapper analyzer, ~55M observation rows). Verified all the panels across 24h/7d/30d.

One heads-up for anyone with a big table: migration 015 (adding payload_type and backfilling the last 7 days) ran for ~1h43m on that instance — a 6.8 GB packet_observations table, single-statement UPDATE with parallelism off, holding an exclusive lock the whole time, so the app can't finish startup until it's done. The matview builds after it were quick (seconds). If you're deploying against a large DB, expect that migration to be the long pole and maybe run it in a maintenance window.

The IATA filter ran a correlated EXISTS over packets/observations with
ILIKE, which skipped the iata index and took ~7s live. Keep a small
channel_iatas table at ingest (like node_iatas) and filter against it.
Also honor the iatas= param so multi-site regions stop getting the
global list. Filter now ages out with packet retention rather than
matching any retained packet.
Refresh last_heard on duplicate observations too, capped at hourly, so
steady traffic can't age a channel out of the filter while its packets
stay retained (dedup key has no heard_at). Guard the seed join against
the docker /dev/shm cap like the trace one, and drop the unused
last_heard index column so the upserts stay HOT.
The trace list joined every trace packet to ~50M observations to apply
the IATA filter; the parallel hash join overran docker's 64MB /dev/shm
and the filtered list errored. Keep a small trace_iatas table at ingest
(like channel_iatas) and group packets by tag instead. Filtered stats
are now per tag rather than per matching packet.
Refresh last_heard on duplicate observations too, capped at hourly, so
steady traffic can't age a trace out of the filter (dedup key has no
heard_at). Drop the unused last_heard index column so upserts stay HOT,
and share one retention cutoff across the cleanup deletes.
GetScopeStats left-joined packets, observer_scopes and nodes at once,
multiplying rows into the millions before COUNT(DISTINCT) deduped them
(~10s per call). Count each table on its own and index packets(scope_id).
The routes list orders by last_seen with no index, so every page
seq-scanned and sorted ~150k rows. Same fix as the packets list got
in 008.
…queries

The payload-breakdown and top-observers matviews stored a single 7-day
total per group, so the 24h/7d/30d selector did nothing: payload ignored
the window entirely (the store discarded `since`) and top-observers only
varied which rows cleared the last_heard cutoff, never the counts.

Bucket both views by hour over a 30-day horizon and sum the buckets in
range, mirroring mv_hourly_iata_stats. The stats endpoints now honour the
requested window while still reading a small precomputed table.
@MrAlders0n

Copy link
Copy Markdown
Member Author

Take a peak at https://analyzer.meshmapper.net/?tab=Packets&types=4&iata=SEA

And click around :)

@MrAlders0n
MrAlders0n marked this pull request as ready for review July 24, 2026 17:23
@MrAlders0n
MrAlders0n requested a review from 446564 as a code owner July 24, 2026 17:23
@446564
446564 merged commit cbde3d0 into MeshCore-Beacon:dev Jul 24, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants