Skip to content

Commit d7dffbd

Browse files
author
jonas
committed
refactor(navigator): cap safe point batch size and better memory handling
1 parent 2a15a27 commit d7dffbd

15 files changed

Lines changed: 174 additions & 222 deletions

boards/px4/fmu-v6c/visionTargetEstStatic.px4board

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,4 @@ CONFIG_DRIVERS_GNSS_SEPTENTRIO=n
9494
CONFIG_DRIVERS_IMU_BOSCH_BMI055=n
9595
CONFIG_DRIVERS_IMU_INVENSENSE_ICM42688P=y
9696
CONFIG_RTL_MISSION_CACHE_SIZE=300
97-
CONFIG_RTL_SAFE_POINT_BATCH_SIZE=64
97+
CONFIG_RTL_SAFE_POINT_BATCH_SIZE=32

docs/en/concept/mission_route_planning.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Because the default is `0` on non-testing boards, route planning is disabled unt
2323
:::
2424

2525
::: warning
26-
Rally points are scored in fixed-size batches of `CONFIG_RTL_SAFE_POINT_BATCH_SIZE` (default **1**, **64** on testing builds).
26+
Rally points are scored in fixed-size batches of `CONFIG_RTL_SAFE_POINT_BATCH_SIZE` (default **1**, **32** on testing builds).
2727
When more rally points are configured than fit in one batch the planner re-scans the full mission route once per batch, so a mission with many rally points can be walked several times per planning pass. See [Safe-Point Batching](#safe-point-batching).
2828
:::
2929

@@ -229,8 +229,8 @@ If a new Return is requested, the vehicle flies straight to the rally point (`R`
229229

230230
### Safe-Point Batching {#safe-point-batching}
231231

232-
The planner scores eligible safe points using a single fixed-size projection batch buffer, sized by `CONFIG_RTL_SAFE_POINT_BATCH_SIZE` (default **1**, **64** on testing builds).
233-
The batch buffer is reused for every planning pass and is allocated in static RAM, costing roughly `sizeof(ProjectionReference) * CONFIG_RTL_SAFE_POINT_BATCH_SIZE` bytes (about 370 bytes per slot).
232+
The planner scores eligible safe points using a single fixed-size projection batch buffer, sized by `CONFIG_RTL_SAFE_POINT_BATCH_SIZE` (default **1**, **32** on testing builds).
233+
The batch buffer is reused for every planning pass and is allocated in static RAM, costing roughly `sizeof(ProjectionReference) * CONFIG_RTL_SAFE_POINT_BATCH_SIZE` bytes (about 380 bytes per slot).
234234
The default of **1** keeps that cost negligible on boards that leave route planning disabled (`CONFIG_RTL_MISSION_CACHE_SIZE=0`); boards that enable route planning should raise it.
235235

236236

@@ -239,10 +239,10 @@ If the number of eligible rally points exceeds `CONFIG_RTL_SAFE_POINT_BATCH_SIZE
239239
To keep every rally point in a single mission scan, raise `CONFIG_RTL_SAFE_POINT_BATCH_SIZE` on boards that have the RAM budget for it.
240240
:::
241241

242-
`CONFIG_RTL_SAFE_POINT_BATCH_SIZE` must stay between `1` and `255` because the batch counters and indices are stored in `uint8_t`.
242+
`CONFIG_RTL_SAFE_POINT_BATCH_SIZE` must stay between `1` and `32`: the upper bound matches `DM_KEY_SAFE_POINTS_MAX`, the maximum number of storable safe points, so larger batches could never be filled.
243243

244244
```ini
245-
CONFIG_RTL_SAFE_POINT_BATCH_SIZE=64
245+
CONFIG_RTL_SAFE_POINT_BATCH_SIZE=32
246246
```
247247

248248
## Route Cache

src/lib/dataman_client/DatamanClient.cpp

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -600,13 +600,17 @@ bool DatamanCache::loadWait(dm_item_t item, uint32_t index, uint8_t *buffer, uin
600600
bool item_found = false;
601601

602602
if (_items) {
603-
for (uint32_t i = 0; i < _num_items; ++i) {
603+
// Start scanning at the last hit
604+
for (uint32_t n = 0; n < _num_items; ++n) {
605+
const uint32_t i = (_search_hint + n) % _num_items;
606+
604607
if ((_items[i].response.item == item) &&
605608
(_items[i].response.index == index)) {
606609
item_found = true;
607610

608611
if (_items[i].cache_state == State::ResponseReceived) {
609612
memcpy(buffer, _items[i].response.data, length);
613+
_search_hint = i;
610614
success = true;
611615
break;
612616
}
@@ -672,17 +676,14 @@ bool DatamanCache::updateCachedItem(dm_item_t item, uint32_t index, const uint8_
672676
return false;
673677
}
674678

679+
// Only patch data that is fully received and stable. Slots in other states are skipped:
680+
// an in-flight async read will fetch the new SD card data shortly anyway, and invalidated
681+
// slots can still carry a stale key match for the same item/index.
675682
for (uint32_t i = 0; i < _num_items; ++i) {
676-
if ((_items[i].response.item == item) && (_items[i].response.index == index)) {
677-
678-
// Only patch data that is fully received and stable. If it is RequestPrepared,
679-
// an async read is flying and will fetch the new SD card data shortly anyway.
680-
if (_items[i].cache_state == State::ResponseReceived) {
681-
memcpy(_items[i].response.data, buffer, length);
682-
return true;
683-
}
684-
685-
return false; // Item exists but is not in a safe state to patch
683+
if ((_items[i].response.item == item) && (_items[i].response.index == index)
684+
&& (_items[i].cache_state == State::ResponseReceived)) {
685+
memcpy(_items[i].response.data, buffer, length);
686+
return true;
686687
}
687688
}
688689

@@ -760,6 +761,7 @@ void DatamanCache::resetCacheState()
760761
_update_index = 0;
761762
_item_counter = 0;
762763
_load_index = 0;
764+
_search_hint = 0;
763765
}
764766

765767
void DatamanCache::invalidate()

src/lib/dataman_client/DatamanClient.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ class DatamanCache
317317
uint32_t _update_index{0}; ///< index for tracking last index used by update function
318318
uint32_t _item_counter{0}; ///< number of items to process with update function
319319
uint32_t _num_items{0}; ///< number of items that cache can store
320+
uint32_t _search_hint{0}; ///< slot of the last loadWait hit, speeds up sequential lookups
320321

321322
DatamanClient _client{};
322323

src/modules/navigator/Kconfig

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ config RTL_MISSION_CACHE_SIZE
4343

4444
config RTL_SAFE_POINT_BATCH_SIZE
4545
int "Maximum rally points scored per mission-route planning batch"
46-
default 64 if BOARD_TESTING
46+
default 32 if BOARD_TESTING
4747
default 1
48-
range 1 255
48+
range 1 32
4949
depends on MODULES_NAVIGATOR
5050
---help---
5151
Number of rally (safe) points held in the fixed-size projection batch buffer that the
@@ -58,11 +58,11 @@ config RTL_SAFE_POINT_BATCH_SIZE
5858
boards with spare RAM can raise it so all rally points fit in a single pass.
5959

6060
The projection batch is allocated in static RAM and is the larger hidden cost: it reserves
61-
roughly sizeof(ProjectionReference) * CONFIG_RTL_SAFE_POINT_BATCH_SIZE bytes (about 370 bytes
62-
per slot, i.e. ~24 KB at a batch size of 64).
61+
roughly sizeof(ProjectionReference) * CONFIG_RTL_SAFE_POINT_BATCH_SIZE bytes (about 380 bytes
62+
per slot, i.e. ~12 KB at the maximum batch size of 32).
6363

6464
The default is 1 so that boards which leave route planning disabled (CONFIG_RTL_MISSION_CACHE_SIZE=0)
6565
do not pay the full batch RAM cost; boards that enable route planning should raise it.
6666

67-
The configured batch size must stay <= 255 because the batch counters and
68-
indices are stored in uint8_t.
67+
The upper bound of 32 comes from DM_KEY_SAFE_POINTS_MAX, the maximum number of storable
68+
safe points: batch slots beyond that count can never be filled.

src/modules/navigator/mission_route_cache.cpp

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,17 @@
4444

4545
#include "mission_route_cache.h"
4646

47+
#include "mission_route_types.h"
48+
4749
#include <inttypes.h>
4850

4951
#include <px4_platform_common/log.h>
5052

53+
// The projection batch only ever holds safe points, so a batch larger than the
54+
// maximum number of storable safe points is wasted RAM.
55+
static_assert(mission_route::kMaxSafePointBatch <= DM_KEY_SAFE_POINTS_MAX,
56+
"CONFIG_RTL_SAFE_POINT_BATCH_SIZE must not exceed DM_KEY_SAFE_POINTS_MAX");
57+
5158
void MissionRouteCache::update(const mission_s &mission)
5259
{
5360
updateMissionCache(mission);
@@ -75,12 +82,6 @@ bool MissionRouteCache::queueMissionCacheLoads(const mission_s &mission)
7582
// Rebuild the mission cache from scratch, callers retry the full queue on failure.
7683
_dataman_cache_mission.invalidate();
7784

78-
if (static_cast<int32_t>(_dataman_cache_mission.size()) < mission.count) {
79-
PX4_ERR("Mission cache capacity too small! requested: %d, capacity: %d",
80-
static_cast<int>(mission.count), static_cast<int>(_dataman_cache_mission.size()));
81-
return false;
82-
}
83-
8485
const dm_item_t mission_dataman_id = static_cast<dm_item_t>(mission.mission_dataman_id);
8586

8687
for (int32_t index = 0; index < mission.count; ++index) {
@@ -223,7 +224,7 @@ bool MissionRouteCache::getMissionLandItem(int32_t &index, mission_item_s &land_
223224
return false;
224225
}
225226

226-
if (cached_land_item.nav_cmd != NAV_CMD_LAND && cached_land_item.nav_cmd != NAV_CMD_VTOL_LAND) {
227+
if (!mission_route::isLandingCmd(cached_land_item.nav_cmd)) {
227228
return false;
228229
}
229230

@@ -284,6 +285,13 @@ void MissionRouteCache::updateMissionCache(const mission_s &mission)
284285
state.ready = mission.count == 0;
285286
state.too_large = missionExceedsCacheLimit(mission);
286287

288+
// If mission exceed the capacity (e.g. the boot-time cache allocation failed) no recovery is possible.
289+
if (!state.too_large && static_cast<int32_t>(_dataman_cache_mission.size()) < mission.count) {
290+
PX4_ERR("Mission cache capacity too small, cache disabled! requested: %d, capacity: %d",
291+
static_cast<int>(mission.count), static_cast<int>(_dataman_cache_mission.size()));
292+
state.too_large = true;
293+
}
294+
287295
if (!state.too_large && mission.count > 0) {
288296
state.validation_pending = queueMissionCacheLoads(mission);
289297

src/modules/navigator/mission_route_planner.cpp

Lines changed: 44 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -54,27 +54,22 @@
5454
using namespace mission_route;
5555

5656
/**
57-
* The ProjectionReferenceBatch is reused by collectVehicleProjection and selectSafePoint.
58-
* Defined static so navigator task stack does not scale with the batch size (~370 bytes per kMaxSafePointBatch slot,
59-
* i.e. ~24 KB at a batch size of 64, tunable via CONFIG_RTL_SAFE_POINT_BATCH_SIZE).
57+
* Shared scratch buffer reused by collectVehicleProjection and selectSafePoint, kept off the
58+
* navigator task stack because it scales with CONFIG_RTL_SAFE_POINT_BATCH_SIZE (~380 bytes per slot).
6059
*
61-
* Because the type defaults some fields to NAN/-1/NAV_CMD_INVALID, raw storage is kept in .bss and constructed
62-
* at runtime. This preserves the normal default object state without storing the full initialized image
63-
* in flash.
60+
* The buffer must not be a plain static object: ProjectionReferenceBatch has non-zero member
61+
* defaults (NAN/-1/NAV_CMD_INVALID), so a static instance would be initialized into
62+
* .data and store its full byte image in flash, which does not fit on some boards. Zeroed raw
63+
* storage stays in .bss (RAM reserved at link time, no flash image) and the object is
64+
* constructed in place on first use. Placement new on static storage cannot fail, so the
65+
* returned reference is always valid.
6466
*/
65-
alignas(ProjectionReferenceBatch)
66-
static uint8_t _projection_reference_batch_storage[sizeof(ProjectionReferenceBatch)] {};
67-
68-
static ProjectionReferenceBatch *_projection_reference_batch{nullptr};
69-
70-
static ProjectionReferenceBatch *projectionReferenceBatch()
67+
static ProjectionReferenceBatch &projectionReferenceBatch()
7168
{
72-
if (_projection_reference_batch == nullptr) {
73-
_projection_reference_batch =
74-
new (_projection_reference_batch_storage) ProjectionReferenceBatch{};
75-
}
69+
alignas(ProjectionReferenceBatch) static uint8_t storage[sizeof(ProjectionReferenceBatch)];
70+
static ProjectionReferenceBatch *batch = new (storage) ProjectionReferenceBatch{};
7671

77-
return _projection_reference_batch;
72+
return *batch;
7873
}
7974

8075
namespace
@@ -129,47 +124,40 @@ MissionRoutePlanner::MissionRoutePlanner(const Provider &provider) :
129124
_goal_selector(provider, _projection),
130125
_reference_batch(projectionReferenceBatch())
131126
{
132-
if (_reference_batch == nullptr) {
133-
PX4_ERR("RTL route planner scratch init failed");
134-
}
135127
}
136128

137-
VehicleProjectionResult MissionRoutePlanner::collectVehicleProjection(const RoutePlanRequest &request) const
129+
MissionRoutePlanner::~MissionRoutePlanner()
138130
{
139-
if (_reference_batch == nullptr) {
140-
return vehicleProjectionFailure(FailureReason::kInternalError);
141-
}
131+
perf_free(_collect_vehicle_projection_perf);
132+
perf_free(_select_best_goal_perf);
133+
}
142134

143-
if (!request.config.parameters.validForVehicleProjection()) {
135+
VehicleProjectionResult MissionRoutePlanner::collectVehicleProjection(const Position &vehicle_position,
136+
int32_t mission_index, const PlannerConfig &config) const
137+
{
138+
if (!config.parameters.validForVehicleProjection()) {
144139
return vehicleProjectionFailure(FailureReason::kInvalidRequest);
145140
}
146141

147-
perf_begin(_collect_vehicle_projection_perf.counter);
148-
const VehicleProjectionResult result = _projection.collectVehicleProjection(request, *_reference_batch);
149-
perf_end(_collect_vehicle_projection_perf.counter);
142+
perf_begin(_collect_vehicle_projection_perf);
143+
const VehicleProjectionResult result = _projection.collectVehicleProjection(vehicle_position, mission_index,
144+
config, _reference_batch);
145+
perf_end(_collect_vehicle_projection_perf);
150146
return result;
151147
}
152148

153149
GoalSelection MissionRoutePlanner::selectSafePoint(const ProjectionContext &projection_context,
154150
const PlannerConfig &config) const
155151
{
156-
if (_reference_batch == nullptr) {
157-
return {};
158-
}
159-
160-
return _goal_selector.selectSafePoint(projection_context, config, *_reference_batch).selection;
152+
return _goal_selector.selectSafePoint(projection_context, config, _reference_batch).selection;
161153
}
162154

163155
GoalSelection MissionRoutePlanner::selectBestGoal(const ProjectionContext &projection_context,
164156
const PlannerConfig &config) const
165157
{
166-
if (_reference_batch == nullptr) {
167-
return {};
168-
}
169-
170-
perf_begin(_select_best_goal_perf.counter);
171-
const GoalSelectionResult result = _goal_selector.selectBestGoal(projection_context, config, *_reference_batch);
172-
perf_end(_select_best_goal_perf.counter);
158+
perf_begin(_select_best_goal_perf);
159+
const GoalSelectionResult result = _goal_selector.selectBestGoal(projection_context, config, _reference_batch);
160+
perf_end(_select_best_goal_perf);
173161
return result.selection;
174162
}
175163

@@ -179,13 +167,14 @@ bool MissionRoutePlanner::closeToBranchOffSegment(const Position &position, cons
179167
return _goal_selector.closeToBranchOffSegment(position, selection, acceptance_radius, altitude_acceptance_radius);
180168
}
181169

182-
JoinPlanResult MissionRoutePlanner::planMissionResumeJoin(const RoutePlanRequest &request) const
170+
JoinPlanResult MissionRoutePlanner::planMissionResumeJoin(const Position &vehicle_position,
171+
int32_t mission_index, const PlannerConfig &config) const
183172
{
184-
if (!request.vehicle_position.valid()) {
173+
if (!vehicle_position.valid()) {
185174
return joinPlanFailure(FailureReason::kNoValidGlobalPos);
186175
}
187176

188-
const VehicleProjectionResult projection = collectVehicleProjection(request);
177+
const VehicleProjectionResult projection = collectVehicleProjection(vehicle_position, mission_index, config);
189178

190179
if (!projection.success) {
191180
return joinPlanFailure(projection.failure_reason);
@@ -195,8 +184,8 @@ JoinPlanResult MissionRoutePlanner::planMissionResumeJoin(const RoutePlanRequest
195184
plan.projection_context = projection.projection_context;
196185

197186
plan.path = _goal_selector.findShortestPathAlongRoute(plan.projection_context.route_length,
198-
plan.projection_context, request.config, PathDirectionMode::kForceNominal);
199-
plan.join_context = _goal_selector.buildJoinContext(request.vehicle_position, plan.projection_context, plan.path);
187+
plan.projection_context, config, PathDirectionMode::kForceNominal);
188+
plan.join_context = _goal_selector.buildJoinContext(vehicle_position, plan.projection_context, plan.path);
200189

201190
if (!plan.valid()) {
202191
return joinPlanFailure(FailureReason::kNoValidPath);
@@ -205,13 +194,14 @@ JoinPlanResult MissionRoutePlanner::planMissionResumeJoin(const RoutePlanRequest
205194
return joinPlanSuccess(plan);
206195
}
207196

208-
RoutePlanResult MissionRoutePlanner::planRouteToGoal(const RoutePlanRequest &request) const
197+
RoutePlanResult MissionRoutePlanner::planRouteToGoal(const Position &vehicle_position,
198+
int32_t mission_index, const PlannerConfig &config) const
209199
{
210-
if (!request.config.parameters.validForRouteToGoal()) {
200+
if (!config.parameters.validForRouteToGoal()) {
211201
return routePlanFailure(FailureReason::kInvalidRequest);
212202
}
213203

214-
const VehicleProjectionResult projection = collectVehicleProjection(request);
204+
const VehicleProjectionResult projection = collectVehicleProjection(vehicle_position, mission_index, config);
215205

216206
if (!projection.success) {
217207
return routePlanFailure(projection.failure_reason);
@@ -225,20 +215,20 @@ RoutePlanResult MissionRoutePlanner::planRouteToGoal(const RoutePlanRequest &req
225215
plan.projection_context.mission_loops_remaining = 0;
226216

227217
// Find closest safe point, falling back to mission end points if none found
228-
perf_begin(_select_best_goal_perf.counter);
229-
const GoalSelectionResult selection = _goal_selector.selectBestGoal(plan.projection_context, request.config,
230-
*_reference_batch);
231-
perf_end(_select_best_goal_perf.counter);
218+
perf_begin(_select_best_goal_perf);
219+
const GoalSelectionResult selection = _goal_selector.selectBestGoal(plan.projection_context, config,
220+
_reference_batch);
221+
perf_end(_select_best_goal_perf);
232222

233223
if (!selection.success) {
234224
return routePlanFailure(selection.failure_reason);
235225
}
236226

237227
plan.selection = selection.selection;
238228
plan.selection.skip_route_to_safe_point =
239-
_goal_selector.canSkipRouteFollowToSelectedGoal(request.vehicle_position, plan.selection, request.config);
229+
_goal_selector.canSkipRouteFollowToSelectedGoal(vehicle_position, plan.selection, config);
240230

241-
plan.join_context = _goal_selector.buildJoinContext(request.vehicle_position, plan.projection_context,
231+
plan.join_context = _goal_selector.buildJoinContext(vehicle_position, plan.projection_context,
242232
plan.selection.path);
243233

244234
if (!plan.valid()) {

0 commit comments

Comments
 (0)