Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Introduce PacketBatch with Growable buffer #29859

Closed
wants to merge 3 commits into from

Conversation

steviez
Copy link
Contributor

@steviez steviez commented Jan 24, 2023

WIP: Still pulling stuff in, so this PR should be seeing some heavy adds for the next couple days. In any case, I'm being intentional with splitting things up per-commit. The "scaffolding" changes are currently left for sake of reducing the diff, and I have already identified a piece or two that can be carved out in their own PR's. Working on that to keep what will be the end diff reasonable.

Problem

We want to support transactions that exceed the current 1232 byte limit. In order to do so, we need to use a container that is capable of holding transactions that are larger in serialized size. Another PR (#29055) took a blanket approach of adjusting the buffer for all Packets along the Tx processing path, but that approach uses additional memory for all transactions, even those that fit well within the original 1232 byte buffer.

Summary of Changes

Introduce a data structure, TxPacketBatch (leaning towards rename to GrowablePacketBatch). This data structure is meant to be a drop-in for the existing PacketBatch with following callouts:

  • The buffer length is the same for all packets in the batch, but can be grown at runtime (ie if a caller wants more space than the default 1232 bytes)
  • The buffers are laid out contiguously in memory (as opposed to a Vec<Vec<u8>>) for sake of memory locality (perf + ability to sigverify with GPU)

Fixes #

@steviez steviez added the noCI Suppress CI on this Pull Request label Jan 24, 2023
full_packet_batches_count.fetch_add(1, Ordering::Relaxed);
}

let packet_batch = TxPacketBatch::from_packet_batch(packet_batch);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This while recv_loop2() & receiver2() is a kind of a hack I made at the moment in lieu of deferring a design decision to review.

The question is whether we want to use the new data structure TxPacketBatch all throughout the codebase (Tx's, shreds, gossip), or keep its' use limited to the Tx path. As I have it coded now, I keep the change limited to the Tx processing path, and this is a shim for minimizing code duplication (can probably get something cleaner in the end result).

So, the direction question that I'd like feedback on is: Should we keep the existing PacketBatch data structure around for uses where we will continue to use UDP (and thus keep the 1232 byte restriction), or do we want to go all in and use the new data structure everywhere?

Comment on lines 39 to 46
/// Current capacity of batch (packets)
capacity: usize,
/// Current length of batch (packets)
len: usize,
/// Length of batch packet's payload buffer (bytes)
buffer_len: usize,
/// Offset to ensure Meta references from buffer are properly aligned
alignment_offset: usize,
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if it would be useful to use wrapper types to put information as to the kind of value counted by these usize fields into types.
The (packets), (bytes) clarifications in the field comments might be pointing in that direction.

Here is a relevant discussion: https://www.reddit.com/r/rust/comments/hp6dx4/idiomatic_custom_number_types/

So something like

use derive_more::{Add, Display, From, Into};

#[derive(PartialEq, Eq, Debug, From, Add)]
struct PacketCount(usize);

#[derive(PartialEq, Eq, Debug, From, Add)]
struct BufferSize(usize);

pub struct TxPacketBatch {
    /// Shared buffer for all packets in batch
    // TODO: change to u64 so we get guaranteed same alignment as Packet (8)
    data: PinnedVec<u8>,
    /// Current capacity of batch (packets)
    capacity: PacketCount,
    /// Current length of batch (packets)
    len: PacketCount,
    /// Length of batch packet's payload buffer (bytes)
    buffer_len: BufferSize,
    /// Offset to ensure Meta references from buffer are properly aligned
    // TODO Not sure what unit is the right one to use here.  `ByteOffset`?
    alignment_offset: usize,
}

I have not seen much use of wrapper types in other cases.
So, this might be out of scope for this change.

perf/src/tx_packet_batch.rs Outdated Show resolved Hide resolved
perf/src/tx_packet_batch.rs Outdated Show resolved Hide resolved
Comment on lines +96 to +126
pub fn from_packet_batch(old_batch: PacketBatch) -> Self {
// This implementation adapted from an example provided in the
// std::mem::transmute documentation
// https://doc.rust-lang.org/std/mem/fn.transmute.html
let mut old_batch = ManuallyDrop::new(old_batch);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be good to explain the reasoning behind the transformations that happen here.
A reference to transmute is useful, but it is better to just be explicit.
My guess is that we are trying to reinterpret PacketBatch content directly as a sequence of bytes.
But you might have additional reasoning, such as performance expectations.

Comment on lines 110 to 142
let data = unsafe {
PinnedVec::from_vec(Vec::from_raw_parts(
ptr as *mut u8,
len * size_of::<Packet>(),
capacity * size_of::<Packet>(),
))
};
Copy link
Contributor

Choose a reason for hiding this comment

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

This transformation looks pretty scary.
It is only correct, if both PacketBatch is Copy.
I wonder if there is a way to enforce that.

At the same time, my understanding is that elsewhere, when we send data into the network or receive it from the network, we do not directly treat memory content as bytes.
We use bincode and other serialization libraries.
Maybe instead of reinterpreting bytes directly here, there is a safer way that provides the same performance?
For example, serializing PacketBatch into a byte buffer that will be later sent over the network?

perf/src/tx_packet_batch.rs Outdated Show resolved Hide resolved
perf/src/tx_packet_batch.rs Show resolved Hide resolved
Comment on lines +179 to +205
let new_buffer_len = TxPacketSize::Double;
let new_buffer_len = new_buffer_len.into();
Copy link
Contributor

Choose a reason for hiding this comment

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

minor

Maybe just

        let new_buffer_len = TxPacketSize::Double.into();

@@ -216,27 +202,11 @@ impl SigVerifierStats {
}
}

impl SigVerifier for DisabledSigVerifier {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe add a note into the commit description as to why you are removing this trait?
Looks like it had only one use case - part of the API of TransactionSigVerifier.
Was it an unnecessary abstraction?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Was it an unnecessary abstraction?

Yes, made things a little easier with this change. I intentionally have that change broken out into its' own commit, and should we proceed with this PR, I will open a separate PR for just that commit and rebase this PR on top

@@ -85,14 +83,14 @@ impl Deduper {

pub fn dedup_packets_and_count_discards(
&self,
batches: &mut [PacketBatch],
mut process_received_packet: impl FnMut(&mut Packet, bool, bool),
batches: &mut Vec<TxPacketBatch>,
Copy link
Contributor

Choose a reason for hiding this comment

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

A change from &mut [T] to &mut Vec[T] is reducing generalization.
As the rest of the changes in this patch do not need that, maybe keep &mut [T]?

#[inline]
/// Returns a mutable pointer to the beginning of the data buffer.
pub fn as_mut_ptr(&mut self) -> *mut u8 {
unsafe { self.data.as_mut_ptr().cast::<u8>().add(0) }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops, I'll clean this up

@steviez
Copy link
Contributor Author

steviez commented Jan 27, 2023

Had a good conversation with @ilya-bobyr - this approach temporarily on hold in favor of another

@github-actions github-actions bot added the stale [bot only] Added to stale content; results in auto-close after a week. label Feb 10, 2023
@github-actions github-actions bot closed this Feb 20, 2023
@steviez steviez deleted the XxTx branch May 25, 2023 04:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
noCI Suppress CI on this Pull Request stale [bot only] Added to stale content; results in auto-close after a week.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants