Skip to content

perf(core): speed up event stream decoding on JavaScriptCore - #134

Merged
dinwwwh merged 1 commit into
mainfrom
claude/cr-line-endings-messages-194cdf
Sep 29, 2026
Merged

dinwwwh merged 1 commit into
mainfrom
claude/cr-line-endings-messages-194cdf

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 29, 2026

Copy link
Copy Markdown
Member

Event stream decoding is now much faster on Bun and Safari, where the message delimiter regex hit a slow path in JavaScriptCore's regex engine. Small messages decode about 3x faster on Bun and large ones up to 22x faster, while Node gets about 20% faster for streams that deliver one message per chunk. Decoded output is unchanged.

Performance

  • Bun, one message per chunk: ~150B messages 1325 → 414 ns, 2KB 16.0 → 1.3 µs, 20KB 139 → 6.2 µs
  • Node, one message per chunk: small messages ~20% faster (530 → 428 ns); other workloads flat
  • The slow path was the quantified group (?:…){2,}, not the lookahead: JavaScriptCore scans any such group ~70–100x slower than a character class

Correctness

  • The reported "a single CR splits messages on JavaScriptCore" did not reproduce on Bun 1.4.1/1.4.2; CR, LF and CRLF decode the same as on Node
  • 300k randomly chunked CR/LF/CRLF streams decode identically to main on both Node and Bun

Testing

  • New Bun test asserts CR, LF and CRLF decoding on JavaScriptCore, including every delimiter split at every position; it fails if a CRLF is counted as two line endings
  • New unit test: line endings on either side of a chunk without any never pair up into a blank line

The message delimiter regex used a quantified group, `(?:...){2,}`, which
JavaScriptCore (Bun, Safari) scans about 100x slower than V8. Replace it with
an equivalent pattern built from character classes, drop the redundant
lookahead from the line ending regex, and skip two per-feed costs that every
single-message chunk paid: the leading line ending regex and joining a
one-element pending buffer.

A report claimed the old regex also splits messages on a single CR under
JavaScriptCore; that did not reproduce on Bun 1.4.1/1.4.2. A Bun test now
asserts CR, LF and CRLF decoding on JavaScriptCore to guard it.
@pkg-pr-new

pkg-pr-new Bot commented Sep 29, 2026

Copy link
Copy Markdown
@standard-server/aws-lambda

npm i https://pkg.pr.new/@standard-server/aws-lambda@134

@standard-server/core

npm i https://pkg.pr.new/@standard-server/core@134

@standard-server/fastify

npm i https://pkg.pr.new/@standard-server/fastify@134

@standard-server/fetch

npm i https://pkg.pr.new/@standard-server/fetch@134

@standard-server/node

npm i https://pkg.pr.new/@standard-server/node@134

@standard-server/peer

npm i https://pkg.pr.new/@standard-server/peer@134

@standard-server/shared

npm i https://pkg.pr.new/@standard-server/shared@134

commit: 886bc1d

@codspeed

codspeed Bot commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 108 skipped benchmarks1


Comparing claude/cr-line-endings-messages-194cdf (886bc1d) with main (75cffb5)

Open in CodSpeed

Footnotes

  1. 108 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@codecov

codecov Bot commented Sep 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • Delimiter regex rewritten for JavaScriptCore — MESSAGE_DELIMITER_REGEX moves from the quantified group (?:\r\n|\r(?!\n)|\n){2,} to /[\r\n]{3,}|\r\r|\n[\r\n]/g, and LINE_ENDING_REGEX simplifies to /\r\n?|\n/. I verified both are behaviorally identical to the originals by brute-forcing every string over {a, \r, \n} up to length 7 (matchAll index/length for the delimiter, split boundaries for line endings): zero mismatches. A lone \r\n (one line ending) is still correctly excluded, and \r\r/\n\n/\n\r remain delimiters.
  • Decoder fast paths in feed() — the leading [\r\n]+ strip now runs only when the chunk's first code unit is CR/LF, and pending.join('') is skipped for a single buffered chunk. pending.length === 1 implies pending[0] === chunk, so the buffered substitution preserves the existing offset mapping.
  • Tests — a new Node case covers line endings on either side of a chunk with no line ending; a new tests/bun suite repeats the single-CR and every-delimiter-split cases on JavaScriptCore. tests/* is a workspace package with test: bun test and CI pins Bun before the recursive test run, so the new suite executes in CI.

pnpm exec vitest run packages/core/src/event-stream/decoder.test.ts (32 passed), pnpm --filter @standard-server/core run type:check, and eslint on the changed files all pass.

Pullfrog  | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

@dinwwwh
dinwwwh merged commit 39c1ce1 into main Sep 29, 2026
11 checks passed
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