perf: find SSE line terminators with ByteString.indexOf - #1226
Open
pjfanning wants to merge 1 commit into
Open
Conversation
Motivation:
LineParser scanned its buffer one byte at a time with `bs(at)`. The buffer is
built as `buffer ++ grab(in)`, so it is a multi-fragment ByteString whenever a
line spans several chunks, and `ByteStrings.apply` walks the fragment list from
the first fragment on every access. Parsing an SSE line that arrives in many
chunks therefore costs O(bytes * fragments).
Modification:
Locate the next CR or LF with ByteString.indexOf, which is fragment aware and
scans several bytes at a time, instead of testing every byte. The three
identical line-emitting blocks are factored into a local helper. Line
termination semantics (CR, LF, CRLF, and a CRLF split across chunks) are
unchanged.
Result:
Large SSE lines delivered in small chunks parse dramatically faster; small
lines are unaffected.
lineSize chunkSize before after
1 KB 512 0.265 ms 0.189 ms
1 KB 8192 0.295 ms 0.183 ms
128 KB 512 71.210 ms 1.013 ms
128 KB 8192 3.269 ms 0.342 ms
1 MB 512 9821.281 ms 54.361 ms
Tests:
- sbt "http-tests / Test / testOnly org.apache.pekko.http.scaladsl.unmarshalling.sse.*" - 48 passed (46 existing plus 2 new)
- sbt "http-bench-jmh/Jmh/run -f 1 -wi 3 -i 3 -p lineSize=... -p chunkSize=... .*LineParserBenchmark.*" - numbers above, JDK 21
- scalafmt --mode diff-ref=upstream/main - clean
References:
None - found while auditing ByteString usage across the code base
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
LineParserscanned its buffer one byte at a time:The buffer is built as
buffer ++ grab(in), so it is a multi-fragmentByteStringwhenever a line spans more than one chunk.ByteString$ByteStrings.applyhas no offset index or position cache - it walks the fragment vector from the first fragment on every access - so scanning an SSE line that arrives in N chunks costs O(bytes × N).ByteString.indexOf(byte, from)is the opposite: it walks fragments once and, inside a fragment, usesSWARUtilto test 8 bytes per step.Modification
Jump to the next CR or LF with
indexOfinstead of testing every byte. The three identical line-emitting blocks are factored into a locallineAthelper.Line termination semantics are unchanged - CR, LF, CRLF, a lone CR followed by a non-LF byte, and a CRLF split across two chunks (the
lastCharWasCrflag) all behave exactly as before. The oversized-line handling (maxLineSizeand everyOversizedSseStrategy) is untouched, just moved into the helper.Result
LineParserBenchmark, JDK 21,-f 1 -wi 3 -i 3:Small lines are unaffected; the gain scales with how many chunks a line is split across, which is exactly the shape of a real SSE stream over a network.
Note this is independent of the pending pekko-core change that gives
ByteStrings.applya fragment hint. That hint helps monotonic forward scans, butindexOfstill avoids one virtual call per byte and tests 8 bytes at a time, and this change also benefits the 1.x line, which is pinned to pekko 1.1.5.Tests
sbt "http-tests / Test / testOnly org.apache.pekko.http.scaladsl.unmarshalling.sse.*"- 48 passed (46 existing, plus 2 new inLineParserSpec: a CRLF split across two chunks, and lines parsed out of an explicitly multi-fragmentByteString, asserted non-compact so the test really covers the rope path).sbt "http-bench-jmh/Jmh/run …"- numbers above, measured before and after on the same machine.scalafmt --mode diff-ref=upstream/main- clean.References
None - found while auditing ByteString usage across the code base