Skip to content

Always use SSLTransport_Transport when ktls is not requested - #56

Open
tarasko wants to merge 5 commits into
masterfrom
refactor_ssltr
Open

Always use SSLTransport_Transport when ktls is not requested#56
tarasko wants to merge 5 commits into
masterfrom
refactor_ssltr

Conversation

@tarasko

@tarasko tarasko commented Jul 29, 2026

Copy link
Copy Markdown
Owner

No description provided.

@codspeed-hq

codspeed-hq Bot commented Jul 29, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 14.01%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 3 regressed benchmarks
✅ 37 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
test_benchmark_writelines[ssl_mbio-large] 57.4 ms 70.4 ms -18.45%
test_benchmark_write[ssl_mbio-buffered-large] 94.1 ms 106.6 ms -11.72%
test_benchmark_write[ssl_mbio-simple-large] 93.4 ms 105.8 ms -11.68%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing refactor_ssltr (b9e8318) with master (de9feaf)

Open in CodSpeed

@tarasko

tarasko commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

@codspeedbot explain why it is slower?

@codecov-commenter

codecov-commenter commented Jul 29, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.60%. Comparing base (de9feaf) to head (b9e8318).
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master      #56      +/-   ##
==========================================
- Coverage   90.97%   90.60%   -0.37%     
==========================================
  Files          15       15              
  Lines        2913     2853      -60     
  Branches       17       17              
==========================================
- Hits         2650     2585      -65     
- Misses        257      262       +5     
  Partials        6        6              
Flag Coverage Δ
Linux 88.76% <100.00%> (-0.42%) ⬇️
Windows 82.78% <100.00%> (-0.50%) ⬇️
macOS 85.47% <100.00%> (-0.24%) ⬇️

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

Files with missing lines Coverage Δ
aiofastnet/__init__.py 100.00% <100.00%> (ø)
aiofastnet/ssl_engine_direct.pyx 89.71% <100.00%> (-0.25%) ⬇️
aiofastnet/ssl_transport.pyx 93.26% <100.00%> (-1.05%) ⬇️

... and 1 file with indirect coverage changes

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

@codspeed-hq

codspeed-hq Bot commented Jul 29, 2026

Copy link
Copy Markdown

@codspeedbot explain why it is slower?

The whole regression is one extra memory copy on the write path, introduced by routing memory-BIO TLS through SSLTransport_Transport + SocketTransport instead of the fused SSLTransport_Socket.

What the profiles show

__memcpy_avx_unaligned_erms self time on test_benchmark_write[ssl_mbio-simple-large]: 52.0 ms → 66.4 ms. The callers list changed, and a new one appears on HEAD:

Run memcpy caller Time
master static_mem_bio_write, static_mem_bio_read, libssl internals
refactor_ssltr transport._make_write_request_from_ptr (new) 12.4 ms

That single new call site accounts for essentially the entire delta:

Benchmark BASE HEAD Delta memcpy under _make_write_request_from_ptr
write[ssl_mbio-simple-large] 93.4 ms 105.9 ms +12.5 ms 12.4 ms
writelines[ssl_mbio-large] 57.4 ms 70.4 ms +13.0 ms 12.5 ms
write[ssl_mbio-buffered-large] 94.1 ms 106.6 ms +12.5 ms ~12.5 ms

The function does not exist in the base profiles at all, and it does not appear in the -small variants of the same benchmarks — which is exactly the set of benchmarks that did not regress.

Why

On master, ssl_mbio (direct engine, no kTLS) used SSLTransport_Socket, whose _flush_outgoing_bio sent encrypted bytes straight out of the outgoing BIO with aiofn_send(self._sock_fd, ptr, sz). On a partial write or EAGAIN it consumed only what the kernel accepted and left the remainder sitting in the BIO, retrying from the same memory later. The outgoing BIO was the write buffer — zero copy, zero allocation.

This PR always uses SSLTransport_Transport for memory BIO, so _flush_outgoing_bio_direct now hands the data to the underlying transport with (<Transport>self._transport).write_c(ptr, sz) and then immediately calls outgoing_bio_consume(sz). Since the BIO region is released right away, Transport.write_c cannot keep a reference to it: as soon as the send is partial or returns EAGAIN, or the backlog is already non-empty, it must snapshot the chunk into a fresh bytes object (_make_write_request_from_ptrmalloc + memcpy).

With large payloads the socket send buffer fills on almost every iteration, so nearly every encrypted chunk goes through that copy. With small payloads each send() completes in one go, the backlog stays empty, and no copy ever happens — hence the clean split between -large (regressed 12–18%) and -small (untouched).

The read side is unaffected: SSLTransport_Transport.get_buffer_c still exposes the incoming BIO write buffer directly to the socket transport, so receives remain zero-copy.

Options if you want to keep the simplification

  1. Let the BIO stay the write buffer. Have write_c report how many bytes it actually handed to the kernel and only outgoing_bio_consume(bytes_sent), keeping the unsent tail in the outgoing BIO and re-flushing it from the writer callback. This restores the master behaviour inside the layered design, at the cost of the invariant that the BIO must not be refilled while a flush is pending.
  2. Zero-copy backlog entry. Add a write-request variant that borrows the BIO memory plus an owner token instead of copying, and only consume the BIO once the request is fully written.
  3. Accept the cost as the price of dropping the dual transport implementations — it is bounded to bulk writes under backpressure, roughly one extra copy of the ciphertext.

Caveat on the numbers

The two runs landed on different runners (EPYC 7763 → 9V74), which the report flagged. That does not change the conclusion here: the delta is attributable to a specific new call site added by this diff, and its measured cost matches the reported slowdown to within a few hundred microseconds on all three benchmarks.

No code changes were made — this was analysis only.

Summary
  • Warming up the performance wizard...
  • Investigating the ssl_mbio regression — Root cause identified: the layered SSLTransport_Transport write path adds a copy of the outgoing BIO data into the socket transport write backlog
  • Handling comment — Comment handling finished successfully

Was this response helpful? React with 👍 or 👎 to help us improve CodSpeedBot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants