Skip to content

Commit

Permalink
Rewrite in Rust (obviously)
Browse files Browse the repository at this point in the history
- Refactored code for more readability (e.g. use UnsafeCell instead of AtomicU32)
- Fix "ghost-pixels" caused by wrong parsing (striping some commands)
- Improve performance
- Add ffmpeg sink
  • Loading branch information
sbernauer committed Jun 10, 2023
1 parent 14aa08f commit d544848
Show file tree
Hide file tree
Showing 22 changed files with 2,522 additions and 1,230 deletions.
997 changes: 872 additions & 125 deletions Cargo.lock

Large diffs are not rendered by default.

39 changes: 29 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,27 +1,46 @@
[package]
name = "breakwater"
version = "0.0.1"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "3.2", features = ["derive"] }
clap = { version = "4.3", features = ["derive"] }
rusttype = "0.9"
number_prefix = "0.4"
prometheus = "0.13"
env_logger = "0.10"
lazy_static = "1.4"
log = "0.4"
prometheus_exporter = "0.8"
serde_json = "1.0"
rstest = "0.17"
serde = { version = "1.0", features = ["derive"] }
vncserver = "0.2"
serde_json = "1.0"
simple_moving_average = "0.1"
thread-priority = "0.13"
tokio = { version = "1.28", features = ["fs", "rt-multi-thread", "net", "io-util", "macros", "process", "signal", "sync", "time"] }
vncserver = { version ="0.2", optional = true}
chrono = "0.4.26"

[dev-dependencies]
rstest = "0.12"
lazy_static = "1.4"
criterion = {version = "0.5", features = ["async_tokio"]}

[features]
default = ["vnc"]
vnc = ["dep:vncserver"]

[lib]
name = "breakwater"
path = "src/lib.rs"

[[bin]]
name = "breakwater"
path = "src/main.rs"

[[bench]]
name = "benchmarks"
harness = false

[profile.dev]
opt-level = 3

[profile.release]
opt-level = 3

