Reduce common allocations across the codebase - #2708

Merged
TheBlueMatt merged 12 commits into
lightningdevkit:mainfrom
TheBlueMatt:2023-11-less-graph-memory-frag
Nov 13, 2023
Merged

Reduce common allocations across the codebase#2708
TheBlueMatt merged 12 commits into
lightningdevkit:mainfrom
TheBlueMatt:2023-11-less-graph-memory-frag

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

My node has been experiencing more and more memory fragmentation lately, and while it seems the majority of that is #2706 and #2707, there's still plenty of room for misc improvements all over the place. With this and fixes for the other two issues we should be in a pretty good place, with allocations dominated by farrrr by block deserialization when syncing.

There's two commits here that could be performance regressions:

  • Pre-allocate the full require Vec prior to serializing into vecs which runs through our serialization logic twice in many cases before writing. I played around with a lower_bound Writeable method to optimize out some cases of having to run through the logic, but it doesn't really help in ChannelManager and ChannelMonitor or other deeply-nested structs because we're calling write there which hits our LengthCalculatingWriter instead of being able to use an optimized version. We could totally restructure the API to have Writeables call a magic method on the Writer which can short-circuit the write, but that's a lot of indirection and I'm lazy.
  • Avoid allocating when checking gossip message signatures probably isn't a huge regression, cause hashers are buffered, in essence, anyway, but I didn't check.

When we're reading a `NetworkGraph`, we know how many
nodes/channels we are reading, there's no reason not to
pre-allocate the `IndexedMap`'s inner `HashMap` and `Vec`, which we
do here.
This seems to reduce on-startup heap fragmentation with glibc by
something like 100MiB.
It does the same thing and its much simpler.
When forwarding gossip, rather than relying on Vec doubling,
pre-allocate the message encoding buffer.
...as LLVM will handle it just fine for us, in most cases.
@TheBlueMattTheBlueMatt added this to the 0.0.119 milestone Nov 4, 2023
@codecov-commenter

codecov-commenter commented Nov 4, 2023

Copy link
Copy Markdown

Codecov Report

Attention: 14 lines in your changes are missing coverage. Please review.

Comparison is base (281a0ae) 88.81% compared to head (7a951b1) 89.16%.
Report is 12 commits behind head on main.

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@ Coverage Diff @@## main #2708 +/- ##
==========================================
+ Coverage 88.81% 89.16% +0.34% 
==========================================
Files 113 113 Lines 89116 91476 +2360 Branches 89116 91476 +2360 ==========================================
+ Hits 79152 81561 +2409 + Misses 7722 7709 -13 + Partials 2242 2206 -36 
FilesCoverage Δ
lightning/src/blinded_path/utils.rs96.36% <100.00%> (-0.13%)⬇️
lightning/src/ln/channel.rs88.68% <ø> (+0.03%)⬆️
lightning/src/ln/script.rs93.57% <ø> (-0.14%)⬇️
lightning/src/routing/gossip.rs86.45% <100.00%> (+0.12%)⬆️
lightning/src/sign/type_resolver.rs75.00% <ø> (ø)
lightning/src/util/indexed_map.rs92.59% <100.00%> (+0.43%)⬆️
lightning/src/util/ser.rs76.74% <100.00%> (+0.23%)⬆️
lightning-net-tokio/src/lib.rs76.40% <94.11%> (+2.46%)⬆️
lightning/src/util/chacha20poly1305rfc.rs89.57% <75.00%> (-0.29%)⬇️
lightning/src/ln/peer_channel_encryptor.rs93.71% <95.23%> (+0.03%)⬆️
... and 1 more

... and 11 files with indirect coverage changes

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

