Skip to content

navmap_core, navmap_ros: fixes for large maps (DistanceField, set_are… - #42

Open
jmguerreroh wants to merge 1 commit into
EasyNavigation:rollingfrom
jmguerreroh:navmap-core-fixes
Open

jmguerreroh wants to merge 1 commit into
EasyNavigation:rollingfrom
jmguerreroh:navmap-core-fixes

Conversation

@jmguerreroh

Copy link
Copy Markdown

navmap_core / navmap_ros: four fixes found by benchmarking large maps

Branch base: rolling. Files: navmap_core (DistanceField, NavMap::set_area, NavMap::operator=)
and navmap_ros (navmap_io). Independent of the navmap_tools PR; neither needs the other.

These came out of a comparison of NavMap and a level-of-detail maps manager on maps of 100 to
10 000 cells per side (50 m to 5 km at 0.5 m). Each one is a separate commit-sized change with its
own regression test.

1. DistanceField: indexing a seed cost the square of its length

DistanceField::index_seed put a segment seed (an open edge of the mesh) into every bucket of its
bounding box. For an axis-aligned edge that is a line of buckets, but for a diagonal one it is a
square: an edge of 16 m at 45° filled ~1 000 buckets instead of ~40, and every query near it then
scanned them. Meshes with long diagonal open edges (coarse NavCels of a level-of-detail map, a
rotated map with holes) spent seconds in compute().

Now a seed is indexed by points sampled along it every bucket_ metres (deduplicated), and the
queries in update() look one bucket further on each side, so no seed within reach is missed
(every point of the segment is within half a bucket of a sample).

Measured (the same mesh, 22 944 NavCels, 9 586 open edges, with legs of 0.5 to 16 m; time of
compute()). Rotated 45°, before / after:

leg before after (axis-aligned, after)
0.5 m 2.9 ms 2.4 ms 3.2 ms
4 m 39.0 ms 6.7 ms 8.0 ms
8 m 303.4 ms 13.3 ms 16.8 ms
16 m 1 705.1 ms 34.6 ms 36.0 ms

A diagonal mesh now costs what an axis-aligned one does (linear in the edge length, as the
number of NavCels it touches).

Test: test_incremental gets a fourth map in its equivalence sweep (4 m cells with holes, rotated
30°: the incremental field must equal the full recomputation there too), and
LongDiagonalOpenEdgesCostAboutAsMuchAsShortOnes (the 16 m mesh within 25× the 0.5 m one; it was
~600×).

2. NavMap::set_area: O(map) per call, and its writes were invisible to incremental filters

  • The BFS kept std::vector<char> visited(navcels.size()): writing a 0.5 m disc on a map of
    50 M NavCels allocated and zeroed 50 MB per call. It is now an unordered_set of the NavCels the
    area actually visits: the cost is the area's.
  • It wrote mutable_data() directly, so the layer's change list (changed(), used by the
    incremental InflationFilter of EasyNav) did not see the new values: an obstacle written with
    set_area was not inflated until something else marked the layer. Every NavCel written is now
    recorded with mark_changed, as layer_set does.

Test: NavMap_SetArea_Systematic.TheWrittenNavCelsAreRecordedAsChangedForIncrementalConsumers
(the change list equals exactly the NavCels written). The existing set_area tests are unchanged and
pass.

3. NavMap::operator=: crash when a layer changed type

When the destination already had a layer of the same name but another type (the source removed it
and re-created it, e.g. U8 → F32), operator= asked add_or_get<T> for a view of the new type,
got nullptr and dereferenced it. This happens in EasyNav when a node copies the map from the
NavState, re-creates a layer with another type and sets it back. The old layer is now removed
before the copy when the types differ.

Test: NavMapAssign.ALayerReCreatedWithAnotherTypeIsReplacedNotDereferencedAsNull (U8 → F32 and
back). It segfaulted before the fix.

4. navmap_io: a map too big for one CDR message failed with a bare I/O error

Fast-CDR cannot serialise more than 4 GiB. save_msg_to_file / save_to_file of a bigger map
(about 180 M NavCels with one layer) allocated gigabytes, then failed deep inside the serialiser
with a non-std exception that was reported as EIO. Now:

  • SaveOptions::max_serialized_bytes (default 4 GiB − 1 MiB) and
    estimated_serialized_size(const msg::NavMap &), a close upper estimate computed from the array
    sizes, without serialising;
  • the saver refuses a map above the limit before allocating anything: std::errc::file_too_large
    (EFBIG) and a log line with the estimated size. Nothing is written.

Tests: NavMapIoMsg.TheSizeEstimateIsCloseAboveTheSerialisedSize (above the real size by less than
a few KB), NavMapIoMsg.AMapBeyondWhatAMessageHoldsIsRefusedAsTooLargeAndNothingIsWritten (a small
limit, so the test needs no 4 GB map).

No API is removed or changed in a breaking way: one field added to SaveOptions (with the old
behaviour's effective limit as default) and one free function added.

Tests

colcon test --packages-select navmap_core navmap_ros

navmap_core: test_incremental (8), test_navmap_easy (19), test_distance_field (8),
test_navmap_assign (7) and the rest of the suite pass; navmap_ros: test_navmap_io (6) and the
rest pass. Built and run on ROS 2 Rolling, Ubuntu 24.04.

This branch has not been deployed

No deployments
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.

1 participant