feat(benches): add perfenc

Make some internal APIs publicly visible thanks to "visibility" when
compiling with the "__bench" feature.

("testsuite-core" also learned "__bench", because fast_path.rs is a
shared file)

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
This commit is contained in:
Marc-André Lureau 2025-02-11 17:52:40 +04:00 committed by Benoît Cortier
parent fcb390140d
commit dd787af5a0
11 changed files with 285 additions and 3 deletions

33
Cargo.lock generated
View file

@ -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"

View file

@ -1,6 +1,7 @@
[workspace]
members = [
"crates/*",
"benches",
"xtask",
"ffi",
]

31
benches/Cargo.toml Normal file
View file

@ -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

193
benches/src/perfenc.rs Normal file
View file

@ -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] <RGBX_INPUT_FILENAME>");
println!();
println!("Measure the performance of the IronRDP server encoder, given a raw RGBX video input file.");
println!();
println!("Options:");
println!(" --width <WIDTH> Width of the display (default: 3840)");
println!(" --height <HEIGHT> Height of the display (default: 2400)");
println!(" --codec <CODEC> Codec to use (default: remotefx)");
println!(" Valid values: remotefx, bitmap, none");
println!(" --fps <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<Instant>,
}
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<DisplayUpdate> {
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<Self, Self::Err> {
match s {
"remotefx" => Ok(Self::RemoteFX),
"bitmap" => Ok(Self::Bitmap),
"none" => Ok(Self::None),
_ => Err(anyhow::anyhow!("unknown codec: {}", s)),
}
}
}

View file

@ -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"] }

View file

@ -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<u8>,
#[doc(hidden)] // not part of the public API, used by benchmarks
pub data: Vec<u8>,
position: usize,
}

View file

@ -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<Framebuffer>,
@ -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<Result<UpdateFragmenter>> {
loop {
let state = core::mem::take(&mut self.state);

View file

@ -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};
}
}

View file

@ -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"

View file

@ -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

View file

@ -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()?;