Repository navigation
postprocess: scan SSE lines linearly, and skip chunks without the field - #32
Merged
Merged
Conversation
With strip_actual_usage on, every SSE frame cost ~8 us of CPU, though only the final usage frame is ever rewritten. ~7 us of it was the line pattern ^data:%s*(.-)%s*\r?\n?$: its lazy capture re-tries the rest of the line at every character, so a 216-byte frame paid for ~200 attempts. - sse_payload(): the same result as that pattern, from one linear scan (prefix check, then whitespace trimmed from both ends; "whitespace" is exactly Lua's %s). Used by both handlers. - json_rewrite_handler(rewrite_json, needle): a stream chunk that does not contain the needle is passed on without being split into lines, except a line it ends in the middle of, which is held back with the next chunk -- the needle may straddle the boundary. strip_actual_usage passes "actual_usage"; vllm_format, which touches every frame, passes none. Measured on one core (test/bench_postprocess.lua, 2000 content frames of ~216 bytes, 1 frame per chunk), ns per frame: off 21 -> 21 strip_actual_usage 8038 -> 270 vllm_format 14614 -> 5316 test/utest_postprocess_sse_scan.lua: - sse_payload agrees with the pattern on 20025 lines (edge cases and random); - 500 random chunkings: a stream without the field comes back byte-identical every time, and a stream with it is stripped every time; - a needle split exactly across two chunks is still stripped.
"Streams are not held" was not quite true: a line a chunk ends in the middle of waits for the next chunk (it always has; the line splitter needs the whole line). Complete lines still go out with the chunk they arrived in.
zoboth
approved these changes
Nov 10, 2026
zoboth
left a comment
Collaborator
There was a problem hiding this comment.
I read the whole diff and found no blockers.
Correctness checks
sse_payloadvs^data:%s*(.-)%s*\r?\n?$: the leading%s*is greedy and the lazy capture stops at the last non-%sbyte.\rand\nare both in%s, so the trailing\r?\n?never matters. The pattern therefore returns "everything between the first and last non-space byte afterdata:, or""", which is what the two loops compute. When the line is all whitespace, the result issub(s, s-1) == "". Baredata:givessub(6, 5) == "". An empty line givesbyte("",1) == nil, so the result isnil. All three match the pattern.is_space(9–13 and 32) is exactly Lua's%sin the C locale.- Trailing newline in
rewrite_sse_line: the new code gives"\r\n","\n"or"", the same asline:match("\r?\n$") or "". - Fast path: a frame containing the needle is either a complete line inside
pending(thenfindhits and the slow path runs) or the partial last line, which is held back and joined with the next chunk. A needle that straddles the boundary can't be missed. Every edge case I traced gives the right result: on eof everything is flushed; a chunk with no\nreturns""and becomes the leftover; an emptypendinggivesbyte("",-1) == nil, so the leftover is""and the output is"".
Non-blocking notes
- The diff doesn't touch
test/run_lua_unit_tests.sh. I assume it globstest/utest_*.lua, since the description says CI runs the new test. If it uses an explicit list instead, the new file needs adding there. - The needle is the literal
"actual_usage". The fast path would skip a backend that escaped the key in the JSON (e.g."actual_usage"), even though the decoded key would match. No real backend does that, and in that case the field would pass through, as it did with the feature off. Still, a comment on the needle could note the assumption that the key is unescaped. - In
utest_postprocess_sse_scan.lua, the comment on thevllm_formatcase says the/comes back escaped, but the test only checks thatsystem_fingerprintis dropped. Either add that check or reword the comment. - The README now says that a partial last line waits for the next chunk. That was already true of the slow path, so this is just a doc correction, not a behaviour change. Good.
Review follow-ups: say that the strip_actual_usage fast path assumes the key is not escaped in the JSON (a backend that escaped it would have the field passed through, as with post-processing off), and stop claiming a check the vllm_format case in utest_postprocess_sse_scan.lua does not make.
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.
What
sse_payload(): takes the payload of an SSE line in one linear scan.line:match("^data:%s*(.-)%s*\r?\n?$")with the same result: a prefix check, then whitespace trimmed from both ends.%s.json_rewrite_handler(rewrite_json, needle).needleis passed on without being split into lines.strip_actual_usagepasses"actual_usage".vllm_formattouches every frame, so it passes none.test/bench_postprocess.lua: the benchmark below, to reproduce it.Why
With
strip_actual_usageon (#30), every SSE frame cost about 8 µs of CPU, though only the final usage frame is ever rewritten. About 7 µs of that was the line pattern. Its lazy capture re-tries the rest of the line at every character, so a 216-byte frame paid for about 200 attempts.Numbers
One core,
resty test/bench_postprocess.lua: 2000 content frames of about 216 bytes, 1 frame per chunk, ns per frame.strip_actual_usagevllm_formatWith 4 frames per chunk:
strip_actual_usage251,vllm_format4883.Tests
New
test/utest_postprocess_sse_scan.lua, run bytest/run_lua_unit_tests.shand CI:sse_payloadagrees with the old pattern on 20025 lines: 25 edge cases (\r\n,\v/\f, no newline, baredata:, non-data lines, …) plus 20000 random ones. 0 mismatches."choices":[]and[DONE]intact.postprocess.luathe test fails.All 13 Lua unit tests pass (the existing
vllm_formatandstrip_actual_usagetests included).