FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(uavcan): correct TX queue block accounting and report queue pressure by dakejahl · Pull Request #28395 · PX4/PX4-Autopilot · GitHub

fix(uavcan): correct TX queue block accounting and report queue pressure - #28395

Merged
dakejahl merged 2 commits into
mainfrom
dakejahl/uavcan-tx-queue-diagnostics
Aug 26, 2026
Merged

dakejahl merged 2 commits into
mainfrom
dakejahl/uavcan-tx-queue-diagnostics

Conversation

Copy link
Copy Markdown
Contributor

Summary

Two changes to the DroneCAN TX path: a fix for a block-accounting bug that permanently shrinks a TX queue, and per-interface queue diagnostics in uavcan status.

Problem

LimitedPoolAllocator::allocate() incremented used_blocks_ before asking the underlying allocator for the block. When the shared pool could not satisfy the request, the quota was spent on a block that was never returned, and nothing gives it back — deallocate() only runs for a pointer that was handed out. Every queue that asked for memory while the pool was empty stays permanently smaller than its configured limit, and the loss accumulates until the node restarts. The pool is shared between the RX side and one TX queue per interface, so it does empty transiently under load; serving a node firmware update is enough to do it.

Separately, there was no way to see any of this from a running system. CanTxQueue counts rejected frames, but tx_queues_ is private and CanIOManager exposed no accessor. The count also conflates three distinct causes — a frame arriving with its deadline already passed, a queued frame evicted as expired to make room, and a frame dropped with the queue at its block limit. Only the latter two are memory pressure, and the responses are opposite, so one number points the wrong way as often as not.

Solution

Count the block only after the allocator returns one.

Add CanIOManager::getTxQueuePerfCounters() and split the reject count, so uavcan status reports per interface:

	TX queue peak: 44/84 blocks
	TX rejected:   80 frames (78 expired, 2 no memory)

The peak against the limit is what makes the count actionable. The limit is pool_soft / (num_ifaces + 1) + 1, derived from CapacitySoftLimit in allocator.hpp, so a peak sitting at the limit says raising that capacity will help, and a peak well under it says frames are ageing out for another reason and a larger pool will not.

Measured on an FMU-v6XRT serving DroneCAN node firmware updates over two buses: one node holds 44 of 84 blocks and drops nothing, while another pins the limit and drops hundreds per update. The raw counts did not distinguish those, and quadrupling CapacitySoftLimit moved neither node's download time — the second node simply refilled the larger queue (84/84, then 167/167, then 334/334).

LimitedPoolAllocator::allocate() incremented used_blocks_ before asking the
underlying allocator, so an allocation the pool could not satisfy still spent
quota. Nothing ever hands that block back, because deallocate() only runs for a
pointer that was returned, so every queue that asked for memory while the pool
was empty stays permanently smaller than its configured limit.

The pool is shared between the RX side and one TX queue per interface, so it
does empty transiently under load -- a DroneCAN node firmware update is enough.
The damage accumulates and is only cleared by restarting the node.
`uavcan status` gains, per interface:

	TX queue peak: 44/84 blocks
	TX rejected:   80 frames (78 expired, 2 no memory)

CanTxQueue already counted rejected frames but tx_queues_ is private and
CanIOManager exposed no accessor, so the count was unreachable. It also conflated
three causes: a frame handed over with its deadline already passed, a queued frame
evicted as expired to make room, and a frame dropped because the queue was at its
block limit. Only the last two are memory pressure, and they call for opposite
responses, so a single count sends you the wrong way.

The peak is what makes the count actionable. The limit is
pool_soft / (num_ifaces + 1) + 1, derived from the soft capacity in
allocator.hpp, so a peak sitting at the limit says raising that capacity will
help and a peak well under it says the frames are ageing out for some other
reason and a larger pool will not.

Measured on an FMU-v6XRT serving a DroneCAN node firmware update: one node holds
44 of 84 blocks and drops nothing, another pins the limit and drops hundreds. The
counts alone did not distinguish those.
github-actions Bot added kind:bug Something is broken or behaving incorrectly. scope:drivers Device drivers and hardware interfaces. scope:middleware DDS, ROS 2, Cyphal/UAVCAN, zenoh, or bridge layers. labels Aug 26, 2026

Copy link
Copy Markdown
Contributor

🔎 FLASH Analysis

px4_fmu-v5x [Total VM Diff: 288 byte (0.01 %)]
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0%    +288  +0.0%    +288    .text
    [NEW]    +110  [NEW]    +110    uavcan::CanIOManager::getTxQueuePerfCounters()
    +0.1%     +88  +0.1%     +88    [section .text]
    +8.6%     +56  +8.6%     +56    UavcanNode::print_info()
     +92%     +22   +92%     +22    uavcan::LimitedPoolAllocator::allocate()
    [NEW]     +18  [NEW]     +18    uavcan::CanTxQueue::registerExpiredFrame()
    [NEW]     +18  [NEW]     +18    uavcan::CanTxQueue::registerOutOfMemoryFrame()
   -85.0%     +12 -85.0%     +12    [6 Others]
    +3.8%      +8  +3.8%      +8    uavcan::CanIOManager::CanIOManager()
    +2.0%      +4  +2.0%      +4    UavcanNode::publish_can_interface_statuses()
     +33%      +2   +33%      +2    uavcan::Dispatcher::hasServer()
     +33%      +2   +33%      +2    uavcan::Dispatcher::hasSubscriber()
    -0.2%      -2  -0.2%      -2    matrix::Matrix<>::operator=()
    -0.5%      -2  -0.5%      -2    uavcan::CanIOManager::send()
    -1.0%      -4  -1.0%      -4    EKFGSF_yaw::fuseVelocity()
    -1.1%      -4  -1.1%      -4    UavcanNode::cb_getset()
    -3.4%      -4  -3.4%      -4    UavcanNode::cb_restart()
    -4.5%      -4  -4.5%      -4    UavcanNode::param_opcode()
    -1.4%      -4  -1.4%      -4    UavcanNode::publish_node_statuses()
    -0.8%      -8  -0.8%      -8    UavcanNode::UavcanNode()
    -1.4%      -8  -1.4%      -8    UavcanNode::~UavcanNode()
    -0.7%     -12  -0.7%     -12    UavcanNode::Run()
  +0.0%     +67  [ = ]       0    .debug_abbrev
  +0.0%     +24  [ = ]       0    .debug_aranges
  +0.0%     +92  [ = ]       0    .debug_frame
  +0.0% +14.3Ki  [ = ]       0    .debug_info
  +0.0%    +509  [ = ]       0    .debug_line
     +25%      +1  [ = ]       0    [Unmapped]
    +0.0%    +508  [ = ]       0    [section .debug_line]
  +0.0%    +345  [ = ]       0    .debug_loclists
  +0.0%     +91  [ = ]       0    .debug_rnglists
  +0.0%    +896  [ = ]       0    .debug_str
  +0.8%      +2  [ = ]       0    .shstrtab
  +0.0%    +150  [ = ]       0    .strtab
   -39.0%     -16  [ = ]       0    __nxsem_clockwait_veneer
     +80%     +16  [ = ]       0    __sched_lock_veneer
    [NEW]     +52  [ = ]       0    uavcan::CanIOManager::getTxQueuePerfCounters()
    [NEW]     +47  [ = ]       0    uavcan::CanTxQueue::registerExpiredFrame()
    [NEW]     +51  [ = ]       0    uavcan::CanTxQueue::registerOutOfMemoryFrame()
  +0.0%     +96  [ = ]       0    .symtab
    +100%     +32  [ = ]       0    EKFGSF_yaw::fuseVelocity()
     +20%     +16  [ = ]       0    UavcanNode::print_params()
   -50.0%     -16  [ = ]       0    UavcanNode::update_params()
    -0.5%     -64  [ = ]       0    [section .symtab]
   -40.0%     -32  [ = ]       0    __nxsem_clockwait_veneer
     +67%     +32  [ = ]       0    __sched_lock_veneer
    [NEW]     +32  [ = ]       0    uavcan::CanIOManager::getTxQueuePerfCounters()
    +100%     +16  [ = ]       0    uavcan::CanIOManager::send()
     +50%     +16  [ = ]       0    uavcan::CanTxQueue::push()
    [NEW]     +32  [ = ]       0    uavcan::CanTxQueue::registerExpiredFrame()
    [NEW]     +32  [ = ]       0    uavcan::CanTxQueue::registerOutOfMemoryFrame()
  -2.9%    -288  [ = ]       0    [Unmapped]
  +0.0% +16.5Ki  +0.0%    +288    TOTAL

