Skip to content

Keep sender-side batching for TLS ciphertext input - #3737

Closed
pjfanning wants to merge 1 commit into
apache:mainfrom
pjfanning:tls-unbatched-plain-input-only
Closed

pjfanning wants to merge 1 commit into
apache:mainfrom
pjfanning:tls-unbatched-plain-input-only

Conversation

@pjfanning

Copy link
Copy Markdown
Member

Motivation

#3727 opted the GraphStage TLS engine's input boundaries out of sender-side batching so plaintext is not overtaken by a later plaintext-output cancellation. The opt-out (UnbatchedInput) was checked against the TLS stage's attributes, which every input boundary of the TLS island inherits. So ciphertext input from the transport also crossed the async boundary as one actor message per element, although it has no such ordering constraint and it is the hot inbound path.

Modification

  • UnbatchedInput now names the inlets it applies to. In GraphStageIsland.takePublisher, the materializer marks a BatchingActorInputBoundary unbatched only when the inlet it feeds is listed. ActorOutputBoundary checks that flag instead of the attribute.
  • TlsGraphStage.StreamTlsAttributes lists only the plaintext input inlet.
  • ActorOutputBoundary.batchesToSubscriber (private[stream]) exposes the chosen mode for the test.

Result

Plaintext input keeps the #3727 ordering guarantee. Ciphertext input is batched across the async boundary again.

Tests

  • stream-tests/testOnly org.apache.pekko.stream.io.TlsGraphStageIsolatedSpec -- -z "batch transport input": new test, passes. It runs a real TlsGraphStage and checks the mode chosen by the output boundary in front of each inlet. It fails when cipherIn is added back to UnbatchedInput (false was not equal to true on the ciphertext assertion).
  • stream-tests/testOnly org.apache.pekko.stream.io.Tls*: 11 suites, 286 tests passed
  • stream/mimaReportBinaryIssues: no issues (UnbatchedInput is internal and unreleased)
  • scalafmt --mode diff-ref=upstream/main: clean

References

Refs #3727

Motivation:
apache#3727 opted the GraphStage TLS engine's input boundaries out of sender-side
batching so plaintext is not overtaken by a later plaintext-output
cancellation. The opt-out applied to every input boundary of the TLS island,
so ciphertext input from the transport also crossed the async boundary as one
actor message per element, although it has no such ordering constraint.

Modification:
- UnbatchedInput now names the inlets it applies to. The materializer marks a
  BatchingActorInputBoundary unbatched only when the inlet it feeds is listed.
- TlsGraphStage lists only its plaintext input inlet.

Result:
Plaintext input keeps the apache#3727 ordering guarantee; ciphertext input is batched
across the async boundary again.

Tests:
- stream-tests/testOnly TlsGraphStageIsolatedSpec -z "batch transport input": passes; fails with
  cipherIn added back to UnbatchedInput (false was not equal to true)
- stream-tests/testOnly org.apache.pekko.stream.io.Tls*: 286 passed
- stream/mimaReportBinaryIssues: no issues
- scalafmt --mode diff-ref=upstream/main: clean

References:
Refs apache#3727
@pjfanning pjfanning added this to the 2.0.0-M5 milestone Oct 30, 2026
@pjfanning

Copy link
Copy Markdown
Member Author

Quick JMH run of TlsBenchmark.warmRoundTrip (implementation=graphstage): 1000 records through an in-memory client/server TLS pair. The server's ciphertext input crosses an async boundary from the client stage, so this exercises the changed path.

sbt "bench-jmh/Jmh/run -wi 3 -i 5 -f1 -t1 -p implementation=graphstage .*TlsBenchmark.warmRoundTrip", JDK 17, macOS laptop. Baseline = this branch with cipherIn added back to UnbatchedInput (i.e. the behaviour on main).

payload this PR (ops/ms) baseline (ops/ms)
256 B 94.7 ± 53.8 111.2 ± 62.7
4 KiB 21.5 ± 19.7 33.6 ± 31.4
64 KiB 4.90 ± 2.23 4.97 ± 2.43

This does not show an improvement. The error bars overlap everywhere, but the baseline is nominally faster at 256 B and 4 KiB (4 KiB iterations: 15–27 ops/ms here vs 24–46 baseline), and 64 KiB is flat. One fork, short iterations and a laptop make this noisy, and the two variants ran sequentially rather than interleaved, so it isn't conclusive either way. This benchmark is also round-trip/echo shaped, where batches are often a single element; it doesn't model bulk inbound transfer.

I'd treat the perf benefit of this PR as unproven until a longer run (more forks, interleaved variants) or a bulk-inbound benchmark shows it.

@pjfanning pjfanning removed this from the 2.0.0-M5 milestone Oct 30, 2026
@pjfanning
pjfanning marked this pull request as draft October 30, 2026 10:12
@pjfanning

Copy link
Copy Markdown
Member Author

Follow-up JMH run with more forks and interleaved variants, addressing the caveats in my previous comment.

Setup. Both variants are in one JMH invocation through a temporary (uncommitted) @Param in TlsBenchmark that picks the TLS attributes at runtime, so nothing is rebuilt between them:

  • plain = this PR (only plaintext input unbatched)
  • both = main (ciphertext input also unbatched)

4 rounds of bench-jmh/Jmh/run -wi 5 -i 10 -w 1s -r 1s -f1 -t1 -p implementation=graphstage .*TlsBenchmark.warmRoundTrip, alternating which variant runs first (rounds 1 and 3: plain,both; rounds 2 and 4: both,plain; the order was confirmed in the JMH output). That is 4 forks × 10 measured iterations per variant and payload. JDK 17, macOS laptop.

Score per round (ops/ms, higher is better):

payload variant round 1 round 2 round 3 round 4 mean
256 B this PR 92.2 138.8 125.1 137.4 123.4
256 B main 121.5 112.2 138.6 131.9 126.0
4 KiB this PR 34.3 49.8 47.5 48.9 45.1
4 KiB main 41.2 50.3 50.7 51.8 48.5
64 KiB this PR 4.97 4.81 4.79 4.80 4.84
64 KiB main 4.58 4.92 4.85 5.01 4.84

Difference within each round (this PR vs main):

payload round 1 round 2 round 3 round 4 mean
256 B −24.1% +23.8% −9.8% +4.2% −1.5%
4 KiB −16.8% −0.9% −6.4% −5.7% −7.5%
64 KiB +8.5% −2.2% −1.2% −4.2% +0.2%

Reading:

  • 256 B: no signal. The sign follows run order (whichever variant ran second won in every round), so position effects dominate.
  • 4 KiB: this PR is slower in all 4 rounds, whichever variant ran first. Round 1 looks cold (both variants low); without it the gap is about −4%. A small, consistent regression, not noise.
  • 64 KiB: flat.

So on this echo/round-trip benchmark, batching ciphertext input does not help, and at 4 KiB it costs a few percent. A plausible cause is that in a ping-pong exchange, batches are usually one element, so batching adds bookkeeping without saving messages. This benchmark does not model bulk inbound transfer, where batching could still pay off, but there is no evidence for that yet.

I'd not merge this as a performance change on the current evidence.

@pjfanning

Copy link
Copy Markdown
Member Author

Closing: the interleaved JMH runs show no throughput gain from batching TLS ciphertext input (and a small regression at 4 KiB). Plaintext ordering relative to cancellation is already handled on main.

@pjfanning pjfanning closed this Oct 30, 2026
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.

1 participant