diff --git a/x11rb-protocol/Cargo.toml b/x11rb-protocol/Cargo.toml index e60df451..104936ac 100644 --- a/x11rb-protocol/Cargo.toml +++ b/x11rb-protocol/Cargo.toml @@ -15,6 +15,9 @@ keywords = ["xcb", "X11"] [dependencies] +[dev-dependencies] +criterion = "0.3" + [target.'cfg(unix)'.dependencies] nix = "0.23" @@ -47,3 +50,7 @@ xselinux = [] xtest = [] xv = ["shm"] xvmc = ["xv"] + +[[bench]] +name = "proto_connection" +harness = false \ No newline at end of file diff --git a/x11rb-protocol/benches/proto_connection.rs b/x11rb-protocol/benches/proto_connection.rs new file mode 100644 index 00000000..774b4108 --- /dev/null +++ b/x11rb-protocol/benches/proto_connection.rs @@ -0,0 +1,335 @@ +//! Benchmark the `x11rb_protocol::Connection` type's method, at varying levels of +//! capacity. + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use std::{ + io::{Read, Write}, + mem::{replace, size_of}, + net::{TcpListener, TcpStream}, + thread, +}; +use x11rb_protocol::{ + connection::{Connection, ReplyFdKind}, + protocol::xproto::{Depth, Rectangle, Screen}, + x11_utils::{Serialize, TryParse}, +}; + +#[cfg(unix)] +use std::os::unix::net::UnixStream; + +fn pow(i: i32, p: i32) -> i32 { + let mut result = 1; + for _ in 0..p { + result *= i; + } + result +} + +fn enqueue_packet_test(c: &mut Criterion) { + // take the cartesian product of the following conditions: + // - the packet is an event, a reply, or an error + // - pending_events and pending_replies are empty, have one element, or have + // many elements + + enum PacketType { + Event, + Reply, + Error, + } + + enum PacketCount { + Empty, + One, + Many, + } + + use PacketCount::*; + use PacketType::*; + + let mut group = c.benchmark_group("enqueue_packet"); + for packet_ty in &[Event, Reply, Error] { + for packet_count in &[Empty, One, Many] { + let packet_ty_desc = match packet_ty { + Event => "event", + Reply => "reply", + Error => "error", + }; + + let packet_count_desc = match packet_count { + Empty => "no", + One => "one", + Many => "many", + }; + + let name = format!( + "enqueue_packet {} with {} packets", + packet_ty_desc, packet_count_desc + ); + group.bench_function(name, |b| { + // generate a valid packet with the given first byte and sequence number + let mut seqno = 0u16; + let mut packet = move |ind: u8| { + let our_seqno = seqno + 1; + seqno += 1; + + let mut v = vec![0; 32]; + v[0] = ind; + + // copy our_seqno to bytes 3 and 4 + (&mut v[2..4]).copy_from_slice(&our_seqno.to_ne_bytes()); + + v + }; + + // we need another one for make_conn + let mut packet2 = packet; + + let queue_count = match packet_count { + PacketCount::Empty => 0, + PacketCount::One => 1, + PacketCount::Many => pow(2, 8), + }; + + // create a connection with the given stats + let mut make_conn = || { + let mut conn = Connection::new(); + + for _ in 0..queue_count { + // push a new event + conn.enqueue_packet(packet2(2)); + } + + for _ in 0..queue_count { + // push a new reply + conn.enqueue_packet(packet2(1)); + } + + conn + }; + + let mut conn = make_conn(); + let packet = packet(match packet_ty { + Event => 2, + Reply => 1, + Error => 0, + }); + + b.iter(move || { + conn.enqueue_packet(packet.clone()); + }) + }); + } + } +} + +fn send_and_receive_request(c: &mut Criterion) { + // permutations: + // - send queue is empty or very full + // - receive queue is empty of very full + enum SendQueue { + SEmpty, + SFull, + } + + enum RecvQueue { + REmpty, + RFull, + } + + use RecvQueue::*; + use SendQueue::*; + + let mut group = c.benchmark_group("send_and_receive_request"); + + for send_queue in &[SEmpty, SFull] { + for recv_queue in &[REmpty, RFull] { + let name = format!( + "send_and_receive_request (send {}, recv {})", + match send_queue { + SEmpty => "empty", + SFull => "full", + }, + match recv_queue { + REmpty => "empty", + RFull => "full", + } + ); + + group.bench_function(name, |b| { + // create a new connection + let mut conn = Connection::new(); + + // if the send queue needs to be full, flood it with sent requests + if matches!(send_queue, SFull) { + for _ in 0..pow(2, 14) { + conn.send_request(match recv_queue { + REmpty => ReplyFdKind::NoReply, + RFull => ReplyFdKind::ReplyWithoutFDs, + }); + } + } + + // if the recv queue needs to be full, flood it with replies + if matches!(recv_queue, RFull) { + for _ in 0..pow(2, 14) { + let mut packet = vec![0; 32]; + packet[0] = 1; + conn.enqueue_packet(packet); + } + } + + // create a new packet + let mut packet = vec![0u8; 32]; + packet[0] = 1; + + b.iter(move || { + // send our request + let seq = conn.send_request(ReplyFdKind::ReplyWithoutFDs).unwrap(); + + // truncate to a u16 + let seq_trunc = seq as u16; + + // insert the sequence number at positions 2 and 3 + (&mut packet[2..4]).copy_from_slice(&seq_trunc.to_ne_bytes()); + + // enqueue the packet + conn.enqueue_packet(black_box(replace(&mut packet, vec![0u8; 32]))); + + // pop the reply + conn.poll_for_reply_or_error(seq) + }) + }); + } + } +} + +fn try_parse_small_struct(c: &mut Criterion) { + // xproto::Rectangle is a pointer wide on 64-bit, use that + c.bench_function("try_parse an xproto::Rectangle", |b| { + let packet = [0x42u8; size_of::()]; + b.iter(|| Rectangle::try_parse(black_box(&packet))) + }); +} + +fn try_parse_large_struct(c: &mut Criterion) { + // xproto::Screen is a significantly larger structure, use that + const SCREEN_BASE_SIZE: usize = size_of::() - size_of::>() + size_of::(); + const NUM_DEPTHS: usize = 3; + const DEPTH_SIZE: usize = 8; + const TOTAL_SIZE: usize = SCREEN_BASE_SIZE + (NUM_DEPTHS * DEPTH_SIZE); + + c.bench_function("try_parse an xproto::Screen", |b| { + let mut packet = [0; TOTAL_SIZE]; + packet[SCREEN_BASE_SIZE - 1] = NUM_DEPTHS as u8; + b.iter(|| Screen::try_parse(black_box(&packet))) + }); +} + +fn serialize_struct(c: &mut Criterion) { + // try the following: + // - send it down a TCP socket + // - send it down a Unix socket (if linux) + // + // this should relatively accurately tell us what kind of impact the buffering + // and writing have on the serialization time + // + // note that send() and recv() degenerate into sendmsg() and recvmsg(), at least + // on the Linux kernel end, so not using those functions should have no effect + enum SocketTy { + TryTcp, + TryUnix, + } + + enum StructType { + Small, + Large, + } + + use SocketTy::*; + use StructType::*; + + let mut group = c.benchmark_group("serialize_struct"); + for socket_ty in &[TryTcp, TryUnix] { + let mut fd: Box = match socket_ty { + TryTcp => { + const PORT: u16 = 41234; + + let listen = TcpListener::bind(("::1", PORT)).unwrap(); + thread::spawn(move || { + let (mut sock, _) = listen.accept().unwrap(); + + // read until other sock gets dropped + let mut buf = [0u8; 1024]; + loop { + if sock.read(&mut buf).is_err() { + break; + } + } + }); + let sock = TcpStream::connect(("::1", PORT)).unwrap(); + Box::new(sock) + } + TryUnix => { + #[cfg(unix)] + { + let (mut left, right) = UnixStream::pair().unwrap(); + thread::spawn(move || { + let mut buf = [0u8; 1024]; + loop { + if left.read(&mut buf).is_err() { + break; + } + } + }); + Box::new(right) + } + #[cfg(not(unix))] + { + continue; + } + } + }; + + let try_desc = match socket_ty { + TryTcp => "TCP", + TryUnix => "Unix", + }; + + for struct_size in &[Small, Large] { + let size_desc = match struct_size { + Small => "small", + Large => "large", + }; + + let name = format!("serialize_struct {} {}", try_desc, size_desc); + group.bench_function(name, |b| { + b.iter(|| { + let bytes = match struct_size { + Small => { + let rect = Rectangle::default(); + black_box(rect.serialize()).to_vec() + } + Large => { + let mut screen = Screen::default(); + screen.allowed_depths.resize_with(3, Default::default); + black_box(screen.serialize()) + } + }; + + // write the serialized bytes tothe output + fd.write_all(&bytes).unwrap(); + }) + }); + } + } +} + +criterion_group!( + benches, + enqueue_packet_test, + send_and_receive_request, + try_parse_small_struct, + try_parse_large_struct, + serialize_struct +); +criterion_main!(benches);