sseer 0.3.0

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

Note for 0.3.x

Should be mostly non-breaking, the generic variant of EventStream has been replaced by the Bytes specific version which now has a bound of Into<Bytes>. If you define your own types you may need to add an Into<Bytes> implementation, most relevant std types already implement this. I would recommend the upgrade as it fixes a performance issue.

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);

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
mixed unaligned 188.1µs 103.2µs (1.8x)
mixed line-aligned 231.1µs 112.9µs (2.0x)
ai_stream unaligned 342.9µs 72.3µs (4.7x)
ai_stream line-aligned 201.2µs 64.0µs (3.1x)
evenish_distribution unaligned 51.5µs 32.8µs (1.6x)

Memory

This is available under the example with cargo run --example memory_usage --features std. 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. Because the stream accepts anything Into<Bytes> (so bytes::Bytes and Vec<u8> convert for free), we slice events straight out of the incoming chunk and avoid allocating any time a stream item completes a line, hence why the line-aligned case looks so good for us

Workload Chunking Metric eventsource-stream sseer
mixed unaligned (128B) alloc calls 4,753 535 (8.9x)
mixed unaligned (128B) total bytes 188.1 KiB 34.2 KiB (5.5x)
mixed unaligned (128B) peak live 488 B 739 B (0.7x)
mixed line-aligned alloc calls 6,034 306 (19.7x)
mixed line-aligned total bytes 92.8 KiB 11.5 KiB (8.1x)
mixed line-aligned peak live 171 B 93 B (1.8x)
ai_stream unaligned (128B) alloc calls 4,094 7 (584.9x)
ai_stream unaligned (128B) total bytes 669.2 KiB 7.9 KiB (84.6x)
ai_stream unaligned (128B) peak live 6.7 KiB 6.0 KiB (1.1x)
ai_stream line-aligned alloc calls 3,576 0 ()
ai_stream line-aligned total bytes 515.3 KiB 0 B ()
ai_stream line-aligned peak live 7.3 KiB 0 B ()