Skip to content

perf: reduce EventSourceStream parser allocations#5032

Merged
trivikr merged 4 commits intonodejs:mainfrom
trivikr:eventsource-stream-2
Apr 30, 2026
Merged

perf: reduce EventSourceStream parser allocations#5032
trivikr merged 4 commits intonodejs:mainfrom
trivikr:eventsource-stream-2

Conversation

@trivikr
Copy link
Copy Markdown
Member

@trivikr trivikr commented Apr 15, 2026

This relates to...

Fixes: #2630

Rationale

EventSourceStream was doing avoidable work on long-lived SSE streams by concatenating the buffered input for every chunk, allocating strings for every parsed field name, and replacing the internal event object after each dispatch. That creates unnecessary allocation churn in the hot path, especially when servers deliver heavily fragmented chunks.

Changes

  • Replaced the single-buffer Buffer.concat([buffer, chunk]) approach with chunk-list buffering plus cursors.
  • Kept line parsing lazy so buffers are only concatenated when a logical line spans multiple chunks.
  • Matched data, event, id, and retry field names from bytes instead of decoding field strings first.
  • Validated retry and id directly from bytes before decoding their values.
  • Reset the parser event state in place instead of allocating a fresh event object on every event boundary.
  • Added focused EventSourceStream benchmarks covering realistic payloads, fragmented chunking, and BOM-prefixed input.

Features

N/A

Bug Fixes

Reduced allocation pressure in the EventSourceStream parser hot path for fragmented SSE streams.

Breaking Changes and Deprecations

  • No public API changes.
  • Internally, processEvent(event) now receives a reused parser event object instead of a freshly allocated object each time.

Status

@trivikr
Copy link
Copy Markdown
Member Author

trivikr commented Apr 15, 2026

Benchmarks

Before

$ node benchmarks/eventsource/eventsource-stream.mjs
clk: ~3.18 GHz
cpu: Apple M1 Pro
runtime: node 24.14.0 (arm64-darwin)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
• EventSourceStream parsing (250 events)
------------------------------------------- -------------------------------
single chunk                 523.13 µs/iter 517.21 µs █                    
                    (493.29 µs … 853.88 µs) 731.96 µs █                    
                    (167.25 kb …   1.18 mb) 659.19 kb █▆▂▁▁▂▃▃▂▂▁▁▁▁▁▁▁▁▁▁▁

256-byte chunks              544.53 µs/iter 538.29 µs █                    
                      (519.42 µs … 1.71 ms) 749.08 µs █▆                   
                    (110.61 kb …   2.11 mb) 711.91 kb ██▆▂▂▂▂▂▂▁▁▂▁▂▁▁▁▁▁▁▁

64-byte chunks               597.89 µs/iter 600.50 µs  █                   
                    (580.54 µs … 844.83 µs) 726.08 µs ▆█                   
                    (148.83 kb …   1.41 mb) 868.50 kb ██▇▇▄▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁

8-byte chunks                  1.14 ms/iter   1.14 ms  █ ▃                 
                        (1.11 ms … 1.30 ms)   1.27 ms  █▇█▅                
                    (659.22 kb …   2.29 mb)   2.29 mb ▆████▆▃▂▂▂▂▁▁▁▁▂▂▃▂▁▂

1-byte chunks                  5.49 ms/iter   5.56 ms ▃█▆▇                 
                        (5.20 ms … 6.90 ms)   6.63 ms █████▃▂              
                    ( 13.86 mb …  13.86 mb)  13.86 mb ███████▆▂█▁▁▃▁▂▁▂▁▁▂▂

• EventSourceStream parsing with BOM
------------------------------------------- -------------------------------
BOM + 8-byte chunks            1.16 ms/iter   1.17 ms  █                   
                        (1.13 ms … 1.39 ms)   1.34 ms  █ ▄                 
                    (  2.29 mb …   2.30 mb)   2.29 mb ▆███▅▅▂▂▂▁▁▁▁▁▁▁▁▁▁▂▁

BOM + 1-byte chunks            5.42 ms/iter   5.49 ms  █▂                  
                        (5.32 ms … 5.74 ms)   5.70 ms  ██                  
                    ( 13.86 mb …  13.87 mb)  13.86 mb ▃██▆▂▆▄▂▂██▂▃▅▁▁▂▁▂▂▂

After

$ node benchmarks/eventsource/eventsource-stream.mjs
clk: ~3.14 GHz
cpu: Apple M1 Pro
runtime: node 24.14.0 (arm64-darwin)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
• EventSourceStream parsing (250 events)
------------------------------------------- -------------------------------
single chunk                 356.42 µs/iter 333.46 µs █▇                   
                      (320.71 µs … 6.80 ms) 661.83 µs ██                   
                    ( 10.12 kb …   1.12 mb) 210.89 kb ██▃▄▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

256-byte chunks              364.56 µs/iter 363.38 µs   █                  
                    (352.96 µs … 517.63 µs) 443.25 µs ███                  
                    ( 62.13 kb …   1.30 mb) 301.20 kb ███▆▂▂▂▂▂▂▂▁▁▁▁▁▁▁▁▁▁

64-byte chunks               465.56 µs/iter 463.33 µs  █▅                  
                    (445.42 µs … 650.08 µs) 588.88 µs ▅██                  
                    ( 14.05 kb …   1.09 mb) 552.85 kb ███▆▄▂▂▂▂▂▂▁▂▁▁▁▁▁▁▁▁

8-byte chunks                  1.01 ms/iter   1.00 ms    █                 
                      (976.13 µs … 1.22 ms)   1.13 ms  ▆▄█                 
                    (  1.08 mb …   2.11 mb)   1.83 mb ▆████▄▃▂▂▂▁▁▁▁▁▁▂▂▂▂▁

1-byte chunks                  5.10 ms/iter   5.10 ms  █                   
                        (4.84 ms … 6.46 ms)   6.43 ms  █ ▃                 
                    ( 11.93 mb …  11.93 mb)  11.93 mb ▄███▄▃▁▁▂▁▁▁▂▁▁▁▂▁▂▁▁

• EventSourceStream parsing with BOM
------------------------------------------- -------------------------------
BOM + 8-byte chunks            1.02 ms/iter   1.02 ms   █                  
                      (993.00 µs … 1.33 ms)   1.19 ms   █                  
                    (  1.84 mb …   1.88 mb)   1.84 mb ▄▃██▃▃▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁

BOM + 1-byte chunks            5.13 ms/iter   5.20 ms     ▆█               
                        (4.96 ms … 5.37 ms)   5.36 ms     ██▄ ▄   ▃        
                    ( 11.93 mb …  11.93 mb)  11.93 mb ▅██▅█████▇█▆█▆▇▇▅▅▅▅▂

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 15, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.16%. Comparing base (2eb9ddc) to head (f660a6e).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5032      +/-   ##
==========================================
+ Coverage   93.14%   93.16%   +0.01%     
==========================================
  Files         110      110              
  Lines       36117    36212      +95     
==========================================
+ Hits        33642    33736      +94     
- Misses       2475     2476       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@metcoder95 metcoder95 requested a review from Uzlopak April 15, 2026 09:24
Copy link
Copy Markdown
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

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

lgtm

@mcollina mcollina requested a review from KhafraDev April 22, 2026 08:07
Copy link
Copy Markdown
Member

@KhafraDev KhafraDev left a comment

Choose a reason for hiding this comment

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

EventSource isn't really my area of expertise.

@trivikr trivikr merged commit aeed21a into nodejs:main Apr 30, 2026
35 checks passed
@trivikr trivikr deleted the eventsource-stream-2 branch May 1, 2026 00:29
@github-actions github-actions Bot mentioned this pull request May 1, 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.

Reduce Buffer operations in EventSource

4 participants