mirror of https://github.com/mastodon/flodgatt
Benchmark & performance tune (#132)
* Add temporary perf metrics * Add load testing and tune performance
This commit is contained in:
parent
37b652ad79
commit
10fa24c5d3
|
@ -1,2 +1,4 @@
|
||||||
/target
|
/target
|
||||||
|
load_test.sh
|
||||||
|
/benches/perf_statistics
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
language: rust
|
language: rust
|
||||||
rust:
|
rust:
|
||||||
- stable
|
- stable
|
||||||
|
- beta
|
||||||
|
jobs:
|
||||||
|
fast_finish: true
|
||||||
cache: cargo
|
cache: cargo
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
|
|
@ -406,7 +406,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flodgatt"
|
name = "flodgatt"
|
||||||
version = "0.8.4"
|
version = "0.8.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"criterion 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"criterion 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
@ -419,7 +419,6 @@ dependencies = [
|
||||||
"pretty_env_logger 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"pretty_env_logger 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"r2d2 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"r2d2 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"r2d2_postgres 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"r2d2_postgres 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"regex 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
|
||||||
"serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde_derive 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde_derive 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde_json 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde_json 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "flodgatt"
|
name = "flodgatt"
|
||||||
description = "A blazingly fast drop-in replacement for the Mastodon streaming api server"
|
description = "A blazingly fast drop-in replacement for the Mastodon streaming api server"
|
||||||
version = "0.8.4"
|
version = "0.8.5"
|
||||||
authors = ["Daniel Long Sockwell <daniel@codesections.com", "Julian Laubstein <contact@julianlaubstein.de>"]
|
authors = ["Daniel Long Sockwell <daniel@codesections.com", "Julian Laubstein <contact@julianlaubstein.de>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
@ -28,13 +28,12 @@ hashbrown = "0.7.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = "0.3"
|
criterion = "0.3"
|
||||||
regex = "1.3.2"
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "parse_redis"
|
name = "parse_redis"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [ "production" ]
|
default = [ "production" ]
|
||||||
bench = []
|
bench = []
|
||||||
|
@ -43,7 +42,7 @@ production = []
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "fat"
|
lto = "fat"
|
||||||
#panic = "abort"
|
panic = "abort"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
use flodgatt::{
|
use flodgatt::{
|
||||||
messages::*,
|
event::*,
|
||||||
parse_client_request::{Content::*, Reach::*, Stream::*, Timeline},
|
request::{Content::*, Reach::*, Stream::*, Timeline},
|
||||||
redis_to_client_stream::{RedisMsg, RedisParseOutput},
|
response::{RedisMsg, RedisParseOutput},
|
||||||
};
|
};
|
||||||
use lru::LruCache;
|
use lru::LruCache;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
@ -19,16 +19,15 @@ fn parse_long_redis_input<'a>(input: &'a str) -> RedisMsg<'a> {
|
||||||
fn parse_to_timeline(msg: RedisMsg) -> Timeline {
|
fn parse_to_timeline(msg: RedisMsg) -> Timeline {
|
||||||
let trimmed_tl_txt = &msg.timeline_txt["timeline:".len()..];
|
let trimmed_tl_txt = &msg.timeline_txt["timeline:".len()..];
|
||||||
let tl = Timeline::from_redis_text(trimmed_tl_txt, &mut LruCache::new(1000)).unwrap();
|
let tl = Timeline::from_redis_text(trimmed_tl_txt, &mut LruCache::new(1000)).unwrap();
|
||||||
assert_eq!(tl, Timeline(User(1), Federated, All));
|
assert_eq!(tl, Timeline(User(Id(1)), Federated, All));
|
||||||
tl
|
tl
|
||||||
}
|
}
|
||||||
fn parse_to_checked_event(msg: RedisMsg) -> Event {
|
fn parse_to_checked_event(msg: RedisMsg) -> Event {
|
||||||
Event::TypeSafe(serde_json::from_str(msg.event_txt).unwrap())
|
Event::TypeSafe(serde_json::from_str(msg.event_txt).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_to_dyn_event(msg: RedisMsg) -> String {
|
fn parse_to_dyn_event(msg: RedisMsg) -> Event {
|
||||||
let event: Event = Event::Dynamic(serde_json::from_str(msg.event_txt).unwrap());
|
Event::Dynamic(serde_json::from_str(msg.event_txt).unwrap())
|
||||||
event.to_json_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn redis_msg_to_event_string(msg: RedisMsg) -> String {
|
fn redis_msg_to_event_string(msg: RedisMsg) -> String {
|
||||||
|
@ -43,16 +42,16 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||||
let input = ONE_MESSAGE_FOR_THE_USER_TIMLINE_FROM_REDIS;
|
let input = ONE_MESSAGE_FOR_THE_USER_TIMLINE_FROM_REDIS;
|
||||||
let mut group = c.benchmark_group("Parse redis RESP array");
|
let mut group = c.benchmark_group("Parse redis RESP array");
|
||||||
|
|
||||||
// group.bench_function("parse redis input to RedisMsg", |b| {
|
group.bench_function("parse redis input to RedisMsg", |b| {
|
||||||
// b.iter(|| black_box(parse_long_redis_input(input)))
|
b.iter(|| black_box(parse_long_redis_input(input)))
|
||||||
// });
|
});
|
||||||
|
|
||||||
let msg = parse_long_redis_input(input);
|
let msg = parse_long_redis_input(input);
|
||||||
// group.bench_function("parse RedisMsg to Timeline", |b| {
|
group.bench_function("parse RedisMsg to Timeline", |b| {
|
||||||
// b.iter(|| black_box(parse_to_timeline(msg.clone())))
|
b.iter(|| black_box(parse_to_timeline(msg.clone())))
|
||||||
// });
|
});
|
||||||
|
|
||||||
group.bench_function("parse RedisMsg -> DynamicEvent -> JSON string", |b| {
|
group.bench_function("parse RedisMsg -> DynamicEvent", |b| {
|
||||||
b.iter(|| black_box(parse_to_dyn_event(msg.clone())))
|
b.iter(|| black_box(parse_to_dyn_event(msg.clone())))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/sh
|
||||||
|
instance='127.0.0.1:4000'
|
||||||
|
timeline='public:local'
|
||||||
|
number_of_ws=300
|
||||||
|
|
||||||
|
command -v websocat >/dev/null || { echo >&2 "Install websocat with `cargo install websocat` to use this script"; exit 1; }
|
||||||
|
|
||||||
|
|
||||||
|
echo "Opening $number_of_ws WebSockets to $timeline"
|
||||||
|
for i in $(seq 0 $number_of_ws); do
|
||||||
|
sleep 0.1
|
||||||
|
websocat wss://${instance}/api/v1/streaming/?stream=${timeline} --no-close > /dev/null &
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "$number_of_ws WebSocket connections established..."
|
||||||
|
|
||||||
|
sleep 60
|
||||||
|
|
||||||
|
echo "Closing WebSockets"
|
||||||
|
echo "Done"
|
18
src/main.rs
18
src/main.rs
|
@ -78,14 +78,23 @@ fn main() -> Result<(), FatalErr> {
|
||||||
.allow_methods(cfg.cors.allowed_methods)
|
.allow_methods(cfg.cors.allowed_methods)
|
||||||
.allow_headers(cfg.cors.allowed_headers);
|
.allow_headers(cfg.cors.allowed_headers);
|
||||||
|
|
||||||
|
// use futures::future::Future;
|
||||||
let streaming_server = move || {
|
let streaming_server = move || {
|
||||||
let manager = shared_manager.clone();
|
let manager = shared_manager.clone();
|
||||||
let stream = Interval::new(Instant::now(), poll_freq)
|
let stream = Interval::new(Instant::now(), poll_freq)
|
||||||
|
// .take(1200)
|
||||||
.map_err(|e| log::error!("{}", e))
|
.map_err(|e| log::error!("{}", e))
|
||||||
.for_each(move |_| {
|
.for_each(
|
||||||
let mut manager = manager.lock().unwrap_or_else(redis::Manager::recover);
|
move |_| {
|
||||||
manager.poll_broadcast().map_err(FatalErr::log)
|
let mut manager = manager.lock().unwrap_or_else(redis::Manager::recover);
|
||||||
});
|
manager.poll_broadcast().map_err(FatalErr::log)
|
||||||
|
}, // ).and_then(|_| {
|
||||||
|
// log::info!("shutting down!");
|
||||||
|
// std::process::exit(0);
|
||||||
|
// futures::future::ok(())
|
||||||
|
// }
|
||||||
|
);
|
||||||
|
|
||||||
warp::spawn(lazy(move || stream));
|
warp::spawn(lazy(move || stream));
|
||||||
warp::serve(ws.or(sse).with(cors).or(status).recover(Handler::err))
|
warp::serve(ws.or(sse).with(cors).or(status).recover(Handler::err))
|
||||||
};
|
};
|
||||||
|
@ -95,7 +104,6 @@ fn main() -> Result<(), FatalErr> {
|
||||||
fs::remove_file(socket).unwrap_or_default();
|
fs::remove_file(socket).unwrap_or_default();
|
||||||
let incoming = UnixListener::bind(socket)?.incoming();
|
let incoming = UnixListener::bind(socket)?.incoming();
|
||||||
fs::set_permissions(socket, PermissionsExt::from_mode(0o666))?;
|
fs::set_permissions(socket, PermissionsExt::from_mode(0o666))?;
|
||||||
|
|
||||||
tokio::run(lazy(|| streaming_server().serve_incoming(incoming)));
|
tokio::run(lazy(|| streaming_server().serve_incoming(incoming)));
|
||||||
} else {
|
} else {
|
||||||
let server_addr = SocketAddr::new(*cfg.address, *cfg.port);
|
let server_addr = SocketAddr::new(*cfg.address, *cfg.port);
|
||||||
|
|
|
@ -10,7 +10,7 @@ use crate::request::{Stream, Timeline};
|
||||||
use futures::{Async, Poll};
|
use futures::{Async, Poll};
|
||||||
use lru::LruCache;
|
use lru::LruCache;
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::convert::{TryFrom, TryInto};
|
||||||
use std::io::{Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
use std::str;
|
use std::str;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -25,6 +25,7 @@ pub struct RedisConn {
|
||||||
tag_id_cache: LruCache<String, i64>,
|
tag_id_cache: LruCache<String, i64>,
|
||||||
tag_name_cache: LruCache<i64, String>,
|
tag_name_cache: LruCache<i64, String>,
|
||||||
redis_input: Vec<u8>,
|
redis_input: Vec<u8>,
|
||||||
|
cursor: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RedisConn {
|
impl RedisConn {
|
||||||
|
@ -43,34 +44,32 @@ impl RedisConn {
|
||||||
// the tag number instead of the tag name. This would save us from dealing
|
// the tag number instead of the tag name. This would save us from dealing
|
||||||
// with a cache here and would be consistent with how lists/users are handled.
|
// with a cache here and would be consistent with how lists/users are handled.
|
||||||
redis_namespace: redis_cfg.namespace.clone().0,
|
redis_namespace: redis_cfg.namespace.clone().0,
|
||||||
redis_input: Vec::new(),
|
redis_input: vec![0_u8; 5000],
|
||||||
|
cursor: 0,
|
||||||
};
|
};
|
||||||
Ok(redis_conn)
|
Ok(redis_conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn poll_redis(&mut self) -> Poll<Option<(Timeline, Event)>, ManagerErr> {
|
pub fn poll_redis(&mut self) -> Poll<Option<(Timeline, Event)>, ManagerErr> {
|
||||||
let mut size = 100; // large enough to handle subscribe/unsubscribe notice
|
|
||||||
let (mut buffer, mut first_read) = (vec![0_u8; size], true);
|
|
||||||
loop {
|
loop {
|
||||||
match self.primary.read(&mut buffer) {
|
match self.primary.read(&mut self.redis_input[self.cursor..]) {
|
||||||
Ok(n) if n != size => break self.redis_input.extend_from_slice(&buffer[..n]),
|
Ok(n) => {
|
||||||
Ok(n) => self.redis_input.extend_from_slice(&buffer[..n]),
|
self.cursor += n;
|
||||||
Err(_) => break,
|
if self.redis_input.len() - 1 == self.cursor {
|
||||||
|
self.redis_input.resize(self.redis_input.len() * 2, 0);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) if matches!(e.kind(), io::ErrorKind::WouldBlock) => {
|
||||||
|
return Ok(Async::NotReady);
|
||||||
|
}
|
||||||
|
Err(e) => break log::error!("{}", e),
|
||||||
};
|
};
|
||||||
if first_read {
|
|
||||||
size = 2000;
|
|
||||||
buffer = vec![0_u8; size];
|
|
||||||
first_read = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.redis_input.is_empty() {
|
// at this point, we have the raw bytes; now, parse a msg
|
||||||
return Ok(Async::NotReady);
|
let input = &self.redis_input[..self.cursor];
|
||||||
}
|
|
||||||
|
|
||||||
// at this point, we have the raw bytes; now, parse what we can and leave the remainder
|
|
||||||
let input = self.redis_input.clone();
|
|
||||||
self.redis_input.clear();
|
|
||||||
|
|
||||||
let (input, invalid_bytes) = str::from_utf8(&input)
|
let (input, invalid_bytes) = str::from_utf8(&input)
|
||||||
.map(|input| (input, &b""[..]))
|
.map(|input| (input, &b""[..]))
|
||||||
|
@ -100,8 +99,26 @@ impl RedisConn {
|
||||||
Err(RedisParseErr::Incomplete) => (Ok(NotReady), input),
|
Err(RedisParseErr::Incomplete) => (Ok(NotReady), input),
|
||||||
Err(other_parse_err) => (Err(ManagerErr::RedisParseErr(other_parse_err)), input),
|
Err(other_parse_err) => (Err(ManagerErr::RedisParseErr(other_parse_err)), input),
|
||||||
};
|
};
|
||||||
self.redis_input.extend_from_slice(leftover.as_bytes());
|
|
||||||
self.redis_input.extend_from_slice(invalid_bytes);
|
self.cursor = [leftover.as_bytes(), invalid_bytes]
|
||||||
|
.concat()
|
||||||
|
.bytes()
|
||||||
|
.fold(0, |acc, cur| {
|
||||||
|
// TODO - make clearer and comment side-effect
|
||||||
|
self.redis_input[acc] = cur.expect("TODO");
|
||||||
|
acc + 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// self.cursor = 0;
|
||||||
|
// for (i, byte) in [leftover.as_bytes(), invalid_bytes]
|
||||||
|
// .concat()
|
||||||
|
// .bytes()
|
||||||
|
// .enumerate()
|
||||||
|
// {
|
||||||
|
// self.redis_input[i] = byte.expect("TODO");
|
||||||
|
// self.cursor += 1;
|
||||||
|
// }
|
||||||
|
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,8 +36,6 @@ pub enum RedisParseOutput<'a> {
|
||||||
NonMsg(&'a str),
|
NonMsg(&'a str),
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO -- should this impl Iterator?
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct RedisMsg<'a> {
|
pub struct RedisMsg<'a> {
|
||||||
pub timeline_txt: &'a str,
|
pub timeline_txt: &'a str,
|
||||||
|
|
Loading…
Reference in New Issue