Skip to content

perf: extract decoder data into concrete ephemeris and clock structs#52

Draft
giove-a wants to merge 1 commit into
masterfrom
sc/performance
Draft

perf: extract decoder data into concrete ephemeris and clock structs#52
giove-a wants to merge 1 commit into
masterfrom
sc/performance

Conversation

@giove-a

@giove-a giove-a commented Jul 19, 2026

Copy link
Copy Markdown

Problem

Every navigation-data field on a GNSSDecoderState's data is Union{Nothing, T} (fields fill in as the message decodes). The Kepler propagation and clock correction read ~20 of them in long arithmetic chains — too many small unions for the compiler to split — so every intermediate gets boxed. Measured on the benchmark fixtures, this was over half of calc_pvt's runtime and roughly 70% of its allocations.

Change

Extract the fields once per satellite per solve into concrete Float64 structs behind a function barrier (src/ephemeris.jl):

  • Ephemeris — the broadcast orbit plus propagation constants. The directly-broadcast Keplerian (LNAV / I/NAV / F/NAV) and quasi-Keplerian CNAV/CNAV-2 parameterisations are normalised at extraction by orbital_terms (replacing orbital_elements), so one set of kernels serves all four navigation messages.
  • ClockModel — the clock polynomial, the ranging-signal group delay folded to a constant via the existing (directly-tested) correct_by_group_delay dispatch, and the Ephemeris the relativistic term is computed from.

All orbit and clock kernels now run on these, allocation-free and bit-identical to before. The decoder-taking public API is unchanged (thin extraction wrappers), and a decoder with a complete orbit but not-yet-decoded clock can still be propagated (the test/cnav.jl self-consistency tests exercise exactly that, which is why orbit and clock are separate structs).

Results

calc_pvt on the benchmark fixtures (median, same machine):

Case Before After
9 GPS sats, warm 18.6 µs / 21.7 KiB / 622 allocs 11.3 µs / 14.5 KiB / 183 allocs
9 GPS sats, cold 31.5 µs / 31.5 KiB / 780 allocs 24.7 µs / 24.3 KiB / 341 allocs
5 Galileo sats, warm 10.7 µs / 15.9 KiB / 422 allocs 6.8 µs / 11.9 KiB / 179 allocs

Full test suite passes; docs build cleanly (the new internals are documented on the API page).

Relation to a possible GNSSDecoder refactor

Longer-term, GNSSDecoder could publish concrete ephemeris/clock substructs itself at validate_data promotion time (nullable at the top, concrete inside), which would let the extraction layer here shrink to a thin adapter or disappear — see the companion GNSSDecoder issue. The kernel rewrites in this PR carry over unchanged in that scenario, so merging this now doesn't conflict with that direction.

🤖 Generated with Claude Code

Every navigation-data field on a decoder is Union{Nothing, T} (they fill
in as the message decodes), and the Kepler propagation and clock
correction read ~20 of them in long arithmetic chains — too many small
unions for the compiler to split, so every intermediate is boxed. That
made the ephemeris side of calc_pvt over half its runtime and roughly
70% of its allocations.

Extract the fields once per satellite per solve into concrete Float64
structs behind a function barrier: Ephemeris (the broadcast orbit plus
propagation constants, with the directly-broadcast Keplerian and
quasi-Keplerian CNAV/CNAV-2 parameterisations normalised by
orbital_terms) and ClockModel (the clock polynomial, the signal group
delay folded to a constant via the existing correct_by_group_delay
dispatch, and the Ephemeris the relativistic term is computed from).
All orbit and clock kernels now run on these, allocation-free and
bit-identical to before; the decoder-taking public API is unchanged
(thin extraction wrappers), and a decoder with a complete orbit but
not-yet-decoded clock can still be propagated.

calc_pvt on the benchmark fixtures (median, same machine):
- 9 GPS sats, warm:     18.6 µs / 21.7 KiB / 622 allocs → 11.3 µs / 14.5 KiB / 183 allocs
- 9 GPS sats, cold:     31.5 µs / 31.5 KiB / 780 allocs → 24.7 µs / 24.3 KiB / 341 allocs
- 5 Galileo sats, warm: 10.7 µs / 15.9 KiB / 422 allocs →  6.8 µs / 11.9 KiB / 179 allocs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.50000% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.90%. Comparing base (5e45f31) to head (b7a8769).

Files with missing lines Patch % Lines
src/sat_position.jl 80.00% 4 Missing ⚠️
src/sat_time.jl 76.92% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #52      +/-   ##
==========================================
- Coverage   98.30%   96.90%   -1.41%     
==========================================
  Files           7        8       +1     
  Lines         473      484      +11     
==========================================
+ Hits          465      469       +4     
- Misses          8       15       +7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results (Julia v1)

Time benchmarks
master b7a8769... master / b7a8769...
calc_pvt/GPSL1/4sats/cold 0.0419 ± 0.0067 ms 0.0345 ± 0.0014 ms 1.21 ± 0.2
calc_pvt/GPSL1/4sats/warm 0.0372 ± 0.0071 ms 30.3 ± 6.4 μs 1.23 ± 0.35
calc_pvt/GPSL1/9sats/cold 0.0617 ± 0.0011 ms 0.0462 ± 0.00097 ms 1.34 ± 0.037
calc_pvt/GPSL1/9sats/warm 0.0382 ± 0.00074 ms 22.4 ± 0.58 μs 1.7 ± 0.055
calc_pvt/GalileoE1B/4sats/cold 0.0375 ± 0.0074 ms 30.6 ± 6.7 μs 1.23 ± 0.36
calc_pvt/GalileoE1B/4sats/warm 18.3 ± 2.5 μs 11.1 ± 0.63 μs 1.65 ± 0.24
calc_pvt/GalileoE1B/5sats/cold 0.0404 ± 0.0073 ms 0.0319 ± 0.0067 ms 1.27 ± 0.35
calc_pvt/GalileoE1B/5sats/warm 21.4 ± 4.5 μs 12.4 ± 0.59 μs 1.73 ± 0.38
time_to_load 2.38 ± 0.04 s 2.39 ± 0.016 s 0.994 ± 0.018
Memory benchmarks
master b7a8769... master / b7a8769...
calc_pvt/GPSL1/4sats/cold 0.552 k allocs: 25.2 kB 0.358 k allocs: 22 kB 1.14
calc_pvt/GPSL1/4sats/warm 0.493 k allocs: 21.5 kB 0.299 k allocs: 18.4 kB 1.17
calc_pvt/GPSL1/9sats/cold 0.78 k allocs: 31.5 kB 0.341 k allocs: 24.3 kB 1.29
calc_pvt/GPSL1/9sats/warm 0.622 k allocs: 21.7 kB 0.183 k allocs: 14.5 kB 1.49
calc_pvt/GalileoE1B/4sats/cold 0.541 k allocs: 24.5 kB 0.347 k allocs: 21.4 kB 1.15
calc_pvt/GalileoE1B/4sats/warm 0.372 k allocs: 14.7 kB 0.178 k allocs: 11.5 kB 1.27
calc_pvt/GalileoE1B/5sats/cold 0.58 k allocs: 25.2 kB 0.337 k allocs: 21.2 kB 1.19
calc_pvt/GalileoE1B/5sats/warm 0.422 k allocs: 15.9 kB 0.179 k allocs: 11.9 kB 1.33
time_to_load 0.149 k allocs: 11.2 kB 0.149 k allocs: 11.2 kB 1

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