Skip to content

Performance refactor: ETS routing, Forwarder, and slim Router#25

Merged
robinhilliard merged 12 commits into
masterfrom
cursor/performance-refactor-3991
Jun 13, 2026
Merged

Performance refactor: ETS routing, Forwarder, and slim Router#25
robinhilliard merged 12 commits into
masterfrom
cursor/performance-refactor-3991

Conversation

@robinhilliard

@robinhilliard robinhilliard commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Parse hot-path optimizations

Binary CRC (drop bin_to_list)

  • validate_checksum/2 and validate_and_unpack/2 compute X.25 CRC directly over binary_part(raw, 1, len) using the existing binary x25_crc/2 path.

Optional raw-byte subscriber path

  • Router.subscribe(as_raw: true) delivers wire bytes (mavlink_1_raw / mavlink_2_raw) instead of a decoded struct.
  • Connections call Frame.prepare_for_route/2, which skips payload unpack when RouteTable.decode_required?/2 is false (e.g. broadcast message + only as_raw subscribers, no wire peers).
  • RouteTable caches subscriber decode flags and wire-peer count; dialect gains message_module_for/1 for message-type matching without unpack.

Array unpack as bitstring comprehensions

  • mix mavlink now generates (for <<elem::... <- field_f>>, do: elem) for array fields instead of unpack_array/2.

Tests

  • test/frame_test.exs: binary vs list CRC equivalence, prepare_for_route skip-unpack, Data16 comprehension unpack
  • test/route_table_test.exs: as_raw wire-byte delivery
  • 51 tests passing (MIX_ENV=test mix test --no-start)
Open in Web Open in Cursor 

cursoragent and others added 12 commits June 13, 2026 05:29
Bump Elixir to ~> 1.14, circuits_uart to 1.5.5, and ex_doc to 0.34.
Add :xmerl to extra_applications for parser tests.
Add mix benchmark alias for TCP throughput bench.

Co-authored-by: Robin Hilliard <robinhilliard@users.noreply.github.com>
- Three-layer router test suite (delegate, pipeline, routing, API)
- Test support harness with TestMavlink dialect fixtures
- TCP 1v1 benchmark harness; baseline ~1379 msg/s on CI host
- elixirc_paths for test/support; consolidate_protocols disabled for bench

Co-authored-by: Robin Hilliard <robinhilliard@users.noreply.github.com>
- Add RouteTable GenServer owning public ETS tables for routes, wire peers,
  and subscribers with direct subscriber dispatch
- Add Forwarder for ETS-based wire and subscriber routing
- Promote LocalConnection to GenServer for pack_and_send API
- Slim Router: delegate subscribe/unsubscribe to RouteTable, routing to
  Forwarder, handle {:mavlink_forward, ...} for wire I/O
- Update supervisor to start RouteTable and LocalConnection with one_for_one
- Fix TCPOutConnection gen_tcp.send bug
- Add route_table and forwarder tests; update existing tests for ETS routes
- Record after-phase4 TCP benchmark (1092 msg/s vs 1379 baseline on CI)

Co-authored-by: Robin Hilliard <robinhilliard@users.noreply.github.com>
The elixir:1.9.1-slim image is based on EOL Debian stretch; apt-get
update fails with 404 before any project code runs. Align CI with
mix.exs (~> 1.14) and install erlang-xmerl for parser tests.

Co-authored-by: Robin Hilliard <robinhilliard@users.noreply.github.com>
- Add ConnectionSupervisor (DynamicSupervisor) to own wire connection processes
- Promote TCPOut, UDPOut, UDPIn, and Serial to GenServers with direct I/O
- Add WireConnection helper; Forwarder sends mavlink_forward_raw on wire path
- Slim Router to API facade and test connection registry only
- Rename delegate parsers to parse_incoming/3 to avoid GenServer clashes
- Update integration tests to inject I/O into connection processes
- Record after-phase5 TCP benchmark (895 msg/s on CI host)

