diff --git a/Cargo.lock b/Cargo.lock index a049bb21..40b36728 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -416,6 +416,20 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb97d56060ee67d285efb8001fec9d2a4c710c32efd2e14b5cbb5ba71930fc2d" +[[package]] +name = "benches" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "bytesize", + "ironrdp", + "pico-args", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "bindgen" version = "0.69.5" @@ -582,6 +596,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "bytesize" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba" + [[package]] name = "calloop" version = "0.13.0" @@ -2707,6 +2727,7 @@ dependencies = [ "tokio", "tokio-rustls", "tracing", + "visibility", "x509-cert", ] @@ -2765,6 +2786,7 @@ dependencies = [ "pretty_assertions", "proptest", "rstest", + "visibility", ] [[package]] @@ -5685,6 +5707,17 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "visibility" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "vswhom" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index e0020e93..004d84dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/*", + "benches", "xtask", "ffi", ] diff --git a/benches/Cargo.toml b/benches/Cargo.toml new file mode 100644 index 00000000..2c266605 --- /dev/null +++ b/benches/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "benches" +version = "0.1.0" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[[bin]] +name = "perfenc" +path = "src/perfenc.rs" + +[dependencies] +anyhow = "1.0.98" +async-trait = "0.1.88" +bytesize = "2.0.1" +ironrdp = { path = "../crates/ironrdp", features = [ + "server", + "pdu", + "__bench", +] } +pico-args = "0.5.0" +tokio = { version = "1", features = ["sync", "fs"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing = { version = "0.1", features = ["log"] } + +[lints] +workspace = true diff --git a/benches/src/perfenc.rs b/benches/src/perfenc.rs new file mode 100644 index 00000000..00e1427b --- /dev/null +++ b/benches/src/perfenc.rs @@ -0,0 +1,193 @@ +#![allow(unused_crate_dependencies)] // False positives because there are both a library and a binary. +#![allow(clippy::print_stderr)] +#![allow(clippy::print_stdout)] + +use core::time::Duration; +use std::{io::Write, time::Instant}; + +use anyhow::Context; +use ironrdp::pdu::rdp::capability_sets::{CmdFlags, EntropyBits}; +use ironrdp::server::{ + bench::encoder::{UpdateEncoder, UpdateEncoderCodecs}, + BitmapUpdate, DesktopSize, DisplayUpdate, PixelFormat, RdpServerDisplayUpdates, +}; +use tokio::{fs::File, io::AsyncReadExt, time::sleep}; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), anyhow::Error> { + setup_logging()?; + let mut args = pico_args::Arguments::from_env(); + + if args.contains(["-h", "--help"]) { + println!("Usage: perfenc [OPTIONS] "); + println!(); + println!("Measure the performance of the IronRDP server encoder, given a raw RGBX video input file."); + println!(); + println!("Options:"); + println!(" --width Width of the display (default: 3840)"); + println!(" --height Height of the display (default: 2400)"); + println!(" --codec Codec to use (default: remotefx)"); + println!(" Valid values: remotefx, bitmap, none"); + println!(" --fps Frames per second (default: none)"); + std::process::exit(0); + } + + let width = args.opt_value_from_str("--width")?.unwrap_or(3840); + let height = args.opt_value_from_str("--height")?.unwrap_or(2400); + let codec = args.opt_value_from_str("--codec")?.unwrap_or_else(OptCodec::default); + let fps = args.opt_value_from_str("--fps")?.unwrap_or(0); + + let filename: String = args.free_from_str().context("missing RGBX input filename")?; + let file = File::open(&filename) + .await + .with_context(|| format!("Failed to open file: {}", filename))?; + + let mut flags = CmdFlags::all(); + let mut update_codecs = UpdateEncoderCodecs::new(); + + match codec { + OptCodec::RemoteFX => update_codecs.set_remotefx(Some((EntropyBits::Rlgr3, 0))), + OptCodec::Bitmap => { + flags -= CmdFlags::SET_SURFACE_BITS; + } + OptCodec::None => {} + }; + + let mut encoder = UpdateEncoder::new(DesktopSize { width, height }, flags, update_codecs); + + let mut total_raw = 0u64; + let mut total_enc = 0u64; + let mut n_updates = 0u64; + let mut updates = DisplayUpdates::new(file, DesktopSize { width, height }, fps); + while let Some(up) = updates.next_update().await { + if let DisplayUpdate::Bitmap(ref up) = up { + total_raw += up.data.len() as u64; + } else { + eprintln!("Invalid update"); + break; + } + let mut iter = encoder.update(up); + loop { + let Some(frag) = iter.next().await else { + break; + }; + let len = frag?.data.len() as u64; + total_enc += len; + } + n_updates += 1; + print!("."); + std::io::stdout().flush().unwrap(); + } + println!(); + + let ratio = total_enc as f64 / total_raw as f64; + let percent = 100.0 - ratio * 100.0; + println!("Encoder: {:?}", encoder); + println!("Nb updates: {:?}", n_updates); + println!( + "Sum of bytes: {}/{} ({:.2}%)", + bytesize::ByteSize(total_enc), + bytesize::ByteSize(total_raw), + percent, + ); + Ok(()) +} + +struct DisplayUpdates { + file: File, + desktop_size: DesktopSize, + fps: u64, + last_update_time: Option, +} + +impl DisplayUpdates { + fn new(file: File, desktop_size: DesktopSize, fps: u64) -> Self { + Self { + file, + desktop_size, + fps, + last_update_time: None, + } + } +} + +#[async_trait::async_trait] +impl RdpServerDisplayUpdates for DisplayUpdates { + async fn next_update(&mut self) -> Option { + let stride = self.desktop_size.width as usize * 4; + let frame_size = stride * self.desktop_size.height as usize; + let mut buf = vec![0u8; frame_size]; + if self.file.read_exact(&mut buf).await.is_err() { + return None; + } + + let now = Instant::now(); + if let Some(last_update_time) = self.last_update_time { + let elapsed = now - last_update_time; + if self.fps > 0 && elapsed < Duration::from_millis(1000 / self.fps) { + sleep(Duration::from_millis( + 1000 / self.fps - u64::try_from(elapsed.as_millis()).unwrap(), + )) + .await; + } + } + self.last_update_time = Some(now); + + let up = DisplayUpdate::Bitmap(BitmapUpdate { + x: 0, + y: 0, + width: self.desktop_size.width.try_into().unwrap(), + height: self.desktop_size.height.try_into().unwrap(), + format: PixelFormat::RgbX32, + data: buf.into(), + stride, + }); + Some(up) + } +} + +fn setup_logging() -> anyhow::Result<()> { + use tracing::metadata::LevelFilter; + use tracing_subscriber::prelude::*; + use tracing_subscriber::EnvFilter; + + let fmt_layer = tracing_subscriber::fmt::layer().compact(); + + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::WARN.into()) + .with_env_var("IRONRDP_LOG") + .from_env_lossy(); + + tracing_subscriber::registry() + .with(fmt_layer) + .with(env_filter) + .try_init() + .context("failed to set tracing global subscriber")?; + + Ok(()) +} + +enum OptCodec { + RemoteFX, + Bitmap, + None, +} + +impl Default for OptCodec { + fn default() -> Self { + Self::RemoteFX + } +} + +impl core::str::FromStr for OptCodec { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "remotefx" => Ok(Self::RemoteFX), + "bitmap" => Ok(Self::Bitmap), + "none" => Ok(Self::None), + _ => Err(anyhow::anyhow!("unknown codec: {}", s)), + } + } +} diff --git a/crates/ironrdp-server/Cargo.toml b/crates/ironrdp-server/Cargo.toml index e2a26728..352b7214 100644 --- a/crates/ironrdp-server/Cargo.toml +++ b/crates/ironrdp-server/Cargo.toml @@ -22,7 +22,7 @@ rayon = ["dep:rayon"] # Internal (PRIVATE!) features used to aid testing. # Don't rely on these whatsoever. They may disappear at any time. -__bench = [] +__bench = ["dep:visibility"] [dependencies] anyhow = "1.0" @@ -46,6 +46,7 @@ x509-cert = { version = "0.2.5", optional = true } rustls-pemfile = { version = "2.2.0", optional = true } rayon = { version = "1.10.0", optional = true } bytes = "1" +visibility = { version = "0.1", optional = true } [dev-dependencies] tokio = { version = "1", features = ["sync"] } diff --git a/crates/ironrdp-server/src/encoder/fast_path.rs b/crates/ironrdp-server/src/encoder/fast_path.rs index 08883c42..c63a8ce0 100644 --- a/crates/ironrdp-server/src/encoder/fast_path.rs +++ b/crates/ironrdp-server/src/encoder/fast_path.rs @@ -8,10 +8,13 @@ const MAX_FASTPATH_UPDATE_SIZE: usize = 16_374; const FASTPATH_HEADER_SIZE: usize = 6; +#[allow(unreachable_pub)] +#[cfg_attr(feature = "__bench", visibility::make(pub))] pub(crate) struct UpdateFragmenter { code: UpdateCode, index: usize, - data: Vec, + #[doc(hidden)] // not part of the public API, used by benchmarks + pub data: Vec, position: usize, } diff --git a/crates/ironrdp-server/src/encoder/mod.rs b/crates/ironrdp-server/src/encoder/mod.rs index 919f8903..1bd102ad 100644 --- a/crates/ironrdp-server/src/encoder/mod.rs +++ b/crates/ironrdp-server/src/encoder/mod.rs @@ -29,16 +29,19 @@ enum CodecId { None = 0x0, } +#[cfg_attr(feature = "__bench", visibility::make(pub))] #[derive(Debug)] pub(crate) struct UpdateEncoderCodecs { remotefx: Option<(EntropyBits, u8)>, } impl UpdateEncoderCodecs { + #[cfg_attr(feature = "__bench", visibility::make(pub))] pub(crate) fn new() -> Self { Self { remotefx: None } } + #[cfg_attr(feature = "__bench", visibility::make(pub))] pub(crate) fn set_remotefx(&mut self, remotefx: Option<(EntropyBits, u8)>) { self.remotefx = remotefx } @@ -50,6 +53,7 @@ impl Default for UpdateEncoderCodecs { } } +#[cfg_attr(feature = "__bench", visibility::make(pub))] pub(crate) struct UpdateEncoder { desktop_size: DesktopSize, framebuffer: Option, @@ -65,6 +69,7 @@ impl fmt::Debug for UpdateEncoder { } impl UpdateEncoder { + #[cfg_attr(feature = "__bench", visibility::make(pub))] pub(crate) fn new(desktop_size: DesktopSize, surface_flags: CmdFlags, codecs: UpdateEncoderCodecs) -> Self { let bitmap_updater = if surface_flags.contains(CmdFlags::SET_SURFACE_BITS) { let mut bitmap = BitmapUpdater::None(NoneHandler); @@ -85,6 +90,7 @@ impl UpdateEncoder { } } + #[cfg_attr(feature = "__bench", visibility::make(pub))] pub(crate) fn update(&mut self, update: DisplayUpdate) -> EncoderIter<'_> { EncoderIter { encoder: self, @@ -218,12 +224,14 @@ enum State { Ended, } +#[cfg_attr(feature = "__bench", visibility::make(pub))] pub(crate) struct EncoderIter<'a> { encoder: &'a mut UpdateEncoder, state: State, } impl EncoderIter<'_> { + #[cfg_attr(feature = "__bench", visibility::make(pub))] pub(crate) async fn next(&mut self) -> Option> { loop { let state = core::mem::take(&mut self.state); diff --git a/crates/ironrdp-server/src/lib.rs b/crates/ironrdp-server/src/lib.rs index 0503b497..9a86ad43 100644 --- a/crates/ironrdp-server/src/lib.rs +++ b/crates/ironrdp-server/src/lib.rs @@ -35,5 +35,7 @@ pub mod bench { pub mod rfx { pub use crate::encoder::rfx::bench::{rfx_enc, rfx_enc_tile}; } + + pub use crate::encoder::{UpdateEncoder, UpdateEncoderCodecs}; } } diff --git a/crates/ironrdp-testsuite-core/Cargo.toml b/crates/ironrdp-testsuite-core/Cargo.toml index 12ae59f5..0cc87ae5 100644 --- a/crates/ironrdp-testsuite-core/Cargo.toml +++ b/crates/ironrdp-testsuite-core/Cargo.toml @@ -10,6 +10,12 @@ autotests = false doctest = false test = false +[features] +# Internal (PRIVATE!) features used to aid testing. +# Don't rely on these whatsoever. They may disappear at any time. +# Added here because it includes/link to some files from other crates +__bench = ["dep:visibility"] + [[test]] name = "integration_tests_core" path = "tests/main.rs" @@ -22,6 +28,7 @@ ironrdp-core.path = "../ironrdp-core" ironrdp-pdu.path = "../ironrdp-pdu" lazy_static.workspace = true # TODO: remove in favor of https://doc.rust-lang.org/std/sync/struct.OnceLock.html paste = "1" +visibility = { version = "0.1", optional = true } [dev-dependencies] anyhow = "1" diff --git a/crates/ironrdp/Cargo.toml b/crates/ironrdp/Cargo.toml index bd596bae..3abbcb7d 100644 --- a/crates/ironrdp/Cargo.toml +++ b/crates/ironrdp/Cargo.toml @@ -31,6 +31,9 @@ dvc = ["dep:ironrdp-dvc"] rdpdr = ["dep:ironrdp-rdpdr"] rdpsnd = ["dep:ironrdp-rdpsnd"] displaycontrol = ["dep:ironrdp-displaycontrol"] +# Internal (PRIVATE!) features used to aid testing. +# Don't rely on these whatsoever. They may disappear at any time. +__bench = ["ironrdp-server/__bench"] [dependencies] ironrdp-core = { path = "../ironrdp-core", version = "0.1", optional = true } # public diff --git a/xtask/src/check.rs b/xtask/src/check.rs index 3bc4c360..ed44a721 100644 --- a/xtask/src/check.rs +++ b/xtask/src/check.rs @@ -20,7 +20,7 @@ pub fn lints(sh: &Shell) -> anyhow::Result<()> { // TODO: when 1.74 is released use `--keep-going`: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#keep-going cmd!( sh, - "{CARGO} clippy --workspace --all-targets --features helper --locked -- -D warnings" + "{CARGO} clippy --workspace --all-targets --features helper,__bench --locked -- -D warnings" ) .run()?;