Skip to content

Avoid heap-allocating background processor futures#4733

Open
Abeeujah wants to merge 1 commit into
lightningdevkit:mainfrom
Abeeujah:drop-box
Open

Avoid heap-allocating background processor futures#4733
Abeeujah wants to merge 1 commit into
lightningdevkit:mainfrom
Abeeujah:drop-box

Conversation

@Abeeujah

Copy link
Copy Markdown
Contributor
The persistence loop in process_events_async boxed each task future
(ChannelManager, network graph, scorer, sweeper, LiquidityManager)
before handing it to the Joiner, incurring a heap allocation per task
on every iteration. Box::pin was only needed to satisfy the Joiner's
Unpin bound when our MSRV predated core::pin::pin!.

Now that the MSRV is 1.75, replace Box::pin with core::pin::pin! so the
futures live on the stack. Pin<&mut F> is Unpin, so the Joiner bounds
still hold. To pin every future before the Joiner borrows it, build all
five uniformly at the end of the iteration: run the synchronous timer
checks and side effects first, capture their outcomes in flags, and
gate each future's work behind those flags. Futures whose work is
skipped resolve to Ok(()) immediately, preserving the previous behavior
where an unset Joiner slot was treated as Ready(Ok(())).

The eager single poll of the ChannelManager future and the early
loop-exit (break) semantics on the scorer/sweeper timers are unchanged.

@ldk-reviews-bot

ldk-reviews-bot commented Jun 20, 2026

Copy link
Copy Markdown

I've assigned @valentinewallace as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@ldk-claude-review-bot

ldk-claude-review-bot commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

I've re-verified the current file against the diff and my prior review. The code is unchanged and my previous analysis holds.

No issues found.

All previously verified concerns remain correctly handled:

  • pending_monitor_writes (lib.rs:1129) captured synchronously inside the eager-polled cm_fut body, after get_and_clear_needs_persistence() (lib.rs:1125), no intervening await — race window preserved.
  • network_graph_to_persist (lib.rs:1209) holds the prunable_network_graph() borrow until the deferred future resolves.
  • Eager-poll → set_a_res/set_a mapping (lib.rs:1158-1161) reproduces old Poll::Ready/Poll::Pending slot semantics; unset slot A (when !needs_cm_persist) resolves to Ok(()).
  • All break timer semantics (lib.rs:1180, 1199, 1257, 1266) and synchronous side effects (archive, network graph prune, scorer time_passed) preserved before futures are built.
  • Skipped-work futures (b/c/d/e) all resolve Ok(()), matching old unset-slot behavior.

The only outstanding note (non-blocking, already raised in a prior pass) is the non-standard line break on scorer_fut at lib.rs:1288-1289 that rustfmt would reformat. No new issues to report.

Comment thread lightning-background-processor/src/lib.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @valentinewallace! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

// Captured by the ChannelManager persistence future below. Only meaningful when
// `needs_cm_persist` is set, declared before the futures so it outlives them
// (drop order).
let pending_monitor_writes =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can stay in the if/future and avoid the if.

let mut waker = dummy_waker();
let mut ctx = task::Context::from_waker(&mut waker);
match core::pin::Pin::new(&mut fut).poll(&mut ctx) {
task::Poll::Ready(res) => futures.set_a_res(res),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe there's any reason to change this, it would probably be simpler to keep the code structured the way it was.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've simplified it, although, I couldn't fully restore the original now that the futures are stack-pinned, Joiner holds Pin<&mut> references into them and so must be declared after all of them.

Meaning it doesn't exist yet at the eager-poll site, so the outcome has to be stashed and fed in once the Joiner is constructed. Open to a cleaner approach if you have any recommendations.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I had only tested a smaller patch and was hoping it would move up, but at least this reduces patch size marginally:

diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs
index b2b0c8d9b2..f2b3cdd183 100644
--- a/lightning-background-processor/src/lib.rs
+++ b/lightning-background-processor/src/lib.rs
@@ -1122,2 +1122,4 @@ where

+               let mut futures = Joiner::new();
+
                let needs_cm_persist = channel_manager.get_cm().get_and_clear_needs_persistence();
@@ -1149,3 +1151,2 @@ where
                // outcome and feed it into the `Joiner` once it is constructed.
-               let mut cm_persist_res = None;
                if needs_cm_persist {
@@ -1156,4 +1157,5 @@ where
                        let mut ctx = task::Context::from_waker(&mut waker);
-                       if let task::Poll::Ready(res) = cm_fut.as_mut().poll(&mut ctx) {
-                               cm_persist_res = Some(res);
+                       match cm_fut.as_mut().poll(&mut ctx) {
+                               task::Poll::Ready(res) => futures.set_a_res(res),
+                               task::Poll::Pending => futures.set_a(cm_fut),
                        }
@@ -1283,2 +1285,4 @@ where
                });
+               futures.set_b(network_graph_fut);
+
                let scorer_fut =
@@ -1302,2 +1306,4 @@ where
                        });
+               futures.set_c(scorer_fut);
+
                let sweeper_fut = core::pin::pin!(async {
@@ -1310,2 +1316,4 @@ where
                });
+               futures.set_d(sweeper_fut);
+
                let lm_fut = core::pin::pin!(async {
@@ -1329,12 +1337,2 @@ where
                });
-
-               let mut futures = Joiner::new();
-               match cm_persist_res {
-                       Some(res) => futures.set_a_res(res),
-                       None if needs_cm_persist => futures.set_a(cm_fut),
-                       None => {},
-               }
-               futures.set_b(network_graph_fut);
-               futures.set_c(scorer_fut);
-               futures.set_d(sweeper_fut);
                futures.set_e(lm_fut);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, thanks, this works 👍

@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.81250% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.95%. Comparing base (6965bc9) to head (3cad6af).
⚠️ Report is 3224 commits behind head on main.

Files with missing lines Patch % Lines
lightning-background-processor/src/lib.rs 82.81% 9 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4733      +/-   ##
==========================================
+ Coverage   84.55%   86.95%   +2.39%     
==========================================
  Files         137      161      +24     
  Lines       77617   111645   +34028     
  Branches    77617   111645   +34028     
==========================================
+ Hits        65627    97076   +31449     
- Misses       9948    12061    +2113     
- Partials     2042     2508     +466     
Flag Coverage Δ
fuzzing-fake-hashes 8.43% <ø> (?)
fuzzing-real-hashes 32.40% <ø> (?)
tests 86.27% <82.81%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Abeeujah Abeeujah force-pushed the drop-box branch 2 times, most recently from 16d7eef to f91e367 Compare June 24, 2026 11:06
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 2nd Reminder

Hey @valentinewallace! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Replace Box::pin with core::pin::pin! in process_events_async now that
MSRV is 1.75. This eliminates a heap allocation per task on every
loop iteration by pinning the futures directly to the stack.

To satisfy lifetime and Joiner bounds, the loop logic was refactored
to run synchronous timer checks first, using flags to conditionally
execute the stack-pinned futures. Existing eager polling and early-break
semantics are preserved.
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.

5 participants