px4_fmu-v6x [Total VM Diff: 296 byte (0.02 %)]
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0%    +296  +0.0%    +296    .text
    [NEW]    +110  [NEW]    +110    uavcan::CanIOManager::getTxQueuePerfCounters()
    +0.1%     +88  +0.1%     +88    [section .text]
    +8.6%     +56  +8.6%     +56    UavcanNode::print_info()
     +92%     +22   +92%     +22    uavcan::LimitedPoolAllocator::allocate()
    [NEW]     +18  [NEW]     +18    uavcan::CanTxQueue::registerExpiredFrame()
    [NEW]     +18  [NEW]     +18    uavcan::CanTxQueue::registerOutOfMemoryFrame()
   -83.9%     +10 -83.9%     +10    [5 Others]
    +3.8%      +8  +3.8%      +8    uavcan::CanIOManager::CanIOManager()
    +2.0%      +4  +2.0%      +4    UavcanNode::publish_can_interface_statuses()
    +0.2%      +2  +0.2%      +2    matrix::Matrix<>::operator=()
     +33%      +2   +33%      +2    uavcan::Dispatcher::hasServer()
     +33%      +2   +33%      +2    uavcan::Dispatcher::hasSubscriber()
     +11%      +2   +11%      +2    uavcan::Dispatcher::registerMessageListener()
    -0.5%      -2  -0.5%      -2    uavcan::CanIOManager::send()
    -1.1%      -4  -1.1%      -4    UavcanNode::cb_getset()
    -3.4%      -4  -3.4%      -4    UavcanNode::cb_restart()
    -4.5%      -4  -4.5%      -4    UavcanNode::param_opcode()
    -1.4%      -4  -1.4%      -4    UavcanNode::publish_node_statuses()
    -0.8%      -8  -0.8%      -8    UavcanNode::UavcanNode()
    -1.4%      -8  -1.4%      -8    UavcanNode::~UavcanNode()
    -0.7%     -12  -0.7%     -12    UavcanNode::Run()
  +0.0%     +67  [ = ]       0    .debug_abbrev
  +0.0%     +24  [ = ]       0    .debug_aranges
  +0.0%     +92  [ = ]       0    .debug_frame
  +0.1% +14.3Ki  [ = ]       0    .debug_info
  +0.0%    +511  [ = ]       0    .debug_line
    +150%      +3  [ = ]       0    [Unmapped]
    +0.0%    +508  [ = ]       0    [section .debug_line]
  +0.0%    +352  [ = ]       0    .debug_loclists
  +0.0%     +92  [ = ]       0    .debug_rnglists
    +100%      +1  [ = ]       0    [Unmapped]
    +0.0%     +91  [ = ]       0    [section .debug_rnglists]
  +0.0%    +896  [ = ]       0    .debug_str
  -0.8%      -2  [ = ]       0    .shstrtab
  +0.0%    +150  [ = ]       0    .strtab
    [NEW]     +52  [ = ]       0    uavcan::CanIOManager::getTxQueuePerfCounters()
    [NEW]     +47  [ = ]       0    uavcan::CanTxQueue::registerExpiredFrame()
    [NEW]     +51  [ = ]       0    uavcan::CanTxQueue::registerOutOfMemoryFrame()
  +0.0%     +96  [ = ]       0    .symtab
     +20%     +16  [ = ]       0    UavcanNode::print_params()
   -50.0%     -16  [ = ]       0    UavcanNode::update_params()
    -0.3%     -32  [ = ]       0    [section .symtab]
    [NEW]     +32  [ = ]       0    uavcan::CanIOManager::getTxQueuePerfCounters()
    +100%     +16  [ = ]       0    uavcan::CanIOManager::send()
     +50%     +16  [ = ]       0    uavcan::CanTxQueue::push()
    [NEW]     +32  [ = ]       0    uavcan::CanTxQueue::registerExpiredFrame()
    [NEW]     +32  [ = ]       0    uavcan::CanTxQueue::registerOutOfMemoryFrame()
  -5.2%    -296  [ = ]       0    [Unmapped]
  +0.0% +16.5Ki  +0.0%    +296    TOTAL

Updated: 2026-08-26T21:21:14

