sseer 0.2.1

Various helpers for getting Event streams out of your SSE responses
Documentation

sseer (sse - er)

What?

A collection of utilities for getting Events out of your SSE streams

Why?

As a bit of a learning project, in making a 'real' rust crate. Also got to learn a bit about the async ecosystem and hopefully make some improvements over the original code reqwest-eventsource and eventsource-stream by Julian Popescu

How?

With reqwest:

let request = client.get("https://example.com/events");
let mut event_source = EventSource::new(request)?;
while let Some(result) = event_source.next().await { 
    // your code here :3
}

Make an EventSource stream over your RequestBuilder and it turns into a Stream. Or if you want to handle it yourself then hit up the event_stream module for a Stream that parses bytes into Events. Or use the parser module if you want to parse it yourself.

let mut event_stream = EventStream::new(stream);
let mut event_stream = EventStreamBytes::new(stream); // we have a special version optimised for bytes::Bytes streams

No std?

Yes, without the reqwest feature it's all no_std.

Benches

Benches were run on my personal computer with 96GB of 6000Mhz DDR5 + Ryzen 9 9950X3D. We benchmark against eventsource-stream as a baseline since that's the main competitor and the inspiration I hoped to do better than, and the ratios are compared to it.

parse_line

For these benches we just run the parser on a single line of different types. The main difference we get is on long lines such as the "big 1024-byte line" benchmark, since we use memchr instead of nom for the parser any benchmarks involving long lines are weighted in our favour.

Line type eventsource-stream sseer
data field 31.8ns 5.2ns (6.1x)
comment 18.9ns 5.0ns (3.8x)
event field 28.4ns 7.3ns (3.9x)
id field 23.1ns 5.5ns (4.2x)
empty line 17.1ns 4.1ns (4.1x)
no value 21.0ns 5.2ns (4.0x)
no space 25.8ns 6.5ns (3.9x)
big 1024-byte line 617.6ns 12.0ns (51x)

event_stream

These benchmarks run the full stream implementation across some sample events in two conditions: events aligned to line boundaries (thus more individual stream items) and 'dumb' 128 byte chunks.

  • mixed is just a sort of random mixed set of different line types, with no particularly long data lines. 512 events.
  • ai_stream has its line lengths and ratios based on some responses I captured from OpenRouter, so is almost entirely made of data lines with some being quite long and some quite short. 512 events.
  • evenish_distribution just takes our data, comment, event and id field lines we use in the parse_line benchmark and stacks them end to end 128 times and also splits into 128 byte chunks.
Workload Chunking eventsource-stream sseer (generic) sseer (bytes)
mixed unaligned 171.5µs 105.3µs (1.6x) 105.3µs (1.6x)
mixed line-aligned 215.9µs 152.2µs (1.4x) 109.8µs (2.0x)
ai_stream unaligned 331.8µs 75.2µs (4.4x) 75.1µs (4.4x)
ai_stream line-aligned 200.0µs 102.1µs (2.0x) 60.2µs (3.3x)
evenish_distribution unaligned 53.7µs 34.1µs (1.6x) 33.0µs (1.6x)

Memory

This is available under the example with cargo run --example memory_usage. I just use a global allocator that tracks calls to alloc and stores some stats, it's probably not perfectly accurate but hopefully it lets you get the gist. The main advantage sseer has over eventsource-stream is that we use bytes::Bytes as much as possible to reduce allocation, and we also avoid allocating a buffer for the data line in cases where there's only one data line. On the stream specialised on just bytes::Bytes streams instead of AsRef<[u8]> we also avoid allocating any time a new stream item makes a complete line, hence why the line-aligned case looks so good for us

Workload Chunking Metric eventsource-stream sseer (generic) sseer (bytes)
mixed unaligned (128B) alloc calls 4,753 546 (8.7x) 535 (8.9x)
mixed unaligned (128B) total bytes 188.1 KiB 35.8 KiB (5.3x) 34.2 KiB (5.5x)
mixed unaligned (128B) peak live 488 B 742 B (0.7x) 739 B (0.7x)
mixed line-aligned alloc calls 6,034 1,743 (3.5x) 306 (19.7x)
mixed line-aligned total bytes 92.8 KiB 49.9 KiB (1.9x) 11.5 KiB (8.1x)
mixed line-aligned peak live 171 B 299 B (0.6x) 93 B (1.8x)
ai_stream unaligned (128B) alloc calls 4,094 7 (584.9x) 7 (584.9x)
ai_stream unaligned (128B) total bytes 669.2 KiB 7.9 KiB (84.6x) 7.9 KiB (84.6x)
ai_stream unaligned (128B) peak live 6.7 KiB 6.0 KiB (1.1x) 6.0 KiB (1.1x)
ai_stream line-aligned alloc calls 3,576 1,537 (2.3x) 0 ()
ai_stream line-aligned total bytes 515.3 KiB 123.9 KiB (4.2x) 0 B ()
ai_stream line-aligned peak live 7.3 KiB 1.5 KiB (4.7x) 0 B ()