You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Cut poller/backend CPU on small hosts: lazy entity builds, DR dedup, Whisper cap
Fixes the CPU regression observed after PR #117 (both cores pegged,
load ~5.0 on a 2-core VPS). Benchmarked the hot paths with synthetic
BEAST traffic and addressed the measured costs:
Poller:
- Frame worker no longer builds the full entity dict (trail copy,
comm-B snapshot, DR projection, ISO timestamp) on every decoded
frame — only for the <=1/s-per-aircraft publishes. New decoder
methods ingest_frame()/entity_from_state(); ingest() kept as a
compat wrapper. ~22% cheaper per frame.
- Registry tick loop rebuilds decoder entities once per 5s snapshot
tick (right before its only consumer) instead of every second,
cutting that O(aircraft) rebuild by 80%.
- Dead-reckoned aircraft no longer republish every second: their
lat/lon/altitude are wall-clock projections that always tripped the
_entity_changed dedup, flooding Redis pub/sub, the backend WS
fan-out, DB writes, and frontend renders — the main per-message
regression from PR #117. The frontend already projects DR tracks
client-side (pvb.ts), so projection-only changes are now skipped;
real telemetry and DR on/off transitions still publish immediately.
- Dropped the redundant second sanitize_payload deep-copy per publish
(write_entity_observation re-sanitized what publish_entity already
had, walking all 150 trail points each time).
- BEAST transport parses escape-free frames (the common case) via
bytearray.find slicing instead of the per-byte Python loop:
3.7x faster (4.0us -> 1.1us per frame).
Transcription:
- New WHISPER_CPU_THREADS setting (default 1) passed to WhisperModel,
and compose CPU limit 2.0 -> 1.0, so ctranslate2 can no longer
saturate every core whenever a P25 call transcribes.
Tests: new test_beast_transport.py (11 tests) covering the fast path,
escaped frames, garbage resync, and incomplete buffers; 5 new DR-dedup
tests in test_bus.py. Full poller suite: 171 passed. tsc clean,
docker compose config clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017n1uFNfMESakg9E9qoMDrh
Copy file name to clipboardExpand all lines: TASK_LOG.md
+13Lines changed: 13 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,6 +5,19 @@ Format: `## YYYY-MM-DD — <summary>` with bullet points for details.
5
5
6
6
---
7
7
8
+
## 2026-07-06 — CPU usage optimization for small hosts (2-core VPS)
9
+
10
+
-**Motivation**: User reported both cores pegged (load ~5.0 on 2 cores) after the ADS-B pipeline overhaul (PR #117). Benchmarked the hot paths with synthetic BEAST traffic and fixed the measured costs.
11
+
-**Lazy entity building in the BEAST frame worker** ([beast_decoder.py](poller/normalizers/beast_decoder.py), [adsb.py](poller/pollers/adsb.py)):
12
+
- Split `ingest()` into `ingest_frame()` (state-only decode, runs per frame) and `entity_from_state()` (full entity dict build). The frame worker now builds the entity dict — trail copy, comm-B snapshot, DR projection, ISO timestamp — only when the ≤1/s-per-aircraft publish gate passes instead of on every decoded frame (~22% cheaper per frame; `ingest()` kept as a compat wrapper).
13
+
- Best Mode arbitration semantics preserved: "beast seen" is still only recorded for positioned aircraft.
14
+
-**Registry tick loop rebuild moved to snapshot cadence** ([adsb.py](poller/pollers/adsb.py)): `snapshot_entities()` (an O(aircraft) full entity rebuild) ran every second while its only consumer — the enriched snapshot publish — runs every 5s. It now runs once per snapshot tick, immediately before the publish, cutting that work by 80%. DR still advances through a total feed outage since the rebuild happens on the same tick that publishes.
15
+
-**Dead-reckoning republish suppression** ([bus.py](poller/bus.py)): while an aircraft is dead-reckoned, its lat/lon/altitude are synthetic wall-clock projections that made `_entity_changed` report a change every second — so previously-quiet stale aircraft flooded Redis pub/sub, the backend WS fan-out, DB writes, and frontend renders (the main per-message CPU regression from PR #117). Since the frontend already projects DR tracks client-side (pvb.ts), `_entity_changed` now skips lat/lon/altitude when both states are DR. Real telemetry changes and DR on/off transitions (`position_dr` added to compare keys) still publish immediately.
16
+
-**Halved per-publish sanitize cost** ([bus.py](poller/bus.py), [db.py](poller/db.py)): `write_entity_observation` re-ran `sanitize_payload` (a recursive deep-copy that walks all 150 trail points, ~216µs per call) on entities its only caller `publish_entity` had already sanitized. New `sanitized=True` flag skips the redundant pass.
17
+
-**BEAST transport fast path** ([beast_transport.py](poller/pollers/beast_transport.py)): frames whose wire body contains no 0x1A escape byte (the overwhelmingly common case) are now sliced out at C speed with `bytearray.find` instead of the per-byte Python loop — 3.7× faster frame parsing (4.0µs → 1.1µs/frame). Escaped/incomplete/garbage frames still go through `parse_frame`.
18
+
-**Whisper transcription CPU cap** ([docker-compose.yml](docker-compose.yml), [transcription/config.py](transcription/config.py), [transcription/main.py](transcription/main.py), [.env.example](.env.example)): ctranslate2 grabbed every core whenever a P25 call transcribed, starving the poller/backend on a 2-core host. New `WHISPER_CPU_THREADS` setting (default 1) passed to `WhisperModel`, and the container's compose CPU limit reduced from 2.0 to 1.0. Base/int8 on one thread still transcribes short P25 clips faster than realtime.
19
+
-**Tests**: new [test_beast_transport.py](poller/tests/test_beast_transport.py) (11 tests: fast path, escaped MLAT/signal/message bytes, garbage resync, incomplete buffers, 0x31 skipping) and 5 DR-dedup tests in [test_bus.py](poller/tests/test_bus.py). Full poller suite: 171 passed.
0 commit comments