sseer 0.1.8

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

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.

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 sseer eventsource-stream ratio
data field 5.3ns 31.9ns 6.0x
comment 4.9ns 19.0ns 3.9x
event field 7.3ns 28.8ns 3.9x
id field 5.5ns 23.1ns 4.2x
empty line 4.3ns 17.3ns 4.0x
no value 5.3ns 21.2ns 4.0x
no space 6.7ns 26.3ns 3.9x
big 1024-byte line 11.4ns 619.0ns 54x

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 sseer eventsource-stream ratio
mixed unaligned 107.8µs 169.4µs 1.6x
mixed aligned 155.2µs 220.4µs 1.4x
ai_stream unaligned 76.5µs 343.1µs 4.5x
ai_stream aligned 103.6µs 204.5µs 2.0x
evenish_distribution unaligned 34.5µs 57.6µs 1.7x

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.

Workload Metric sseer eventsource-stream ratio
mixed alloc calls 546 4,753 8.7x
mixed total bytes 35.5 KiB 188.1 KiB 5.3x
ai_stream alloc calls 7 4,094 585x
ai_stream total bytes 7.9 KiB 669.2 KiB 85x