Co-authored-by: Robin Hilliard <robinhilliard@users.noreply.github.com>
Profile runs show both architectures are backlog-limited (~170k-190k TCP
messages queued) with similar rates (~900-960 msg/s). Bottleneck is
single-process active TCP parsing, not ETS routing or subscriber indexing.

Co-authored-by: Robin Hilliard <robinhilliard@users.noreply.github.com>
- TCPOutConnection uses {:active, :once} with re-arm after each message
- GCSCounter uses :atomics instead of Agent for benchmark subscriber
- Throughput improves from ~900 to ~1200 msg/s; TCP mailbox max drops ~171k to ~17k

Co-authored-by: Robin Hilliard <robinhilliard@users.noreply.github.com>
After ingesting one {:tcp, ...} message, drain additional {:tcp, ^socket, _}
messages from the GenServer mailbox (including self-sent empty triggers for
multi-frame buffers) before calling inet:setopts once.

Fixes drain_tcp/2 argument order when used in a pipeline (state, socket).

Benchmark (mix benchmark.backpressure): ~200k msg/s vs ~1.2k with active-once
alone; the prior ceiling was GenServer dispatch overhead per frame in coalesced
TCP segments, not parse throughput.

Co-authored-by: Robin Hilliard <robinhilliard@users.noreply.github.com>
Introduce MAVLink.MailboxDrain with selective-receive drain functions for
tcp, udp, and circuits_uart message shapes. Refactor TCPOut to use the helper.

UDPIn and UDPOut now default to {:active, :once} with mailbox drain and a
single inet:setopts rearm per batch, matching TCPOut backpressure.

Serial uses passive UART mode (active: false) with a read loop for kernel
backpressure, plus circuits_uart mailbox drain for coalesced chunks and
self-sent multi-frame triggers. Test mode keeps direct message injection.

Co-authored-by: Robin Hilliard <robinhilliard@users.noreply.github.com>
Multi-vehicle / multi-GCS benchmark with realistic telemetry mix (VFR_HUD,
HEARTBEAT, DATA16) across N TCP vehicles and M GCS subscribers. Includes
throughput and profile harnesses plus profile_indexing_compare.sh for
before/after indexing comparison.

RouteTable indexing:
- :mavlink_subscriber_index bag keyed by {source_system, message|:all}
- matching_subscribers/1 uses :ets.lookup/2 on index keys instead of tab2list
- matching_peers/2 uses direct lookup for exact targets
- all_wire_peers/0 uses :ets.match_object/2

Frame fixtures extended for VfrHud and Data16. GCSCounter supports multi-slot
atomics with subscribe barrier.

Co-authored-by: Robin Hilliard <robinhilliard@users.noreply.github.com>
Bench vehicles no longer register as wire peers, preventing broadcast
cross-forward between TCP connections that caused only one vehicle to
deliver to GCS subscribers.

Add delivery balance reporting, routing microbench, and compare scripts.
Measured with 100 subscribers: indexing ~2.6x faster on matching_subscribers;
end-to-end fleet throughput ~10% faster at 4 vehicles / 5 GCS.

Co-authored-by: Robin Hilliard <robinhilliard@users.noreply.github.com>
- Validate checksum over binary slices instead of bin_to_list
- Add prepare_for_route/2 to skip payload unpack when only as_raw
  subscribers need delivery; RouteTable tracks decode requirements
- Subscribe with as_raw: true delivers mavlink_1_raw/mavlink_2_raw bytes
- Generate array field unpack as bitstring comprehensions; add
  message_module_for/1 for routing without full decode
- Regenerate test dialect via mix mavlink (DialectFixture)

Co-authored-by: Robin Hilliard <robinhilliard@users.noreply.github.com>
@robinhilliard robinhilliard marked this pull request as ready for review June 13, 2026 14:16
@robinhilliard robinhilliard merged commit 7d3af4d into master Jun 13, 2026
1 check 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