[features]
count_pixels = []
115 changes: 48 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# breakwater
breakwater is a very fast [Pixelflut](https://wiki.cccgoe.de/wiki/Pixelflut) server written in Rust. It is heavily inspired by [Shoreline](https://github.com/TobleMiner/shoreline).

It claims to be **the fastest Pixelflut server in existence** - at least at the time of writing 02/2022.
It claims to be the fastest Pixelflut server in existence - at least at the time of writing 02/2022.
![breakwater logo](docs/images/breakwater.png)

# Features
1. Accepts Pixelflut commands
2. Provides VNC server so that everybody can watch
3. Exposes Prometheus metrics
4. IPv6 and legacy IP support
2. Can provide a VNC server so that everybody can watch
3. As an alternative it can stream to a RTMP sink, so that you can e.g. directly live-stream into Twitch or YouTube
4. Exposes Prometheus metrics
5. IPv6 and legacy IP support

# Available Pixelflut commands
Commands must be sent newline-separated, for more details see [Pixelflut](https://wiki.cccgoe.de/wiki/Pixelflut)
Expand Down Expand Up @@ -45,47 +46,41 @@ cargo run --release -- --help

```bash
cargo run --release -- --help
Finished release [optimized] target(s) in 0.03s
Finished release [optimized] target(s) in 0.04s
Running `target/release/breakwater --help`
breakwater 0.0.1

USAGE:
breakwater [OPTIONS]

OPTIONS:
-f, --fps <FPS>
Frames per second the VNC server should aim for [default: 30]

--font <FONT>
The font used to render the text on the screen. Should be a ttf file [default:
Arial.ttf]

-h, --height <HEIGHT>
Height of the drawing surface [default: 720]

--help
Print help information

-l, --listen-address <LISTEN_ADDRESS>
Listen address to bind to. The default value will listen on all interfaces for IPv4 and
v6 packets [default: [::]:1234]

-p, --prometheus-listen-address <PROMETHEUS_LISTEN_ADDRESS>
Listen address zhe prometheus exporter should listen om. The default value will listen
on all interfaces for IPv4 and v6 packets [default: [::]:9100]

-t, --text <TEXT>
Text to display on the screen. The text will be followed by "on <listen_address>"
[default: "Breakwater Pixelflut server"]

-v, --vnc-port <VNC_PORT>
Port of the VNC server [default: 5900]

-V, --version
Print version information

-w, --width <WIDTH>
Width of the drawing surface [default: 1280]
Usage: breakwater [OPTIONS]

Options:
-l, --listen-address <LISTEN_ADDRESS>
Listen address to bind to. The default value will listen on all interfaces for IPv4 and IPv6 packets [default: [::]:1234]
--width <WIDTH>
Width of the drawing surface [default: 1280]
--height <HEIGHT>
Height of the drawing surface [default: 720]
-f, --fps <FPS>
Frames per second the server should aim for [default: 30]
-t, --text <TEXT>
Text to display on the screen. The text will be followed by "on <listen_address>" [default: "Pixelflut server (breakwater)"]
--font <FONT>
The font used to render the text on the screen. Should be a ttf file. If you use the default value a copy that ships with breakwater will be used - no need to download and provide the font [default: Arial.ttf]
-p, --prometheus-listen-address <PROMETHEUS_LISTEN_ADDRESS>
Listen address the prometheus exporter should listen on [default: [::]:9100]
--statistics-save-file <STATISTICS_SAVE_FILE>
Save file where statistics are periodically saved. The save file will be read during startup and statistics are restored. To reset the statistics simply remove the file [default: statistics.json]
--statistics-save-interval-s <STATISTICS_SAVE_INTERVAL_S>
Interval (in seconds) in which the statistics save file should be updated [default: 10]
--disable-statistics-save-file
Disable periodical saving of statistics into save file
--rtmp-address <RTMP_ADDRESS>
Enable rtmp streaming to configured address, e.g. `rtmp://127.0.0.1:1935/live/test`
--save-video-to-file
Enable dump of video stream into file. File name will be `pixelflut_dump_{timestamp}.mp4
-v, --vnc-port <VNC_PORT>
Port of the VNC server [default: 5900]
-h, --help
Print help
-V, --version
Print version
```
</details>

Expand All @@ -94,18 +89,22 @@ You can also build the binary with `cargo build --release`. The binary will be p
## Compile time features
Breakwater also has some compile-time features for performance reasons.
You can get the list of available features by looking at the [Cargo.toml](Cargo.toml).
To e.g. count the actual pixels colored by every IP enable the future `count_pixels` as follows.
Please note that this will have a very larger performance penality.
As of writing the following features are supported

* `vnc` (enabled by default): Starts a VNC server, where users can connect to. Needs `libvncserver-dev` to be installed. Please note that the VNC server offers basically no latency, but consumes quite some CPU.

To e.g. turn the VNS server off, build with

```bash
cargo run --release --features count_pixels
cargo run --release --no-default-features # --features feature-to-enable
```

# Run in docker container
This command will start the Pixelflut server in a docker container
```bash
docker run --rm -p 1234:1234 -p 5900:5900 -p 9100:9100 sbernauer/breakwater # --help
```
The following command stops the server again (if there are some problems with SIGINT)
The following command stops the server again (if there are some problems with `SIGINT`)
```bash
docker stop $(docker ps -q --filter ancestor=sbernauer/breakwater)
```
Expand Down Expand Up @@ -154,23 +153,5 @@ The servers were connected with two 40G and one 10G links, through which traffic
| [Shoreline](https://github.com/TobleMiner/shoreline) | C | 34 Gbit/s |
| [Breakwater](https://github.com/sbernauer/breakwater) | Rust | 52 Gbit/s |

## Usage of [Tokio](https://crates.io/crates/tokio)
You can find a prototype with Tokio in the `tokio` branch.
Performance measurements have shown that the usage of Tokio decreased the average performance from 22.9 to 21.6 Gbit/s.
<details>
<summary>Used benchmark</summary>

```bash
for i in $(seq 1 20); do
for branch in master tokio; do
git checkout $branch
cargo run --release >/dev/null 2>/dev/null & sleep 2; ../sturmflut/sturmflut 127.0.0.1 ../sturmflut/cat.jpg -t 24 >/dev/null 2>/dev/null & sleep 10; bmon -b -p lo -o ascii:quitafter=3 | tail -n 1 | awk '{ print $2 }' | tee -a "perf/$branch"; killall sturmflut; killall breakwater
sleep 1
done
done
```
</details>


# TODOs
* Implement Alpha channel feature. For performance reasons there should be a compile-time switch (similar to `#ifdef` in C)
* Implement Alpha channel feature. For performance reasons there should be a compile-time switch for it
122 changes: 122 additions & 0 deletions benches/benchmarks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use breakwater::{
framebuffer::FrameBuffer,
parser::{from_hex_char_lookup, from_hex_char_map, parse_pixelflut_commands, ParserState},
test::helpers::{get_commands_to_draw_rect, DevNullTcpStream},
};
use criterion::{
BenchmarkId, Criterion, {criterion_group, criterion_main},
};
use std::{sync::Arc, time::Duration};

const FRAMEBUFFER_WIDTH: usize = 1920;
const FRAMEBUFFER_HEIGHT: usize = 1080;

async fn invoke_parse_pixelflut_commands(
input: &[u8],
fb: &Arc<FrameBuffer>,
parser_state: ParserState,
) {
let mut stream = DevNullTcpStream::default();
parse_pixelflut_commands(input, fb, &mut stream, parser_state).await;
}

#[allow(unused)] // Benchmarks are commented out by default
fn invoke_from_hex_char_map() -> u8 {
// So that we actually compute something
let mut result = 0;
for char in b'0'..=b'9' {
result |= from_hex_char_map(char);
}
for char in b'a'..=b'f' {
result |= from_hex_char_map(char);
}
for char in b'A'..=b'F' {
result |= from_hex_char_map(char);
}
result |= from_hex_char_map(b'\n');
result |= from_hex_char_map(b' ');
result |= from_hex_char_map(b';');
result |= from_hex_char_map(b'%');
result
}

#[allow(unused)] // Benchmarks are commented out by default
fn invoke_from_hex_char_lookup() -> u8 {
// So that we actually compute something
let mut result = 0;
for char in b'0'..=b'9' {
result |= from_hex_char_lookup(char);
}
for char in b'a'..=b'f' {
result |= from_hex_char_lookup(char);
}
for char in b'A'..=b'F' {
result |= from_hex_char_lookup(char);
}
result |= from_hex_char_lookup(b'\n');
result |= from_hex_char_lookup(b' ');
result |= from_hex_char_lookup(b';');
result |= from_hex_char_lookup(b'%');
result
}

fn from_elem(c: &mut Criterion) {
let draw_commands = get_commands_to_draw_rect(FRAMEBUFFER_WIDTH, FRAMEBUFFER_HEIGHT, 0x123456);
let draw_commands = draw_commands.as_bytes();

c.bench_with_input(
BenchmarkId::new(
"parse_draw_commands",
format!("{FRAMEBUFFER_WIDTH} x {FRAMEBUFFER_HEIGHT}"),
),
&draw_commands,
|b, input| {
let fb = Arc::new(FrameBuffer::new(FRAMEBUFFER_WIDTH, FRAMEBUFFER_HEIGHT));
let parser_state = ParserState::default();
b.to_async(tokio::runtime::Runtime::new().unwrap())
.iter(|| invoke_parse_pixelflut_commands(input, &fb, parser_state.clone()));
},
);

// let read_commands = get_commands_to_read_rect(FRAMEBUFFER_WIDTH, FRAMEBUFFER_HEIGHT);
// let read_commands = read_commands.as_bytes();

// c.bench_with_input(
// BenchmarkId::new(
// "parse_read_commands",
// format!("{FRAMEBUFFER_WIDTH} x {FRAMEBUFFER_HEIGHT}"),
// ),
// &read_commands,
// |b, input| {
// let fb = Arc::new(FrameBuffer::new(FRAMEBUFFER_WIDTH, FRAMEBUFFER_HEIGHT));
// let parser_state = ParserState::default();
// b.to_async(tokio::runtime::Runtime::new().unwrap())
// .iter(|| invoke_parse_pixelflut_commands(input, &fb, parser_state.clone()));
// },
// );

// c.bench_function("from_hex_char_map", |b: &mut criterion::Bencher| {
// b.iter(invoke_from_hex_char_map)
// });
// c.bench_function("from_hex_char_lookup", |b: &mut criterion::Bencher| {
// b.iter(invoke_from_hex_char_lookup)
// });
}

criterion_group!(
name = benches;
config = Criterion::default().warm_up_time(Duration::from_secs(10)).measurement_time(Duration::from_secs(30));
targets = from_elem
);
criterion_main!(benches);

// Performance numbers

// Starting point (while loop) 17.967 ms (does not count) 26.289 ms 26.725 ms
// Loop {} instead of while 19.118 ms (does not count) 26.563 ms 28.125 ms
// => Change not worth it. Use while loop for better readability

// Starting point (checking for command by indexing [u8] buffer) 27.332 ms 27.175 ms 27.848 ms
// Check for command by reading u32 25.590 ms 25.327 ms 23.174 ms
// => Accepted the change :) So far we have only read changed the parsing for "PX " logic, lets also change the other parsing logics
// Check for command by reading u32 everywhere 24.465 ms 23.435 ms 22.087 ms
Loading

0 comments on commit d544848

Please sign in to comment.