diff --git a/Cargo.lock b/Cargo.lock index 3281c61..8f391e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,6 +131,21 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -455,6 +470,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.2.0" @@ -528,6 +549,18 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -536,7 +569,7 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", ] [[package]] @@ -660,6 +693,7 @@ dependencies = [ "json-patch", "linkme", "procfs", + "proptest", "ratatui", "regex", "rstest", @@ -1087,6 +1121,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -1126,6 +1169,31 @@ dependencies = [ "hex", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.13.2", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift 0.4.0", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.47" @@ -1135,6 +1203,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1149,17 +1223,27 @@ checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" dependencies = [ "autocfg 0.1.8", "libc", - "rand_chacha", + "rand_chacha 0.1.1", "rand_core 0.4.3", "rand_hc", "rand_isaac", "rand_jitter", "rand_os", "rand_pcg", - "rand_xorshift", + "rand_xorshift 0.1.1", "winapi", ] +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand_chacha" version = "0.1.1" @@ -1170,6 +1254,16 @@ dependencies = [ "rand_core 0.3.2", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.3.2" @@ -1185,6 +1279,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e5937858e6fd18cd595d558f90bb5de3b72ae23f9e3763af0e805949b04ef60" +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_hc" version = "0.1.0" @@ -1247,6 +1350,15 @@ dependencies = [ "rand_core 0.3.2", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "ratatui" version = "0.30.2" @@ -1423,6 +1535,18 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.23" @@ -1570,7 +1694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49d57902bb128e5e38b5218d3681215ae3e322d99f65d5420e9849730d2ea372" dependencies = [ "num", - "rand", + "rand 0.6.5", ] [[package]] @@ -1629,7 +1753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys", @@ -1879,6 +2003,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.26" @@ -1952,12 +2082,30 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.129" @@ -2058,6 +2206,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "zbus" version = "5.12.0" @@ -2114,6 +2268,26 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zerocopy" +version = "0.8.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879fb705ce98c32e41ebdb970fbe1204f8492423b314c6ab0354c3e7b5542866" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708882a28301d604fa039cc7727607a98d04c4b86dc76ec9cc683f805d709759" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zmij" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index 945f6d4..2c82a4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,3 +101,4 @@ rstest = "0.26" tempfile = "3" test-log = { version = "0.2", features = ["trace"] } tokio = { version = "1", features = ["test-util"] } +proptest = "1" diff --git a/src/backends/qemu/helpers.rs b/src/backends/qemu/helpers.rs index eda816a..1b120f6 100644 --- a/src/backends/qemu/helpers.rs +++ b/src/backends/qemu/helpers.rs @@ -320,44 +320,108 @@ pub fn next_qemu_iothread_id(existing: &[String]) -> String { #[cfg(test)] mod tests { + use proptest::prelude::*; + use super::*; - /// Test that vQ round-robin mapping spreads queues across IOThreads - /// as evenly as possible. + proptest! { + /// Test that vQ round-robin mapping spreads queues across IOThreads + /// as evenly as possible. + #[test] + fn test_qemu_round_robin_partitions_queues( + ids in prop::collection::vec("[a-z]{1,4}", 0..8), + vq_count in 0u16..128, + ) { + // We run a mapping on arbitrary IOThread ids and number of queues. + let mapping = round_robin_vq_mapping(&ids, vq_count); + if ids.is_empty() || vq_count == 0 { + prop_assert!(mapping.is_empty()); + return Ok(()); + } + + // Check every queue has been assigned once. + prop_assert_eq!(mapping.len(), ids.len()); + let mut seen = vec![false; usize::from(vq_count)]; + for (idx, entry) in mapping.iter().enumerate() { + prop_assert_eq!(&entry.iothread, &ids[idx]); + for &vq in &entry.vqs { + prop_assert_eq!(usize::from(vq) % ids.len(), idx); + prop_assert!(!seen[usize::from(vq)]); + seen[usize::from(vq)] = true; + } + } + prop_assert!(seen.into_iter().all(|present| present)); + + // Check queues are evenly distributed. + let sizes: Vec = mapping.iter().map(|entry| entry.vqs.len()).collect(); + let min = sizes.iter().copied().min().unwrap(); + let max = sizes.iter().copied().max().unwrap(); + prop_assert!(max - min <= 1); + } + + /// Ensure the next IOThread to have a queue assigned is the one with + /// the lowest id. + #[test] + fn test_qemu_next_id_is_the_lowest_missing_iot( + // IOThreads with assigned queues, these are excluded from the search. + present in prop::collection::btree_set(0u32..1024, 0..32), + ) { + let ids: Vec = present.iter().map(|n| format!("iot{n}")).collect(); + // Find the thread with the lowest id. + let expected = (0..1024).find(|n| !present.contains(n)).unwrap(); + prop_assert_eq!(next_qemu_iothread_id(&ids), format!("iot{expected}")); + } + } + + /// Test that managed `iotN` threads are kept in id order and other ids are + /// dropped. #[test] - fn round_robin_distributes_evenly() { - let ids = vec!["iot0".to_string(), "iot1".to_string(), "iot2".to_string()]; - let mapping = round_robin_vq_mapping(&ids, 7); - let counts: Vec = mapping.iter().map(|m| m.vqs.len()).collect(); - assert_eq!(counts, vec![3, 2, 2]); - assert_eq!(mapping[0].vqs, vec![0, 3, 6]); - assert_eq!(mapping[1].vqs, vec![1, 4]); - assert_eq!(mapping[2].vqs, vec![2, 5]); + fn test_qemu_topology_keeps_managed_threads_in_id_order() { + let body = r#" +qemu_iothread_info{id="iot1",tid="124"} 1 +qemu_iothread_info{id="dirtybitmap",tid="999"} 1 +qemu_iothread_info{id="iot0",tid="123"} 1 +"#; + let topo = QemuTopology::new(body); + assert_eq!(topo.iothreads, vec!["iot0", "iot1"]); + assert_eq!(topo.iothread_tids.get("iot0"), Some(&123)); + assert_eq!(topo.iothread_tids.get("iot1"), Some(&124)); + assert_eq!(topo.iothread_tids.len(), 2); } - /// Test that `next_qemu_iothread_id` fills the lowest missing - /// `iotN` id. + /// Test that `thread_id` and `path` are accepted in place of `tid` and + /// `device`. #[test] - fn next_id_fills_gaps_in_order() { - let ids = vec!["iot0".to_string(), "iot2".to_string()]; - assert_eq!(next_qemu_iothread_id(&ids), "iot1"); - let ids = vec!["iot0".to_string(), "iot1".to_string()]; - assert_eq!(next_qemu_iothread_id(&ids), "iot2"); + fn test_qemu_topology_accepts_label_aliases() { + let body = r#" +qemu_iothread_info{id="iot0",thread_id="321"} 1 +qemu_virtio_scsi_num_queues{path="/machine/peripheral/scsi0"} 4 +"#; + let topo = QemuTopology::new(body); + assert_eq!(topo.iothreads, vec!["iot0"]); + assert_eq!(topo.iothread_tids.get("iot0"), Some(&321)); + assert_eq!(topo.device_path, "/machine/peripheral/scsi0"); + assert_eq!(topo.vq_count, 4); } - /// Test that prometheus scrape text yields IOThread ids, TIDs, and - /// the virtio-scsi device path. + /// Test that the first device wins, noise lines are ignored, and a later + /// thread is kept. + // TODO make sure this is intended #[test] - fn topology_parses_prometheus_body() { - let body = r#"# HELP foo -qemu_iothread_info{id="iot0",tid="123"} 1 -qemu_iothread_info{id="iot1",tid="124"} 1 -qemu_iothread_info{id="dirtybitmap",tid="999"} 1 + fn test_qemu_topology_keeps_the_first_device_and_later_threads() { + let body = r#" +# HELP foo +not a metric +qemu_virtio_scsi_num_queues{device=""} 3 +qemu_iothread_info{id="iot7",tid="nope"} 1 qemu_virtio_scsi_num_queues{device="/machine/peripheral/scsi0"} 4 +qemu_iothread_info{id="iot0",tid="123"} 1 +qemu_virtio_scsi_num_queues{device="/machine/peripheral/scsi1"} 99 "#; let topo = QemuTopology::new(body); - assert_eq!(topo.iothreads, vec!["iot0", "iot1"]); + assert_eq!(topo.iothreads, vec!["iot0"]); assert_eq!(topo.iothread_tids.get("iot0"), Some(&123)); + assert_eq!(topo.iothread_tids.len(), 1); assert_eq!(topo.device_path, "/machine/peripheral/scsi0"); assert_eq!(topo.vq_count, 4); assert!(topo.vq_inflight.is_empty());