Comment threadlightning/src/util/ser.rs
Comment threadlightning/src/util/chacha20poly1305rfc.rs Outdated
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning-net-tokio/src/lib.rs Outdated
Comment threadpending_changelog/113-channel-ser-compat.txt
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated
Comment threadlightning/src/routing/gossip.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 761aaad to f9ef511CompareNovember 6, 2023 16:58
pub(super) fn decrypt_in_place(&mut self, input_output: &mut [u8]) {
pub fn decrypt_in_place(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
self.just_decrypt_in_place(input_output);
if self.finish_and_check_tag(tag) { Ok(()) } else { Err(()) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Doubt: should tag be checked before decrypting cipher_text?
RFC: https://www.rfc-editor.org/rfc/rfc7539#appendix-A.5

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Doesn't mater, as long as we take the same amount of time in both the valid and invalid cases, and aren't actually doing anything with the decoded bytes until we check the mac. Theoretically its faster, I guess, if we check the mac first, but, like, its not a common case lol.

Comment threadlightning/src/util/ser.rs
peer.pending_outbound_buffer.pop_front();
// Try to keep the buffer to no more than 170 elements
const VEC_SIZE: usize = ::core::mem::size_of::<Vec<u8>>();
let large_capacity = peer.pending_outbound_buffer.capacity() > 4096 / VEC_SIZE;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what is the logic behind this?
"why 170" might be more helpful than "it is 170" in comment above.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Eh, I just dropped it. It wasn't saying anything the code wasn't already.

Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated

fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {
let mut engine = Sha256Hash::engine();
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

shouldn't we be specific?
"Gossip msg should not fail to serialize"

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Panic messages have file/line in them, that's more specific than any message we ever write :)


let mut key_data = VecWriter(Vec::new());
// TODO (taproot|arik): Introduce serialization distinction for non-ECDSA signers.
self.context.holder_signer.as_ecdsa().expect("Only ECDSA signers may be serialized").write(&mut key_data)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question: why did we used to write them?
So, nowadays we write channel_keys_id instead?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We used to write them because we didn't really have a fully-formed concept for how key derivation was supposed to work. Now we do and writing the signers is just redundant.

Comment threadCONTRIBUTING.md Outdated
pub fn decrypt_in_place(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
self.just_decrypt_in_place(input_output);
if self.finish_and_check_tag(tag) { Ok(()) } else { Err(()) }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: encrypt_full_message_in_place can be changed to encrypt_in_place to match/align with this.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Went with check_decrypt_in_place since I think its clearer and a bit more symmetric. Maybe we should rename the encryption side to mac_encrypt_in_place but we can do that another time.

We end up generating a substantial amount of allocations just
doubling `Vec`s when serializing to them, and our
`serialized_length` method is generally rather effecient, so we
just rely on it and allocate correctly up front.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 3f6969e to 74887dfCompareNovember 7, 2023 04:23
In the next commit we'll use this to avoid an allocation when
deserializing messages from the wire.
When decrypting P2P messages, we already have a read buffer that we
read the message into. There's no reason to allocate a new `Vec` to
store the decrypted message when we can just overwrite the read
buffer and call it a day.
When buffering outbound messages for peers, `LinkedList` adds
rather substantial allocation overhead, which we avoid here by
swapping for a `VecDeque`.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 74887df to a69dcc3CompareNovember 7, 2023 18:13
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Squashed with jeff's suggestion:

$ git diff-tree -U1 74887df8 a69dcc3a
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index 21792175a..fe7903d88 100644
--- a/lightning/src/routing/gossip.rs
+++ b/lightning/src/routing/gossip.rs
@@ -19,2 +19,3 @@ use bitcoin::secp256k1;
use bitcoin::hashes::sha256::Hash as Sha256Hash;
+use bitcoin::hashes::sha256d::Hash as Sha256dHash;
use bitcoin::hashes::Hash;
@@ -417,3 +418,3 @@ fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");
-	Sha256Hash::hash(&Sha256Hash::from_engine(engine)[..]).into_inner()
+	Sha256dHash::from_engine(engine).into_inner()
}

Comment threadlightning/src/routing/gossip.rs Outdated
Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated

@G8XSUG8XSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Lgmt! (apart from CI fix)

When we forward gossip messages, we store them in a separate buffer
before we encrypt them (and commit to the order in which they'll
appear on the wire). Rather than storing that buffer encoded with
no headroom, requiring re-allocating to add the message length and
two MAC blocks, we here add the headroom prior to pushing it into
the gossip buffer, avoiding an allocation.
Whenever we go to send bytes to a peer, we need to construct a
waker for tokio to call back into if we need to finish sending
later. That waker needs some reference to the peer's read task to
wake it up, hidden behind a single `*const ()`. To do this, we'd
previously simply stored a `Box<tokio::mpsc::Sender>` in that
pointer, which requires a `clone` for each waker construction. This
leads to substantial malloc traffic.
Instead, here, we replace this box with an `Arc`, leaving a single
`tokio::mpsc::Sender` floating around and simply change the
refcounts whenever we construct a new waker, which we can do
without allocations.
When we check gossip message signatures, there's no reason to
serialize out the full gossip message before hashing, and it
generates a lot of allocations during the initial startup when we
fetch the full gossip from peers.
This breaks backwards compatibility with versions of LDK prior to
0.0.113 as they expect to always read signer data.
This also substantially reduces allocations during `ChannelManager`
serialization, as we currently don't pre-allocate the `Vec` that
the signer gets written in to. We could alternatively pre-allocate
that `Vec`, but we've been set up to skip the write entirely for a
while, and 0.0.113 was released nearly a year ago. Users
downgrading to LDK 0.0.112 and before at this point should not be
expected.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from a69dcc3 to 7a951b1CompareNovember 9, 2023 22:28
@TheBlueMatt

TheBlueMatt commented Nov 9, 2023

Copy link
Copy Markdown
CollaboratorAuthor

Should pass this time, sorry about that:

$ git diff-tree -U1 a69dcc3ab 7a951b1bf
diff --git a/lightning/src/ln/peer_channel_encryptor.rs b/lightning/src/ln/peer_channel_encryptor.rs
index 298ff39b9..8569fa60f 100644
--- a/lightning/src/ln/peer_channel_encryptor.rs+++ b/lightning/src/ln/peer_channel_encryptor.rs@@ -436,4 +436,3 @@ impl PeerChannelEncryptor {
/// For effeciency, the [`Vec::capacity`] should be at least 16 bytes larger than the
-	/// [`Vec::length`], to avoid reallocating for the message MAC, which will be appended to the-	/// vec.+	/// [`Vec::len`], to avoid reallocating for the message MAC, which will be appended to the vec.
fn encrypt_message_with_header_0s(&mut self, msgbuf: &mut Vec<u8>) {
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index fe7903d88..ff8b084b7 100644
--- a/lightning/src/routing/gossip.rs+++ b/lightning/src/routing/gossip.rs@@ -18,3 +18,2 @@ use bitcoin::secp256k1;
-use bitcoin::hashes::sha256::Hash as Sha256Hash;
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
@@ -415,6 +414,6 @@ macro_rules! get_pubkey_from_node_id {
-fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {-	let mut engine = Sha256Hash::engine();+fn message_sha256d_hash<M: Writeable>(msg: &M) -> Sha256dHash {+	let mut engine = Sha256dHash::engine();
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");
-	Sha256dHash::from_engine(engine).into_inner()+	Sha256dHash::from_engine(engine)
}

@G8XSUG8XSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!
Feel moderately confident about this change.
(mainly moderate because of 18dc7f2)

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, now tracking the serialization cleanup over at #2724

@G8XSU

Copy link
Copy Markdown
Contributor

On a separate note: I do wonder if MAX_ALLOC_SIZE spread across multiple places in code while reading different structs needs re-visiting.

@TheBlueMatt
TheBlueMatt merged commit 103180d into lightningdevkit:mainNov 13, 2023
Sign up for freeto 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

@TheBlueMatt@codecov-commenter@G8XSU@tnull@jkczyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Reduce common allocations across the codebase - #2708

Merged
TheBlueMatt merged 12 commits into
lightningdevkit:mainfrom
TheBlueMatt:2023-11-less-graph-memory-frag
Nov 13, 2023
Merged

Reduce common allocations across the codebase#2708
TheBlueMatt merged 12 commits into
lightningdevkit:mainfrom
TheBlueMatt:2023-11-less-graph-memory-frag

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

My node has been experiencing more and more memory fragmentation lately, and while it seems the majority of that is #2706 and #2707, there's still plenty of room for misc improvements all over the place. With this and fixes for the other two issues we should be in a pretty good place, with allocations dominated by farrrr by block deserialization when syncing.

There's two commits here that could be performance regressions:

  • Pre-allocate the full require Vec prior to serializing into vecs which runs through our serialization logic twice in many cases before writing. I played around with a lower_bound Writeable method to optimize out some cases of having to run through the logic, but it doesn't really help in ChannelManager and ChannelMonitor or other deeply-nested structs because we're calling write there which hits our LengthCalculatingWriter instead of being able to use an optimized version. We could totally restructure the API to have Writeables call a magic method on the Writer which can short-circuit the write, but that's a lot of indirection and I'm lazy.
  • Avoid allocating when checking gossip message signatures probably isn't a huge regression, cause hashers are buffered, in essence, anyway, but I didn't check.

When we're reading a `NetworkGraph`, we know how many
nodes/channels we are reading, there's no reason not to
pre-allocate the `IndexedMap`'s inner `HashMap` and `Vec`, which we
do here.
This seems to reduce on-startup heap fragmentation with glibc by
something like 100MiB.
It does the same thing and its much simpler.
When forwarding gossip, rather than relying on Vec doubling,
pre-allocate the message encoding buffer.
...as LLVM will handle it just fine for us, in most cases.
@TheBlueMattTheBlueMatt added this to the 0.0.119 milestone Nov 4, 2023
@codecov-commenter

codecov-commenter commented Nov 4, 2023

Copy link
Copy Markdown

Codecov Report

Attention: 14 lines in your changes are missing coverage. Please review.

Comparison is base (281a0ae) 88.81% compared to head (7a951b1) 89.16%.
Report is 12 commits behind head on main.

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@ Coverage Diff @@## main #2708 +/- ##
==========================================
+ Coverage 88.81% 89.16% +0.34% 
==========================================
Files 113 113 Lines 89116 91476 +2360 Branches 89116 91476 +2360 ==========================================
+ Hits 79152 81561 +2409 + Misses 7722 7709 -13 + Partials 2242 2206 -36 
FilesCoverage Δ
lightning/src/blinded_path/utils.rs96.36% <100.00%> (-0.13%)⬇️
lightning/src/ln/channel.rs88.68% <ø> (+0.03%)⬆️
lightning/src/ln/script.rs93.57% <ø> (-0.14%)⬇️
lightning/src/routing/gossip.rs86.45% <100.00%> (+0.12%)⬆️
lightning/src/sign/type_resolver.rs75.00% <ø> (ø)
lightning/src/util/indexed_map.rs92.59% <100.00%> (+0.43%)⬆️
lightning/src/util/ser.rs76.74% <100.00%> (+0.23%)⬆️
lightning-net-tokio/src/lib.rs76.40% <94.11%> (+2.46%)⬆️
lightning/src/util/chacha20poly1305rfc.rs89.57% <75.00%> (-0.29%)⬇️
lightning/src/ln/peer_channel_encryptor.rs93.71% <95.23%> (+0.03%)⬆️
... and 1 more

... and 11 files with indirect coverage changes

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

Comment threadlightning/src/util/ser.rs
Comment threadlightning/src/util/chacha20poly1305rfc.rs Outdated
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning-net-tokio/src/lib.rs Outdated
Comment threadpending_changelog/113-channel-ser-compat.txt
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated
Comment threadlightning/src/routing/gossip.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 761aaad to f9ef511CompareNovember 6, 2023 16:58
pub(super) fn decrypt_in_place(&mut self, input_output: &mut [u8]) {
pub fn decrypt_in_place(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
self.just_decrypt_in_place(input_output);
if self.finish_and_check_tag(tag) { Ok(()) } else { Err(()) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Doubt: should tag be checked before decrypting cipher_text?
RFC: https://www.rfc-editor.org/rfc/rfc7539#appendix-A.5

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Doesn't mater, as long as we take the same amount of time in both the valid and invalid cases, and aren't actually doing anything with the decoded bytes until we check the mac. Theoretically its faster, I guess, if we check the mac first, but, like, its not a common case lol.

Comment threadlightning/src/util/ser.rs
peer.pending_outbound_buffer.pop_front();
// Try to keep the buffer to no more than 170 elements
const VEC_SIZE: usize = ::core::mem::size_of::<Vec<u8>>();
let large_capacity = peer.pending_outbound_buffer.capacity() > 4096 / VEC_SIZE;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what is the logic behind this?
"why 170" might be more helpful than "it is 170" in comment above.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Eh, I just dropped it. It wasn't saying anything the code wasn't already.

Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated

fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {
let mut engine = Sha256Hash::engine();
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

shouldn't we be specific?
"Gossip msg should not fail to serialize"

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Panic messages have file/line in them, that's more specific than any message we ever write :)


let mut key_data = VecWriter(Vec::new());
// TODO (taproot|arik): Introduce serialization distinction for non-ECDSA signers.
self.context.holder_signer.as_ecdsa().expect("Only ECDSA signers may be serialized").write(&mut key_data)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question: why did we used to write them?
So, nowadays we write channel_keys_id instead?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We used to write them because we didn't really have a fully-formed concept for how key derivation was supposed to work. Now we do and writing the signers is just redundant.

Comment threadCONTRIBUTING.md Outdated
pub fn decrypt_in_place(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
self.just_decrypt_in_place(input_output);
if self.finish_and_check_tag(tag) { Ok(()) } else { Err(()) }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: encrypt_full_message_in_place can be changed to encrypt_in_place to match/align with this.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Went with check_decrypt_in_place since I think its clearer and a bit more symmetric. Maybe we should rename the encryption side to mac_encrypt_in_place but we can do that another time.

We end up generating a substantial amount of allocations just
doubling `Vec`s when serializing to them, and our
`serialized_length` method is generally rather effecient, so we
just rely on it and allocate correctly up front.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 3f6969e to 74887dfCompareNovember 7, 2023 04:23
In the next commit we'll use this to avoid an allocation when
deserializing messages from the wire.
When decrypting P2P messages, we already have a read buffer that we
read the message into. There's no reason to allocate a new `Vec` to
store the decrypted message when we can just overwrite the read
buffer and call it a day.
When buffering outbound messages for peers, `LinkedList` adds
rather substantial allocation overhead, which we avoid here by
swapping for a `VecDeque`.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 74887df to a69dcc3CompareNovember 7, 2023 18:13
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Squashed with jeff's suggestion:

$ git diff-tree -U1 74887df8 a69dcc3a
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index 21792175a..fe7903d88 100644
--- a/lightning/src/routing/gossip.rs
+++ b/lightning/src/routing/gossip.rs
@@ -19,2 +19,3 @@ use bitcoin::secp256k1;
use bitcoin::hashes::sha256::Hash as Sha256Hash;
+use bitcoin::hashes::sha256d::Hash as Sha256dHash;
use bitcoin::hashes::Hash;
@@ -417,3 +418,3 @@ fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");
-	Sha256Hash::hash(&Sha256Hash::from_engine(engine)[..]).into_inner()
+	Sha256dHash::from_engine(engine).into_inner()
}

Comment threadlightning/src/routing/gossip.rs Outdated
Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated

@G8XSUG8XSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Lgmt! (apart from CI fix)

When we forward gossip messages, we store them in a separate buffer
before we encrypt them (and commit to the order in which they'll
appear on the wire). Rather than storing that buffer encoded with
no headroom, requiring re-allocating to add the message length and
two MAC blocks, we here add the headroom prior to pushing it into
the gossip buffer, avoiding an allocation.
Whenever we go to send bytes to a peer, we need to construct a
waker for tokio to call back into if we need to finish sending
later. That waker needs some reference to the peer's read task to
wake it up, hidden behind a single `*const ()`. To do this, we'd
previously simply stored a `Box<tokio::mpsc::Sender>` in that
pointer, which requires a `clone` for each waker construction. This
leads to substantial malloc traffic.
Instead, here, we replace this box with an `Arc`, leaving a single
`tokio::mpsc::Sender` floating around and simply change the
refcounts whenever we construct a new waker, which we can do
without allocations.
When we check gossip message signatures, there's no reason to
serialize out the full gossip message before hashing, and it
generates a lot of allocations during the initial startup when we
fetch the full gossip from peers.
This breaks backwards compatibility with versions of LDK prior to
0.0.113 as they expect to always read signer data.
This also substantially reduces allocations during `ChannelManager`
serialization, as we currently don't pre-allocate the `Vec` that
the signer gets written in to. We could alternatively pre-allocate
that `Vec`, but we've been set up to skip the write entirely for a
while, and 0.0.113 was released nearly a year ago. Users
downgrading to LDK 0.0.112 and before at this point should not be
expected.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from a69dcc3 to 7a951b1CompareNovember 9, 2023 22:28
@TheBlueMatt

TheBlueMatt commented Nov 9, 2023

Copy link
Copy Markdown
CollaboratorAuthor

Should pass this time, sorry about that:

$ git diff-tree -U1 a69dcc3ab 7a951b1bf
diff --git a/lightning/src/ln/peer_channel_encryptor.rs b/lightning/src/ln/peer_channel_encryptor.rs
index 298ff39b9..8569fa60f 100644
--- a/lightning/src/ln/peer_channel_encryptor.rs+++ b/lightning/src/ln/peer_channel_encryptor.rs@@ -436,4 +436,3 @@ impl PeerChannelEncryptor {
/// For effeciency, the [`Vec::capacity`] should be at least 16 bytes larger than the
-	/// [`Vec::length`], to avoid reallocating for the message MAC, which will be appended to the-	/// vec.+	/// [`Vec::len`], to avoid reallocating for the message MAC, which will be appended to the vec.
fn encrypt_message_with_header_0s(&mut self, msgbuf: &mut Vec<u8>) {
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index fe7903d88..ff8b084b7 100644
--- a/lightning/src/routing/gossip.rs+++ b/lightning/src/routing/gossip.rs@@ -18,3 +18,2 @@ use bitcoin::secp256k1;
-use bitcoin::hashes::sha256::Hash as Sha256Hash;
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
@@ -415,6 +414,6 @@ macro_rules! get_pubkey_from_node_id {
-fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {-	let mut engine = Sha256Hash::engine();+fn message_sha256d_hash<M: Writeable>(msg: &M) -> Sha256dHash {+	let mut engine = Sha256dHash::engine();
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");
-	Sha256dHash::from_engine(engine).into_inner()+	Sha256dHash::from_engine(engine)
}

@G8XSUG8XSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!
Feel moderately confident about this change.
(mainly moderate because of 18dc7f2)

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, now tracking the serialization cleanup over at #2724

@G8XSU

Copy link
Copy Markdown
Contributor

On a separate note: I do wonder if MAX_ALLOC_SIZE spread across multiple places in code while reading different structs needs re-visiting.

@TheBlueMatt
TheBlueMatt merged commit 103180d into lightningdevkit:mainNov 13, 2023
Sign up for freeto 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

@TheBlueMatt@codecov-commenter@G8XSU@tnull@jkczyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Reduce common allocations across the codebase - #2708

Merged
TheBlueMatt merged 12 commits into
lightningdevkit:mainfrom
TheBlueMatt:2023-11-less-graph-memory-frag
Nov 13, 2023
Merged

Reduce common allocations across the codebase#2708
TheBlueMatt merged 12 commits into
lightningdevkit:mainfrom
TheBlueMatt:2023-11-less-graph-memory-frag

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

My node has been experiencing more and more memory fragmentation lately, and while it seems the majority of that is #2706 and #2707, there's still plenty of room for misc improvements all over the place. With this and fixes for the other two issues we should be in a pretty good place, with allocations dominated by farrrr by block deserialization when syncing.

There's two commits here that could be performance regressions:

  • Pre-allocate the full require Vec prior to serializing into vecs which runs through our serialization logic twice in many cases before writing. I played around with a lower_bound Writeable method to optimize out some cases of having to run through the logic, but it doesn't really help in ChannelManager and ChannelMonitor or other deeply-nested structs because we're calling write there which hits our LengthCalculatingWriter instead of being able to use an optimized version. We could totally restructure the API to have Writeables call a magic method on the Writer which can short-circuit the write, but that's a lot of indirection and I'm lazy.
  • Avoid allocating when checking gossip message signatures probably isn't a huge regression, cause hashers are buffered, in essence, anyway, but I didn't check.

When we're reading a `NetworkGraph`, we know how many
nodes/channels we are reading, there's no reason not to
pre-allocate the `IndexedMap`'s inner `HashMap` and `Vec`, which we
do here.
This seems to reduce on-startup heap fragmentation with glibc by
something like 100MiB.
It does the same thing and its much simpler.
When forwarding gossip, rather than relying on Vec doubling,
pre-allocate the message encoding buffer.
...as LLVM will handle it just fine for us, in most cases.
@TheBlueMattTheBlueMatt added this to the 0.0.119 milestone Nov 4, 2023
@codecov-commenter

codecov-commenter commented Nov 4, 2023

Copy link
Copy Markdown

Codecov Report

Attention: 14 lines in your changes are missing coverage. Please review.

Comparison is base (281a0ae) 88.81% compared to head (7a951b1) 89.16%.
Report is 12 commits behind head on main.

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@ Coverage Diff @@## main #2708 +/- ##
==========================================
+ Coverage 88.81% 89.16% +0.34% 
==========================================
Files 113 113 Lines 89116 91476 +2360 Branches 89116 91476 +2360 ==========================================
+ Hits 79152 81561 +2409 + Misses 7722 7709 -13 + Partials 2242 2206 -36 
FilesCoverage Δ
lightning/src/blinded_path/utils.rs96.36% <100.00%> (-0.13%)⬇️
lightning/src/ln/channel.rs88.68% <ø> (+0.03%)⬆️
lightning/src/ln/script.rs93.57% <ø> (-0.14%)⬇️
lightning/src/routing/gossip.rs86.45% <100.00%> (+0.12%)⬆️
lightning/src/sign/type_resolver.rs75.00% <ø> (ø)
lightning/src/util/indexed_map.rs92.59% <100.00%> (+0.43%)⬆️
lightning/src/util/ser.rs76.74% <100.00%> (+0.23%)⬆️
lightning-net-tokio/src/lib.rs76.40% <94.11%> (+2.46%)⬆️
lightning/src/util/chacha20poly1305rfc.rs89.57% <75.00%> (-0.29%)⬇️
lightning/src/ln/peer_channel_encryptor.rs93.71% <95.23%> (+0.03%)⬆️
... and 1 more

... and 11 files with indirect coverage changes

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

Comment threadlightning/src/util/ser.rs
Comment threadlightning/src/util/chacha20poly1305rfc.rs Outdated
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning-net-tokio/src/lib.rs Outdated
Comment threadpending_changelog/113-channel-ser-compat.txt
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated
Comment threadlightning/src/routing/gossip.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 761aaad to f9ef511CompareNovember 6, 2023 16:58
pub(super) fn decrypt_in_place(&mut self, input_output: &mut [u8]) {
pub fn decrypt_in_place(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
self.just_decrypt_in_place(input_output);
if self.finish_and_check_tag(tag) { Ok(()) } else { Err(()) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Doubt: should tag be checked before decrypting cipher_text?
RFC: https://www.rfc-editor.org/rfc/rfc7539#appendix-A.5

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Doesn't mater, as long as we take the same amount of time in both the valid and invalid cases, and aren't actually doing anything with the decoded bytes until we check the mac. Theoretically its faster, I guess, if we check the mac first, but, like, its not a common case lol.

Comment threadlightning/src/util/ser.rs
peer.pending_outbound_buffer.pop_front();
// Try to keep the buffer to no more than 170 elements
const VEC_SIZE: usize = ::core::mem::size_of::<Vec<u8>>();
let large_capacity = peer.pending_outbound_buffer.capacity() > 4096 / VEC_SIZE;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what is the logic behind this?
"why 170" might be more helpful than "it is 170" in comment above.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Eh, I just dropped it. It wasn't saying anything the code wasn't already.

Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated

fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {
let mut engine = Sha256Hash::engine();
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

shouldn't we be specific?
"Gossip msg should not fail to serialize"

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Panic messages have file/line in them, that's more specific than any message we ever write :)


let mut key_data = VecWriter(Vec::new());
// TODO (taproot|arik): Introduce serialization distinction for non-ECDSA signers.
self.context.holder_signer.as_ecdsa().expect("Only ECDSA signers may be serialized").write(&mut key_data)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question: why did we used to write them?
So, nowadays we write channel_keys_id instead?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We used to write them because we didn't really have a fully-formed concept for how key derivation was supposed to work. Now we do and writing the signers is just redundant.

Comment threadCONTRIBUTING.md Outdated
pub fn decrypt_in_place(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
self.just_decrypt_in_place(input_output);
if self.finish_and_check_tag(tag) { Ok(()) } else { Err(()) }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: encrypt_full_message_in_place can be changed to encrypt_in_place to match/align with this.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Went with check_decrypt_in_place since I think its clearer and a bit more symmetric. Maybe we should rename the encryption side to mac_encrypt_in_place but we can do that another time.

We end up generating a substantial amount of allocations just
doubling `Vec`s when serializing to them, and our
`serialized_length` method is generally rather effecient, so we
just rely on it and allocate correctly up front.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 3f6969e to 74887dfCompareNovember 7, 2023 04:23
In the next commit we'll use this to avoid an allocation when
deserializing messages from the wire.
When decrypting P2P messages, we already have a read buffer that we
read the message into. There's no reason to allocate a new `Vec` to
store the decrypted message when we can just overwrite the read
buffer and call it a day.
When buffering outbound messages for peers, `LinkedList` adds
rather substantial allocation overhead, which we avoid here by
swapping for a `VecDeque`.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 74887df to a69dcc3CompareNovember 7, 2023 18:13
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Squashed with jeff's suggestion:

$ git diff-tree -U1 74887df8 a69dcc3a
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index 21792175a..fe7903d88 100644
--- a/lightning/src/routing/gossip.rs
+++ b/lightning/src/routing/gossip.rs
@@ -19,2 +19,3 @@ use bitcoin::secp256k1;
use bitcoin::hashes::sha256::Hash as Sha256Hash;
+use bitcoin::hashes::sha256d::Hash as Sha256dHash;
use bitcoin::hashes::Hash;
@@ -417,3 +418,3 @@ fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");
-	Sha256Hash::hash(&Sha256Hash::from_engine(engine)[..]).into_inner()
+	Sha256dHash::from_engine(engine).into_inner()
}

Comment threadlightning/src/routing/gossip.rs Outdated
Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated

@G8XSUG8XSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Lgmt! (apart from CI fix)

When we forward gossip messages, we store them in a separate buffer
before we encrypt them (and commit to the order in which they'll
appear on the wire). Rather than storing that buffer encoded with
no headroom, requiring re-allocating to add the message length and
two MAC blocks, we here add the headroom prior to pushing it into
the gossip buffer, avoiding an allocation.
Whenever we go to send bytes to a peer, we need to construct a
waker for tokio to call back into if we need to finish sending
later. That waker needs some reference to the peer's read task to
wake it up, hidden behind a single `*const ()`. To do this, we'd
previously simply stored a `Box<tokio::mpsc::Sender>` in that
pointer, which requires a `clone` for each waker construction. This
leads to substantial malloc traffic.
Instead, here, we replace this box with an `Arc`, leaving a single
`tokio::mpsc::Sender` floating around and simply change the
refcounts whenever we construct a new waker, which we can do
without allocations.
When we check gossip message signatures, there's no reason to
serialize out the full gossip message before hashing, and it
generates a lot of allocations during the initial startup when we
fetch the full gossip from peers.
This breaks backwards compatibility with versions of LDK prior to
0.0.113 as they expect to always read signer data.
This also substantially reduces allocations during `ChannelManager`
serialization, as we currently don't pre-allocate the `Vec` that
the signer gets written in to. We could alternatively pre-allocate
that `Vec`, but we've been set up to skip the write entirely for a
while, and 0.0.113 was released nearly a year ago. Users
downgrading to LDK 0.0.112 and before at this point should not be
expected.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from a69dcc3 to 7a951b1CompareNovember 9, 2023 22:28
@TheBlueMatt

TheBlueMatt commented Nov 9, 2023

Copy link
Copy Markdown
CollaboratorAuthor

Should pass this time, sorry about that:

$ git diff-tree -U1 a69dcc3ab 7a951b1bf
diff --git a/lightning/src/ln/peer_channel_encryptor.rs b/lightning/src/ln/peer_channel_encryptor.rs
index 298ff39b9..8569fa60f 100644
--- a/lightning/src/ln/peer_channel_encryptor.rs+++ b/lightning/src/ln/peer_channel_encryptor.rs@@ -436,4 +436,3 @@ impl PeerChannelEncryptor {
/// For effeciency, the [`Vec::capacity`] should be at least 16 bytes larger than the
-	/// [`Vec::length`], to avoid reallocating for the message MAC, which will be appended to the-	/// vec.+	/// [`Vec::len`], to avoid reallocating for the message MAC, which will be appended to the vec.
fn encrypt_message_with_header_0s(&mut self, msgbuf: &mut Vec<u8>) {
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index fe7903d88..ff8b084b7 100644
--- a/lightning/src/routing/gossip.rs+++ b/lightning/src/routing/gossip.rs@@ -18,3 +18,2 @@ use bitcoin::secp256k1;
-use bitcoin::hashes::sha256::Hash as Sha256Hash;
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
@@ -415,6 +414,6 @@ macro_rules! get_pubkey_from_node_id {
-fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {-	let mut engine = Sha256Hash::engine();+fn message_sha256d_hash<M: Writeable>(msg: &M) -> Sha256dHash {+	let mut engine = Sha256dHash::engine();
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");
-	Sha256dHash::from_engine(engine).into_inner()+	Sha256dHash::from_engine(engine)
}

@G8XSUG8XSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!
Feel moderately confident about this change.
(mainly moderate because of 18dc7f2)

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, now tracking the serialization cleanup over at #2724

@G8XSU

Copy link
Copy Markdown
Contributor

On a separate note: I do wonder if MAX_ALLOC_SIZE spread across multiple places in code while reading different structs needs re-visiting.

@TheBlueMatt
TheBlueMatt merged commit 103180d into lightningdevkit:mainNov 13, 2023
Sign up for freeto 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

@TheBlueMatt@codecov-commenter@G8XSU@tnull@jkczyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Reduce common allocations across the codebase - #2708

Merged
TheBlueMatt merged 12 commits into
lightningdevkit:mainfrom
TheBlueMatt:2023-11-less-graph-memory-frag
Nov 13, 2023
Merged

Reduce common allocations across the codebase#2708
TheBlueMatt merged 12 commits into
lightningdevkit:mainfrom
TheBlueMatt:2023-11-less-graph-memory-frag

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

My node has been experiencing more and more memory fragmentation lately, and while it seems the majority of that is #2706 and #2707, there's still plenty of room for misc improvements all over the place. With this and fixes for the other two issues we should be in a pretty good place, with allocations dominated by farrrr by block deserialization when syncing.

There's two commits here that could be performance regressions:

  • Pre-allocate the full require Vec prior to serializing into vecs which runs through our serialization logic twice in many cases before writing. I played around with a lower_bound Writeable method to optimize out some cases of having to run through the logic, but it doesn't really help in ChannelManager and ChannelMonitor or other deeply-nested structs because we're calling write there which hits our LengthCalculatingWriter instead of being able to use an optimized version. We could totally restructure the API to have Writeables call a magic method on the Writer which can short-circuit the write, but that's a lot of indirection and I'm lazy.
  • Avoid allocating when checking gossip message signatures probably isn't a huge regression, cause hashers are buffered, in essence, anyway, but I didn't check.

When we're reading a `NetworkGraph`, we know how many
nodes/channels we are reading, there's no reason not to
pre-allocate the `IndexedMap`'s inner `HashMap` and `Vec`, which we
do here.
This seems to reduce on-startup heap fragmentation with glibc by
something like 100MiB.
It does the same thing and its much simpler.
When forwarding gossip, rather than relying on Vec doubling,
pre-allocate the message encoding buffer.
...as LLVM will handle it just fine for us, in most cases.
@TheBlueMattTheBlueMatt added this to the 0.0.119 milestone Nov 4, 2023
@codecov-commenter

codecov-commenter commented Nov 4, 2023

Copy link
Copy Markdown

Codecov Report

Attention: 14 lines in your changes are missing coverage. Please review.

Comparison is base (281a0ae) 88.81% compared to head (7a951b1) 89.16%.
Report is 12 commits behind head on main.

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@ Coverage Diff @@## main #2708 +/- ##
==========================================
+ Coverage 88.81% 89.16% +0.34% 
==========================================
Files 113 113 Lines 89116 91476 +2360 Branches 89116 91476 +2360 ==========================================
+ Hits 79152 81561 +2409 + Misses 7722 7709 -13 + Partials 2242 2206 -36 
FilesCoverage Δ
lightning/src/blinded_path/utils.rs96.36% <100.00%> (-0.13%)⬇️
lightning/src/ln/channel.rs88.68% <ø> (+0.03%)⬆️
lightning/src/ln/script.rs93.57% <ø> (-0.14%)⬇️
lightning/src/routing/gossip.rs86.45% <100.00%> (+0.12%)⬆️
lightning/src/sign/type_resolver.rs75.00% <ø> (ø)
lightning/src/util/indexed_map.rs92.59% <100.00%> (+0.43%)⬆️
lightning/src/util/ser.rs76.74% <100.00%> (+0.23%)⬆️
lightning-net-tokio/src/lib.rs76.40% <94.11%> (+2.46%)⬆️
lightning/src/util/chacha20poly1305rfc.rs89.57% <75.00%> (-0.29%)⬇️
lightning/src/ln/peer_channel_encryptor.rs93.71% <95.23%> (+0.03%)⬆️
... and 1 more

... and 11 files with indirect coverage changes

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

Comment threadlightning/src/util/ser.rs
Comment threadlightning/src/util/chacha20poly1305rfc.rs Outdated
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning-net-tokio/src/lib.rs Outdated
Comment threadpending_changelog/113-channel-ser-compat.txt
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated
Comment threadlightning/src/routing/gossip.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 761aaad to f9ef511CompareNovember 6, 2023 16:58
pub(super) fn decrypt_in_place(&mut self, input_output: &mut [u8]) {
pub fn decrypt_in_place(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
self.just_decrypt_in_place(input_output);
if self.finish_and_check_tag(tag) { Ok(()) } else { Err(()) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Doubt: should tag be checked before decrypting cipher_text?
RFC: https://www.rfc-editor.org/rfc/rfc7539#appendix-A.5

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Doesn't mater, as long as we take the same amount of time in both the valid and invalid cases, and aren't actually doing anything with the decoded bytes until we check the mac. Theoretically its faster, I guess, if we check the mac first, but, like, its not a common case lol.

Comment threadlightning/src/util/ser.rs
peer.pending_outbound_buffer.pop_front();
// Try to keep the buffer to no more than 170 elements
const VEC_SIZE: usize = ::core::mem::size_of::<Vec<u8>>();
let large_capacity = peer.pending_outbound_buffer.capacity() > 4096 / VEC_SIZE;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what is the logic behind this?
"why 170" might be more helpful than "it is 170" in comment above.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Eh, I just dropped it. It wasn't saying anything the code wasn't already.

Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated

fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {
let mut engine = Sha256Hash::engine();
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

shouldn't we be specific?
"Gossip msg should not fail to serialize"

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Panic messages have file/line in them, that's more specific than any message we ever write :)


let mut key_data = VecWriter(Vec::new());
// TODO (taproot|arik): Introduce serialization distinction for non-ECDSA signers.
self.context.holder_signer.as_ecdsa().expect("Only ECDSA signers may be serialized").write(&mut key_data)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question: why did we used to write them?
So, nowadays we write channel_keys_id instead?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We used to write them because we didn't really have a fully-formed concept for how key derivation was supposed to work. Now we do and writing the signers is just redundant.

Comment threadCONTRIBUTING.md Outdated
pub fn decrypt_in_place(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
self.just_decrypt_in_place(input_output);
if self.finish_and_check_tag(tag) { Ok(()) } else { Err(()) }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: encrypt_full_message_in_place can be changed to encrypt_in_place to match/align with this.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Went with check_decrypt_in_place since I think its clearer and a bit more symmetric. Maybe we should rename the encryption side to mac_encrypt_in_place but we can do that another time.

We end up generating a substantial amount of allocations just
doubling `Vec`s when serializing to them, and our
`serialized_length` method is generally rather effecient, so we
just rely on it and allocate correctly up front.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 3f6969e to 74887dfCompareNovember 7, 2023 04:23
In the next commit we'll use this to avoid an allocation when
deserializing messages from the wire.
When decrypting P2P messages, we already have a read buffer that we
read the message into. There's no reason to allocate a new `Vec` to
store the decrypted message when we can just overwrite the read
buffer and call it a day.
When buffering outbound messages for peers, `LinkedList` adds
rather substantial allocation overhead, which we avoid here by
swapping for a `VecDeque`.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 74887df to a69dcc3CompareNovember 7, 2023 18:13
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Squashed with jeff's suggestion:

$ git diff-tree -U1 74887df8 a69dcc3a
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index 21792175a..fe7903d88 100644
--- a/lightning/src/routing/gossip.rs
+++ b/lightning/src/routing/gossip.rs
@@ -19,2 +19,3 @@ use bitcoin::secp256k1;
use bitcoin::hashes::sha256::Hash as Sha256Hash;
+use bitcoin::hashes::sha256d::Hash as Sha256dHash;
use bitcoin::hashes::Hash;
@@ -417,3 +418,3 @@ fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");
-	Sha256Hash::hash(&Sha256Hash::from_engine(engine)[..]).into_inner()
+	Sha256dHash::from_engine(engine).into_inner()
}

Comment threadlightning/src/routing/gossip.rs Outdated
Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated

@G8XSUG8XSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Lgmt! (apart from CI fix)

When we forward gossip messages, we store them in a separate buffer
before we encrypt them (and commit to the order in which they'll
appear on the wire). Rather than storing that buffer encoded with
no headroom, requiring re-allocating to add the message length and
two MAC blocks, we here add the headroom prior to pushing it into
the gossip buffer, avoiding an allocation.
Whenever we go to send bytes to a peer, we need to construct a
waker for tokio to call back into if we need to finish sending
later. That waker needs some reference to the peer's read task to
wake it up, hidden behind a single `*const ()`. To do this, we'd
previously simply stored a `Box<tokio::mpsc::Sender>` in that
pointer, which requires a `clone` for each waker construction. This
leads to substantial malloc traffic.
Instead, here, we replace this box with an `Arc`, leaving a single
`tokio::mpsc::Sender` floating around and simply change the
refcounts whenever we construct a new waker, which we can do
without allocations.
When we check gossip message signatures, there's no reason to
serialize out the full gossip message before hashing, and it
generates a lot of allocations during the initial startup when we
fetch the full gossip from peers.
This breaks backwards compatibility with versions of LDK prior to
0.0.113 as they expect to always read signer data.
This also substantially reduces allocations during `ChannelManager`
serialization, as we currently don't pre-allocate the `Vec` that
the signer gets written in to. We could alternatively pre-allocate
that `Vec`, but we've been set up to skip the write entirely for a
while, and 0.0.113 was released nearly a year ago. Users
downgrading to LDK 0.0.112 and before at this point should not be
expected.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from a69dcc3 to 7a951b1CompareNovember 9, 2023 22:28
@TheBlueMatt

TheBlueMatt commented Nov 9, 2023

Copy link
Copy Markdown
CollaboratorAuthor

Should pass this time, sorry about that:

$ git diff-tree -U1 a69dcc3ab 7a951b1bf
diff --git a/lightning/src/ln/peer_channel_encryptor.rs b/lightning/src/ln/peer_channel_encryptor.rs
index 298ff39b9..8569fa60f 100644
--- a/lightning/src/ln/peer_channel_encryptor.rs+++ b/lightning/src/ln/peer_channel_encryptor.rs@@ -436,4 +436,3 @@ impl PeerChannelEncryptor {
/// For effeciency, the [`Vec::capacity`] should be at least 16 bytes larger than the
-	/// [`Vec::length`], to avoid reallocating for the message MAC, which will be appended to the-	/// vec.+	/// [`Vec::len`], to avoid reallocating for the message MAC, which will be appended to the vec.
fn encrypt_message_with_header_0s(&mut self, msgbuf: &mut Vec<u8>) {
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index fe7903d88..ff8b084b7 100644
--- a/lightning/src/routing/gossip.rs+++ b/lightning/src/routing/gossip.rs@@ -18,3 +18,2 @@ use bitcoin::secp256k1;
-use bitcoin::hashes::sha256::Hash as Sha256Hash;
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
@@ -415,6 +414,6 @@ macro_rules! get_pubkey_from_node_id {
-fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {-	let mut engine = Sha256Hash::engine();+fn message_sha256d_hash<M: Writeable>(msg: &M) -> Sha256dHash {+	let mut engine = Sha256dHash::engine();
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");
-	Sha256dHash::from_engine(engine).into_inner()+	Sha256dHash::from_engine(engine)
}

@G8XSUG8XSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!
Feel moderately confident about this change.
(mainly moderate because of 18dc7f2)

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, now tracking the serialization cleanup over at #2724

@G8XSU

Copy link
Copy Markdown
Contributor

On a separate note: I do wonder if MAX_ALLOC_SIZE spread across multiple places in code while reading different structs needs re-visiting.

@TheBlueMatt
TheBlueMatt merged commit 103180d into lightningdevkit:mainNov 13, 2023
Sign up for freeto 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

@TheBlueMatt@codecov-commenter@G8XSU@tnull@jkczyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Reduce common allocations across the codebase - #2708

Merged
TheBlueMatt merged 12 commits into
lightningdevkit:mainfrom
TheBlueMatt:2023-11-less-graph-memory-frag
Nov 13, 2023
Merged

Reduce common allocations across the codebase#2708
TheBlueMatt merged 12 commits into
lightningdevkit:mainfrom
TheBlueMatt:2023-11-less-graph-memory-frag

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

My node has been experiencing more and more memory fragmentation lately, and while it seems the majority of that is #2706 and #2707, there's still plenty of room for misc improvements all over the place. With this and fixes for the other two issues we should be in a pretty good place, with allocations dominated by farrrr by block deserialization when syncing.

There's two commits here that could be performance regressions:

  • Pre-allocate the full require Vec prior to serializing into vecs which runs through our serialization logic twice in many cases before writing. I played around with a lower_bound Writeable method to optimize out some cases of having to run through the logic, but it doesn't really help in ChannelManager and ChannelMonitor or other deeply-nested structs because we're calling write there which hits our LengthCalculatingWriter instead of being able to use an optimized version. We could totally restructure the API to have Writeables call a magic method on the Writer which can short-circuit the write, but that's a lot of indirection and I'm lazy.
  • Avoid allocating when checking gossip message signatures probably isn't a huge regression, cause hashers are buffered, in essence, anyway, but I didn't check.

When we're reading a `NetworkGraph`, we know how many
nodes/channels we are reading, there's no reason not to
pre-allocate the `IndexedMap`'s inner `HashMap` and `Vec`, which we
do here.
This seems to reduce on-startup heap fragmentation with glibc by
something like 100MiB.
It does the same thing and its much simpler.
When forwarding gossip, rather than relying on Vec doubling,
pre-allocate the message encoding buffer.
...as LLVM will handle it just fine for us, in most cases.
@TheBlueMattTheBlueMatt added this to the 0.0.119 milestone Nov 4, 2023
@codecov-commenter

codecov-commenter commented Nov 4, 2023

Copy link
Copy Markdown

Codecov Report

Attention: 14 lines in your changes are missing coverage. Please review.

Comparison is base (281a0ae) 88.81% compared to head (7a951b1) 89.16%.
Report is 12 commits behind head on main.

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@ Coverage Diff @@## main #2708 +/- ##
==========================================
+ Coverage 88.81% 89.16% +0.34% 
==========================================
Files 113 113 Lines 89116 91476 +2360 Branches 89116 91476 +2360 ==========================================
+ Hits 79152 81561 +2409 + Misses 7722 7709 -13 + Partials 2242 2206 -36 
FilesCoverage Δ
lightning/src/blinded_path/utils.rs96.36% <100.00%> (-0.13%)⬇️
lightning/src/ln/channel.rs88.68% <ø> (+0.03%)⬆️
lightning/src/ln/script.rs93.57% <ø> (-0.14%)⬇️
lightning/src/routing/gossip.rs86.45% <100.00%> (+0.12%)⬆️
lightning/src/sign/type_resolver.rs75.00% <ø> (ø)
lightning/src/util/indexed_map.rs92.59% <100.00%> (+0.43%)⬆️
lightning/src/util/ser.rs76.74% <100.00%> (+0.23%)⬆️
lightning-net-tokio/src/lib.rs76.40% <94.11%> (+2.46%)⬆️
lightning/src/util/chacha20poly1305rfc.rs89.57% <75.00%> (-0.29%)⬇️
lightning/src/ln/peer_channel_encryptor.rs93.71% <95.23%> (+0.03%)⬆️
... and 1 more

... and 11 files with indirect coverage changes

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

Comment threadlightning/src/util/ser.rs
Comment threadlightning/src/util/chacha20poly1305rfc.rs Outdated
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning-net-tokio/src/lib.rs Outdated
Comment threadpending_changelog/113-channel-ser-compat.txt
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated
Comment threadlightning/src/routing/gossip.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 761aaad to f9ef511CompareNovember 6, 2023 16:58
pub(super) fn decrypt_in_place(&mut self, input_output: &mut [u8]) {
pub fn decrypt_in_place(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
self.just_decrypt_in_place(input_output);
if self.finish_and_check_tag(tag) { Ok(()) } else { Err(()) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Doubt: should tag be checked before decrypting cipher_text?
RFC: https://www.rfc-editor.org/rfc/rfc7539#appendix-A.5

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Doesn't mater, as long as we take the same amount of time in both the valid and invalid cases, and aren't actually doing anything with the decoded bytes until we check the mac. Theoretically its faster, I guess, if we check the mac first, but, like, its not a common case lol.

Comment threadlightning/src/util/ser.rs
peer.pending_outbound_buffer.pop_front();
// Try to keep the buffer to no more than 170 elements
const VEC_SIZE: usize = ::core::mem::size_of::<Vec<u8>>();
let large_capacity = peer.pending_outbound_buffer.capacity() > 4096 / VEC_SIZE;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what is the logic behind this?
"why 170" might be more helpful than "it is 170" in comment above.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Eh, I just dropped it. It wasn't saying anything the code wasn't already.

Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated

fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {
let mut engine = Sha256Hash::engine();
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

shouldn't we be specific?
"Gossip msg should not fail to serialize"

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Panic messages have file/line in them, that's more specific than any message we ever write :)


let mut key_data = VecWriter(Vec::new());
// TODO (taproot|arik): Introduce serialization distinction for non-ECDSA signers.
self.context.holder_signer.as_ecdsa().expect("Only ECDSA signers may be serialized").write(&mut key_data)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question: why did we used to write them?
So, nowadays we write channel_keys_id instead?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We used to write them because we didn't really have a fully-formed concept for how key derivation was supposed to work. Now we do and writing the signers is just redundant.

Comment threadCONTRIBUTING.md Outdated
pub fn decrypt_in_place(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
self.just_decrypt_in_place(input_output);
if self.finish_and_check_tag(tag) { Ok(()) } else { Err(()) }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: encrypt_full_message_in_place can be changed to encrypt_in_place to match/align with this.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Went with check_decrypt_in_place since I think its clearer and a bit more symmetric. Maybe we should rename the encryption side to mac_encrypt_in_place but we can do that another time.

We end up generating a substantial amount of allocations just
doubling `Vec`s when serializing to them, and our
`serialized_length` method is generally rather effecient, so we
just rely on it and allocate correctly up front.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 3f6969e to 74887dfCompareNovember 7, 2023 04:23
In the next commit we'll use this to avoid an allocation when
deserializing messages from the wire.
When decrypting P2P messages, we already have a read buffer that we
read the message into. There's no reason to allocate a new `Vec` to
store the decrypted message when we can just overwrite the read
buffer and call it a day.
When buffering outbound messages for peers, `LinkedList` adds
rather substantial allocation overhead, which we avoid here by
swapping for a `VecDeque`.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 74887df to a69dcc3CompareNovember 7, 2023 18:13
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Squashed with jeff's suggestion:

$ git diff-tree -U1 74887df8 a69dcc3a
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index 21792175a..fe7903d88 100644
--- a/lightning/src/routing/gossip.rs
+++ b/lightning/src/routing/gossip.rs
@@ -19,2 +19,3 @@ use bitcoin::secp256k1;
use bitcoin::hashes::sha256::Hash as Sha256Hash;
+use bitcoin::hashes::sha256d::Hash as Sha256dHash;
use bitcoin::hashes::Hash;
@@ -417,3 +418,3 @@ fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");
-	Sha256Hash::hash(&Sha256Hash::from_engine(engine)[..]).into_inner()
+	Sha256dHash::from_engine(engine).into_inner()
}

Comment threadlightning/src/routing/gossip.rs Outdated
Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated

@G8XSUG8XSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Lgmt! (apart from CI fix)

When we forward gossip messages, we store them in a separate buffer
before we encrypt them (and commit to the order in which they'll
appear on the wire). Rather than storing that buffer encoded with
no headroom, requiring re-allocating to add the message length and
two MAC blocks, we here add the headroom prior to pushing it into
the gossip buffer, avoiding an allocation.
Whenever we go to send bytes to a peer, we need to construct a
waker for tokio to call back into if we need to finish sending
later. That waker needs some reference to the peer's read task to
wake it up, hidden behind a single `*const ()`. To do this, we'd
previously simply stored a `Box<tokio::mpsc::Sender>` in that
pointer, which requires a `clone` for each waker construction. This
leads to substantial malloc traffic.
Instead, here, we replace this box with an `Arc`, leaving a single
`tokio::mpsc::Sender` floating around and simply change the
refcounts whenever we construct a new waker, which we can do
without allocations.
When we check gossip message signatures, there's no reason to
serialize out the full gossip message before hashing, and it
generates a lot of allocations during the initial startup when we
fetch the full gossip from peers.
This breaks backwards compatibility with versions of LDK prior to
0.0.113 as they expect to always read signer data.
This also substantially reduces allocations during `ChannelManager`
serialization, as we currently don't pre-allocate the `Vec` that
the signer gets written in to. We could alternatively pre-allocate
that `Vec`, but we've been set up to skip the write entirely for a
while, and 0.0.113 was released nearly a year ago. Users
downgrading to LDK 0.0.112 and before at this point should not be
expected.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from a69dcc3 to 7a951b1CompareNovember 9, 2023 22:28
@TheBlueMatt

TheBlueMatt commented Nov 9, 2023

Copy link
Copy Markdown
CollaboratorAuthor

Should pass this time, sorry about that:

$ git diff-tree -U1 a69dcc3ab 7a951b1bf
diff --git a/lightning/src/ln/peer_channel_encryptor.rs b/lightning/src/ln/peer_channel_encryptor.rs
index 298ff39b9..8569fa60f 100644
--- a/lightning/src/ln/peer_channel_encryptor.rs+++ b/lightning/src/ln/peer_channel_encryptor.rs@@ -436,4 +436,3 @@ impl PeerChannelEncryptor {
/// For effeciency, the [`Vec::capacity`] should be at least 16 bytes larger than the
-	/// [`Vec::length`], to avoid reallocating for the message MAC, which will be appended to the-	/// vec.+	/// [`Vec::len`], to avoid reallocating for the message MAC, which will be appended to the vec.
fn encrypt_message_with_header_0s(&mut self, msgbuf: &mut Vec<u8>) {
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index fe7903d88..ff8b084b7 100644
--- a/lightning/src/routing/gossip.rs+++ b/lightning/src/routing/gossip.rs@@ -18,3 +18,2 @@ use bitcoin::secp256k1;
-use bitcoin::hashes::sha256::Hash as Sha256Hash;
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
@@ -415,6 +414,6 @@ macro_rules! get_pubkey_from_node_id {
-fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {-	let mut engine = Sha256Hash::engine();+fn message_sha256d_hash<M: Writeable>(msg: &M) -> Sha256dHash {+	let mut engine = Sha256dHash::engine();
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");
-	Sha256dHash::from_engine(engine).into_inner()+	Sha256dHash::from_engine(engine)
}

@G8XSUG8XSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!
Feel moderately confident about this change.
(mainly moderate because of 18dc7f2)

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, now tracking the serialization cleanup over at #2724

@G8XSU

Copy link
Copy Markdown
Contributor

On a separate note: I do wonder if MAX_ALLOC_SIZE spread across multiple places in code while reading different structs needs re-visiting.

@TheBlueMatt
TheBlueMatt merged commit 103180d into lightningdevkit:mainNov 13, 2023
Sign up for freeto 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

@TheBlueMatt@codecov-commenter@G8XSU@tnull@jkczyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Reduce common allocations across the codebase - #2708

Merged
TheBlueMatt merged 12 commits into
lightningdevkit:mainfrom
TheBlueMatt:2023-11-less-graph-memory-frag
Nov 13, 2023
Merged

Reduce common allocations across the codebase#2708
TheBlueMatt merged 12 commits into
lightningdevkit:mainfrom
TheBlueMatt:2023-11-less-graph-memory-frag

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

My node has been experiencing more and more memory fragmentation lately, and while it seems the majority of that is #2706 and #2707, there's still plenty of room for misc improvements all over the place. With this and fixes for the other two issues we should be in a pretty good place, with allocations dominated by farrrr by block deserialization when syncing.

There's two commits here that could be performance regressions:

  • Pre-allocate the full require Vec prior to serializing into vecs which runs through our serialization logic twice in many cases before writing. I played around with a lower_bound Writeable method to optimize out some cases of having to run through the logic, but it doesn't really help in ChannelManager and ChannelMonitor or other deeply-nested structs because we're calling write there which hits our LengthCalculatingWriter instead of being able to use an optimized version. We could totally restructure the API to have Writeables call a magic method on the Writer which can short-circuit the write, but that's a lot of indirection and I'm lazy.
  • Avoid allocating when checking gossip message signatures probably isn't a huge regression, cause hashers are buffered, in essence, anyway, but I didn't check.

When we're reading a `NetworkGraph`, we know how many
nodes/channels we are reading, there's no reason not to
pre-allocate the `IndexedMap`'s inner `HashMap` and `Vec`, which we
do here.
This seems to reduce on-startup heap fragmentation with glibc by
something like 100MiB.
It does the same thing and its much simpler.
When forwarding gossip, rather than relying on Vec doubling,
pre-allocate the message encoding buffer.
...as LLVM will handle it just fine for us, in most cases.
@TheBlueMattTheBlueMatt added this to the 0.0.119 milestone Nov 4, 2023
@codecov-commenter

codecov-commenter commented Nov 4, 2023

Copy link
Copy Markdown

Codecov Report

Attention: 14 lines in your changes are missing coverage. Please review.

Comparison is base (281a0ae) 88.81% compared to head (7a951b1) 89.16%.
Report is 12 commits behind head on main.

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@ Coverage Diff @@## main #2708 +/- ##
==========================================
+ Coverage 88.81% 89.16% +0.34% 
==========================================
Files 113 113 Lines 89116 91476 +2360 Branches 89116 91476 +2360 ==========================================
+ Hits 79152 81561 +2409 + Misses 7722 7709 -13 + Partials 2242 2206 -36 
FilesCoverage Δ
lightning/src/blinded_path/utils.rs96.36% <100.00%> (-0.13%)⬇️
lightning/src/ln/channel.rs88.68% <ø> (+0.03%)⬆️
lightning/src/ln/script.rs93.57% <ø> (-0.14%)⬇️
lightning/src/routing/gossip.rs86.45% <100.00%> (+0.12%)⬆️
lightning/src/sign/type_resolver.rs75.00% <ø> (ø)
lightning/src/util/indexed_map.rs92.59% <100.00%> (+0.43%)⬆️
lightning/src/util/ser.rs76.74% <100.00%> (+0.23%)⬆️
lightning-net-tokio/src/lib.rs76.40% <94.11%> (+2.46%)⬆️
lightning/src/util/chacha20poly1305rfc.rs89.57% <75.00%> (-0.29%)⬇️
lightning/src/ln/peer_channel_encryptor.rs93.71% <95.23%> (+0.03%)⬆️
... and 1 more

... and 11 files with indirect coverage changes

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

Comment threadlightning/src/util/ser.rs
Comment threadlightning/src/util/chacha20poly1305rfc.rs Outdated
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning-net-tokio/src/lib.rs Outdated
Comment threadpending_changelog/113-channel-ser-compat.txt
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated
Comment threadlightning/src/routing/gossip.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 761aaad to f9ef511CompareNovember 6, 2023 16:58
pub(super) fn decrypt_in_place(&mut self, input_output: &mut [u8]) {
pub fn decrypt_in_place(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
self.just_decrypt_in_place(input_output);
if self.finish_and_check_tag(tag) { Ok(()) } else { Err(()) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Doubt: should tag be checked before decrypting cipher_text?
RFC: https://www.rfc-editor.org/rfc/rfc7539#appendix-A.5

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Doesn't mater, as long as we take the same amount of time in both the valid and invalid cases, and aren't actually doing anything with the decoded bytes until we check the mac. Theoretically its faster, I guess, if we check the mac first, but, like, its not a common case lol.

Comment threadlightning/src/util/ser.rs
peer.pending_outbound_buffer.pop_front();
// Try to keep the buffer to no more than 170 elements
const VEC_SIZE: usize = ::core::mem::size_of::<Vec<u8>>();
let large_capacity = peer.pending_outbound_buffer.capacity() > 4096 / VEC_SIZE;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what is the logic behind this?
"why 170" might be more helpful than "it is 170" in comment above.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Eh, I just dropped it. It wasn't saying anything the code wasn't already.

Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated

fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {
let mut engine = Sha256Hash::engine();
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

shouldn't we be specific?
"Gossip msg should not fail to serialize"

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Panic messages have file/line in them, that's more specific than any message we ever write :)


let mut key_data = VecWriter(Vec::new());
// TODO (taproot|arik): Introduce serialization distinction for non-ECDSA signers.
self.context.holder_signer.as_ecdsa().expect("Only ECDSA signers may be serialized").write(&mut key_data)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question: why did we used to write them?
So, nowadays we write channel_keys_id instead?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We used to write them because we didn't really have a fully-formed concept for how key derivation was supposed to work. Now we do and writing the signers is just redundant.

Comment threadCONTRIBUTING.md Outdated
pub fn decrypt_in_place(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
self.just_decrypt_in_place(input_output);
if self.finish_and_check_tag(tag) { Ok(()) } else { Err(()) }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: encrypt_full_message_in_place can be changed to encrypt_in_place to match/align with this.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Went with check_decrypt_in_place since I think its clearer and a bit more symmetric. Maybe we should rename the encryption side to mac_encrypt_in_place but we can do that another time.

We end up generating a substantial amount of allocations just
doubling `Vec`s when serializing to them, and our
`serialized_length` method is generally rather effecient, so we
just rely on it and allocate correctly up front.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 3f6969e to 74887dfCompareNovember 7, 2023 04:23
In the next commit we'll use this to avoid an allocation when
deserializing messages from the wire.
When decrypting P2P messages, we already have a read buffer that we
read the message into. There's no reason to allocate a new `Vec` to
store the decrypted message when we can just overwrite the read
buffer and call it a day.
When buffering outbound messages for peers, `LinkedList` adds
rather substantial allocation overhead, which we avoid here by
swapping for a `VecDeque`.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 74887df to a69dcc3CompareNovember 7, 2023 18:13
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Squashed with jeff's suggestion:

$ git diff-tree -U1 74887df8 a69dcc3a
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index 21792175a..fe7903d88 100644
--- a/lightning/src/routing/gossip.rs
+++ b/lightning/src/routing/gossip.rs
@@ -19,2 +19,3 @@ use bitcoin::secp256k1;
use bitcoin::hashes::sha256::Hash as Sha256Hash;
+use bitcoin::hashes::sha256d::Hash as Sha256dHash;
use bitcoin::hashes::Hash;
@@ -417,3 +418,3 @@ fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");
-	Sha256Hash::hash(&Sha256Hash::from_engine(engine)[..]).into_inner()
+	Sha256dHash::from_engine(engine).into_inner()
}

Comment threadlightning/src/routing/gossip.rs Outdated
Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated

@G8XSUG8XSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Lgmt! (apart from CI fix)

When we forward gossip messages, we store them in a separate buffer
before we encrypt them (and commit to the order in which they'll
appear on the wire). Rather than storing that buffer encoded with
no headroom, requiring re-allocating to add the message length and
two MAC blocks, we here add the headroom prior to pushing it into
the gossip buffer, avoiding an allocation.
Whenever we go to send bytes to a peer, we need to construct a
waker for tokio to call back into if we need to finish sending
later. That waker needs some reference to the peer's read task to
wake it up, hidden behind a single `*const ()`. To do this, we'd
previously simply stored a `Box<tokio::mpsc::Sender>` in that
pointer, which requires a `clone` for each waker construction. This
leads to substantial malloc traffic.
Instead, here, we replace this box with an `Arc`, leaving a single
`tokio::mpsc::Sender` floating around and simply change the
refcounts whenever we construct a new waker, which we can do
without allocations.
When we check gossip message signatures, there's no reason to
serialize out the full gossip message before hashing, and it
generates a lot of allocations during the initial startup when we
fetch the full gossip from peers.
This breaks backwards compatibility with versions of LDK prior to
0.0.113 as they expect to always read signer data.
This also substantially reduces allocations during `ChannelManager`
serialization, as we currently don't pre-allocate the `Vec` that
the signer gets written in to. We could alternatively pre-allocate
that `Vec`, but we've been set up to skip the write entirely for a
while, and 0.0.113 was released nearly a year ago. Users
downgrading to LDK 0.0.112 and before at this point should not be
expected.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from a69dcc3 to 7a951b1CompareNovember 9, 2023 22:28
@TheBlueMatt

TheBlueMatt commented Nov 9, 2023

Copy link
Copy Markdown
CollaboratorAuthor

Should pass this time, sorry about that:

$ git diff-tree -U1 a69dcc3ab 7a951b1bf
diff --git a/lightning/src/ln/peer_channel_encryptor.rs b/lightning/src/ln/peer_channel_encryptor.rs
index 298ff39b9..8569fa60f 100644
--- a/lightning/src/ln/peer_channel_encryptor.rs+++ b/lightning/src/ln/peer_channel_encryptor.rs@@ -436,4 +436,3 @@ impl PeerChannelEncryptor {
/// For effeciency, the [`Vec::capacity`] should be at least 16 bytes larger than the
-	/// [`Vec::length`], to avoid reallocating for the message MAC, which will be appended to the-	/// vec.+	/// [`Vec::len`], to avoid reallocating for the message MAC, which will be appended to the vec.
fn encrypt_message_with_header_0s(&mut self, msgbuf: &mut Vec<u8>) {
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index fe7903d88..ff8b084b7 100644
--- a/lightning/src/routing/gossip.rs+++ b/lightning/src/routing/gossip.rs@@ -18,3 +18,2 @@ use bitcoin::secp256k1;
-use bitcoin::hashes::sha256::Hash as Sha256Hash;
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
@@ -415,6 +414,6 @@ macro_rules! get_pubkey_from_node_id {
-fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {-	let mut engine = Sha256Hash::engine();+fn message_sha256d_hash<M: Writeable>(msg: &M) -> Sha256dHash {+	let mut engine = Sha256dHash::engine();
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");
-	Sha256dHash::from_engine(engine).into_inner()+	Sha256dHash::from_engine(engine)
}

@G8XSUG8XSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!
Feel moderately confident about this change.
(mainly moderate because of 18dc7f2)

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, now tracking the serialization cleanup over at #2724

@G8XSU

Copy link
Copy Markdown
Contributor

On a separate note: I do wonder if MAX_ALLOC_SIZE spread across multiple places in code while reading different structs needs re-visiting.

@TheBlueMatt
TheBlueMatt merged commit 103180d into lightningdevkit:mainNov 13, 2023
Sign up for freeto 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

@TheBlueMatt@codecov-commenter@G8XSU@tnull@jkczyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Reduce common allocations across the codebase - #2708

Merged
TheBlueMatt merged 12 commits into
lightningdevkit:mainfrom
TheBlueMatt:2023-11-less-graph-memory-frag
Nov 13, 2023
Merged

Reduce common allocations across the codebase#2708
TheBlueMatt merged 12 commits into
lightningdevkit:mainfrom
TheBlueMatt:2023-11-less-graph-memory-frag

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

My node has been experiencing more and more memory fragmentation lately, and while it seems the majority of that is #2706 and #2707, there's still plenty of room for misc improvements all over the place. With this and fixes for the other two issues we should be in a pretty good place, with allocations dominated by farrrr by block deserialization when syncing.

There's two commits here that could be performance regressions:

  • Pre-allocate the full require Vec prior to serializing into vecs which runs through our serialization logic twice in many cases before writing. I played around with a lower_bound Writeable method to optimize out some cases of having to run through the logic, but it doesn't really help in ChannelManager and ChannelMonitor or other deeply-nested structs because we're calling write there which hits our LengthCalculatingWriter instead of being able to use an optimized version. We could totally restructure the API to have Writeables call a magic method on the Writer which can short-circuit the write, but that's a lot of indirection and I'm lazy.
  • Avoid allocating when checking gossip message signatures probably isn't a huge regression, cause hashers are buffered, in essence, anyway, but I didn't check.

When we're reading a `NetworkGraph`, we know how many
nodes/channels we are reading, there's no reason not to
pre-allocate the `IndexedMap`'s inner `HashMap` and `Vec`, which we
do here.
This seems to reduce on-startup heap fragmentation with glibc by
something like 100MiB.
It does the same thing and its much simpler.
When forwarding gossip, rather than relying on Vec doubling,
pre-allocate the message encoding buffer.
...as LLVM will handle it just fine for us, in most cases.
@TheBlueMattTheBlueMatt added this to the 0.0.119 milestone Nov 4, 2023
@codecov-commenter

codecov-commenter commented Nov 4, 2023

Copy link
Copy Markdown

Codecov Report

Attention: 14 lines in your changes are missing coverage. Please review.

Comparison is base (281a0ae) 88.81% compared to head (7a951b1) 89.16%.
Report is 12 commits behind head on main.

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@ Coverage Diff @@## main #2708 +/- ##
==========================================
+ Coverage 88.81% 89.16% +0.34% 
==========================================
Files 113 113 Lines 89116 91476 +2360 Branches 89116 91476 +2360 ==========================================
+ Hits 79152 81561 +2409 + Misses 7722 7709 -13 + Partials 2242 2206 -36 
FilesCoverage Δ
lightning/src/blinded_path/utils.rs96.36% <100.00%> (-0.13%)⬇️
lightning/src/ln/channel.rs88.68% <ø> (+0.03%)⬆️
lightning/src/ln/script.rs93.57% <ø> (-0.14%)⬇️
lightning/src/routing/gossip.rs86.45% <100.00%> (+0.12%)⬆️
lightning/src/sign/type_resolver.rs75.00% <ø> (ø)
lightning/src/util/indexed_map.rs92.59% <100.00%> (+0.43%)⬆️
lightning/src/util/ser.rs76.74% <100.00%> (+0.23%)⬆️
lightning-net-tokio/src/lib.rs76.40% <94.11%> (+2.46%)⬆️
lightning/src/util/chacha20poly1305rfc.rs89.57% <75.00%> (-0.29%)⬇️
lightning/src/ln/peer_channel_encryptor.rs93.71% <95.23%> (+0.03%)⬆️
... and 1 more

... and 11 files with indirect coverage changes

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

Comment threadlightning/src/util/ser.rs
Comment threadlightning/src/util/chacha20poly1305rfc.rs Outdated
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning-net-tokio/src/lib.rs Outdated
Comment threadpending_changelog/113-channel-ser-compat.txt
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated
Comment threadlightning/src/routing/gossip.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 761aaad to f9ef511CompareNovember 6, 2023 16:58
pub(super) fn decrypt_in_place(&mut self, input_output: &mut [u8]) {
pub fn decrypt_in_place(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
self.just_decrypt_in_place(input_output);
if self.finish_and_check_tag(tag) { Ok(()) } else { Err(()) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Doubt: should tag be checked before decrypting cipher_text?
RFC: https://www.rfc-editor.org/rfc/rfc7539#appendix-A.5

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Doesn't mater, as long as we take the same amount of time in both the valid and invalid cases, and aren't actually doing anything with the decoded bytes until we check the mac. Theoretically its faster, I guess, if we check the mac first, but, like, its not a common case lol.

Comment threadlightning/src/util/ser.rs
peer.pending_outbound_buffer.pop_front();
// Try to keep the buffer to no more than 170 elements
const VEC_SIZE: usize = ::core::mem::size_of::<Vec<u8>>();
let large_capacity = peer.pending_outbound_buffer.capacity() > 4096 / VEC_SIZE;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what is the logic behind this?
"why 170" might be more helpful than "it is 170" in comment above.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Eh, I just dropped it. It wasn't saying anything the code wasn't already.

Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated

fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {
let mut engine = Sha256Hash::engine();
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

shouldn't we be specific?
"Gossip msg should not fail to serialize"

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Panic messages have file/line in them, that's more specific than any message we ever write :)


let mut key_data = VecWriter(Vec::new());
// TODO (taproot|arik): Introduce serialization distinction for non-ECDSA signers.
self.context.holder_signer.as_ecdsa().expect("Only ECDSA signers may be serialized").write(&mut key_data)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question: why did we used to write them?
So, nowadays we write channel_keys_id instead?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We used to write them because we didn't really have a fully-formed concept for how key derivation was supposed to work. Now we do and writing the signers is just redundant.

Comment threadCONTRIBUTING.md Outdated
pub fn decrypt_in_place(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
self.just_decrypt_in_place(input_output);
if self.finish_and_check_tag(tag) { Ok(()) } else { Err(()) }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: encrypt_full_message_in_place can be changed to encrypt_in_place to match/align with this.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Went with check_decrypt_in_place since I think its clearer and a bit more symmetric. Maybe we should rename the encryption side to mac_encrypt_in_place but we can do that another time.

We end up generating a substantial amount of allocations just
doubling `Vec`s when serializing to them, and our
`serialized_length` method is generally rather effecient, so we
just rely on it and allocate correctly up front.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 3f6969e to 74887dfCompareNovember 7, 2023 04:23
In the next commit we'll use this to avoid an allocation when
deserializing messages from the wire.
When decrypting P2P messages, we already have a read buffer that we
read the message into. There's no reason to allocate a new `Vec` to
store the decrypted message when we can just overwrite the read
buffer and call it a day.
When buffering outbound messages for peers, `LinkedList` adds
rather substantial allocation overhead, which we avoid here by
swapping for a `VecDeque`.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 74887df to a69dcc3CompareNovember 7, 2023 18:13
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Squashed with jeff's suggestion:

$ git diff-tree -U1 74887df8 a69dcc3a
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index 21792175a..fe7903d88 100644
--- a/lightning/src/routing/gossip.rs
+++ b/lightning/src/routing/gossip.rs
@@ -19,2 +19,3 @@ use bitcoin::secp256k1;
use bitcoin::hashes::sha256::Hash as Sha256Hash;
+use bitcoin::hashes::sha256d::Hash as Sha256dHash;
use bitcoin::hashes::Hash;
@@ -417,3 +418,3 @@ fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");
-	Sha256Hash::hash(&Sha256Hash::from_engine(engine)[..]).into_inner()
+	Sha256dHash::from_engine(engine).into_inner()
}

Comment threadlightning/src/routing/gossip.rs Outdated
Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated

@G8XSUG8XSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Lgmt! (apart from CI fix)

When we forward gossip messages, we store them in a separate buffer
before we encrypt them (and commit to the order in which they'll
appear on the wire). Rather than storing that buffer encoded with
no headroom, requiring re-allocating to add the message length and
two MAC blocks, we here add the headroom prior to pushing it into
the gossip buffer, avoiding an allocation.
Whenever we go to send bytes to a peer, we need to construct a
waker for tokio to call back into if we need to finish sending
later. That waker needs some reference to the peer's read task to
wake it up, hidden behind a single `*const ()`. To do this, we'd
previously simply stored a `Box<tokio::mpsc::Sender>` in that
pointer, which requires a `clone` for each waker construction. This
leads to substantial malloc traffic.
Instead, here, we replace this box with an `Arc`, leaving a single
`tokio::mpsc::Sender` floating around and simply change the
refcounts whenever we construct a new waker, which we can do
without allocations.
When we check gossip message signatures, there's no reason to
serialize out the full gossip message before hashing, and it
generates a lot of allocations during the initial startup when we
fetch the full gossip from peers.
This breaks backwards compatibility with versions of LDK prior to
0.0.113 as they expect to always read signer data.
This also substantially reduces allocations during `ChannelManager`
serialization, as we currently don't pre-allocate the `Vec` that
the signer gets written in to. We could alternatively pre-allocate
that `Vec`, but we've been set up to skip the write entirely for a
while, and 0.0.113 was released nearly a year ago. Users
downgrading to LDK 0.0.112 and before at this point should not be
expected.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from a69dcc3 to 7a951b1CompareNovember 9, 2023 22:28
@TheBlueMatt

TheBlueMatt commented Nov 9, 2023

Copy link
Copy Markdown
CollaboratorAuthor

Should pass this time, sorry about that:

$ git diff-tree -U1 a69dcc3ab 7a951b1bf
diff --git a/lightning/src/ln/peer_channel_encryptor.rs b/lightning/src/ln/peer_channel_encryptor.rs
index 298ff39b9..8569fa60f 100644
--- a/lightning/src/ln/peer_channel_encryptor.rs+++ b/lightning/src/ln/peer_channel_encryptor.rs@@ -436,4 +436,3 @@ impl PeerChannelEncryptor {
/// For effeciency, the [`Vec::capacity`] should be at least 16 bytes larger than the
-	/// [`Vec::length`], to avoid reallocating for the message MAC, which will be appended to the-	/// vec.+	/// [`Vec::len`], to avoid reallocating for the message MAC, which will be appended to the vec.
fn encrypt_message_with_header_0s(&mut self, msgbuf: &mut Vec<u8>) {
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index fe7903d88..ff8b084b7 100644
--- a/lightning/src/routing/gossip.rs+++ b/lightning/src/routing/gossip.rs@@ -18,3 +18,2 @@ use bitcoin::secp256k1;
-use bitcoin::hashes::sha256::Hash as Sha256Hash;
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
@@ -415,6 +414,6 @@ macro_rules! get_pubkey_from_node_id {
-fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {-	let mut engine = Sha256Hash::engine();+fn message_sha256d_hash<M: Writeable>(msg: &M) -> Sha256dHash {+	let mut engine = Sha256dHash::engine();
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");
-	Sha256dHash::from_engine(engine).into_inner()+	Sha256dHash::from_engine(engine)
}

@G8XSUG8XSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!
Feel moderately confident about this change.
(mainly moderate because of 18dc7f2)

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, now tracking the serialization cleanup over at #2724

@G8XSU

Copy link
Copy Markdown
Contributor

On a separate note: I do wonder if MAX_ALLOC_SIZE spread across multiple places in code while reading different structs needs re-visiting.

@TheBlueMatt
TheBlueMatt merged commit 103180d into lightningdevkit:mainNov 13, 2023
Sign up for freeto 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

@TheBlueMatt@codecov-commenter@G8XSU@tnull@jkczyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Reduce common allocations across the codebase - #2708

Merged
TheBlueMatt merged 12 commits into
lightningdevkit:mainfrom
TheBlueMatt:2023-11-less-graph-memory-frag
Nov 13, 2023
Merged

Reduce common allocations across the codebase#2708
TheBlueMatt merged 12 commits into
lightningdevkit:mainfrom
TheBlueMatt:2023-11-less-graph-memory-frag

Conversation

@TheBlueMatt

Copy link
Copy Markdown
Collaborator

My node has been experiencing more and more memory fragmentation lately, and while it seems the majority of that is #2706 and #2707, there's still plenty of room for misc improvements all over the place. With this and fixes for the other two issues we should be in a pretty good place, with allocations dominated by farrrr by block deserialization when syncing.

There's two commits here that could be performance regressions:

  • Pre-allocate the full require Vec prior to serializing into vecs which runs through our serialization logic twice in many cases before writing. I played around with a lower_bound Writeable method to optimize out some cases of having to run through the logic, but it doesn't really help in ChannelManager and ChannelMonitor or other deeply-nested structs because we're calling write there which hits our LengthCalculatingWriter instead of being able to use an optimized version. We could totally restructure the API to have Writeables call a magic method on the Writer which can short-circuit the write, but that's a lot of indirection and I'm lazy.
  • Avoid allocating when checking gossip message signatures probably isn't a huge regression, cause hashers are buffered, in essence, anyway, but I didn't check.

When we're reading a `NetworkGraph`, we know how many
nodes/channels we are reading, there's no reason not to
pre-allocate the `IndexedMap`'s inner `HashMap` and `Vec`, which we
do here.
This seems to reduce on-startup heap fragmentation with glibc by
something like 100MiB.
It does the same thing and its much simpler.
When forwarding gossip, rather than relying on Vec doubling,
pre-allocate the message encoding buffer.
...as LLVM will handle it just fine for us, in most cases.
@TheBlueMattTheBlueMatt added this to the 0.0.119 milestone Nov 4, 2023
@codecov-commenter

codecov-commenter commented Nov 4, 2023

Copy link
Copy Markdown

Codecov Report

Attention: 14 lines in your changes are missing coverage. Please review.

Comparison is base (281a0ae) 88.81% compared to head (7a951b1) 89.16%.
Report is 12 commits behind head on main.

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@ Coverage Diff @@## main #2708 +/- ##
==========================================
+ Coverage 88.81% 89.16% +0.34% 
==========================================
Files 113 113 Lines 89116 91476 +2360 Branches 89116 91476 +2360 ==========================================
+ Hits 79152 81561 +2409 + Misses 7722 7709 -13 + Partials 2242 2206 -36 
FilesCoverage Δ
lightning/src/blinded_path/utils.rs96.36% <100.00%> (-0.13%)⬇️
lightning/src/ln/channel.rs88.68% <ø> (+0.03%)⬆️
lightning/src/ln/script.rs93.57% <ø> (-0.14%)⬇️
lightning/src/routing/gossip.rs86.45% <100.00%> (+0.12%)⬆️
lightning/src/sign/type_resolver.rs75.00% <ø> (ø)
lightning/src/util/indexed_map.rs92.59% <100.00%> (+0.43%)⬆️
lightning/src/util/ser.rs76.74% <100.00%> (+0.23%)⬆️
lightning-net-tokio/src/lib.rs76.40% <94.11%> (+2.46%)⬆️
lightning/src/util/chacha20poly1305rfc.rs89.57% <75.00%> (-0.29%)⬇️
lightning/src/ln/peer_channel_encryptor.rs93.71% <95.23%> (+0.03%)⬆️
... and 1 more

... and 11 files with indirect coverage changes

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

Comment threadlightning/src/util/ser.rs
Comment threadlightning/src/util/chacha20poly1305rfc.rs Outdated
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning-net-tokio/src/lib.rs Outdated
Comment threadpending_changelog/113-channel-ser-compat.txt
Comment threadlightning/src/ln/peer_channel_encryptor.rs
Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated
Comment threadlightning/src/routing/gossip.rs Outdated
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 761aaad to f9ef511CompareNovember 6, 2023 16:58
pub(super) fn decrypt_in_place(&mut self, input_output: &mut [u8]) {
pub fn decrypt_in_place(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
self.just_decrypt_in_place(input_output);
if self.finish_and_check_tag(tag) { Ok(()) } else { Err(()) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Doubt: should tag be checked before decrypting cipher_text?
RFC: https://www.rfc-editor.org/rfc/rfc7539#appendix-A.5

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Doesn't mater, as long as we take the same amount of time in both the valid and invalid cases, and aren't actually doing anything with the decoded bytes until we check the mac. Theoretically its faster, I guess, if we check the mac first, but, like, its not a common case lol.

Comment threadlightning/src/util/ser.rs
peer.pending_outbound_buffer.pop_front();
// Try to keep the buffer to no more than 170 elements
const VEC_SIZE: usize = ::core::mem::size_of::<Vec<u8>>();
let large_capacity = peer.pending_outbound_buffer.capacity() > 4096 / VEC_SIZE;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what is the logic behind this?
"why 170" might be more helpful than "it is 170" in comment above.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Eh, I just dropped it. It wasn't saying anything the code wasn't already.

Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated

fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {
let mut engine = Sha256Hash::engine();
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

shouldn't we be specific?
"Gossip msg should not fail to serialize"

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Panic messages have file/line in them, that's more specific than any message we ever write :)


let mut key_data = VecWriter(Vec::new());
// TODO (taproot|arik): Introduce serialization distinction for non-ECDSA signers.
self.context.holder_signer.as_ecdsa().expect("Only ECDSA signers may be serialized").write(&mut key_data)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question: why did we used to write them?
So, nowadays we write channel_keys_id instead?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

We used to write them because we didn't really have a fully-formed concept for how key derivation was supposed to work. Now we do and writing the signers is just redundant.

Comment threadCONTRIBUTING.md Outdated
pub fn decrypt_in_place(&mut self, input_output: &mut [u8], tag: &[u8]) -> Result<(), ()> {
self.just_decrypt_in_place(input_output);
if self.finish_and_check_tag(tag) { Ok(()) } else { Err(()) }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: encrypt_full_message_in_place can be changed to encrypt_in_place to match/align with this.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

Went with check_decrypt_in_place since I think its clearer and a bit more symmetric. Maybe we should rename the encryption side to mac_encrypt_in_place but we can do that another time.

We end up generating a substantial amount of allocations just
doubling `Vec`s when serializing to them, and our
`serialized_length` method is generally rather effecient, so we
just rely on it and allocate correctly up front.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 3f6969e to 74887dfCompareNovember 7, 2023 04:23
In the next commit we'll use this to avoid an allocation when
deserializing messages from the wire.
When decrypting P2P messages, we already have a read buffer that we
read the message into. There's no reason to allocate a new `Vec` to
store the decrypted message when we can just overwrite the read
buffer and call it a day.
When buffering outbound messages for peers, `LinkedList` adds
rather substantial allocation overhead, which we avoid here by
swapping for a `VecDeque`.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from 74887df to a69dcc3CompareNovember 7, 2023 18:13
@TheBlueMatt

Copy link
Copy Markdown
CollaboratorAuthor

Squashed with jeff's suggestion:

$ git diff-tree -U1 74887df8 a69dcc3a
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index 21792175a..fe7903d88 100644
--- a/lightning/src/routing/gossip.rs
+++ b/lightning/src/routing/gossip.rs
@@ -19,2 +19,3 @@ use bitcoin::secp256k1;
use bitcoin::hashes::sha256::Hash as Sha256Hash;
+use bitcoin::hashes::sha256d::Hash as Sha256dHash;
use bitcoin::hashes::Hash;
@@ -417,3 +418,3 @@ fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");
-	Sha256Hash::hash(&Sha256Hash::from_engine(engine)[..]).into_inner()
+	Sha256dHash::from_engine(engine).into_inner()
}

Comment threadlightning/src/routing/gossip.rs Outdated
Comment threadlightning/src/ln/peer_channel_encryptor.rs Outdated

@G8XSUG8XSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Lgmt! (apart from CI fix)

When we forward gossip messages, we store them in a separate buffer
before we encrypt them (and commit to the order in which they'll
appear on the wire). Rather than storing that buffer encoded with
no headroom, requiring re-allocating to add the message length and
two MAC blocks, we here add the headroom prior to pushing it into
the gossip buffer, avoiding an allocation.
Whenever we go to send bytes to a peer, we need to construct a
waker for tokio to call back into if we need to finish sending
later. That waker needs some reference to the peer's read task to
wake it up, hidden behind a single `*const ()`. To do this, we'd
previously simply stored a `Box<tokio::mpsc::Sender>` in that
pointer, which requires a `clone` for each waker construction. This
leads to substantial malloc traffic.
Instead, here, we replace this box with an `Arc`, leaving a single
`tokio::mpsc::Sender` floating around and simply change the
refcounts whenever we construct a new waker, which we can do
without allocations.
When we check gossip message signatures, there's no reason to
serialize out the full gossip message before hashing, and it
generates a lot of allocations during the initial startup when we
fetch the full gossip from peers.
This breaks backwards compatibility with versions of LDK prior to
0.0.113 as they expect to always read signer data.
This also substantially reduces allocations during `ChannelManager`
serialization, as we currently don't pre-allocate the `Vec` that
the signer gets written in to. We could alternatively pre-allocate
that `Vec`, but we've been set up to skip the write entirely for a
while, and 0.0.113 was released nearly a year ago. Users
downgrading to LDK 0.0.112 and before at this point should not be
expected.
@TheBlueMatt
TheBlueMattforce-pushed the 2023-11-less-graph-memory-frag branch from a69dcc3 to 7a951b1CompareNovember 9, 2023 22:28
@TheBlueMatt

TheBlueMatt commented Nov 9, 2023

Copy link
Copy Markdown
CollaboratorAuthor

Should pass this time, sorry about that:

$ git diff-tree -U1 a69dcc3ab 7a951b1bf
diff --git a/lightning/src/ln/peer_channel_encryptor.rs b/lightning/src/ln/peer_channel_encryptor.rs
index 298ff39b9..8569fa60f 100644
--- a/lightning/src/ln/peer_channel_encryptor.rs+++ b/lightning/src/ln/peer_channel_encryptor.rs@@ -436,4 +436,3 @@ impl PeerChannelEncryptor {
/// For effeciency, the [`Vec::capacity`] should be at least 16 bytes larger than the
-	/// [`Vec::length`], to avoid reallocating for the message MAC, which will be appended to the-	/// vec.+	/// [`Vec::len`], to avoid reallocating for the message MAC, which will be appended to the vec.
fn encrypt_message_with_header_0s(&mut self, msgbuf: &mut Vec<u8>) {
diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs
index fe7903d88..ff8b084b7 100644
--- a/lightning/src/routing/gossip.rs+++ b/lightning/src/routing/gossip.rs@@ -18,3 +18,2 @@ use bitcoin::secp256k1;
-use bitcoin::hashes::sha256::Hash as Sha256Hash;
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
@@ -415,6 +414,6 @@ macro_rules! get_pubkey_from_node_id {
-fn message_sha256d_hash<M: Writeable>(msg: &M) -> [u8; 32] {-	let mut engine = Sha256Hash::engine();+fn message_sha256d_hash<M: Writeable>(msg: &M) -> Sha256dHash {+	let mut engine = Sha256dHash::engine();
msg.write(&mut engine).expect("In-memory structs should not fail to serialize");
-	Sha256dHash::from_engine(engine).into_inner()+	Sha256dHash::from_engine(engine)
}

@G8XSUG8XSU left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!
Feel moderately confident about this change.
(mainly moderate because of 18dc7f2)

@tnulltnull left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, now tracking the serialization cleanup over at #2724

@G8XSU

Copy link
Copy Markdown
Contributor

On a separate note: I do wonder if MAX_ALLOC_SIZE spread across multiple places in code while reading different structs needs re-visiting.

@TheBlueMatt
TheBlueMatt merged commit 103180d into lightningdevkit:mainNov 13, 2023
Sign up for freeto 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

@TheBlueMatt@codecov-commenter@G8XSU@tnull@jkczyz