dakejahl merged commit 1888206 into main Aug 26, 2026
72 checks passed
dakejahl deleted the dakejahl/uavcan-tx-queue-diagnostics branch August 26, 2026 21:39
mrpollo pushed a commit that referenced this pull request Sep 10, 2026
…ure (#28395)

* fix(uavcan): count a TX queue block only once the pool provides it

LimitedPoolAllocator::allocate() incremented used_blocks_ before asking the
underlying allocator, so an allocation the pool could not satisfy still spent
quota. Nothing ever hands that block back, because deallocate() only runs for a
pointer that was returned, so every queue that asked for memory while the pool
was empty stays permanently smaller than its configured limit.

The pool is shared between the RX side and one TX queue per interface, so it
does empty transiently under load -- a DroneCAN node firmware update is enough.
The damage accumulates and is only cleared by restarting the node.

* feat(uavcan): report TX queue depth and why frames were dropped

`uavcan status` gains, per interface:

	TX queue peak: 44/84 blocks
	TX rejected:   80 frames (78 expired, 2 no memory)

CanTxQueue already counted rejected frames but tx_queues_ is private and
CanIOManager exposed no accessor, so the count was unreachable. It also conflated
three causes: a frame handed over with its deadline already passed, a queued frame
evicted as expired to make room, and a frame dropped because the queue was at its
block limit. Only the last two are memory pressure, and they call for opposite
responses, so a single count sends you the wrong way.

The peak is what makes the count actionable. The limit is
pool_soft / (num_ifaces + 1) + 1, derived from the soft capacity in
allocator.hpp, so a peak sitting at the limit says raising that capacity will
help and a peak well under it says the frames are ageing out for some other
reason and a larger pool will not.

Measured on an FMU-v6XRT serving a DroneCAN node firmware update: one node holds
44 of 84 blocks and drops nothing, another pins the limit and drops hundreds. The
counts alone did not distinguish those.

(cherry picked from commit 1888206)
Assisted-by: GitHub Copilot:gpt-6-astra
mrpollo pushed a commit that referenced this pull request Sep 10, 2026
peek() discards entries whose deadline has passed before returning the head of
the queue, and counted them with a bare registerRejectedFrame(). That is the
fourth drop path; the reported split covers three. The frames landed in the
total and in neither bucket, so `uavcan status` printed lines like

	TX rejected:   64 frames (0 expired, 0 no memory)

which reads as "no drops worth caring about" when 64 frames of in-flight
transfers had just been discarded. Serving a DroneCAN node firmware update hits
this path on every interface.

The total stays a separate counter rather than the sum of the two buckets: it is
what makes an unclassified path visible at all, which is how this one was found.

(cherry picked from commit c2cf9be)
Assisted-by: GitHub Copilot:gpt-6-astra
Backport-note: Companion to #28395: account for expired frames in the new TX-drop diagnostics.
mrpollo pushed a commit that referenced this pull request Sep 10, 2026
…ure (#28395)

* fix(uavcan): count a TX queue block only once the pool provides it

LimitedPoolAllocator::allocate() incremented used_blocks_ before asking the
underlying allocator, so an allocation the pool could not satisfy still spent
quota. Nothing ever hands that block back, because deallocate() only runs for a
pointer that was returned, so every queue that asked for memory while the pool
was empty stays permanently smaller than its configured limit.

The pool is shared between the RX side and one TX queue per interface, so it
does empty transiently under load -- a DroneCAN node firmware update is enough.
The damage accumulates and is only cleared by restarting the node.

* feat(uavcan): report TX queue depth and why frames were dropped

`uavcan status` gains, per interface:

	TX queue peak: 44/84 blocks
	TX rejected:   80 frames (78 expired, 2 no memory)

CanTxQueue already counted rejected frames but tx_queues_ is private and
CanIOManager exposed no accessor, so the count was unreachable. It also conflated
three causes: a frame handed over with its deadline already passed, a queued frame
evicted as expired to make room, and a frame dropped because the queue was at its
block limit. Only the last two are memory pressure, and they call for opposite
responses, so a single count sends you the wrong way.

The peak is what makes the count actionable. The limit is
pool_soft / (num_ifaces + 1) + 1, derived from the soft capacity in
allocator.hpp, so a peak sitting at the limit says raising that capacity will
help and a peak well under it says the frames are ageing out for some other
reason and a larger pool will not.

Measured on an FMU-v6XRT serving a DroneCAN node firmware update: one node holds
44 of 84 blocks and drops nothing, another pins the limit and drops hundreds. The
counts alone did not distinguish those.

(cherry picked from commit 1888206)
Assisted-by: GitHub Copilot:gpt-6-astra
mrpollo pushed a commit that referenced this pull request Sep 10, 2026
peek() discards entries whose deadline has passed before returning the head of
the queue, and counted them with a bare registerRejectedFrame(). That is the
fourth drop path; the reported split covers three. The frames landed in the
total and in neither bucket, so `uavcan status` printed lines like

	TX rejected:   64 frames (0 expired, 0 no memory)

which reads as "no drops worth caring about" when 64 frames of in-flight
transfers had just been discarded. Serving a DroneCAN node firmware update hits
this path on every interface.

The total stays a separate counter rather than the sum of the two buckets: it is
what makes an unclassified path visible at all, which is how this one was found.

(cherry picked from commit c2cf9be)
Assisted-by: GitHub Copilot:gpt-6-astra
Backport-note: Companion to #28395: account for expired frames in the new TX-drop diagnostics.
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

Labels

kind:bug Something is broken or behaving incorrectly. scope:drivers Device drivers and hardware interfaces. scope:middleware DDS, ROS 2, Cyphal/UAVCAN, zenoh, or bridge layers.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant


Back | FazBrowse Home | New Git URL