perf(satellite): compute ground tracks in thread pool, not inline

Ground track computation (90 Skyfield points per satellite) was blocking
the 1Hz tracker loop on every cache miss. On cold start with multiple
tracked satellites this could stall the SSE stream for several seconds.

Tracks are now computed in a 2-worker ThreadPoolExecutor. The tracker
loop emits position without groundTrack on cache miss; clients retain
the previous track via SSE merge until the new one is ready.
This commit is contained in:
James Smith
2026-03-19 21:55:00 +00:00
parent e5abeba11c
commit f549957c0b

View File

@@ -52,6 +52,11 @@ _tle_cache = dict(TLE_SATELLITES)
# TTL is 1800 seconds (30 minutes)
_track_cache: dict = {}
_TRACK_CACHE_TTL = 1800
# Thread pool for background ground-track computation (non-blocking from 1Hz tracker loop)
from concurrent.futures import ThreadPoolExecutor as _ThreadPoolExecutor
_track_executor = _ThreadPoolExecutor(max_workers=2, thread_name_prefix='sat-track')
_track_in_progress: set = set() # cache keys currently being computed
_pass_cache: dict = {}
_PASS_CACHE_TTL = 300
@@ -253,27 +258,43 @@ def _start_satellite_tracker():
'altitude': float(subpoint.elevation.km),
}
# Ground track with caching (90 points, TTL 1800s)
# Ground track with caching (90 points, TTL 1800s).
# If the cache is stale, kick off background computation so the
# 1Hz tracker loop is not blocked. The client retains the previous
# track via SSE merge until the new one arrives next tick.
cache_key_track = (sat_name, tle1[:20])
cached = _track_cache.get(cache_key_track)
if cached and (time.time() - cached[1]) < _TRACK_CACHE_TTL:
pos['groundTrack'] = cached[0]
else:
track = []
for minutes_offset in range(-45, 46, 1):
t_point = ts.utc(now_dt + timedelta(minutes=minutes_offset))
elif cache_key_track not in _track_in_progress:
_track_in_progress.add(cache_key_track)
_sat_ref = satellite
_ts_ref = ts
_now_dt_ref = now_dt
def _compute_track(_sat=_sat_ref, _ts=_ts_ref, _now_dt=_now_dt_ref, _key=cache_key_track):
try:
geo = satellite.at(t_point)
sp = wgs84.subpoint(geo)
track.append({
'lat': float(sp.latitude.degrees),
'lon': float(sp.longitude.degrees),
'past': minutes_offset < 0,
})
track = []
for minutes_offset in range(-45, 46, 1):
t_point = _ts.utc(_now_dt + timedelta(minutes=minutes_offset))
try:
geo = _sat.at(t_point)
sp = wgs84.subpoint(geo)
track.append({
'lat': float(sp.latitude.degrees),
'lon': float(sp.longitude.degrees),
'past': minutes_offset < 0,
})
except Exception:
continue
_track_cache[_key] = (track, time.time())
except Exception:
continue
_track_cache[cache_key_track] = (track, time.time())
pos['groundTrack'] = track
pass
finally:
_track_in_progress.discard(_key)
_track_executor.submit(_compute_track)
# groundTrack omitted this tick; frontend retains prior value
positions.append(pos)
except Exception: