bgpd: reduce data copying in BGP I/O thread receive path#22504
bgpd: reduce data copying in BGP I/O thread receive path#22504enkechen-panw wants to merge 3 commits into
Conversation
3820afb to
9fe5bc3
Compare
|
@greptileai review |
Greptile SummaryThis PR replaces the
Confidence Score: 5/5The change is safe to merge. The core invariant — ibuf_work is non-NULL if and only if ibuf_data_len > 0 — is correctly maintained across all code paths: normal completion, EAGAIN, TCP fatal error, and invalid-header teardown. Buffer size arithmetic ensures ibuf_scratch cannot overflow. The inq_limit back-pressure semantics are intentional and correctly re-armed by the existing bgp_process_packet drain logic. All cleanup paths (bgp_stop, bgp_peer_connection_buffers_free, fatal in bgp_process_reads) correctly reset both ibuf_work and ibuf_data_len. The parse_buffer loop always advances by at least BGP_HEADER_SIZE per iteration and is bounded by the validated pktsize, so no infinite-loop or overflow is possible. The test suite exercises the full scratch/work round-trip, EAGAIN preservation, multi-message batches, and memory-allocation accounting. bgpd/bgp_io.c warrants a careful read for any future modifications, as correctness depends on the ibuf_data_len/ibuf_work pair being updated atomically at the end of each bgp_process_reads() call. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant K as Kernel TCP Buffer
participant IO as I/O Thread (bgp_process_reads)
participant S as ibuf_scratch (static)
participant W as ibuf_work (per-conn, on demand)
participant Q as connection->ibuf (FIFO)
participant M as Main Thread
Note over IO: Check inq_limit before reading
IO->>K: read(fd, ibuf_scratch[+offset], readsize)
K-->>S: new bytes land at ibuf_scratch[ibuf_data_len..]
Note over S: ibuf_scratch[0..ibuf_data_len-1] = prior partial
IO->>IO: parse_buffer(ibuf_scratch, total_len)
loop For each complete packet
IO->>Q: stream_fifo_push(pkt) [under io_mtx]
end
alt Partial tail remains
IO->>W: memcpy(ibuf_work, tail, remaining)
Note over W: ibuf_data_len = remaining
else No remaining data
IO->>W: XFREE(ibuf_work)
Note over W: ibuf_data_len = 0
end
IO->>IO: event_add_read (reschedule)
IO->>M: event_add_event (bgp_process_packet)
M->>Q: stream_fifo_pop [under io_mtx]
alt ibuf was at inq_limit before pop
M->>IO: bgp_reads_on() — re-arm read event
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant K as Kernel TCP Buffer
participant IO as I/O Thread (bgp_process_reads)
participant S as ibuf_scratch (static)
participant W as ibuf_work (per-conn, on demand)
participant Q as connection->ibuf (FIFO)
participant M as Main Thread
Note over IO: Check inq_limit before reading
IO->>K: read(fd, ibuf_scratch[+offset], readsize)
K-->>S: new bytes land at ibuf_scratch[ibuf_data_len..]
Note over S: ibuf_scratch[0..ibuf_data_len-1] = prior partial
IO->>IO: parse_buffer(ibuf_scratch, total_len)
loop For each complete packet
IO->>Q: stream_fifo_push(pkt) [under io_mtx]
end
alt Partial tail remains
IO->>W: memcpy(ibuf_work, tail, remaining)
Note over W: ibuf_data_len = remaining
else No remaining data
IO->>W: XFREE(ibuf_work)
Note over W: ibuf_data_len = 0
end
IO->>IO: event_add_read (reschedule)
IO->>M: event_add_event (bgp_process_packet)
M->>Q: stream_fifo_pop [under io_mtx]
alt ibuf was at inq_limit before pop
M->>IO: bgp_reads_on() — re-arm read event
end
Reviews (4): Last reviewed commit: "tests: add unit tests for BGP ibuf_scrat..." | Re-trigger Greptile |
0b82455 to
a4529c6
Compare
|
@greptileai review |
a4529c6 to
8590189
Compare
|
@greptileai review |
8590189 to
387fb9d
Compare
|
at first glance this breaks the intentional line between the io pthread just reading data and the master pthread handling the actual parsing of packets. Without some serious proof that this is a huge improvement of some sort I'm mroe than a bit skeptical to even take such a change. Especially since this should probably be broken up into more commits instead of just a single big change. |
Hi, @donaldsharp The optimization (and simplification) is only for the BGP I/O thread, and it does not change the interaction with the main thread. The commit title and commit message have been revised and clarified. For splitting the commit, I have made the "input queue limit" change as a separate commit (and self-contained). Thanks. |
387fb9d to
aa13185
Compare
Move the input queue limit check from read_ibuf_work() to before calling read(). This provides TCP back-pressure: when the queue is full, we stop reading, the TCP receive window fills up, and the sender is slowed down. Previously, the limit was checked during read_ibuf_work() after data was already read into the buffer. Now we avoid the read entirely when the queue is at capacity. Messages from a single read are all processed even if temporarily exceeding the limit - the check gates whether we read, not whether we parse what was already read. Signed-off-by: Enke Chen <enchen@paloaltonetworks.com>
The current BGP receive path always copies data from ibuf_scratch to ibuf_work ringbuf, even when packets are complete. This patch minimizes data copying in the critical receive path. BEFORE: 1. read() -> ibuf_scratch 2. ringbuf_put() copies ibuf_scratch -> ibuf_work 3. Validate header from ibuf_work 4. ringbuf_get() copies packet -> stream for main thread AFTER: 1. Copy partial data (if any) from ibuf_work to ibuf_scratch 2. read() -> ibuf_scratch 3. Validate header directly from ibuf_scratch 4. memcpy() packet -> stream for main thread 5. Copy remaining partial data (if any) to ibuf_work For complete packets, steps 1 and 5 are no-ops - no extra copy. Key changes: - Replace ringbuf with simple linear buffer (uint8_t *ibuf_work) - Validate headers directly from ibuf_scratch buffer - Add MTYPE_BGP_IBUF_WORK for memory tracking The changes are limited to the BGP I/O thread. The interface to the main thread is unchanged - complete packets are still pushed to connection->ibuf for processing by bgp_process_packet(). Signed-off-by: Enke Chen <enchen@paloaltonetworks.com>
Add test_ibuf_work.c with tests covering the ibuf_scratch and ibuf_work buffer management: - basic validation: length extraction, marker validation, header/message detection, buffer sizes - buffer handling: complete/partial packets, multiple messages - scratch/work interaction: split messages across reads - ibuf_work reuse: partial->complete->partial without reallocation - EAGAIN handling: symmetric flow with no new data - bgp_read: copy partial from ibuf_work to ibuf_scratch before read - fatal error cleanup - near-full buffer handling - memory tracking to verify allocation only on partial packets Signed-off-by: Enke Chen <enchen@paloaltonetworks.com>
aa13185 to
1171722
Compare
|
If you don't answer what the performance improvements are this PR is gonna go nowhere. Please spend some time quantifying how this is better and how performance is being improved. |
I don't have the numbers right now. Will work on it. |
|
Hi, @donaldsharp I instrumented the code, and got the comparison data: Thanks. |
The current BGP receive path always copies data from ibuf_scratch
to ibuf_work ringbuf, even when packets are complete. This patch
minimizes data copying in the critical receive path.
BEFORE:
AFTER:
For complete packets, steps 1 and 5 are no-ops - no extra copy.
Key changes:
Soft limit: inq_limit is checked before reading, not during parsing.
This keeps the kernel receive buffer full when the app queue is at
limit, allowing TCP to advertise a smaller/zero receive window and
propagate back-pressure to the sender. parse_buffer() processes all
complete messages from a single read, and ibuf_work is used for
partial packets only.
The changes are limited to the BGP I/O thread. The interface to
the main thread is unchanged - complete packets are still pushed
to connection->ibuf for processing by bgp_process_packet().