Publish mapdOut independently of map calculations - #128
Open
FrogAi wants to merge 1 commit into
Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
mapdOuton its documented 20 Hz cadence even while synchronous map calculations are in progress.Motivation
mapdOutis documented as a 20 Hz service, but its send currently runs in the same loop that processes GPS updates, loads map tiles, matches current and upcoming ways, calculates curvatures, and derives target velocities. A slow GPS/map iteration therefore blocks publication as well as calculation.This matters to frequency-aware consumers. A 20 Hz service receives a 500 ms liveness deadline in openpilot's
SubMaster; the captured route repeatedly exceeded that boundary while mapd remained alive and continued calculating. The consumer then discarded otherwise usable map data until the next message arrived.Implementation
The existing state-owning loop still performs every subscriber update and map calculation. At the start of each loop it builds the complete
mapdOutCap'n Proto message with the existing setter list, then atomically hands ownership of that finished message to a dedicated publisher.The publisher:
State;logMonoTimeimmediately before each send;No parallel map calculation, state locking, schema change, or per-field snapshot copy is introduced.
Behavior
mapdOutgoes silent and can be marked deadDeveloper summary
The same 90 captured GPS samples were replayed through the official v2.3.0 ARM64 binary and an ARM64 build of this exact commit on the same offroad comma device. The base reproduced 56 gaps at or above the 500 ms consumer boundary and averaged 8.53 Hz. This change produced zero such gaps and averaged 20.00 Hz.
Three additional base/candidate pairs used alternating execution order against identical 30-sample input. Peak RSS ranges overlapped, so the measurements do not resolve a memory direction. Median process CPU time increased by 1.25% while the candidate published about twice as many messages.
Validation
mapdOutfield populated byState.logMonoTime; the candidate's maximum timestamp gap was 61.2 ms in the primary replay.carState, 21 GPS, 400modelV2, and 2,000selfdriveStatemessages per run. All 26 deterministic final fields matched in all five alternating-order pairs.go test ./...,go test -race ./...,go vet ./..., andgo build ./...passed on Linux/amd64 with Go 1.25.1.Compatibility
mapdOutnow represents the latest complete result rather than calculation progress. The output documentation explicitly records that contract.mapdOutsilence as an implicit main-loop watchdog will no longer get that signal. Process supervision remains separate from output cadence.Engineering record and audit trail
Root cause
The main loop sends
mapdOut, sleeps for 50 ms, then performs subscriber handling and GPS-dependent map work before it can send again. That synchronous work includes tile lookup, current-way selection, upcoming-way traversal, curvature calculation, target-velocity calculation, and related lazy way accessors.The normal no-GPS path stays close to 50 ms. The long tail appears when a GPS update enters the map path. Since openpilot derives a 500 ms liveness deadline from the declared 20 Hz service frequency, calculation latency becomes an unrelated service-liveness failure.
The primary captured replay reproduced this directly:
State ownership
The publisher cannot safely read
Statedirectly: current-way helpers contain lazy caches, and the calculation loop mutates several related fields incrementally. The handoff is therefore a pointer to a completed Cap'n Proto message, not a pointer to mutable state.atomic.Pointer[capnp.Message]publishes that whole completed message as one ownership handoff.The atomic store/load adds no allocation. The message was already allocated by the existing
NewMessagepath, and no second 27-field representation is maintained.Publication-gap distribution
The full gap distribution confirms the problem is the GPS/map-work tail rather than the ordinary 50 ms path. The candidate remains below the 500 ms boundary for the complete observed distribution.
Sanitized raw timing data: mapd-cadence-raw.csv
Semantic parity
For the primary and three alternating-order GPS replays, the harness discarded startup output and hashed each distinct post-map-match state across every populated
mapdOutfield:The primary replay produced the same 58-state sequence and aggregate digest. Every alternating-order pair produced the same 10-state sequence within its pair.
The high-rate replay deliberately added the streams that GPS-only replay leaves mostly static. Exact full-transition hashes are not a valid oracle at 100 Hz: an official-vs-official A/A control also differed because conflated model messages land on scheduling boundaries. Field-level convergence isolated that variability to
visionCurveSpeed, whose moving average depends on which model samples are observed. Across five alternating-order pairs:suggestedSpeed;visionCurveSpeedranged from 9.3103 to 9.4221 m/s;No coordinates, road names, way IDs, or route messages are included in the attached evidence.
Resource impact
Resource measurements used three fresh process pairs with alternating execution order against identical 30-sample input. Bars are medians and dots are individual runs.
The longer fixed-order primary replay measured 68.75 MiB / 51.62 CPU seconds for the base and 69.14 MiB / 52.59 CPU seconds for the candidate. The evidence supports a small CPU cost for the additional publications and does not support either an RSS improvement or a repeatable RSS regression.
Sanitized raw resource data: mapd-resource-raw.csv
Final validation
Source base:
9229fb71f68fd43fecba27bdc04fbd59750c2c9aCandidate commit:
6c87ac124c499518f636d20966b08f8bd7aa6c00The source base is current upstream
main. Its only change after the v2.3.0 source tag is an offline extraction-script coordinate clamp, so the runtime comparison uses the official v2.3.0 release binary.Device binaries:
a08f0a9bea3ec099738932964d8dbf066f6f47539395ecb979530cbc99b748a16e66d7299d208582692ee8660f69a94694bd0ae4a07af439d8ad13515241e532Commands run against the final source in Linux with Go 1.25.1:
The focused publisher test was temporary and removed after execution. It directly exercised the production symbols; therefore the root package was covered by the race detector despite having no permanent upstream test file in this PR.
Every physical-device run used isolated message namespaces while offroad. The production mapd PID and binary hash remained unchanged before and after each run.
Scope limits
mapdOutcadence alone no longer detects a wedged calculation loop.