diff --git a/apps/.ignore b/apps/.ignore index 65ae08069b..ad5506a634 100644 --- a/apps/.ignore +++ b/apps/.ignore @@ -25,6 +25,7 @@ apps/starry/ebpf/rawtp apps/starry/ebpf/upb apps/starry/ebpf/upb2 apps/starry/mysql +apps/starry/net-bench apps/starry/picoclaw-cli apps/starry/hdl-lang apps/starry/java-jse diff --git a/apps/starry/ebpf/net_stats/.gitignore b/apps/starry/ebpf/net_stats/.gitignore new file mode 100644 index 0000000000..b798b63cc4 --- /dev/null +++ b/apps/starry/ebpf/net_stats/.gitignore @@ -0,0 +1,3 @@ +debug/ +target/ +**/*.rs.bk diff --git a/apps/starry/ebpf/net_stats/Cargo.lock b/apps/starry/ebpf/net_stats/Cargo.lock new file mode 100644 index 0000000000..099a9547a9 --- /dev/null +++ b/apps/starry/ebpf/net_stats/Cargo.lock @@ -0,0 +1,772 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "aya" +version = "0.13.2" +source = "git+https://github.com/aya-rs/aya#a47f99a04ca509b20793be6560d440295e1cf86f" +dependencies = [ + "assert_matches", + "aya-obj", + "bitflags", + "hashbrown", + "libc", + "log", + "object", + "once_cell", + "scopeguard", + "thiserror", +] + +[[package]] +name = "aya-build" +version = "0.1.3" +source = "git+https://github.com/aya-rs/aya#a47f99a04ca509b20793be6560d440295e1cf86f" +dependencies = [ + "anyhow", + "cargo_metadata", + "rustc_version", + "which 8.0.4", +] + +[[package]] +name = "aya-ebpf" +version = "0.1.2" +source = "git+https://github.com/aya-rs/aya#a47f99a04ca509b20793be6560d440295e1cf86f" +dependencies = [ + "aya-build", + "aya-ebpf-bindings", + "aya-ebpf-cty", + "aya-ebpf-macros", + "rustversion", +] + +[[package]] +name = "aya-ebpf-bindings" +version = "0.1.2" +source = "git+https://github.com/aya-rs/aya#a47f99a04ca509b20793be6560d440295e1cf86f" +dependencies = [ + "aya-build", + "aya-ebpf-cty", +] + +[[package]] +name = "aya-ebpf-cty" +version = "0.2.3" +source = "git+https://github.com/aya-rs/aya#a47f99a04ca509b20793be6560d440295e1cf86f" +dependencies = [ + "aya-build", +] + +[[package]] +name = "aya-ebpf-macros" +version = "0.1.2" +source = "git+https://github.com/aya-rs/aya#a47f99a04ca509b20793be6560d440295e1cf86f" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + +[[package]] +name = "aya-log" +version = "0.2.2" +source = "git+https://github.com/aya-rs/aya#a47f99a04ca509b20793be6560d440295e1cf86f" +dependencies = [ + "aya", + "aya-log-common", + "log", + "thiserror", +] + +[[package]] +name = "aya-log-common" +version = "0.1.16" +source = "git+https://github.com/aya-rs/aya#a47f99a04ca509b20793be6560d440295e1cf86f" +dependencies = [ + "num_enum", +] + +[[package]] +name = "aya-log-ebpf" +version = "0.1.2" +source = "git+https://github.com/aya-rs/aya#a47f99a04ca509b20793be6560d440295e1cf86f" +dependencies = [ + "aya-ebpf", + "aya-log-common", + "aya-log-ebpf-macros", +] + +[[package]] +name = "aya-log-ebpf-macros" +version = "0.1.1" +source = "git+https://github.com/aya-rs/aya#a47f99a04ca509b20793be6560d440295e1cf86f" +dependencies = [ + "aya-log-common", + "aya-log-parser", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "aya-log-parser" +version = "0.1.14" +source = "git+https://github.com/aya-rs/aya#a47f99a04ca509b20793be6560d440295e1cf86f" +dependencies = [ + "aya-log-common", +] + +[[package]] +name = "aya-obj" +version = "0.2.2" +source = "git+https://github.com/aya-rs/aya#a47f99a04ca509b20793be6560d440295e1cf86f" +dependencies = [ + "bytes", + "log", + "object", + "thiserror", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "cargo_metadata" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "env_filter", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "net_stats" +version = "0.1.0" +dependencies = [ + "anyhow", + "aya", + "aya-build", + "aya-log", + "cargo_metadata", + "clap", + "env_logger", + "libc", + "log", + "net_stats-common", + "net_stats-ebpf", + "tokio", +] + +[[package]] +name = "net_stats-common" +version = "0.1.0" +dependencies = [ + "aya", +] + +[[package]] +name = "net_stats-ebpf" +version = "0.1.0" +dependencies = [ + "aya-ebpf", + "aya-log-ebpf", + "net_stats-common", + "which 6.0.3", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "crc32fast", + "hashbrown", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix", + "winsafe", +] + +[[package]] +name = "which" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d7cd18d4acb58fb3cdfe9ea54e6cd96a4e7d4cc45c56338b236e82dad47248" +dependencies = [ + "libc", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/apps/starry/ebpf/net_stats/Cargo.toml b/apps/starry/ebpf/net_stats/Cargo.toml new file mode 100644 index 0000000000..a27c94ebd4 --- /dev/null +++ b/apps/starry/ebpf/net_stats/Cargo.toml @@ -0,0 +1,32 @@ +[workspace] +resolver = "2" +members = [ + "net_stats", + "net_stats-common", + "net_stats-ebpf", +] +default-members = ["net_stats", "net_stats-common"] + +[workspace.package] +license = "MIT OR Apache-2.0" +edition = "2024" + +[workspace.dependencies] +aya = { git = "https://github.com/aya-rs/aya", default-features = false } +aya-build = { git = "https://github.com/aya-rs/aya", default-features = false } +aya-ebpf = { git = "https://github.com/aya-rs/aya", default-features = false } +aya-log = { git = "https://github.com/aya-rs/aya", default-features = false } +aya-log-ebpf = { git = "https://github.com/aya-rs/aya", default-features = false } + +anyhow = { version = "1", default-features = false } +cargo_metadata = { version = "0.23.0", default-features = false } +clap = { version = "4.5.20", default-features = false, features = ["std", "derive"] } +env_logger = { version = "0.11.5", default-features = false } +libc = { version = "0.2.159", default-features = false } +log = { version = "0.4.22", default-features = false } +tokio = { version = "1.40.0", default-features = false } +which = { version = "6.0.0", default-features = false } + +[profile.release.package.net_stats-ebpf] +debug = 2 +codegen-units = 1 diff --git a/apps/starry/ebpf/net_stats/README.md b/apps/starry/ebpf/net_stats/README.md new file mode 100644 index 0000000000..95a89ece55 --- /dev/null +++ b/apps/starry/ebpf/net_stats/README.md @@ -0,0 +1,192 @@ +# net_stats - Network Statistics eBPF Monitor + +Real-time network statistics monitoring for StarryOS using eBPF kprobes. + +## Features + +- **TX/RX packet and byte counters**: accurate counts for all IP traffic +- **Consistent with `/proc/net/dev`**: probes the exact same counting points + (`DeviceHandle::count_tx` / `DeviceHandle::count_rx`) that maintain the + kernel's own rx_packets/rx_bytes/tx_packets/tx_bytes counters +- **Low overhead**: eBPF-based probing with per-CPU maps, no lock contention +- **Cross-architecture**: x86_64 validated; aarch64, riscv64, loongarch64 + support claimed via Aya's architecture-agnostic `ProbeContext::arg()` but + pending runtime validation +- **Protocol-agnostic**: counts all IP frames at the physical layer (TCP, UDP, ICMP, etc.) + +## Usage + +### Interactive Mode (default) + +Monitor network statistics in real-time with periodic updates: + +```bash +/usr/bin/net_stats +``` + +Press Ctrl+C to exit. + +### Test Mode + +Run validation tests to ensure probes are working: + +```bash +/usr/bin/net_stats --test +``` + +### Single Snapshot + +Print current statistics once and exit: + +```bash +/usr/bin/net_stats --once +``` + +## Output Format + +``` +NET_STATS_BEGIN +tx_pkts=10 tx_bytes=640 +rx_pkts=12 rx_bytes=768 +NET_STATS_END +``` + +- `tx_pkts`: number of frames transmitted +- `tx_bytes`: total bytes transmitted (L2 frame length) +- `rx_pkts`: number of frames received +- `rx_bytes`: total bytes received (L2 frame length) + +**Note**: Byte counts represent L2 frame sizes (including protocol headers and +per-device L2 framing overhead, excluding trailing FCS), aligned with Linux +`/proc/net/dev` semantics. + +## Implementation Details + +### Probe Points + +The eBPF program attaches kprobes at the exact functions where ax-net updates +its `/proc/net/dev` counters: + +- `DeviceHandle::count_tx(&self, len: usize)` → TX_PKTS +1, TX_BYTES +len +- `DeviceHandle::count_rx(&self, len: usize)` → RX_PKTS +1, RX_BYTES +len + +Both functions carry `#[inline(never)]` in `net/ax-net/src/router.rs` so they +survive release-mode inlining and remain attachable by kprobe. + +On x86_64, `&self` is in `rdi` (arg 0) and `len` is in `rsi` (arg 1). The +eBPF probe reads `len` from arg 1 for both TX and RX — simple, symmetric, and +ABI-stable. + +### Why This Approach + +Earlier implementations probed socket-layer methods and read byte counts from +return values (`AxResult` via sret), then switched to probing +`TxToken::consume` / `RxToken::consume` at the smoltcp phy layer. Both had +fundamental issues: + +1. **Re-implements counting**: the eBPF independently counted packets/bytes + instead of observing the kernel's own authoritative counters, leading to + divergence. +2. **RX bytes not available at `RxToken::consume`**: the function signature + `RxToken::consume(self, f: F)` has no `len` argument, and reading the + slice length from the inlined struct required guessing memory offsets. + +By probing `count_rx` / `count_tx` directly: + +- **Observes the authoritative counters**: every call to these functions + corresponds to a `fetch_add` on the `AtomicU64` fields that back + `/proc/net/dev`. The eBPF counters stay naturally consistent. +- **RX and TX are symmetric**: both take `(&self, len: usize)`, so byte + counting works the same way for both directions. +- **Simple, non-generic signatures**: no monomorphized variants — each + function resolves to exactly one symbol. + +### Symbol Resolution + +Symbols are resolved dynamically from `/proc/kallsyms` using Rust v0 name +mangling fragments: + +- TX: `["6ax_net", "6router", "12DeviceHandle", "8count_tx"]` +- RX: `["6ax_net", "6router", "12DeviceHandle", "8count_rx"]` + +Each resolves to exactly one symbol (no monomorphized variants — the methods +are concrete, not generic). + +### Per-CPU Maps + +Counters use `BPF_MAP_TYPE_PERCPU_ARRAY`: each CPU writes to its own private +slot, eliminating cache-line contention and the need for atomic operations. +The userspace loader aggregates by summing across all CPU slots when printing +or testing. + +**Read-side note**: `per_cpu_sum` reads all per-CPU slots in a single +`get()` call. Since eBPF programs may be concurrently incrementing slots on +other CPUs, individual snapshots may show slight over/under counting. This is +acceptable for monitoring and trending; the `--test` mode only checks that +all counters are non-zero, which is not affected by this race. + +## Building + +The eBPF program is built automatically as part of the StarryOS build: + +```bash +cargo xtask starry app qemu --test-case ebpf/net_stats --arch x86_64 +``` + +Manual build: + +```bash +cd apps/starry/ebpf/net_stats +./prebuild.sh # Builds the eBPF object +cargo build --release # Builds the userspace loader +``` + +## Testing + +Run automated validation: + +```bash +cargo xtask starry app qemu --test-case ebpf/net_stats --arch x86_64 +``` + +The test performs TCP and UDP loopback operations on 2 CPUs and verifies +that all four counters (tx_pkts, tx_bytes, rx_pkts, rx_bytes) are non-zero. + +**Coverage note**: `--test` only exercises the loopback fast path +(`dispatch_unicast_packet`, `send_on_device`). The device-worker paths +(`device_rx_worker`, `device_tx_worker`) and deferred ARP counting are +covered by router unit tests (`l2_counter_tests` in `router.rs`). + +## Troubleshooting + +### "no symbols found in /proc/kallsyms" + +Verify the `#[inline(never)]`-annotated functions exist: + +```bash +grep "12DeviceHandle" /proc/kallsyms | grep "8count_tx" +grep "12DeviceHandle" /proc/kallsyms | grep "8count_rx" +``` + +If symbols are missing, check that the running kernel was built with the +`#[inline(never)]` annotation on `DeviceHandle::count_tx` and +`DeviceHandle::count_rx` in `net/ax-net/src/router.rs`. + +### All counters are zero + +1. Check that network operations are actually occurring +2. Verify eBPF programs loaded: check dmesg for load errors +3. Check for eBPF verifier errors in dmesg + +### eBPF logger warning + +``` +[WARN] failed to initialize eBPF logger: AYA_LOGS not found +``` + +This is expected — the aya logging map is optional and does not affect +functionality. + +## License + +Dual MIT/GPL (required for eBPF programs) diff --git a/apps/starry/ebpf/net_stats/net_stats-common/Cargo.toml b/apps/starry/ebpf/net_stats/net_stats-common/Cargo.toml new file mode 100644 index 0000000000..451ab1ace6 --- /dev/null +++ b/apps/starry/ebpf/net_stats/net_stats-common/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "net_stats-common" +version = "0.1.0" +edition.workspace = true +license.workspace = true + +[features] +default = [] +user = ["aya"] + +[dependencies] +aya = { workspace = true, optional = true } + +[lib] +path = "src/lib.rs" diff --git a/apps/starry/ebpf/net_stats/net_stats-common/src/lib.rs b/apps/starry/ebpf/net_stats/net_stats-common/src/lib.rs new file mode 100644 index 0000000000..979e4cf2b5 --- /dev/null +++ b/apps/starry/ebpf/net_stats/net_stats-common/src/lib.rs @@ -0,0 +1,15 @@ +#![no_std] + +// Shared constants for the net_stats eBPF program and userspace loader. +// Both crates depend on net_stats-common so counter indices stay in sync. + +/// Counter index: transmitted packets. +pub const TX_PKTS: u32 = 0; +/// Counter index: transmitted bytes (L2 frame length). +pub const TX_BYTES: u32 = 1; +/// Counter index: received packets. +pub const RX_PKTS: u32 = 2; +/// Counter index: received bytes (L2 frame length). +pub const RX_BYTES: u32 = 3; +/// Total number of counter slots in the `NETSTATS` BPF map. +pub const MAP_SIZE: u32 = 4; diff --git a/apps/starry/ebpf/net_stats/net_stats-ebpf/Cargo.toml b/apps/starry/ebpf/net_stats/net_stats-ebpf/Cargo.toml new file mode 100644 index 0000000000..13a23ae77c --- /dev/null +++ b/apps/starry/ebpf/net_stats/net_stats-ebpf/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "net_stats-ebpf" +version = "0.1.0" +edition.workspace = true + +[dependencies] +net_stats-common = { path = "../net_stats-common" } +aya-ebpf = { workspace = true } +aya-log-ebpf = { workspace = true } + +[build-dependencies] +which = { workspace = true } + +[[bin]] +name = "net_stats" +path = "src/main.rs" diff --git a/apps/starry/ebpf/net_stats/net_stats-ebpf/build.rs b/apps/starry/ebpf/net_stats/net_stats-ebpf/build.rs new file mode 100644 index 0000000000..0bdd7b5c50 --- /dev/null +++ b/apps/starry/ebpf/net_stats/net_stats-ebpf/build.rs @@ -0,0 +1,6 @@ +use which::which; + +fn main() { + let bpf_linker = which("bpf-linker").unwrap(); + println!("cargo:rerun-if-changed={}", bpf_linker.to_str().unwrap()); +} diff --git a/apps/starry/ebpf/net_stats/net_stats-ebpf/src/lib.rs b/apps/starry/ebpf/net_stats/net_stats-ebpf/src/lib.rs new file mode 100644 index 0000000000..3ac3e595b1 --- /dev/null +++ b/apps/starry/ebpf/net_stats/net_stats-ebpf/src/lib.rs @@ -0,0 +1,3 @@ +#![no_std] + +// This file exists to enable the library target. diff --git a/apps/starry/ebpf/net_stats/net_stats-ebpf/src/main.rs b/apps/starry/ebpf/net_stats/net_stats-ebpf/src/main.rs new file mode 100644 index 0000000000..b342ace7d3 --- /dev/null +++ b/apps/starry/ebpf/net_stats/net_stats-ebpf/src/main.rs @@ -0,0 +1,87 @@ +#![no_std] +#![no_main] +#![allow(unexpected_cfgs)] + +// net_stats-ebpf: kprobe-based network statistics collector for ax-net. +// +// Probes the exact functions where ax-net maintains the `/proc/net/dev` +// counters (DeviceHandle::count_rx / DeviceHandle::count_tx), so the +// eBPF counters stay consistent with the kernel's own accounting. +// +// count_tx(&self, len: usize) → TX_PKTS +1, TX_BYTES +len +// count_rx(&self, len: usize) → RX_PKTS +1, RX_BYTES +len +// +// Both functions must carry #[inline(never)] in router.rs so that they +// survive LLVM inlining and remain attachable by kprobe. +// +// Map layout (per-CPU array, index = counter slot): +// 0 TX_PKTS 1 TX_BYTES +// 2 RX_PKTS 3 RX_BYTES +// +// Per-CPU slots avoid cache-line contention: each CPU writes its own +// slot, and the userspace loader sums across CPUs when reading. + +use aya_ebpf::{ + macros::{kprobe, map}, + maps::PerCpuArray, + programs::ProbeContext, +}; +use net_stats_common::{MAP_SIZE, RX_BYTES, RX_PKTS, TX_BYTES, TX_PKTS}; + +#[map] +static NETSTATS: PerCpuArray = PerCpuArray::::with_max_entries(MAP_SIZE, 0); + +/// Increment the counter at `idx` by `delta`. +/// +/// With `PerCpuArray`, `get_ptr_mut(idx)` returns a pointer into the +/// *current CPU's* private slot, so the increment is naturally race-free. +/// +/// Always inlined so the eBPF verifier can trivially prove bounded +/// execution and won't reject the program for function calls. +#[inline(always)] +fn add_to(idx: u32, delta: u64) { + if let Some(slot) = NETSTATS.get_ptr_mut(idx) { + // SAFETY: PerCpuArray::get_ptr_mut returns a valid, properly + // aligned pointer into the current CPU's private map slot. + // The slot is owned exclusively by this CPU (BPF execution is + // non-preemptible), so no concurrent modification is possible. + unsafe { *slot += delta }; + } +} + +// count_tx(&self, len: usize) +// The probe reads len from arg 1. aya's ProbeContext handles +// per-architecture register mapping (rdi/rsi on x86_64, x0/x1 on +// aarch64, a0/a1 on riscv64, a0/a1 on loongarch64). +#[kprobe] +pub fn count_tx(ctx: ProbeContext) -> u32 { + if let Some(len) = ctx.arg::(1) { + add_to(TX_PKTS, 1); + add_to(TX_BYTES, len as u64); + } + 0 +} + +// count_rx(&self, len: usize) +// Same ABI as count_tx — len is always arg 1 regardless of architecture. +#[kprobe] +pub fn count_rx(ctx: ProbeContext) -> u32 { + if let Some(len) = ctx.arg::(1) { + add_to(RX_PKTS, 1); + add_to(RX_BYTES, len as u64); + } + 0 +} + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + // The eBPF verifier forbids infinite loops, so we use + // unreachable_unchecked to tell LLVM to elide the landing pad + // rather than emitting a loop {} that the verifier would reject. + unsafe { core::hint::unreachable_unchecked() } +} + +#[unsafe(link_section = "license")] +#[unsafe(no_mangle)] +static LICENSE: [u8; 13] = *b"Dual MIT/GPL\0"; diff --git a/apps/starry/ebpf/net_stats/net_stats/Cargo.toml b/apps/starry/ebpf/net_stats/net_stats/Cargo.toml new file mode 100644 index 0000000000..7306c90bd2 --- /dev/null +++ b/apps/starry/ebpf/net_stats/net_stats/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "net_stats" +version = "0.1.0" +edition.workspace = true +license.workspace = true + +[dependencies] +net_stats-common = { path = "../net_stats-common", features = ["user"] } + +anyhow = { workspace = true, default-features = true } +aya = { workspace = true } +aya-log = { workspace = true } +env_logger = { workspace = true } +libc = { workspace = true } +log = { workspace = true } +clap = { workspace = true, features = ["derive"] } +tokio = { workspace = true, features = [ + "macros", + "rt", + "rt-multi-thread", + "signal", + "time", +] } + +[build-dependencies] +anyhow = { workspace = true } +aya-build = { workspace = true } +cargo_metadata = { workspace = true } +net_stats-ebpf = { path = "../net_stats-ebpf" } + +[[bin]] +name = "net_stats" +path = "src/main.rs" diff --git a/apps/starry/ebpf/net_stats/net_stats/build.rs b/apps/starry/ebpf/net_stats/net_stats/build.rs new file mode 100644 index 0000000000..e6531271e2 --- /dev/null +++ b/apps/starry/ebpf/net_stats/net_stats/build.rs @@ -0,0 +1,30 @@ +use anyhow::{Context as _, anyhow}; +use aya_build::Toolchain; + +fn main() -> anyhow::Result<()> { + let cargo_metadata::Metadata { packages, .. } = cargo_metadata::MetadataCommand::new() + .no_deps() + .exec() + .context("MetadataCommand::exec")?; + let ebpf_package = packages + .into_iter() + .find(|cargo_metadata::Package { name, .. }| name.as_str() == "net_stats-ebpf") + .ok_or_else(|| anyhow!("net_stats-ebpf package not found"))?; + let cargo_metadata::Package { + name, + manifest_path, + .. + } = ebpf_package; + aya_build::build_ebpf( + [aya_build::Package { + name: name.as_str(), + root_dir: manifest_path + .parent() + .ok_or_else(|| anyhow!("no parent for {manifest_path}"))? + .as_str(), + features: &[], + ..Default::default() + }], + Toolchain::default(), + ) +} diff --git a/apps/starry/ebpf/net_stats/net_stats/src/main.rs b/apps/starry/ebpf/net_stats/net_stats/src/main.rs new file mode 100644 index 0000000000..cc87bff93c --- /dev/null +++ b/apps/starry/ebpf/net_stats/net_stats/src/main.rs @@ -0,0 +1,238 @@ +// net_stats: userspace loader for the ax-net kprobe statistics collector. +// +// Attaches eBPF kprobes to DeviceHandle::count_tx / DeviceHandle::count_rx, +// which are the exact points where ax-net updates the four /proc/net/dev +// counters (rx_packets, rx_bytes, tx_packets, tx_bytes). +// +// Output format (parseable by summarize.py): +// +// NET_STATS_BEGIN +// tx_pkts= tx_bytes= +// rx_pkts= rx_bytes= +// NET_STATS_END + +use aya::{maps::PerCpuArray, programs::KProbe}; +use clap::Parser; +#[rustfmt::skip] +use log::warn; +use std::{ + fs, + io::{BufRead, BufReader, Read, Write}, + net::{TcpListener, TcpStream, UdpSocket}, + thread, +}; + +use net_stats_common::{RX_BYTES, RX_PKTS, TX_BYTES, TX_PKTS}; +use tokio::{signal, time}; + +/// Shorthand for the net_stats PerCpuArray map type used by the loader. +type NetStatsMap<'a> = PerCpuArray<&'a aya::maps::MapData, u64>; + +#[derive(Debug, Parser)] +struct Opt { + /// Print one snapshot immediately and exit (for scripted sampling). + #[clap(long)] + once: bool, + /// Attach probes, make a self-test TCP connection, print stats, then exit. + #[clap(long)] + test: bool, + /// Interval in seconds between periodic snapshots (default 5). + #[clap(long, default_value = "5")] + interval: u64, +} + +/// Find symbols whose name contains all `fragments`. +fn resolve_symbols(fragments: &[&str]) -> anyhow::Result> { + let buf = BufReader::new(fs::File::open("/proc/kallsyms")?); + let mut syms = Vec::new(); + for line in buf.lines() { + let line = line?; + if let Some(name) = line.split_whitespace().nth(2) + && fragments.iter().all(|f| name.contains(f)) + { + syms.push(name.to_string()); + } + } + if syms.is_empty() { + anyhow::bail!( + "no symbols with fragments {:?} found in /proc/kallsyms", + fragments + ); + } + Ok(syms) +} + +/// Read per-CPU counter values and sum across all CPUs. +fn per_cpu_sum(netstats: &NetStatsMap<'_>, idx: u32) -> u64 { + match netstats.get(&idx, 0) { + Ok(vals) => vals.iter().sum(), + Err(e) => { + warn!("per_cpu_sum({idx}) failed: {e}"); + 0 + } + } +} + +fn print_stats(netstats: &NetStatsMap<'_>) { + println!("NET_STATS_BEGIN"); + println!( + "tx_pkts={} tx_bytes={}", + per_cpu_sum(netstats, TX_PKTS), + per_cpu_sum(netstats, TX_BYTES), + ); + println!( + "rx_pkts={} rx_bytes={}", + per_cpu_sum(netstats, RX_PKTS), + per_cpu_sum(netstats, RX_BYTES), + ); + println!("NET_STATS_END"); +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let opt = Opt::parse(); + + env_logger::builder() + .filter_level(log::LevelFilter::Warn) + .format_timestamp(None) + .init(); + + // Rust v0 mangled fragments for ax_net::router::DeviceHandle::count_tx / + // count_rx. These are concrete methods (no generics), so each resolves + // to exactly one symbol. #[inline(never)] on the functions guarantees + // they survive release-mode inlining. + let syms_tx = resolve_symbols(&["6ax_net", "6router", "12DeviceHandle", "8count_tx"])?; + let syms_rx = resolve_symbols(&["6ax_net", "6router", "12DeviceHandle", "8count_rx"])?; + + // Guard against silent double-counting if a future refactor causes + // the symbol to match multiple kallsyms entries. + anyhow::ensure!( + syms_tx.len() == 1, + "expected exactly 1 TX symbol, got {}: {:?}", + syms_tx.len(), + syms_tx + ); + anyhow::ensure!( + syms_rx.len() == 1, + "expected exactly 1 RX symbol, got {}: {:?}", + syms_rx.len(), + syms_rx + ); + + warn!("resolved tx={}, rx={}", syms_tx.len(), syms_rx.len()); + + let rlim = libc::rlimit { + rlim_cur: libc::RLIM_INFINITY, + rlim_max: libc::RLIM_INFINITY, + }; + let ret = unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &rlim) }; + if ret != 0 { + warn!("setrlimit(RLIMIT_MEMLOCK) failed: {ret}"); + } + + let mut ebpf = aya::Ebpf::load(aya::include_bytes_aligned!(concat!( + env!("OUT_DIR"), + "/net_stats" + )))?; + + if let Err(e) = aya_log::EbpfLogger::init(&mut ebpf) { + warn!("failed to initialize eBPF logger: {e}"); + } + + // Each probe now matches exactly one symbol, so the attach loop is + // a single iteration. The helper macro encapsulates the load+attach + // pattern for readability. + macro_rules! attach_one { + ($ebpf:expr, $prog:literal, $sym:expr) => {{ + let p: &mut KProbe = $ebpf.program_mut($prog).unwrap().try_into()?; + p.load()?; + p.attach($sym, 0)?; + }}; + } + + attach_one!(ebpf, "count_tx", &syms_tx[0]); + attach_one!(ebpf, "count_rx", &syms_rx[0]); + + let netstats: NetStatsMap<'_> = PerCpuArray::try_from(ebpf.map("NETSTATS").unwrap())?; + + if opt.once { + print_stats(&netstats); + return Ok(()); + } + + if opt.test { + // Self-contained loopback test. + // + // NOTE: This only exercises the loopback fast path + // (dispatch_unicast_packet, send_on_device). The device-worker + // paths (device_rx_worker, device_tx_worker) and deferred ARP + // counting are exercised by the router unit tests + // (l2_counter_tests in router.rs) but not by this e2e test. + let listener = TcpListener::bind("127.0.0.1:0")?; + let addr = listener.local_addr()?; + let server = thread::spawn(move || { + if let Ok((mut sock, _)) = listener.accept() { + let mut buf = [0u8; 1024]; + if let Ok(n) = sock.read(&mut buf) { + let _ = sock.write_all(&buf[..n]); + } + } + }); + + match TcpStream::connect(addr) { + Ok(mut stream) => { + let payload = b"net_stats-probe-traffic-payload\n"; + let _ = stream.write_all(payload); + let mut buf = [0u8; 1024]; + let _ = stream.read(&mut buf); + } + Err(e) => warn!("test connect failed: {e}"), + } + let _ = server.join(); + + let udp_server = UdpSocket::bind("127.0.0.1:0")?; + let udp_addr = udp_server.local_addr()?; + let udp_peer = UdpSocket::bind("127.0.0.1:0")?; + let udp_payload = b"net_stats-udp-payload\n"; + let _ = udp_peer.send_to(udp_payload, udp_addr); + let mut udp_buf = [0u8; 1024]; + if let Ok((n, from)) = udp_server.recv_from(&mut udp_buf) { + let _ = udp_server.send_to(&udp_buf[..n], from); + } + let _ = udp_peer.recv_from(&mut udp_buf); + + time::sleep(time::Duration::from_millis(300)).await; + print_stats(&netstats); + + // Validate all four counters: the phy layer sees every frame + // regardless of protocol, so loopback TCP + UDP must produce + // non-zero tx_pkts, tx_bytes, rx_pkts, rx_bytes. + let tx_pkts = per_cpu_sum(&netstats, TX_PKTS); + let tx_bytes = per_cpu_sum(&netstats, TX_BYTES); + let rx_pkts = per_cpu_sum(&netstats, RX_PKTS); + let rx_bytes = per_cpu_sum(&netstats, RX_BYTES); + + if tx_pkts == 0 || tx_bytes == 0 || rx_pkts == 0 || rx_bytes == 0 { + anyhow::bail!( + "TEST FAILED: counter(s) are zero despite loopback traffic \ + (tx_pkts={}, tx_bytes={}, rx_pkts={}, rx_bytes={})", + tx_pkts, + tx_bytes, + rx_pkts, + rx_bytes, + ); + } + + println!("TEST PASSED: all counters non-zero"); + return Ok(()); + } + + let mut interval = time::interval(time::Duration::from_secs(opt.interval)); + interval.tick().await; // skip immediate first tick + tokio::select! { + _ = async { loop { interval.tick().await; print_stats(&netstats); } } => {} + _ = signal::ctrl_c() => {} + } + print_stats(&netstats); + Ok(()) +} diff --git a/apps/starry/ebpf/net_stats/prebuild.sh b/apps/starry/ebpf/net_stats/prebuild.sh new file mode 100644 index 0000000000..488adad440 --- /dev/null +++ b/apps/starry/ebpf/net_stats/prebuild.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Build net_stats (aya loader + embedded eBPF bytecode) as a static musl binary +# and install it into the StarryOS rootfs overlay. +set -euo pipefail + +app_dir="${STARRY_APP_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" +overlay_dir="${STARRY_OVERLAY_DIR:?STARRY_OVERLAY_DIR is required}" +arch="${STARRY_ARCH:-x86_64}" + +case "$arch" in + x86_64) musl_target="x86_64-unknown-linux-musl"; cross_prefix="x86_64-linux-musl" ;; + aarch64) musl_target="aarch64-unknown-linux-musl"; cross_prefix="aarch64-linux-musl" ;; + riscv64) musl_target="riscv64gc-unknown-linux-musl"; cross_prefix="riscv64-linux-musl" ;; + loongarch64) musl_target="loongarch64-unknown-linux-musl"; cross_prefix="loongarch64-linux-musl" ;; + *) echo "net_stats prebuild: unsupported arch '$arch'" >&2; exit 1 ;; +esac + +cross_bin="/opt/${cross_prefix}-cross/bin" +[[ -d "$cross_bin" ]] && export PATH="$cross_bin:$PATH" +cc_bin="${cross_prefix}-gcc" + +install_loongarch_loader_link() { + local rootfs="${STARRY_ROOTFS:-}" + [[ -n "$rootfs" && -f "$rootfs" ]] || { + echo "net_stats prebuild: STARRY_ROOTFS required for loongarch64 symlink" >&2; exit 1 + } + debugfs -w "$rootfs" -R "mkdir /lib64" 2>/dev/null || true + debugfs -w "$rootfs" -R "rm /lib64/ld-musl-loongarch-lp64d.so.1" 2>/dev/null || true + debugfs -w "$rootfs" -R "symlink /lib64/ld-musl-loongarch-lp64d.so.1 /lib/ld-musl-loongarch64.so.1" +} + +echo "net_stats prebuild: building for $musl_target (CC=$cc_bin)" +( + cd "$app_dir" + CC="$cc_bin" cargo build --release --target "$musl_target" \ + --config "target.${musl_target}.linker=\"${cc_bin}\"" +) + +bin="$app_dir/target/$musl_target/release/net_stats" +[[ -x "$bin" ]] || { echo "net_stats prebuild: build did not produce $bin" >&2; exit 1; } + +install -Dm0755 "$bin" "$overlay_dir/usr/bin/net_stats" +echo "net_stats prebuild: installed -> /usr/bin/net_stats" + +[[ "$arch" == "loongarch64" ]] && install_loongarch_loader_link || true diff --git a/apps/starry/ebpf/net_stats/qemu-aarch64.toml b/apps/starry/ebpf/net_stats/qemu-aarch64.toml new file mode 100644 index 0000000000..57d137a1e2 --- /dev/null +++ b/apps/starry/ebpf/net_stats/qemu-aarch64.toml @@ -0,0 +1,22 @@ +args = [ + "-nographic", + "-m", + "512M", + "-cpu", + "cortex-a53", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net_stats --test" +success_regex = ["TEST PASSED"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'TEST FAILED'] +timeout = 120 diff --git a/apps/starry/ebpf/net_stats/qemu-loongarch64.toml b/apps/starry/ebpf/net_stats/qemu-loongarch64.toml new file mode 100644 index 0000000000..032efdf4cd --- /dev/null +++ b/apps/starry/ebpf/net_stats/qemu-loongarch64.toml @@ -0,0 +1,24 @@ +args = [ + "-machine", + "virt", + "-cpu", + "la464", + "-nographic", + "-m", + "512M", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-loongarch64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net_stats --test" +success_regex = ["TEST PASSED"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'TEST FAILED'] +timeout = 120 diff --git a/apps/starry/ebpf/net_stats/qemu-riscv64.toml b/apps/starry/ebpf/net_stats/qemu-riscv64.toml new file mode 100644 index 0000000000..bfb8b0c65b --- /dev/null +++ b/apps/starry/ebpf/net_stats/qemu-riscv64.toml @@ -0,0 +1,22 @@ +args = [ + "-nographic", + "-m", + "512M", + "-cpu", + "rv64", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-riscv64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net_stats --test" +success_regex = ["TEST PASSED"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'TEST FAILED'] +timeout = 120 diff --git a/apps/starry/ebpf/net_stats/qemu-x86_64.toml b/apps/starry/ebpf/net_stats/qemu-x86_64.toml new file mode 100644 index 0000000000..930005076b --- /dev/null +++ b/apps/starry/ebpf/net_stats/qemu-x86_64.toml @@ -0,0 +1,26 @@ +args = [ + "-nographic", + "-m", + "512M", + "-smp", + "2", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", +] +# UEFI boot is required for x86_64 PVH entry on QEMU ≥ 6.0 (the kernel +# binary is loaded as a PVH-compatible ELF via the firmware hand-off, +# avoiding the "Error loading uncompressed kernel without PVH ELF Note" +# error seen with direct -kernel boot). +uefi = true +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net_stats --test" +success_regex = ["TEST PASSED"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'TEST FAILED'] +timeout = 120 diff --git a/apps/starry/net-bench/.gitignore b/apps/starry/net-bench/.gitignore new file mode 100644 index 0000000000..e7488fa866 --- /dev/null +++ b/apps/starry/net-bench/.gitignore @@ -0,0 +1,16 @@ +results/* +!results/README.md +!results/baseline-*.txt +.bench-state.json +__pycache__/ +*.pyc + +# Linux baseline generated artifacts (rootfs/kernel are built or fetched +# on demand by run-linux-baseline.sh; never commit generated binaries). +linux-baseline/initramfs.cpio.gz +linux-baseline/rootfs-build/ +linux-baseline/vmlinuz +linux-baseline/*.img +linux-baseline/kernel-pkg/ +linux-baseline/kernel-version +linux-baseline/initramfs-staging/ diff --git a/apps/starry/net-bench/README.md b/apps/starry/net-bench/README.md new file mode 100644 index 0000000000..c3b94f32cd --- /dev/null +++ b/apps/starry/net-bench/README.md @@ -0,0 +1,184 @@ +# StarryOS 网络性能测试套件(net-bench) + +面向 StarryOS 网络栈优化的性能测试套件,提供"入口 + 参数明确"的严肃测试流程, +覆盖吞吐 / PPS / 延迟 / CPU 效率 / 多核扩展等维度,并支持与 Linux 基线同拓扑对照。 + + +## 设计理念 + +- **唯一严肃入口 `run.sh`**:架构 / 场景 / 加速器都显式指定(或用明确默认值), + 保证可复现、可控、可对照。这是日常 KPI 与优化前后对比的标准入口。 +- **智能入口 `bin/bench`(实验性,非默认)**:自动检测本机环境(WSL/裸 Linux/ + KVM/vhost)并推断参数,便于开发期快速验证。它刻意保持实验性、不作为默认严肃 + 测试入口;内部最终委托给 `run.sh` 执行,保证行为一致。 +- **公共流程封装 `core/lib.sh`**:常量(网段/端口/拓扑)、配置矩阵解析、iperf3 + 服务端生命周期、前置检查、环境指纹、结果汇总集中封装,避免散落硬编码。 + +## 快速开始 + +### 严肃测试(推荐,参数明确) + +```bash +# 主力性能拓扑:TAP + vhost-net(需 sudo 预先配置网络) +sudo bash apps/starry/net-bench/bin/setup +bash apps/starry/net-bench/run.sh --scenario vhost --arch x86_64 + +# 多核扩展 +bash apps/starry/net-bench/run.sh --scenario vhost-smp4 --arch x86_64 --repeat 5 + +# 功能冒烟(SLIRP,无需 sudo / 无需网络配置) +bash apps/starry/net-bench/run.sh --scenario slirp --arch x86_64 + +# 清理 +sudo bash apps/starry/net-bench/bin/teardown +``` + +也可直接用 xtask 运行单个 QEMU 配置(run.sh 内部即调用它): + +```bash +cargo xtask starry app qemu \ + --test-case net-bench \ + --arch x86_64 \ + --qemu-config apps/starry/net-bench/qemu/vhost-x86_64-kvm.toml +``` + +> 注意:直接用 xtask 时,TAP/vhost 场景需自行用 `bin/setup` 配好网络并启动 +> iperf3 服务端;`run.sh` 会自动管理 iperf3 服务端生命周期。 + +### 智能入口(实验性,开发期便捷) + +```bash +# 自动检测环境、配置网络、跑测试、自动清理;内部委托 run.sh +bash apps/starry/net-bench/bin/bench vhost +bash apps/starry/net-bench/bin/bench-wsl # WSL2 快捷壳 +``` + +## 测试场景与配置矩阵 + +配置文件命名规范:`qemu/--.toml` + +| 场景 | 拓扑 | 用途 | +|------|------|------| +| slirp | QEMU usermode | 功能冒烟(性能数据无意义)| +| tap | TAP(无 vhost)| 功能 / 趋势兜底 | +| vhost | TAP + vhost-net | 主力性能测试 | +| vhost-smp4 | TAP + vhost-net, smp=4 | 多核扩展 | +| tap-smp4 | TAP, smp=4 | vhost 不可用时的多核兜底 | + +- arch:`aarch64` / `x86_64` +- accel:`kvm`(同架构 + KVM 可用)/ `tcg`(跨架构或无 KVM,仅功能验证) + +## 测试覆盖(guest 侧自动运行) + +| test-id | 说明 | +|---------|------| +| tcp1 | TCP 单流上行(guest → host)| +| tcp4 | TCP 4 并发流上行 | +| tcp1r | TCP 单流下行(host → guest)| +| udp1g | UDP 大包,目标 1 Gbit/s | +| udp64 | UDP 64B 小包 PPS | + +默认每个 test-id:1 次 warmup + 5 次测量(见 `core/net-bench-common.sh`)。 +延迟 / 短连接维度由 `core/net-bench-netperf.sh`(TCP_RR/UDP_RR/TCP_CRR)补充。 + +## 环境要求 + +TAP/vhost 场景的宿主配置由 `bin/setup` 一键接管(裸机可直接运行): + +```bash +sudo bash apps/starry/net-bench/bin/setup +``` + +`setup` 会自动:补齐依赖(`iperf3`/`iproute2`/`bridge-utils`/`jq`/`dnsmasq`, +apt 系统自动安装)、加载 `vhost_net` 模块、放开 `/dev/kvm` 与 `/dev/vhost-net` +权限、创建 `br0`/`tap0` 并配地址、启动 `dnsmasq` DHCP。iperf3 服务端由 +`run.sh` 自管,不在此启动。清理用 `sudo bash apps/starry/net-bench/bin/teardown`。 + +```bash +# 如需手动安装依赖(非 apt 系统): +sudo apt-get install -y iperf3 bridge-utils jq dnsmasq # Debian/Ubuntu +``` + +唯一无法由脚本完成的是 **WSL2 启用嵌套虚拟化**(属于 Windows 侧配置)。 +若 `/dev/kvm` 不存在,编辑 `%USERPROFILE%\.wslconfig`: + +```ini +[wsl2] +nestedVirtualization=true +``` + +然后 `wsl --shutdown` 重启。 + +### guest 如何获取 IP(重要) + +当前 StarryOS/ArceOS 在 `cargo xtask starry app qemu` 路径下的 guest 内核**只支持 +DHCP** 获取地址——没有任何 crate 读取 `AX_IP`/`AX_GW`/`AX_PREFIX_LEN` +(`axruntime` 的 `parse_network_config` 恒为 `NetworkConfig::default()`)。因此: + +- **SLIRP**:QEMU usermode 内建 DHCP 自动应答,guest 直接联网,无需额外配置。 +- **TAP / vhost**:host 侧 bridge 上必须有 DHCP 服务(`bin/setup` 会启动 + `dnsmasq`)。缺 DHCP 时 guest 会 `DHCP bootstrap timed out` 并取不到地址, + net-bench 报 `iperf3 server unreachable`。`run.sh` 的前置检查(`nb_check_tap`) + 会校验 `:67` 上有 DHCP 服务,缺失时直接报错并提示运行 `bin/setup`。 + +> iperf3 服务端由测试入口(`run.sh` / `run-with-perf.sh`)自管生命周期, +> `bin/setup` 不再启动 iperf3,避免与入口争用端口 5201。 + +## 结果分析 + +```bash +# 汇总 mean/stddev(NOISY 标记 >10% stddev) +python3 apps/starry/net-bench/core/summarize.py \ + apps/starry/net-bench/results/starry-*.txt + +# 与 Linux 基线对比 +bash apps/starry/net-bench/run-linux-baseline.sh aarch64 vhost --repeat 5 +python3 apps/starry/net-bench/core/compare-baseline.py \ + results/summary-aarch64-vhost-*.txt \ + results/summary-linux-baseline-aarch64-vhost-*.txt + +# CPU 效率(cycles/byte, IPC) +bash apps/starry/net-bench/run-with-perf.sh --arch aarch64 --scenario vhost +``` + +结果保存在 `results/`:`starry-*`(原始日志)、`summary-*`(汇总)、 +`fingerprint-*`(环境指纹)、`perf-stat-*`(perf 数据)。 + +## 目录结构 + +``` +apps/starry/net-bench/ +├── README.md # 本文档 +├── run.sh # 唯一严肃入口(显式参数) +├── run-with-perf.sh # perf stat 包裹变体 +├── run-linux-baseline.sh # Linux 同拓扑基线 +├── prebuild.sh # rootfs 预构建(apk + guest 脚本安装) +├── bin/ # 入口壳 +│ ├── bench # 智能入口(实验性,委托 run.sh) +│ ├── bench-wsl # WSL2 快捷壳 +│ ├── setup # 配置网络(委托 env/setup-common.sh) +│ └── teardown # 清理(委托 env/teardown.sh) +├── core/ # 核心逻辑 +│ ├── lib.sh # 主机侧公共流程封装 +│ ├── net-bench-common.sh # guest 侧基准核心 +│ ├── net-bench.sh # SLIRP guest 入口 +│ ├── net-bench-tap.sh # TAP/vhost guest 入口 +│ ├── net-bench-netperf.sh # netperf 延迟测试 +│ ├── summarize.py # 结果汇总 +│ └── compare-baseline.py # 基线对比 +├── env/ # 环境管理 +│ ├── detect-env.sh # 环境自动检测(JSON/human) +│ ├── setup-common.sh # 通用网络配置(br0/tap0/dnsmasq DHCP,状态化) +│ └── teardown.sh # 状态化回滚清理 +├── qemu/ # QEMU 配置矩阵 --.toml +├── build-*.toml # 构建配置(启用 virtio-net/virtio-blk) +├── docs/ # 详细文档 +└── results/ # 测试结果 +``` + +## 详细文档 + +- [快速参考](docs/QUICK_START.md) +- [架构设计](docs/STRUCTURE.md) +- [多队列问题](docs/MULTIQUEUE_ISSUE.md) +- [待办事项](docs/TODO.md) diff --git a/apps/starry/net-bench/bin/bench b/apps/starry/net-bench/bin/bench new file mode 100755 index 0000000000..fd10203c09 --- /dev/null +++ b/apps/starry/net-bench/bin/bench @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +# bin/bench - net-bench 智能入口(实验性,非默认严肃测试入口) +# +# 重要:严肃性能测试请使用"入口 + 参数明确"的 run.sh: +# bash apps/starry/net-bench/run.sh --scenario vhost --arch x86_64 +# +# 本脚本是开发期便捷壳:自动检测环境(WSL/裸 Linux/架构/KVM/vhost), +# 推断一组合理参数,再委托给 run.sh 执行实际测试。自动检测能力刻意保持 +# 实验性、不作为默认入口——它便于本机快速验证,但不保证可复现的严肃结论。 +# +# 用法: +# bench [scenario] [options] +# +# scenario: +# vhost TAP+vhost-net (默认,主力测试) +# vhost-smp4 TAP+vhost-net SMP4 +# tap TAP (无 vhost) +# slirp SLIRP (仅功能冒烟) +# +# options: +# --repeat N 重复测试 N 次 +# --arch ARCH 指定架构 (x86_64/aarch64) +# --no-cleanup 测试后不自动清理 +# --setup-only 仅配置环境,不运行测试 +# --skip-setup 跳过环境配置,直接测试 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BENCH_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +# shellcheck source=core/lib.sh +. "$BENCH_ROOT/core/lib.sh" +ENV_DIR="$NB_ENV_DIR" + +SCENARIO="vhost" +REPEAT=1 +ARCH="" +AUTO_CLEANUP=true +SETUP_ONLY=false +SKIP_SETUP=false + +# 解析选项 +while [[ $# -gt 0 ]]; do + case "$1" in + --repeat) REPEAT="$2"; shift 2 ;; + --arch) ARCH="$2"; shift 2 ;; + --no-cleanup) AUTO_CLEANUP=false; shift ;; + --setup-only) SETUP_ONLY=true; shift ;; + --skip-setup) SKIP_SETUP=true; shift ;; + -h|--help) + cat <" + + detect_environment + + if [[ "$SCENARIO" == "slirp" ]]; then + nb_info "slirp 模式不需要网络配置" + elif [[ "$SKIP_SETUP" == "true" ]]; then + nb_info "跳过环境配置(--skip-setup)" + else + setup_environment + fi + + if [[ "$SETUP_ONLY" == "true" ]]; then + nb_info "环境配置完成(--setup-only)" + nb_info "运行测试: bash $BENCH_ROOT/run.sh --scenario $SCENARIO --arch $ARCH --accel $ACCEL" + exit 0 + fi + + if [[ "$AUTO_CLEANUP" == "true" ]]; then + trap cleanup EXIT INT TERM + fi + + # 委托给唯一严肃入口 run.sh 执行实际测试(单一事实来源)。 + # 网络配置已由本脚本完成,故 run.sh 自身的前置检查只做校验。 + nb_section "委托 run.sh 执行测试" + bash "$BENCH_ROOT/run.sh" --scenario "$SCENARIO" --arch "$ARCH" --accel "$ACCEL" --repeat "$REPEAT" + + nb_info "=== 测试流程完成 ===" +} + +main diff --git a/apps/starry/net-bench/bin/bench-wsl b/apps/starry/net-bench/bin/bench-wsl new file mode 100755 index 0000000000..346284f7a3 --- /dev/null +++ b/apps/starry/net-bench/bin/bench-wsl @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# bin/bench-wsl - WSL2 环境的快捷测试入口 +# +# WSL2 推荐配置: +# - x86_64: 使用 KVM 加速(推荐) +# - aarch64: 使用 TCG 模拟(跨架构测试) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# 检查用户是否指定了 --arch +has_arch=false +for arg in "$@"; do + if [[ "$arg" == "--arch" ]]; then + has_arch=true + break + fi +done + +if [[ "$has_arch" == "true" ]]; then + # 用户指定了架构,直接传递 + exec "$SCRIPT_DIR/bench" vhost "$@" +else + # 自动选择架构(WSL2 上推荐 x86_64 + KVM) + echo "=== WSL2 快捷测试(自动选择架构)===" + echo "" + exec "$SCRIPT_DIR/bench" vhost "$@" +fi diff --git a/apps/starry/net-bench/bin/setup b/apps/starry/net-bench/bin/setup new file mode 100755 index 0000000000..1da888f2c1 --- /dev/null +++ b/apps/starry/net-bench/bin/setup @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# bin/setup - 配置 net-bench 主机测试环境(br0/tap0/dnsmasq DHCP、设备权限)。 +# +# 直连 env/setup-common.sh,不经实验性自动检测壳 bin/bench,因此在裸机上也能 +# 一步到位:setup-common.sh 会自动补齐依赖(iperf3/ip/brctl/jq/dnsmasq)、 +# 加载 vhost_net 模块、放开 /dev/kvm 与 /dev/vhost-net 权限、建桥接与 TAP、 +# 启动 DHCP。iperf3 服务端由测试入口(run.sh)自管,这里不启动。 +# +# 用法: +# sudo bash apps/starry/net-bench/bin/setup +# +# 清理: +# sudo bash apps/starry/net-bench/bin/teardown + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BENCH_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +if [[ $EUID -ne 0 ]]; then + echo "需要 root 权限,请用: sudo bash $0" >&2 + exit 1 +fi + +exec bash "$BENCH_ROOT/env/setup-common.sh" "$@" diff --git a/apps/starry/net-bench/bin/teardown b/apps/starry/net-bench/bin/teardown new file mode 100755 index 0000000000..4ca06c0cd0 --- /dev/null +++ b/apps/starry/net-bench/bin/teardown @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# bin/teardown - 清理 net-bench 测试环境 +# +# 用法: +# teardown 清理环境 +# teardown status 显示当前状态 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BENCH_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +ENV_DIR="$BENCH_ROOT/env" + +exec sudo bash "$ENV_DIR/teardown.sh" "$@" diff --git a/apps/starry/net-bench/build-aarch64-unknown-none-softfloat.toml b/apps/starry/net-bench/build-aarch64-unknown-none-softfloat.toml new file mode 100644 index 0000000000..0ebbff8fc1 --- /dev/null +++ b/apps/starry/net-bench/build-aarch64-unknown-none-softfloat.toml @@ -0,0 +1,7 @@ +target = "aarch64-unknown-none-softfloat" +log = "Warn" +max_cpu_num = 4 +features = [ + "ax-driver/virtio-blk", + "ax-driver/virtio-net", +] diff --git a/apps/starry/net-bench/build-x86_64-unknown-none.toml b/apps/starry/net-bench/build-x86_64-unknown-none.toml new file mode 100644 index 0000000000..1996064d3b --- /dev/null +++ b/apps/starry/net-bench/build-x86_64-unknown-none.toml @@ -0,0 +1,7 @@ +target = "x86_64-unknown-none" +log = "Warn" +max_cpu_num = 4 +features = [ + "ax-driver/virtio-blk", + "ax-driver/virtio-net", +] diff --git a/apps/starry/net-bench/core/compare-baseline.py b/apps/starry/net-bench/core/compare-baseline.py new file mode 100755 index 0000000000..0c483b44d8 --- /dev/null +++ b/apps/starry/net-bench/core/compare-baseline.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +""" +apps/starry/net-bench/compare-baseline.py — 对比 Starry 与 Linux 基线性能 + +用法: + python3 compare-baseline.py + +输出三方对比报告: + - Starry 吞吐/PPS + - Linux 基线吞吐/PPS + - Starry 达到 Linux 的百分比 + +对齐 methodology §4.3 "呈现方式" 要求。 +""" + +import sys +import re +from typing import Dict, Optional, Tuple + + +def parse_summary(file_path: str) -> Dict[str, Tuple[float, float, str]]: + """ + Parse summarize.py output into {label: (mean, stddev, unit)}. + + Handles both legacy "±" and current "+/-" separators, and skips + non-matching header sections (/proc/net/dev, Protocol Overhead, + CPU Efficiency) that precede the per-test data. + """ + results = {} + + with open(file_path, 'r') as f: + content = f.read() + + # Match section headers and throughput lines. + # Section: "## TCP 1-stream (uplink) (test=tcp1)" + # Metric: " throughput : 93.2 +/- 2.4 Mbit/s (n=5, warmup=1)" + _SECTION_RE = re.compile(r"^##\s+(.+?)\s+\(test=(\S+)\)\s*$") + _METRIC_RE = re.compile( + r"^\s+throughput\s*:\s*([\d.]+)\s*(?:±|\+/-)\s*([\d.]+)\s*(\S+/s)" + ) + + current_test_id = None + for line in content.splitlines(): + sec = _SECTION_RE.match(line) + if sec: + current_test_id = sec.group(2) + label = sec.group(1).strip() + continue + if current_test_id is None: + continue + m = _METRIC_RE.match(line) + if m: + mean = float(m.group(1)) + stddev = float(m.group(2)) + unit = m.group(3) + + # Normalize to Mbit/s or pkt/s + if 'Gbit' in unit: + mean *= 1000 + stddev *= 1000 + unit = 'Mbit/s' + elif 'Kbit' in unit: + mean /= 1000 + stddev /= 1000 + unit = 'Mbit/s' + + results[current_test_id] = (mean, stddev, unit) + current_test_id = None # consume the section + + return results + + +def compute_percentage(starry_val: float, linux_val: float) -> float: + """计算 Starry 达到 Linux 的百分比""" + if linux_val == 0: + return 0.0 + return (starry_val / linux_val) * 100.0 + + +def print_comparison(starry_results: Dict, linux_results: Dict): + """打印三方对比表格""" + + print("=" * 100) + print("Starry vs Linux Baseline Performance Comparison") + print("=" * 100) + print() + + # Map test_id to display label (aligned with summarize.py TEST_LABELS). + test_labels = { + 'tcp1': 'TCP 1-stream (uplink)', + 'tcp4': 'TCP 4-stream (uplink)', + 'tcp1r': 'TCP 1-stream (reverse/downlink)', + 'udp1g': 'UDP 1G target (large packets)', + 'udp64': 'UDP 64B small-packet PPS', + } + + print(f"{'Test':<30} {'Starry':<25} {'Linux Baseline':<25} {'Starry/Linux':<15}") + print("-" * 100) + + for test_id, label in test_labels.items(): + starry_data = starry_results.get(test_id) + linux_data = linux_results.get(test_id) + + if starry_data and linux_data: + s_mean, s_std, s_unit = starry_data + l_mean, l_std, l_unit = linux_data + percentage = compute_percentage(s_mean, l_mean) + starry_str = f"{s_mean:.1f} +/- {s_std:.1f} {s_unit}" + linux_str = f"{l_mean:.1f} +/- {l_std:.1f} {l_unit}" + pct_str = f"{percentage:.1f}%" + print(f"{label:<30} {starry_str:<25} {linux_str:<25} {pct_str:<15}") + elif starry_data: + s_mean, s_std, s_unit = starry_data + starry_str = f"{s_mean:.1f} +/- {s_std:.1f} {s_unit}" + print(f"{label:<30} {starry_str:<25} {'N/A':<25} {'N/A':<15}") + elif linux_data: + l_mean, l_std, l_unit = linux_data + linux_str = f"{l_mean:.1f} +/- {l_std:.1f} {l_unit}" + print(f"{label:<30} {'N/A':<25} {linux_str:<25} {'N/A':<15}") + + print("-" * 100) + print() + + # 计算平均达成率 + percentages = [] + for test_id in test_labels: + starry_data = starry_results.get(test_id) + linux_data = linux_results.get(test_id) + if starry_data and linux_data: + pct = compute_percentage(starry_data[0], linux_data[0]) + percentages.append(pct) + + if percentages: + avg_pct = sum(percentages) / len(percentages) + print(f"Average Starry/Linux ratio: {avg_pct:.1f}%") + print() + + # 关键差距分析 + print("Key Gaps (methodology §6.2):") + print() + + for test_id, label in test_labels.items(): + starry_data = starry_results.get(test_id) + linux_data = linux_results.get(test_id) + + if starry_data and linux_data: + s_mean = starry_data[0] + l_mean = linux_data[0] + percentage = compute_percentage(s_mean, l_mean) + + if percentage < 50: + gap = l_mean - s_mean + print(f" ❌ {label}: Starry {percentage:.1f}% of Linux (gap: {gap:.1f} {starry_data[2]})") + elif percentage < 80: + gap = l_mean - s_mean + print(f" ⚠️ {label}: Starry {percentage:.1f}% of Linux (gap: {gap:.1f} {starry_data[2]})") + else: + print(f" ✅ {label}: Starry {percentage:.1f}% of Linux") + + print() + print("=" * 100) + + +def main(): + if len(sys.argv) != 3: + print("usage: python3 compare-baseline.py ", file=sys.stderr) + sys.exit(1) + + starry_file = sys.argv[1] + linux_file = sys.argv[2] + + try: + starry_results = parse_summary(starry_file) + linux_results = parse_summary(linux_file) + except FileNotFoundError as e: + print(f"error: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"error parsing summary files: {e}", file=sys.stderr) + sys.exit(1) + + if not starry_results: + print(f"error: no results found in {starry_file}", file=sys.stderr) + sys.exit(1) + + if not linux_results: + print(f"error: no results found in {linux_file}", file=sys.stderr) + sys.exit(1) + + print_comparison(starry_results, linux_results) + + +if __name__ == '__main__': + main() diff --git a/apps/starry/net-bench/core/lib.sh b/apps/starry/net-bench/core/lib.sh new file mode 100644 index 0000000000..453c4c8f6d --- /dev/null +++ b/apps/starry/net-bench/core/lib.sh @@ -0,0 +1,270 @@ +#!/usr/bin/env bash +# core/lib.sh — net-bench 主机侧公共流程封装(host-side shared library) +# +# 目标:把"配置常量、配置文件解析、iperf3 服务端生命周期、环境前置检查、 +# 环境指纹、结果汇总"等公用环节集中封装,使各入口(显式入口 run.sh、实验性 +# 智能入口 bin/bench、Linux 基线 run-linux-baseline.sh)路径清晰、行为一致。 +# +# 用法:调用方先 `source core/lib.sh`,再使用 nb_* 函数。本文件不自带 `set`, +# 由调用方控制 shell 选项;所有函数对未配置项给出明确错误并返回非零。 +# +# 设计原则: +# - 严肃测试入口"入口 + 参数明确",不依赖隐式自动检测。 +# - 拓扑/网段/端口集中为常量,避免散落硬编码(评估文档"硬编码分散"问题)。 +# - SLIRP 仅功能冒烟,性能拓扑用 TAP / vhost-net。 + +# ---- 公共常量(集中管理,消除散落硬编码) -------------------------------- + +# TAP / vhost 拓扑网段(host 端在 br0 上,guest 静态地址 + 网关指向 host)。 +NB_TAP_HOST_IP="${NB_TAP_HOST_IP:-192.168.100.1}" +NB_TAP_GUEST_IP="${NB_TAP_GUEST_IP:-192.168.100.2}" +NB_TAP_PREFIX_LEN="${NB_TAP_PREFIX_LEN:-24}" +NB_TAP_IFACE="${NB_TAP_IFACE:-tap0}" +NB_BRIDGE="${NB_BRIDGE:-br0}" + +# SLIRP(QEMU usermode)拓扑下,guest 通过网关 10.0.2.2 访问 host 服务。 +NB_SLIRP_HOST_IP="${NB_SLIRP_HOST_IP:-10.0.2.2}" + +# iperf3 服务端端口(guest 侧 net-bench-common.sh 默认同值)。 +NB_IPERF3_PORT="${NB_IPERF3_PORT:-5201}" + +# ---- 路径推导 ------------------------------------------------------------ + +# net-bench 根目录(core/ 的上一级)。 +NB_BENCH_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +NB_WORKSPACE="$(cd "$NB_BENCH_ROOT/../../.." && pwd)" +NB_QEMU_DIR="$NB_BENCH_ROOT/qemu" +NB_CORE_DIR="$NB_BENCH_ROOT/core" +NB_ENV_DIR="$NB_BENCH_ROOT/env" +NB_RESULTS_DIR="$NB_BENCH_ROOT/results" +NB_SUMMARIZER="$NB_CORE_DIR/summarize.py" + +# ---- 日志输出 ------------------------------------------------------------ + +if [[ -t 2 ]]; then + NB_C_GREEN=$'\033[0;32m'; NB_C_YELLOW=$'\033[1;33m' + NB_C_RED=$'\033[0;31m'; NB_C_BLUE=$'\033[0;34m'; NB_C_NC=$'\033[0m' +else + NB_C_GREEN=""; NB_C_YELLOW=""; NB_C_RED=""; NB_C_BLUE=""; NB_C_NC="" +fi + +nb_info() { echo "${NB_C_GREEN}[INFO]${NB_C_NC} $*"; } +nb_warn() { echo "${NB_C_YELLOW}[WARN]${NB_C_NC} $*" >&2; } +nb_error() { echo "${NB_C_RED}[ERROR]${NB_C_NC} $*" >&2; } +nb_section() { echo "${NB_C_BLUE}===== $* =====${NB_C_NC}"; } +nb_die() { nb_error "$*"; exit 1; } + +# ---- 场景 / 架构 / 加速器 ------------------------------------------------- + +# 支持的场景与架构(严肃入口需"参数明确",集中校验)。 +NB_SCENARIOS="slirp tap vhost vhost-smp4 tap-smp4" +NB_ARCHES="aarch64 x86_64" + +# nb_validate_scenario +nb_validate_scenario() { + case " $NB_SCENARIOS " in + *" $1 "*) return 0 ;; + *) nb_error "未知场景: $1(有效: $NB_SCENARIOS)"; return 1 ;; + esac +} + +# nb_validate_arch +nb_validate_arch() { + case " $NB_ARCHES " in + *" $1 "*) return 0 ;; + *) nb_error "未知架构: $1(有效: $NB_ARCHES)"; return 1 ;; + esac +} + +# nb_host_arch — 返回 uname -m 归一化后的 host 架构。 +nb_host_arch() { + case "$(uname -m)" in + x86_64|amd64) echo "x86_64" ;; + aarch64|arm64) echo "aarch64" ;; + *) uname -m ;; + esac +} + +# nb_default_accel — 同架构且 /dev/kvm 可用时用 kvm,否则 tcg。 +nb_default_accel() { + local arch="$1" + if [[ "$arch" == "$(nb_host_arch)" && -e /dev/kvm && -r /dev/kvm && -w /dev/kvm ]]; then + echo "kvm" + else + echo "tcg" + fi +} + +# nb_qemu_config — 返回统一矩阵中的配置文件路径。 +# vhost 与 tap 共享同一加速维度;smp4 变体追加 -smp4 后缀。 +nb_qemu_config() { + local scenario="$1" arch="$2" accel="$3" + echo "$NB_QEMU_DIR/${scenario}-${arch}-${accel}.toml" +} + +# nb_scenario_host_ip — 该场景下 guest 访问 host 的地址。 +nb_scenario_host_ip() { + case "$1" in + slirp) echo "$NB_SLIRP_HOST_IP" ;; + *) echo "$NB_TAP_HOST_IP" ;; + esac +} + +# nb_scenario_needs_tap — TAP/vhost 场景需要 host 侧 bridge+tap。 +nb_scenario_needs_tap() { + [[ "$1" != "slirp" ]] +} + +# nb_scenario_needs_vhost — vhost 场景需要 /dev/vhost-net。 +nb_scenario_needs_vhost() { + case "$1" in vhost|vhost-smp4) return 0 ;; *) return 1 ;; esac +} + +# nb_guest_env_vars — 打印该场景下需要传入 guest 的环境变量 +# (每行一个 KEY=VALUE)。 +# +# 重要:当前 StarryOS/ArceOS 在 `cargo xtask starry app qemu` 路径下的 guest +# 内核只支持 DHCP 获取地址——没有任何 crate 读取 AX_IP/AX_GW/AX_PREFIX_LEN +# (axruntime 的 parse_network_config 恒为 NetworkConfig::default(), +# 见 os/arceos/modules/axruntime/src/devices.rs)。因此本函数不再注入这些 +# 无效变量。guest 地址由网络侧的 DHCP 服务决定: +# - SLIRP:QEMU usermode 内建 DHCP 自动应答; +# - TAP/vhost:需 host 在 bridge 上运行 DHCP 服务(见 nb_check_tap)。 +# 待 guest 支持静态 IP(读 AX_IP 等)后,可在此恢复注入。 +nb_guest_env_vars() { + : # 目前无需注入任何 guest 环境变量(guest 走 DHCP)。 +} + +# ---- 前置检查 ------------------------------------------------------------ + +nb_require_cmd() { + command -v "$1" >/dev/null 2>&1 || nb_die "缺少命令: $1${2:+($2)}" +} + +nb_check_kvm() { + [[ -e /dev/kvm ]] || nb_die "/dev/kvm 不存在(KVM 加速所需)。WSL2 需启用嵌套虚拟化。" +} + +nb_check_vhost() { + [[ -e /dev/vhost-net ]] || nb_die "/dev/vhost-net 不存在(vhost 场景所需)。尝试: sudo modprobe vhost_net" +} + +# nb_check_tap — 确认 TAP/bridge 已配置、host 侧持有目标地址,且有 DHCP 服务 +# 应答(guest 走 DHCP,缺 DHCP 服务时 guest 会 "DHCP bootstrap timed out")。 +nb_check_tap() { + nb_require_cmd ip + ip link show "$NB_TAP_IFACE" >/dev/null 2>&1 || nb_die \ + "TAP 接口 $NB_TAP_IFACE 不存在。先配置: sudo bash $NB_BENCH_ROOT/bin/setup" + if ip link show "$NB_BRIDGE" >/dev/null 2>&1; then + ip -4 addr show dev "$NB_BRIDGE" | grep -q "$NB_TAP_HOST_IP/$NB_TAP_PREFIX_LEN" || nb_die \ + "Bridge $NB_BRIDGE 缺少地址 $NB_TAP_HOST_IP/$NB_TAP_PREFIX_LEN。重新配置: sudo bash $NB_BENCH_ROOT/bin/setup" + else + ip -4 addr show dev "$NB_TAP_IFACE" | grep -q "$NB_TAP_HOST_IP/$NB_TAP_PREFIX_LEN" || nb_die \ + "TAP 接口 $NB_TAP_IFACE 缺少地址 $NB_TAP_HOST_IP/$NB_TAP_PREFIX_LEN。配置: sudo bash $NB_BENCH_ROOT/bin/setup" + fi + nb_check_dhcp_server +} + +# nb_check_dhcp_server — 确认 bridge/tap 上有 DHCP 服务监听 :67。guest 内核只 +# 支持 DHCP(见 nb_guest_env_vars 说明),缺此服务 guest 取不到 IP 而失败。 +nb_check_dhcp_server() { + command -v ss >/dev/null 2>&1 || return 0 # 无 ss 时跳过软校验 + ss -H -ulnp "sport = :67" 2>/dev/null | grep -q . || nb_die \ + "未检测到 DHCP 服务监听 :67。TAP/vhost 场景的 guest 走 DHCP,需在 $NB_BRIDGE 上启动 DHCP 服务。配置: sudo bash $NB_BENCH_ROOT/bin/setup(含 dnsmasq)" +} + +# nb_check_scenario_prereq — 按场景/加速器校验前置条件。 +nb_check_scenario_prereq() { + local scenario="$1" accel="$2" + [[ "$accel" == "kvm" ]] && nb_check_kvm + if nb_scenario_needs_vhost "$scenario"; then + nb_check_kvm + nb_check_vhost + fi + nb_scenario_needs_tap "$scenario" && nb_check_tap + return 0 +} + +# ---- iperf3 服务端生命周期 ------------------------------------------------ + +# 模块级变量:当前 iperf3 服务端 PID(nb_stop_iperf3 使用)。 +NB_IPERF3_PID="" + +nb_iperf3_port_busy() { + command -v ss >/dev/null 2>&1 || return 1 + ss -H -tln "sport = :$NB_IPERF3_PORT" 2>/dev/null | grep -q . +} + +# nb_start_iperf3 +# bind_addr 为空时监听 0.0.0.0(SLIRP 场景)。设置 NB_IPERF3_PID,并注册 +# 进程级 EXIT/INT/TERM 清理,确保即使调用方中途 die/被中断也不残留服务端。 +nb_start_iperf3() { + local bind_addr="$1" log_file="$2" + if nb_iperf3_port_busy; then + nb_error "TCP 端口 $NB_IPERF3_PORT 已被占用,先停止已有 iperf3 服务端" + command -v ss >/dev/null 2>&1 && ss -tlnp "sport = :$NB_IPERF3_PORT" >&2 || true + return 1 + fi + if [[ -n "$bind_addr" ]]; then + nb_info "启动 host iperf3 服务端 $bind_addr:$NB_IPERF3_PORT" + iperf3 -s -p "$NB_IPERF3_PORT" -B "$bind_addr" > "$log_file" 2>&1 & + else + nb_info "启动 host iperf3 服务端 0.0.0.0:$NB_IPERF3_PORT" + iperf3 -s -p "$NB_IPERF3_PORT" > "$log_file" 2>&1 & + fi + NB_IPERF3_PID=$! + # 进程级兜底清理:RETURN trap 只覆盖正常函数返回,die/中断需 EXIT 兜底。 + trap 'nb_stop_iperf3' EXIT INT TERM + sleep 1 + kill -0 "$NB_IPERF3_PID" 2>/dev/null || { nb_error "iperf3 服务端启动失败,见 $log_file"; return 1; } +} + +# nb_stop_iperf3 — 终止当前 iperf3 服务端(幂等)。 +nb_stop_iperf3() { + [[ -n "$NB_IPERF3_PID" ]] || return 0 + kill "$NB_IPERF3_PID" 2>/dev/null || true + NB_IPERF3_PID="" +} + +# ---- 环境指纹 ------------------------------------------------------------ + +# nb_write_fingerprint +# 记录可复现性所需的环境指纹(methodology §3.4 / plan §6.3)。 +nb_write_fingerprint() { + local file="$1" arch="$2" scenario="$3" accel="$4" repeat="$5" + { + echo "# net-bench environment fingerprint" + echo "timestamp : $(date +%Y%m%d-%H%M%S)" + echo "arch : $arch" + echo "scenario : $scenario" + echo "accel : $accel" + echo "repeat : $repeat" + echo "host_uname : $(uname -a)" + echo "host_nproc : $(nproc 2>/dev/null || echo '?')" + local qemu_bin; qemu_bin="$(command -v "qemu-system-$arch" 2>/dev/null || true)" + if [[ -n "$qemu_bin" ]]; then + echo "qemu : $("$qemu_bin" --version 2>/dev/null | head -1)" + echo "qemu_accel : $("$qemu_bin" -accel help 2>/dev/null | tail -n +2 | tr '\n' ' ')" + fi + echo "iperf3_host : $(iperf3 --version 2>/dev/null | head -1)" + echo "kvm : $([[ -e /dev/kvm ]] && echo present || echo absent)" + echo "vhost_net : $([[ -e /dev/vhost-net ]] && echo present || echo absent)" + echo "starry_commit: $(git -C "$NB_WORKSPACE" rev-parse --short HEAD 2>/dev/null || echo '?')" + } > "$file" + nb_info "环境指纹 -> $file" + cat "$file" +} + +# ---- 结果汇总 ------------------------------------------------------------ + +# nb_summarize [--perf ...] +# 调用 summarize.py 产出 mean/stddev。所有额外参数(含 --perf)透传给 summarize.py。 +nb_summarize() { + local summary_file="$1"; shift + if ! command -v python3 >/dev/null 2>&1; then + nb_warn "python3 未找到,跳过自动汇总" + return 0 + fi + nb_info "汇总结果 -> $summary_file" + python3 "$NB_SUMMARIZER" "$@" | tee "$summary_file" +} diff --git a/apps/starry/net-bench/core/net-bench-common.sh b/apps/starry/net-bench/core/net-bench-common.sh new file mode 100644 index 0000000000..efa5a531dd --- /dev/null +++ b/apps/starry/net-bench/core/net-bench-common.sh @@ -0,0 +1,98 @@ +#!/bin/sh +# StarryOS network benchmark core (guest side). +# +# Shared by net-bench.sh (SLIRP) and net-bench-tap.sh (TAP). The caller must +# export HOST_IP before exec-ing this script. Each measurement emits its raw +# iperf3 JSON wrapped in parseable markers so the host-side summarizer +# (summarize.py) can aggregate mean/stddev across repeated iterations without +# guessing block boundaries. +# +# Marker contract (consumed by summarize.py): +# NET_BENCH_BEGIN test= iter= warmup=<0|1> +# +# NET_BENCH_END test= iter= +# +# warmup=1 iterations are recorded but excluded from statistics (cache/route +# warmup per methodology §3.4). The whole run ends with NET_BENCH_PASSED. + +HOST_IP="${HOST_IP:-10.0.2.2}" +PORT="${NET_BENCH_PORT:-5201}" +# Per-iteration duration (seconds) and repetition count. Kept short so the full +# matrix fits inside the QEMU test timeout; stats come from ITERS repetitions. +DURATION="${NET_BENCH_DURATION:-5}" +ITERS="${NET_BENCH_ITERS:-5}" +WARMUP="${NET_BENCH_WARMUP:-1}" +RESULT_PREFIX="NET_BENCH" + +fail() { + echo "${RESULT_PREFIX}_FAILED: $*" + exit 1 +} + +# run_test +# Runs WARMUP warmup iterations followed by ITERS measured iterations, each +# wrapped in BEGIN/END markers. Warmup failures are tolerated; measured +# failures abort the whole benchmark. +# +# /proc/net/dev snapshots are emitted before and after each iperf3 call so +# summarize.py can compute per-interface L2 byte/packet deltas. +run_test() { + test_id="$1" + shift + total=$((WARMUP + ITERS)) + iter=0 + while [ "$iter" -lt "$total" ]; do + if [ "$iter" -lt "$WARMUP" ]; then + warm=1 + else + warm=0 + fi + echo "${RESULT_PREFIX}_BEGIN test=${test_id} iter=${iter} warmup=${warm}" + echo "NET_STATS_BEGIN warmup=${warm}" + cat /proc/net/dev + echo "NET_STATS_END" + if iperf3 -c "$HOST_IP" -p "$PORT" -t "$DURATION" -J "$@"; then + echo "${RESULT_PREFIX}_END test=${test_id} iter=${iter}" + else + echo "${RESULT_PREFIX}_END test=${test_id} iter=${iter}" + if [ "$warm" -eq 0 ]; then + fail "${test_id} iteration ${iter}" + fi + echo "${RESULT_PREFIX}_WARN: ${test_id} warmup iteration ${iter} failed (ignored)" + fi + echo "NET_STATS_BEGIN warmup=${warm}" + cat /proc/net/dev + echo "NET_STATS_END" + iter=$((iter + 1)) + done +} + +# Wait for the iperf3 server to be reachable (QEMU usermode ICMP is unreliable, +# so probe with a short TCP test; smoltcp sends ARP on the first TAP packet). +i=0 +while ! iperf3 -c "$HOST_IP" -p "$PORT" -t 1 >/dev/null 2>&1; do + i=$((i + 1)) + [ "$i" -gt 15 ] && fail "iperf3 server $HOST_IP:$PORT unreachable after 15 retries" + sleep 2 +done + +echo "=== StarryOS iperf3 network benchmark ===" +echo "target=$HOST_IP:$PORT duration=${DURATION}s iters=${ITERS} warmup=${WARMUP}" + +# Throughput, single TCP stream (uplink: guest -> host). +run_test tcp1 + +# Throughput, 4 parallel TCP streams. +run_test tcp4 -P 4 + +# Throughput, single TCP stream reverse (downlink: host -> guest). +run_test tcp1r -R + +# UDP throughput at 1 Gbit/s target (large packets). +run_test udp1g -u -b 1G + +# UDP small-packet PPS: 64-byte payloads bounded to 100 Mbit/s to avoid +# flooding the smoke setup while still exposing per-packet overhead. +run_test udp64 -u -l 64 -b 100M + +echo "${RESULT_PREFIX}_PASSED" diff --git a/apps/starry/net-bench/core/net-bench-netperf.sh b/apps/starry/net-bench/core/net-bench-netperf.sh new file mode 100755 index 0000000000..fdfdca129f --- /dev/null +++ b/apps/starry/net-bench/core/net-bench-netperf.sh @@ -0,0 +1,77 @@ +#!/bin/sh +# apps/starry/net-bench/net-bench-netperf.sh — guest 侧 netperf 延迟测试 +# +# 用途:补充 iperf3 无法覆盖的延迟测试维度(methodology §2.1) +# - TCP_RR: TCP 请求-响应延迟(暴露 poll/yield/调度尾延迟) +# - UDP_RR: UDP 请求-响应延迟 +# - TCP_CRR: TCP 短连接速率(压 listen_table/端口分配锁) +# +# 对齐 methodology §1 六维指标中的"延迟"和"连接速率"维度。 + +set -e + +HOST_IP="${HOST_IP:-10.0.2.2}" +WARMUP_ITER="${WARMUP_ITER:-1}" +MEASURE_ITER="${MEASURE_ITER:-5}" +TEST_DURATION="${TEST_DURATION:-10}" + +echo "=== net-bench netperf latency tests ===" +echo "HOST_IP=$HOST_IP, warmup=$WARMUP_ITER, measure=$MEASURE_ITER, duration=${TEST_DURATION}s" + +# 等待 netserver 就绪 +echo "=== waiting for netserver on $HOST_IP:12865 ===" +retry=0 +while [ $retry -lt 15 ]; do + if nc -z "$HOST_IP" 12865 2>/dev/null; then + echo "=== netserver ready ===" + break + fi + retry=$((retry + 1)) + sleep 1 +done + +if [ $retry -eq 15 ]; then + echo "NET_BENCH_FAILED: netserver unreachable after 15 retries" + exit 1 +fi + +# netperf 通用包装器 +# +# 标记格式与 net-bench-common.sh 对齐(test= iter= warmup=<0|1>), +# 但 netperf 输出为文本而非 iperf3 JSON,summarize.py 仅解析 iperf3 块, +# 因此 netperf 结果由本脚本自带的可读输出呈现,不进入 summarize.py 吞吐统计。 +run_netperf_test() { + local test_id="$1" + local netperf_test="$2" + shift 2 + local extra_args="$*" + + # warmup + echo "NET_BENCH_BEGIN test=$test_id iter=0 warmup=1" + netperf -H "$HOST_IP" -t "$netperf_test" -l "$TEST_DURATION" $extra_args || echo "warmup: netperf returned non-zero" + echo "NET_BENCH_END test=$test_id iter=0" + + # 测量迭代 + iter=1 + while [ $iter -le "$MEASURE_ITER" ]; do + echo "NET_BENCH_BEGIN test=$test_id iter=$iter warmup=0" + netperf -H "$HOST_IP" -t "$netperf_test" -l "$TEST_DURATION" $extra_args + echo "NET_BENCH_END test=$test_id iter=$iter" + iter=$((iter + 1)) + done +} + +# TCP_RR: TCP 请求-响应延迟(每秒事务数 + 延迟) +# 输出格式: +echo "=== TCP_RR: TCP request-response latency ===" +run_netperf_test tcp_rr TCP_RR -- -o THROUGHPUT,MEAN_LATENCY,P50_LATENCY,P90_LATENCY,P99_LATENCY + +# UDP_RR: UDP 请求-响应延迟 +echo "=== UDP_RR: UDP request-response latency ===" +run_netperf_test udp_rr UDP_RR -- -o THROUGHPUT,MEAN_LATENCY,P50_LATENCY,P90_LATENCY,P99_LATENCY + +# TCP_CRR: TCP 连接-请求-响应 (含 connect/accept/close 的短连接速率) +echo "=== TCP_CRR: TCP connection rate (short-lived connections) ===" +run_netperf_test tcp_crr TCP_CRR -- -o THROUGHPUT,MEAN_LATENCY + +echo "NET_BENCH_PASSED" diff --git a/apps/starry/net-bench/core/net-bench-tap.sh b/apps/starry/net-bench/core/net-bench-tap.sh new file mode 100644 index 0000000000..962df29898 --- /dev/null +++ b/apps/starry/net-bench/core/net-bench-tap.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# StarryOS network benchmark via TAP: guest client -> host server. +# Host must run: iperf3 -s -p 5201 -B 192.168.100.1 +# Static ARP is normally unnecessary; add it only when debugging ARP issues. + +HOST_IP="${HOST_IP:-192.168.100.1}" +export HOST_IP + +. /usr/bin/net-bench-common.sh diff --git a/apps/starry/net-bench/core/net-bench.sh b/apps/starry/net-bench/core/net-bench.sh new file mode 100644 index 0000000000..bc63ccb27b --- /dev/null +++ b/apps/starry/net-bench/core/net-bench.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# StarryOS network benchmark: iperf3 client against host server (via SLIRP). +# Host must be running: iperf3 -s -p 5201 (listening on 0.0.0.0). +# QEMU usermode (SLIRP) exposes the host gateway at 10.0.2.2. + +HOST_IP="${HOST_IP:-10.0.2.2}" +export HOST_IP + +. /usr/bin/net-bench-common.sh diff --git a/apps/starry/net-bench/core/summarize.py b/apps/starry/net-bench/core/summarize.py new file mode 100644 index 0000000000..a833904758 --- /dev/null +++ b/apps/starry/net-bench/core/summarize.py @@ -0,0 +1,599 @@ +#!/usr/bin/env python3 +"""Summarize StarryOS net-bench run logs into per-test mean/stddev metrics. + +The guest bench core (net-bench-common.sh) wraps each iperf3 -J measurement in +markers: + + NET_BENCH_BEGIN test= iter= warmup=<0|1> + + NET_BENCH_END test= iter= + +/proc/net/dev snapshots are embedded between NET_STATS_BEGIN/END markers +(with optional `warmup=<0|1>` on the BEGIN line so this parser can exclude +warmup traffic from protocol-overhead aggregation). + +This script extracts those blocks, parses the iperf3 JSON, drops warmup +iterations, and reports mean +/- stddev across the measured iterations for each +test id. It only depends on the Python standard library (no jq), so it runs in +the minimal WSL2 host environment. + +Per methodology §3.4: data points need >=5 iterations with mean+stddev, and a +relative stddev above ~10% is flagged as noisy and not trustworthy. + +Usage: + summarize.py RUN_LOG [RUN_LOG ...] + summarize.py --json RUN_LOG # machine-readable output +""" + +from __future__ import annotations + +import argparse +import json +import math +import re +import sys +from dataclasses import dataclass, field + +BEGIN_RE = re.compile( + r"^NET_BENCH_BEGIN\s+test=(\S+)\s+iter=(\d+)\s+warmup=([01])\s*$" +) +END_RE = re.compile(r"^NET_BENCH_END\s+test=(\S+)\s+iter=(\d+)\s*$") + +# NET_STATS_BEGIN may carry an optional warmup=<0|1> flag emitted by the +# guest-side shell scripts so the protocol-overhead section can exclude +# warmup iterations. +NETSTATS_BEGIN_RE = re.compile( + r"^NET_STATS_BEGIN(?:\s+warmup=([01]))?\s*$" +) +NETSTATS_END_RE = re.compile(r"^NET_STATS_END\s*$") +# Relative stddev (stddev/mean) above this fraction is flagged as noisy. +NOISE_THRESHOLD = 0.10 + +# Human-friendly descriptions, also fixes report ordering. +TEST_ORDER = ["tcp1", "tcp4", "tcp1r", "udp1g", "udp64"] +TEST_LABELS = { + "tcp1": "TCP 1-stream (uplink)", + "tcp4": "TCP 4-stream (uplink)", + "tcp1r": "TCP 1-stream (reverse/downlink)", + "udp1g": "UDP 1G target (large packets)", + "udp64": "UDP 64B small-packet PPS", +} + +# Test IDs whose traffic direction is reverse (downlink: host -> guest). +# Used for protocol-overhead direction attribution. +_REVERSE_TEST_IDS: frozenset[str] = frozenset({"tcp1r"}) + + +@dataclass +class Sample: + """One measured metric from a single iteration.""" + + mbps: float + pps: float | None = None + lost_percent: float | None = None + retransmits: int | None = None + app_bytes: int = 0 # application-layer bytes from iperf3 sum_received/sum + + +@dataclass +class TestStats: + test_id: str + measured: list[Sample] = field(default_factory=list) + warmup_count: int = 0 + parse_errors: int = 0 + + +def _extract_metric(doc: dict) -> Sample: + """Pull throughput / PPS / loss / retransmits out of one iperf3 JSON doc.""" + end = doc.get("end", {}) + # UDP results live under sum; TCP under sum_received (fallback sum_sent). + summary = end.get("sum_received") or end.get("sum") or end.get("sum_sent") + if not summary: + raise ValueError("no sum/sum_received/sum_sent block") + + mbps = float(summary.get("bits_per_second", 0.0)) / 1e6 + app_bytes = int(summary.get("bytes", 0)) + + pps = None + lost_percent = None + retransmits = None + + seconds = float(summary.get("seconds", 0.0)) or None + packets = summary.get("packets") + if packets is not None and seconds: + pps = float(packets) / seconds + if "lost_percent" in summary: + lost_percent = float(summary["lost_percent"]) + if "retransmits" in summary: + retransmits = int(summary["retransmits"]) + # TCP retransmits also appear under sum_sent. + elif "sum_sent" in end and "retransmits" in end["sum_sent"]: + retransmits = int(end["sum_sent"]["retransmits"]) + + return Sample( + mbps=mbps, pps=pps, lost_percent=lost_percent, retransmits=retransmits, + app_bytes=app_bytes, + ) + + +@dataclass +class NetDevSnapshot: + """One /proc/net/dev snapshot parsed from a NET_STATS_BEGIN/END block. + + Each key is an interface name (e.g. 'eth0', 'lo'). Values are dicts + with keys matching /proc/net/dev columns: rx_bytes, rx_packets, + rx_errors, rx_dropped, tx_bytes, tx_packets, tx_errors, tx_dropped. + """ + + interfaces: dict[str, dict[str, int]] = field(default_factory=dict) + warmup: bool = False # True if this snapshot belongs to a warmup iteration + + +# /proc/net/dev row parser: extracts interface name + 16 column values. +# Format: " iface: rx_bytes rx_pkts ... | tx_bytes tx_pkts ..." +_IFACE_RE = re.compile( + r"^\s*(\S+):" # interface name (stripped, colon-separated) + r"\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)" # RX 8 cols + r"\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)" # TX 8 cols +) + +# Column index -> /proc/net/dev field name mapping. +_IFACE_FIELDS = [ + "rx_bytes", "rx_packets", "rx_errors", "rx_dropped", + "rx_fifo", "rx_frame", "rx_compressed", "rx_multicast", + "tx_bytes", "tx_packets", "tx_errors", "tx_dropped", + "tx_fifo", "tx_colls", "tx_carrier", "tx_compressed", +] + +# /proc/net/dev header and blank-line pattern — skip before regex parse. +_NETDEV_HEADER_RE = re.compile(r"^\s*(Inter-\||face\s+\||$)") + + +def _parse_proc_net_dev_line(line: str) -> tuple[str, dict[str, int]]: + """Parse one /proc/net/dev data row into (iface_name, fields_dict).""" + m = _IFACE_RE.match(line) + if not m: + raise ValueError(f"cannot parse /proc/net/dev line: {line!r}") + name = m.group(1) + vals = [int(m.group(i + 2)) for i in range(16)] + fields = dict(zip(_IFACE_FIELDS, vals)) + return name, fields + + +def parse_netstats(text: str) -> list[NetDevSnapshot]: + """Extract NET_STATS_BEGIN/END blocks containing /proc/net/dev output.""" + snapshots: list[NetDevSnapshot] = [] + lines = text.splitlines() + i = 0 + skipped_lines = 0 + while i < len(lines): + m = NETSTATS_BEGIN_RE.match(lines[i]) + if not m: + i += 1 + continue + warmup = m.group(1) == "1" + i += 1 + snap = NetDevSnapshot(warmup=warmup) + while i < len(lines) and not NETSTATS_END_RE.match(lines[i]): + if _NETDEV_HEADER_RE.match(lines[i]): + i += 1 + continue + try: + name, fields = _parse_proc_net_dev_line(lines[i]) + snap.interfaces[name] = fields + except ValueError: + skipped_lines += 1 + i += 1 + if i < len(lines): + i += 1 # consume END + if snap.interfaces: + snapshots.append(snap) + if skipped_lines: + print( + f"warning: skipped {skipped_lines} unparseable line(s) " + f"inside NET_STATS_BEGIN/END blocks", + file=sys.stderr, + ) + return snapshots + + +def _fmt_bytes(n: int) -> str: + """Format a byte count in human-readable form.""" + if n >= 1 << 30: + return f"{n / (1 << 30):.2f} GB" + if n >= 1 << 20: + return f"{n / (1 << 20):.2f} MB" + if n >= 1 << 10: + return f"{n / (1 << 10):.2f} KB" + return f"{n} B" + + +def _netdev_delta( + before: NetDevSnapshot, after: NetDevSnapshot +) -> dict[str, dict[str, int]]: + """Compute per-interface counter deltas between two snapshots.""" + delta: dict[str, dict[str, int]] = {} + all_ifaces = set(before.interfaces.keys()) | set(after.interfaces.keys()) + for iface in all_ifaces: + b = before.interfaces.get(iface, {}) + a = after.interfaces.get(iface, {}) + d = {} + for key in _IFACE_FIELDS: + d[key] = a.get(key, 0) - b.get(key, 0) + if any(v != 0 for v in d.values()): + delta[iface] = d + return delta + + +def _sum_deltas( + deltas: list[dict[str, dict[str, int]]] +) -> dict[str, dict[str, int]]: + """Sum multiple per-interface deltas into a single accumulator.""" + total: dict[str, dict[str, int]] = {} + for d in deltas: + for iface, fields in d.items(): + if iface not in total: + total[iface] = {k: 0 for k in _IFACE_FIELDS} + for k, v in fields.items(): + total[iface][k] += v + return total + + +def _pair_deltas( + snapshots: list[NetDevSnapshot], + skip_warmup: bool = False, +) -> tuple[list[dict[str, dict[str, int]]], int, int]: + """Pair consecutive snapshots and return (deltas, tx_total, rx_total). + + When *skip_warmup* is True, snapshot pairs whose `before` snapshot + is tagged warmup are excluded so protocol-overhead analysis only + compares measured-iteration L2 traffic against application-layer bytes. + """ + deltas: list[dict[str, dict[str, int]]] = [] + tx_total = 0 + rx_total = 0 + for j in range(0, len(snapshots) - 1, 2): + before, after = snapshots[j], snapshots[j + 1] + if skip_warmup and before.warmup: + continue + d = _netdev_delta(before, after) + if d: + deltas.append(d) + for fields in d.values(): + tx_total += fields.get("tx_bytes", 0) + rx_total += fields.get("rx_bytes", 0) + return deltas, tx_total, rx_total + + +def render_netstats(snapshots: list[NetDevSnapshot]) -> str: + """Render /proc/net/dev L2 counter deltas across all measured iterations. + + Consecutive snapshots are paired (before, after) and accumulated. + Warmup-tagged snapshots are excluded. + """ + if len(snapshots) < 2: + return "" + deltas, _, _ = _pair_deltas(snapshots, skip_warmup=True) + if not deltas: + return "" + total = _sum_deltas(deltas) + out = ["## /proc/net/dev (kernel interface counters)"] + for iface in sorted(total.keys()): + f = total[iface] + tx_b = f.get("tx_bytes", 0) + rx_b = f.get("rx_bytes", 0) + tx_p = f.get("tx_packets", 0) + rx_p = f.get("rx_packets", 0) + tx_e = f.get("tx_errors", 0) + rx_e = f.get("rx_errors", 0) + tx_d = f.get("tx_dropped", 0) + rx_d = f.get("rx_dropped", 0) + parts = [f" [{iface}]"] + parts.append(f"tx={_fmt_bytes(tx_b)}/{tx_p}pkts") + parts.append(f"rx={_fmt_bytes(rx_b)}/{rx_p}pkts") + if tx_e or rx_e or tx_d or rx_d: + parts.append(f"tx_err={tx_e} tx_drop={tx_d} rx_err={rx_e} rx_drop={rx_d}") + out.append(" ".join(parts)) + out.append("") + return "\n".join(out) + + +# perf stat counter names accepted by parse_perf_stat. +_PERF_COUNTERS: frozenset[str] = frozenset({ + "cycles", "instructions", "cache-references", "cache-misses", +}) + +# perf stat output line pattern: optional commas in number, counter name. +_PERF_STAT_RE = re.compile(r"^\s*([0-9,]+)\s+(\S+)") + + +def parse_perf_stat(text: str) -> dict[str, int]: + """Extract counter values from `perf stat` output.""" + counters: dict[str, int] = {} + for line in text.splitlines(): + m = _PERF_STAT_RE.match(line) + if m: + raw_val = m.group(1).replace(",", "") + name = m.group(2) + if name in _PERF_COUNTERS: + counters[name] = int(raw_val) + return counters + + +def render_perf(counters: dict[str, int]) -> str: + """Render perf stat counters as a markdown section.""" + if not counters: + return "" + out = ["## CPU Efficiency (perf stat)"] + cycles = counters.get("cycles") + instructions = counters.get("instructions") + if cycles: + out.append(f" cycles : {cycles:,}") + if instructions: + out.append(f" instructions : {instructions:,}") + if cycles and instructions: + ipc = instructions / cycles if cycles > 0 else 0.0 + out.append(f" IPC : {ipc:.2f}") + crefs = counters.get("cache-references") + cmiss = counters.get("cache-misses") + if crefs: + out.append(f" cache-refs : {crefs:,}") + if cmiss: + out.append(f" cache-misses : {cmiss:,}") + if crefs and cmiss and crefs > 0: + miss_rate = cmiss / crefs * 100 + out.append(f" cache-miss-rate: {miss_rate:.1f}%") + out.append("") + return "\n".join(out) + + +def parse_log(text: str) -> dict[str, TestStats]: + """Parse a run log into {test_id: TestStats}.""" + lines = text.splitlines() + stats: dict[str, TestStats] = {} + i = 0 + n = len(lines) + while i < n: + m = BEGIN_RE.match(lines[i]) + if not m: + i += 1 + continue + test_id, _iter, warmup = m.group(1), int(m.group(2)), m.group(3) == "1" + # Collect JSON lines until the matching END marker. + # NET_STATS_BEGIN/END blocks (containing /proc/net/dev snapshots) + # that appear between BEGIN and the iperf3 JSON are skipped so + # json.loads() receives a clean payload. + body: list[str] = [] + in_netstats = False + i += 1 + while i < n and not END_RE.match(lines[i]): + # A stray BEGIN means the END was lost; bail out of this block. + if BEGIN_RE.match(lines[i]): + break + if NETSTATS_BEGIN_RE.match(lines[i]): + in_netstats = True + i += 1 + continue + if in_netstats and NETSTATS_END_RE.match(lines[i]): + in_netstats = False + i += 1 + continue + if not in_netstats: + body.append(lines[i]) + i += 1 + if i < n and END_RE.match(lines[i]): + i += 1 # consume END + + ts = stats.setdefault(test_id, TestStats(test_id=test_id)) + if warmup: + ts.warmup_count += 1 + continue + try: + doc = json.loads("\n".join(body)) + ts.measured.append(_extract_metric(doc)) + except (ValueError, json.JSONDecodeError): + ts.parse_errors += 1 + return stats + + +def _mean_std(values: list[float]) -> tuple[float, float]: + if not values: + return (0.0, 0.0) + mean = sum(values) / len(values) + if len(values) < 2: + return (mean, 0.0) + var = sum((v - mean) ** 2 for v in values) / (len(values) - 1) + return (mean, math.sqrt(var)) + + +def _fmt_mbps(mean: float, std: float) -> str: + rel = (std / mean) if mean else 0.0 + flag = " [NOISY >10%]" if rel > NOISE_THRESHOLD else "" + if mean >= 1000: + return f"{mean / 1000:.2f} +/- {std / 1000:.2f} Gbit/s{flag}" + return f"{mean:.2f} +/- {std:.2f} Mbit/s{flag}" + + +def render_text(stats: dict[str, TestStats], snapshots: list[NetDevSnapshot]) -> str: + out: list[str] = [] + ordered = [t for t in TEST_ORDER if t in stats] + ordered += [t for t in stats if t not in TEST_ORDER] + + # Compute aggregate L2 and application-layer byte totals for overhead + # comparison. Warmup-tagged snapshots are excluded so the L2 and + # application-layer totals are comparable. + _, l2_tx_total, l2_rx_total = _pair_deltas(snapshots, skip_warmup=True) + + app_tx_total = 0 + app_rx_total = 0 + for test_id in ordered: + ts = stats[test_id] + for s in ts.measured: + if test_id in _REVERSE_TEST_IDS: + app_rx_total += s.app_bytes + else: + app_tx_total += s.app_bytes + + # Show aggregate L2-vs-app overview. + if l2_tx_total or l2_rx_total: + out.append("## Protocol Overhead (L2 vs Application)") + if l2_tx_total > 0: + if app_tx_total > 0: + ratio = (l2_tx_total - app_tx_total) / app_tx_total * 100 + overhead = f"{ratio:.1f}%" + else: + overhead = "N/A" + out.append( + f" TX L2={_fmt_bytes(l2_tx_total)} " + f"app={_fmt_bytes(app_tx_total)} " + f"overhead={overhead}" + ) + if l2_rx_total > 0: + if app_rx_total > 0: + ratio = (l2_rx_total - app_rx_total) / app_rx_total * 100 + overhead = f"{ratio:.1f}%" + else: + overhead = "N/A" + out.append( + f" RX L2={_fmt_bytes(l2_rx_total)} " + f"app={_fmt_bytes(app_rx_total)} " + f"overhead={overhead}" + ) + out.append("") + for test_id in ordered: + ts = stats[test_id] + label = TEST_LABELS.get(test_id, test_id) + out.append(f"## {label} (test={test_id})") + if not ts.measured: + out.append( + f" no measured iterations " + f"(warmup={ts.warmup_count}, parse_errors={ts.parse_errors})" + ) + out.append("") + continue + mbps_mean, mbps_std = _mean_std([s.mbps for s in ts.measured]) + out.append( + f" throughput : {_fmt_mbps(mbps_mean, mbps_std)} " + f"(n={len(ts.measured)}, warmup={ts.warmup_count})" + ) + pps_vals = [s.pps for s in ts.measured if s.pps is not None] + if pps_vals: + pps_mean, pps_std = _mean_std(pps_vals) + out.append(f" pps : {pps_mean:.0f} +/- {pps_std:.0f} pkt/s") + loss_vals = [ + s.lost_percent for s in ts.measured if s.lost_percent is not None + ] + if loss_vals: + loss_mean, loss_std = _mean_std(loss_vals) + out.append(f" udp loss : {loss_mean:.2f} +/- {loss_std:.2f} %") + retr_vals = [ + s.retransmits for s in ts.measured if s.retransmits is not None + ] + if retr_vals: + retr_mean, retr_std = _mean_std([float(v) for v in retr_vals]) + out.append(f" retransmits: {retr_mean:.1f} +/- {retr_std:.1f}") + if ts.parse_errors: + out.append(f" parse_errors: {ts.parse_errors}") + out.append("") + return "\n".join(out) + + +def render_json(stats: dict[str, TestStats]) -> str: + payload: dict[str, dict] = {} + for test_id, ts in stats.items(): + mbps_mean, mbps_std = _mean_std([s.mbps for s in ts.measured]) + pps_vals = [s.pps for s in ts.measured if s.pps is not None] + loss_vals = [ + s.lost_percent for s in ts.measured if s.lost_percent is not None + ] + retr_vals = [ + float(s.retransmits) + for s in ts.measured + if s.retransmits is not None + ] + entry: dict = { + "label": TEST_LABELS.get(test_id, test_id), + "iterations": len(ts.measured), + "warmup": ts.warmup_count, + "parse_errors": ts.parse_errors, + "throughput_mbps_mean": mbps_mean, + "throughput_mbps_std": mbps_std, + } + if pps_vals: + pm, ps = _mean_std(pps_vals) + entry["pps_mean"], entry["pps_std"] = pm, ps + if loss_vals: + lm, ls = _mean_std(loss_vals) + entry["loss_percent_mean"], entry["loss_percent_std"] = lm, ls + if retr_vals: + rm, rs = _mean_std(retr_vals) + entry["retransmits_mean"], entry["retransmits_std"] = rm, rs + payload[test_id] = entry + return json.dumps(payload, indent=2) + + +def main(argv: list[str]) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("logs", nargs="+", help="run log file(s) to summarize") + parser.add_argument( + "--json", action="store_true", help="emit machine-readable JSON" + ) + parser.add_argument( + "--perf", + action="append", + default=[], + help="perf stat output file(s) for CPU efficiency section", + ) + args = parser.parse_args(argv) + + combined: dict[str, TestStats] = {} + all_netstats: list[NetDevSnapshot] = [] + for path in args.logs: + try: + with open(path, "r", errors="replace") as fh: + text = fh.read() + except OSError as exc: + print(f"error: cannot read {path}: {exc}", file=sys.stderr) + return 1 + for test_id, ts in parse_log(text).items(): + agg = combined.setdefault(test_id, TestStats(test_id=test_id)) + agg.measured.extend(ts.measured) + agg.warmup_count += ts.warmup_count + agg.parse_errors += ts.parse_errors + all_netstats.extend(parse_netstats(text)) + + if not combined: + print( + "warning: no NET_BENCH_BEGIN/END blocks found; " + "is this a current net-bench run log?", + file=sys.stderr, + ) + return 2 + + if args.json: + print(render_json(combined)) + else: + # Perf stat section (if available). + if args.perf: + all_perf: dict[str, int] = {} + for perf_path in args.perf: + try: + with open(perf_path, "r", errors="replace") as fh: + all_perf.update(parse_perf_stat(fh.read())) + except OSError as exc: + print( + f"warning: cannot read perf file {perf_path}: {exc}", + file=sys.stderr, + ) + perf_text = render_perf(all_perf) + if perf_text: + print(perf_text) + ns_text = render_netstats(all_netstats) + if ns_text: + print(ns_text) + print(render_text(combined, all_netstats)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/apps/starry/net-bench/docs/MULTIQUEUE_ISSUE.md b/apps/starry/net-bench/docs/MULTIQUEUE_ISSUE.md new file mode 100644 index 0000000000..859f676aed --- /dev/null +++ b/apps/starry/net-bench/docs/MULTIQUEUE_ISSUE.md @@ -0,0 +1,176 @@ +# 多队列问题处理说明 + +## 问题描述 + +在使用 vhost-net + TAP 网络时,遇到多队列配置错误: + +``` +qemu-system-x86_64: -netdev tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on,queues=4: +could not configure /dev/net/tun (tap0): Invalid argument +``` + +--- + +## 根本原因 + +1. **TAP 设备默认不支持多队列** + - 使用 `ip tuntap add` 创建的 TAP 默认是单队列 + - QEMU 参数中指定 `queues=4` 会失败 + +2. **多队列需要特殊创建** + ```bash + # 错误:默认单队列 + ip tuntap add mode tap tap0 + + # 正确:支持多队列 + ip tuntap add mode tap tap0 multi_queue + ``` + +--- + +## 解决方案 + +### 方案 1:移除多队列参数(已采用) + +**修改前**(旧配置): +```toml +"-device", "virtio-net-pci,netdev=net0,mac=...,mq=on,vectors=10,csum=on,gso=on,host_tso4=on,host_tso6=on,guest_tso4=on,guest_tso6=on", +"-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on,queues=4", +``` + +**修改后**(新配置): +```toml +"-device", "virtio-net-pci,netdev=net0,mac=...", +"-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on", +``` + +**优点**: +- 简单可靠,立即生效 +- 兼容所有环境 +- 无需修改 TAP 创建逻辑 + +**缺点**: +- 性能受限于单队列(但对于基线测试足够) + +--- + +### 方案 2:创建多队列 TAP(未采用) + +需要修改 `env/setup-common.sh` 中的 TAP 创建: + +```bash +# 创建支持多队列的 TAP +setup_tap() { + if tap_exists; then + warn "TAP 设备 $TAP_DEVICE 已存在,跳过创建" + return 0 + fi + + info "创建多队列 TAP 设备 $TAP_DEVICE (queues=4)" + + # 添加 multi_queue 参数 + ip tuntap add mode tap "$TAP_DEVICE" multi_queue || die "创建 TAP 设备失败" + ip link set "$TAP_DEVICE" up || die "启动 TAP 设备失败" + ip link set "$TAP_DEVICE" master "$BRIDGE" || die "挂载 TAP 到 bridge 失败" + + record_resource "tap" "$TAP_DEVICE" "master=$BRIDGE,queues=4" + info "多队列 TAP 设备 $TAP_DEVICE 创建成功" +} +``` + +**优点**: +- 支持多队列,理论性能更好 +- 可以测试多核网络扩展 + +**缺点**: +- 需要内核支持(较新内核) +- 配置更复杂 +- Starry 当前是单队列,多队列优势无法发挥 + +--- + +## 已修改的文件 + +### 1. `qemu/vhost-x86_64-kvm.toml` +```toml +# 移除了多队列参数 +"-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:57", +"-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on", +``` + +### 2. `qemu/vhost-x86_64-tcg.toml` +```toml +# 保持简化配置 +"-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:57", +"-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on", +``` + +### 3. `qemu/vhost-aarch64-kvm.toml` +```toml +# 移除了多队列参数 +"-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:56", +"-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on", +``` + +### 4. `qemu/vhost-aarch64-tcg.toml` +```toml +# 保持简化配置(单队列) +"-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:56", +"-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on", +``` + +--- + +## 测试验证 + +### x86_64 + KVM +- 移除多队列参数后,QEMU 启动成功 +- 但遇到其他问题(网络设备未找到,rootfs 脚本缺失) + +### aarch64 + TCG(WSL2) +- 完全通过,测试成功 +- 已验证 5 种测试场景全部通过 + +--- + +## 未来改进(可选) + +如果需要测试多队列性能: + +1. **修改 TAP 创建逻辑** + - 在 `env/setup-common.sh` 中添加 `multi_queue` 参数 + - 根据场景决定是否启用多队列 + +2. **提供多队列专用配置** + - `vhost-smp4--.toml` 已存在(多核,但当前仍为单队列) + - 在其基础上追加 `mq=on,queues=N`,与现有单队列配置共存 + +3. **自动检测内核支持** + ```bash + # 检测内核是否支持多队列 TAP + if ip tuntap add mode tap test-mq multi_queue 2>/dev/null; then + ip tuntap del mode tap test-mq + echo "支持多队列" + else + echo "不支持多队列,使用单队列" + fi + ``` + +--- + +## 总结 + +**当前策略**:使用单队列配置 +- 简单可靠 +- 兼容性好 +- 满足基线测试需求 + +**原始配置的多队列参数**: +- 对应高性能多核扩展测试场景 +- 适合多核扩展测试 +- 但需要 TAP 设备支持(需要 `multi_queue` 创建) + +**实际处理**: +- 为了快速验证和兼容性,采用单队列配置 +- 保留了未来添加多队列支持的可能性 +- 文档中标注了多队列的实现方案 diff --git a/apps/starry/net-bench/docs/QUICK_START.md b/apps/starry/net-bench/docs/QUICK_START.md new file mode 100644 index 0000000000..e8187f0917 --- /dev/null +++ b/apps/starry/net-bench/docs/QUICK_START.md @@ -0,0 +1,152 @@ +# net-bench 快速参考 + +## 使用模式 + +### 严肃测试(推荐,参数明确) + +显式指定架构 / 场景 / 加速器,保证可复现: + +```bash +# 主力性能拓扑(需先配置网络) +sudo bash apps/starry/net-bench/bin/setup +bash apps/starry/net-bench/run.sh --scenario vhost --arch x86_64 --repeat 5 +sudo bash apps/starry/net-bench/bin/teardown +``` + +也可直接用 xtask 跑单个配置(run.sh 内部即调用它): + +```bash +cargo xtask starry app qemu \ + --test-case net-bench \ + --arch x86_64 \ + --qemu-config apps/starry/net-bench/qemu/vhost-x86_64-kvm.toml +``` + +### 智能入口(实验性,开发期便捷) + +自动检测环境并委托 run.sh: + +```bash +bash apps/starry/net-bench/bin/bench vhost +bash apps/starry/net-bench/bin/bench-wsl +``` + +## run.sh 选项 + +``` +--scenario S slirp|tap|vhost|vhost-smp4|tap-smp4(默认 vhost) +--arch A aarch64|x86_64(默认 aarch64) +--accel A kvm|tcg(默认:同架构且 KVM 可用时 kvm) +--repeat N 每场景重启 QEMU 跑 N 次并汇总(默认 1) +--no-summary 跳过自动汇总 +``` + +## 常用命令 + +```bash +# 单次 vhost +bash apps/starry/net-bench/run.sh --scenario vhost --arch x86_64 + +# 多次重启累积跨启动方差(推荐 >=5) +bash apps/starry/net-bench/run.sh --scenario vhost --arch x86_64 --repeat 5 + +# 多核扩展 +bash apps/starry/net-bench/run.sh --scenario vhost-smp4 --arch x86_64 --repeat 5 + +# 功能冒烟(SLIRP,无需 sudo/网络配置) +bash apps/starry/net-bench/run.sh --scenario slirp --arch x86_64 + +# CPU 效率(perf stat) +bash apps/starry/net-bench/run-with-perf.sh --arch x86_64 --scenario vhost + +# Linux 同拓扑基线(首次运行会从受管 Alpine rootfs 自动构建 initramfs) +bash apps/starry/net-bench/run-linux-baseline.sh aarch64 vhost --repeat 5 +# 强制重建 initramfs(升级 rootfs 或缓存损坏时) +bash apps/starry/net-bench/run-linux-baseline.sh aarch64 vhost --rebuild-rootfs +``` + +## 环境管理 + +```bash +# 检测环境(无需 sudo) +bash apps/starry/net-bench/env/detect-env.sh + +# 配置 TAP 网络(需 sudo,仅 vhost/tap 场景) +sudo bash apps/starry/net-bench/bin/setup + +# 查看状态 / 清理 +bash apps/starry/net-bench/bin/teardown status +sudo bash apps/starry/net-bench/bin/teardown +``` + +注意:`slirp` 场景用 QEMU usermode 网络,无需 sudo 配置;`vhost`/`tap` 需要 TAP 网络。 + +## 测试场景 + +| 场景 | 拓扑 | 用途 | +|------|------|------| +| vhost | TAP+vhost-net | 主力性能测试 | +| vhost-smp4 | TAP+vhost-net+SMP4 | 多核扩展 | +| tap | TAP(无vhost)| 功能/趋势兜底 | +| tap-smp4 | TAP+SMP4 | vhost 不可用时多核兜底 | +| slirp | SLIRP | 仅功能冒烟 | + +## 环境要求 + +```bash +sudo apt-get install -y iperf3 bridge-utils jq dnsmasq +``` + +- KVM:`/dev/kvm` 可用(WSL2 需嵌套虚拟化) +- vhost-net:`sudo modprobe vhost_net` + +## 测试结果 + +```bash +# 查看汇总 +cat apps/starry/net-bench/results/summary-*.txt + +# 手动汇总 +python3 apps/starry/net-bench/core/summarize.py \ + apps/starry/net-bench/results/starry-*.txt + +# 基线对比 +python3 apps/starry/net-bench/core/compare-baseline.py \ + results/summary-aarch64-vhost-*.txt \ + results/summary-linux-baseline-aarch64-vhost-*.txt +``` + +## 故障排查 + +### KVM 不可用 +```bash +ls -l /dev/kvm +# WSL2: 在 %USERPROFILE%\.wslconfig 加 [wsl2] nestedVirtualization=true,然后 wsl --shutdown +# 裸 Linux: sudo modprobe kvm_intel # 或 kvm_amd +``` + +### vhost-net 不可用 +```bash +ls -l /dev/vhost-net +sudo modprobe vhost_net +``` + +### 端口占用 +```bash +ss -tlnp | grep 5201 +sudo pkill -f "iperf3 -s" +``` + +## 关键指标与纪律 + +- 每数据点 ≥5 次迭代,取 mean + stddev;stddev >10% 标注 NOISY +- 每次测试前 warmup(丢弃首次) +- 记录环境指纹(`fingerprint-*.txt`) +- 核心 KPI:吞吐 Mbit/s、PPS、延迟 P50/P99、cycles/byte、多核扩展比 + +## 获取帮助 + +```bash +bash apps/starry/net-bench/run.sh --help +bash apps/starry/net-bench/bin/bench --help +``` diff --git a/apps/starry/net-bench/docs/STRUCTURE.md b/apps/starry/net-bench/docs/STRUCTURE.md new file mode 100644 index 0000000000..7732db8339 --- /dev/null +++ b/apps/starry/net-bench/docs/STRUCTURE.md @@ -0,0 +1,118 @@ +# net-bench 架构设计 + +## 设计目标 + +net-bench 是"入口 + 参数明确"的严肃网络性能测试架构。核心原则: + +1. **唯一严肃入口**:`run.sh` 显式接收架构 / 场景 / 加速器,保证可复现、可对照。 +2. **智能入口实验化**:`bin/bench` 的环境自动检测属于实验性便捷能力,默认不参与 + 严肃测试;它最终委托 `run.sh`,避免出现两套各自演化的执行路径。 +3. **公共流程封装**:把常量、配置矩阵、iperf3 生命周期、前置检查、指纹、汇总集中 + 到 `core/lib.sh`,消除散落硬编码、统一各入口行为。 + +## 目录组织 + +``` +apps/starry/net-bench/ +├── run.sh # 唯一严肃入口(显式参数) +├── run-with-perf.sh # perf stat 包裹变体(复用 lib.sh) +├── run-linux-baseline.sh # Linux 同拓扑基线 +├── prebuild.sh # rootfs 预构建(被 axbuild discovery 调用) +│ +├── bin/ # 入口壳(薄封装) +│ ├── bench # 智能入口(实验性,委托 run.sh) +│ ├── bench-wsl # WSL2 快捷壳 +│ ├── setup # 配置网络(委托 env/setup-common.sh) +│ └── teardown # 清理(委托 env/teardown.sh) +│ +├── core/ # 核心逻辑 +│ ├── lib.sh # 主机侧公共流程封装(nb_* 函数) +│ ├── net-bench-common.sh # guest 侧基准核心(iperf3 + 标记协议) +│ ├── net-bench.sh # SLIRP guest 入口 +│ ├── net-bench-tap.sh # TAP/vhost guest 入口 +│ ├── net-bench-netperf.sh # netperf 延迟测试 +│ ├── summarize.py # 结果汇总(mean/stddev + NOISY) +│ └── compare-baseline.py # Starry vs Linux 基线对比 +│ +├── env/ # 环境管理 +│ ├── detect-env.sh # 自动检测(JSON / human 输出) +│ ├── setup-common.sh # 通用网络配置(br0/tap0/dnsmasq DHCP,状态化) +│ └── teardown.sh # 状态化回滚清理(读取 .bench-state.json) +│ +├── qemu/ # QEMU 配置矩阵 +│ └── --.toml +│ +├── build-.toml # 构建配置(启用 virtio-net/virtio-blk) +├── docs/ # 详细文档 +├── linux-baseline/ # Linux 基线工件(由脚本按需生成,不入库) +│ ├── initramfs.cpio.gz # 从受管 Alpine rootfs 构建(gitignore) +│ └── vmlinuz # 可选:显式放置的 aarch64 内核(gitignore) +└── results/ # 测试结果 +``` + +## 执行路径 + +### 严肃入口 + +``` +run.sh --scenario S --arch A [--accel K] [--repeat N] + │ source core/lib.sh + ├─ 校验 arch/scenario/accel,推导默认 accel + ├─ 解析配置: qemu/--.toml + ├─ nb_check_scenario_prereq(kvm/vhost/tap 前置检查) + ├─ nb_write_fingerprint(环境指纹) + └─ for rep in 1..N: + ├─ nb_start_iperf3(host 服务端) + ├─ cargo xtask starry app qemu --test-case net-bench + │ └─ prebuild.sh 安装 iperf3 + core/net-bench*.sh 到 rootfs + │ └─ guest 跑 net-bench-common.sh(warmup + ITERS 迭代) + ├─ nb_stop_iperf3 + └─ 收集日志 + nb_summarize(summarize.py 汇总) +``` + +### 智能入口(实验性) + +``` +bin/bench [scenario] [opts] + ├─ env/detect-env.sh(检测 WSL/arch/kvm/vhost) + ├─ 推断 ARCH/ACCEL,必要时 vhost→tap 降级 + ├─ env/setup-common.sh(配置网络,记录状态) + ├─ 委托 run.sh --scenario ... --arch ... --accel ... + └─ trap EXIT → env/teardown.sh(状态化回滚) +``` + +## 配置文件命名规范 + +`qemu/--.toml` + +- scenario:`slirp` / `tap` / `vhost` / `vhost-smp4` / `tap-smp4` +- arch:`x86_64` / `aarch64` +- accel:`kvm` / `tcg` + +`run.sh` 与 `bin/bench` 都通过 `nb_qemu_config` 解析该命名,新增场景/架构只需补齐 +对应 toml 文件即可被两个入口识别。 + +## 应用发现与 CI 关系 + +net-bench 列在 `apps/.ignore` 中,因此**不参与默认 app 发现 / CI 全量跑**——这与 +"严肃性能测试是显式触发、非默认测试入口"的设计一致。通过 +`cargo xtask starry app qemu --test-case net-bench ...` 显式指定时,发现逻辑会忽略 +`apps/.ignore` 从而正常构建运行(见 `scripts/axbuild/src/starry/app/`)。 + +`prebuild.sh` 位于 app 根目录,被 axbuild 的 discovery 自动识别为预构建脚本;它负责 +向 rootfs 注入 iperf3 及 `core/` 下的 guest 基准脚本。 + +## 自动回退(状态化) + +`env/setup-common.sh` 把创建的资源与进程记录到 `.bench-state.json`: + +```json +{ + "timestamp": "...", + "created_resources": [{"type": "bridge", "name": "br0"}, {"type": "tap", "name": "tap0"}], + "processes": [{"pid": 12345, "cmd": "iperf3 -s ..."}] +} +``` + +`env/teardown.sh` 逆序读取并清理(SIGTERM→SIGKILL 终止进程、删除 tap/bridge)。 diff --git a/apps/starry/net-bench/docs/TODO.md b/apps/starry/net-bench/docs/TODO.md new file mode 100644 index 0000000000..ebbcf8b773 --- /dev/null +++ b/apps/starry/net-bench/docs/TODO.md @@ -0,0 +1,67 @@ +# net-bench 待办事项 + +## 已完成(PR 反馈修复) + +- Linux 基线 initramfs 改为按需构建:从受管 Alpine rootfs(含 busybox/iperf3/ + ip/nc)提取并打包,不再提交生成物;`run-linux-baseline.sh` 在缺失或校验失败 + (gzip/cpio 完整性 + init 入口)时重建,并支持 `--rebuild-rootfs` 强制重建 +- 删除并 gitignore 失效的占位 `linux-baseline/initramfs.cpio.gz`(20 字节坏档) + 与 `rootfs-build/` 中间产物 +- Linux 基线内核解析显式化:优先 `linux-baseline/vmlinuz`,否则仅在 host 架构 + 与目标一致时回退 host 内核,跨架构明确报错(避免静默用错内核) +- eBPF net_stats:四个架构 QEMU 配置统一 `--test`,`--test` 在任一字节计数为 0 时 + 报错,QEMU `success_regex` 由 `NET_STATS_END` 收紧为 `TEST PASSED` +- eBPF net_stats 重构为 phy 层入口计数:从 socket 层 kretprobe 读 sret 返回值改为 + 在 smoltcp phy 层(`ax_net::router` 的 `TxToken/RxToken::consume`)直接读入口 + 参数/结构字段。TX 字节从 `len` 参数读取,RX 字节从 `RxToken.packet` slice 字段 + 读取。消除了 sret ABI 复杂性、异步路径分裂、WRAPPER_MARKERS 过滤,覆盖所有 + IP 流量(TCP/UDP/ICMP 等),字节数为链路层帧大小(含协议头) + +## 已完成(本次重构) + +- 统一为单一严肃入口 `run.sh`(显式 arch/scenario/accel/repeat),智能入口 + `bin/bench` 降级为实验性便捷壳并委托 `run.sh` +- 抽取主机侧公共流程到 `core/lib.sh`(常量、配置矩阵、iperf3 生命周期、前置检查、 + 指纹、汇总),消除散落硬编码 +- 修复 `prebuild.sh` 引用 `core/` 下 guest 脚本的路径(原断裂) +- 修复结果汇总路径(统一指向 `core/summarize.py`) +- 移除 QEMU 配置中被 ostool 忽略的死字段 `prebuild = "core/prebuild.sh"` +- 补齐配置矩阵:新增 `vhost-smp4`/`tap-smp4` 各 arch/accel 变体;修正 + `vhost-aarch64-tcg` 缺 `vhost=on` 的问题 +- 新增 `build-x86_64-unknown-none.toml`,使 x86_64 配置可正确启用 virtio-net +- 统一 netperf 标记格式与 `net-bench-common.sh` 对齐 +- 删除重复/失效文件:`core/prebuild.sh`、根目录 `net-bench-netperf.sh`、 + 根目录 `compare-baseline.py`、`setup-vhost-tap.sh`、`build-configs/`、旧根目录 + `qemu-aarch64-*.toml` + +## 高优先级 + +### 多队列支持 +- [ ] 在 `env/setup-common.sh` 中按需创建多队列 TAP(`ip tuntap add ... multi_queue`) +- [ ] 提供多队列专用 QEMU 配置(`mq=on,queues=N`),与单队列配置共存 +- [ ] 待 `drivers/net` 多队列改造后再启用 mq 参数(当前单队列,见 MULTIQUEUE_ISSUE.md) + +### x86_64 + KVM 完整验证 +- [ ] 在裸 Linux x86_64 + KVM 环境端到端跑通 vhost 场景 +- [ ] 验证 x86_64 rootfs 中 guest 脚本注入与运行 + +## 中优先级 + +### eBPF net_stats 集成修正 +- [ ] 将 net_stats 采样从 host 侧改为 guest 侧执行,输出纳入 QEMU guest log +- [ ] 由 host 侧 summarize.py 解析 NET_STATS 块(已支持解析,待接入 guest 采样) + +### CI 接入(可选,保持非默认) +- [ ] 提供显式触发的 CI 示例(至少 SLIRP + TCG 冒烟),不纳入默认全量 app 测试 + +## 低优先级 + +- [ ] 支持 riscv64 / loongarch64 配置矩阵 +- [ ] 拓扑 B(guest ↔ guest 双 VM)支持 +- [ ] 结果自动可视化(多核扩展曲线、三方对比图) + +## 备注 + +- net-bench 列于 `apps/.ignore`,默认不参与 app 发现/CI,仅通过 + `--test-case net-bench` 显式触发——符合"严肃测试显式入口、非默认"的设计。 +- 多队列为性能优化项;当前单队列配置满足基线测试需求。 diff --git a/apps/starry/net-bench/env/detect-env.sh b/apps/starry/net-bench/env/detect-env.sh new file mode 100755 index 0000000000..00396e17b6 --- /dev/null +++ b/apps/starry/net-bench/env/detect-env.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +# env/detect-env.sh - 自动检测运行环境并输出推荐配置 +# +# 检测内容: +# - 平台类型(WSL2 / 裸 Linux) +# - CPU 架构(x86_64 / aarch64) +# - KVM 可用性 +# - vhost-net 可用性 +# - 推荐的测试配置 +# +# 输出:JSON 格式(便于脚本解析)或人类可读格式 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUTPUT_FORMAT="${1:-json}" # json | human + +# 检测平台类型 +detect_platform() { + if grep -qi microsoft /proc/version 2>/dev/null; then + echo "wsl2" + else + echo "native-linux" + fi +} + +# 检测 CPU 架构 +detect_arch() { + uname -m +} + +# 检测 KVM 可用性 +check_kvm() { + if [[ ! -e /dev/kvm ]]; then + echo "false" + return + fi + + if [[ -r /dev/kvm ]] && [[ -w /dev/kvm ]]; then + echo "true" + else + # KVM 设备存在但无权限 + echo "no-permission" + fi +} + +# 检测 vhost-net 可用性 +check_vhost() { + if [[ -e /dev/vhost-net ]]; then + echo "true" + else + # 尝试加载模块 + if modprobe vhost_net 2>/dev/null; then + echo "true" + else + echo "false" + fi + fi +} + +# 检测 QEMU 支持的加速器 +check_qemu_accel() { + local arch="$1" + local qemu_bin="qemu-system-${arch}" + + if ! command -v "$qemu_bin" >/dev/null 2>&1; then + echo "qemu-not-found" + return + fi + + local accel_list + accel_list=$("$qemu_bin" -accel help 2>/dev/null | tail -n +2 | tr '\n' ',' || echo "") + echo "$accel_list" +} + +# 推荐测试架构(基于当前 host 架构和 KVM 可用性) +recommend_arch() { + local host_arch="$1" + local kvm_status="$2" + + # 默认使用 x86_64(最常见) + if [[ "$host_arch" == "x86_64" ]]; then + echo "x86_64" + elif [[ "$host_arch" == "aarch64" ]]; then + echo "aarch64" + else + # 其他架构默认用 x86_64(通过 TCG 模拟) + echo "x86_64" + fi +} + +# 推荐加速模式 +recommend_accel() { + local recommended_arch="$1" + local host_arch="$2" + local kvm_status="$3" + + # 如果推荐架构与 host 架构一致且 KVM 可用,使用 KVM + if [[ "$recommended_arch" == "$host_arch" ]] && [[ "$kvm_status" == "true" ]]; then + echo "kvm" + else + echo "tcg" + fi +} + +# 推荐 QEMU 配置文件 +recommend_qemu_config() { + local arch="$1" + local accel="$2" + local vhost="$3" + local scenario="${4:-vhost}" # 默认 vhost 场景 + + case "$scenario" in + slirp) + echo "qemu/slirp-${arch}-${accel}.toml" + ;; + tap) + echo "qemu/tap-${arch}-${accel}.toml" + ;; + vhost) + if [[ "$vhost" == "true" ]]; then + echo "qemu/vhost-${arch}-${accel}.toml" + else + echo "qemu/tap-${arch}-${accel}.toml" + fi + ;; + vhost-smp4) + if [[ "$vhost" == "true" ]]; then + echo "qemu/vhost-smp4-${arch}-${accel}.toml" + else + echo "qemu/tap-smp4-${arch}-${accel}.toml" + fi + ;; + *) + echo "qemu/vhost-${arch}-${accel}.toml" + ;; + esac +} + +# 主检测逻辑 +main() { + local platform host_arch kvm_status vhost_status + local recommended_arch recommended_accel recommended_config + local qemu_accel_list + + platform=$(detect_platform) + host_arch=$(detect_arch) + kvm_status=$(check_kvm) + vhost_status=$(check_vhost) + qemu_accel_list=$(check_qemu_accel "$host_arch") + + recommended_arch=$(recommend_arch "$host_arch" "$kvm_status") + recommended_accel=$(recommend_accel "$recommended_arch" "$host_arch" "$kvm_status") + recommended_config=$(recommend_qemu_config "$recommended_arch" "$recommended_accel" "$vhost_status" "vhost") + + if [[ "$OUTPUT_FORMAT" == "json" ]]; then + cat <&2; } +die() { error "$*"; exit 1; } + +# 检查 root 权限 +check_root() { + if [[ $EUID -ne 0 ]]; then + die "需要 root 权限(使用 sudo)" + fi +} + +# 检查并安装主机依赖(已在 root 下运行)。缺失即尝试 apt-get 安装, +# 非 Debian 系或无网络时给出明确指引并退出。 +check_dependencies() { + local missing=() + local pkg + # 命令名 -> apt 包名映射(多数同名)。 + declare -A pkg_of=( + [iperf3]=iperf3 [ip]=iproute2 [brctl]=bridge-utils + [jq]=jq [dnsmasq]=dnsmasq + ) + for cmd in iperf3 ip brctl jq dnsmasq; do + command -v "$cmd" >/dev/null 2>&1 || missing+=("${pkg_of[$cmd]}") + done + [[ ${#missing[@]} -eq 0 ]] && return 0 + + # 去重 + local uniq + uniq=$(printf '%s\n' "${missing[@]}" | sort -u | tr '\n' ' ') + warn "缺少依赖: $uniq" + if command -v apt-get >/dev/null 2>&1; then + info "尝试安装: apt-get install -y $uniq" + # shellcheck disable=SC2086 + if DEBIAN_FRONTEND=noninteractive apt-get install -y $uniq; then + info "依赖安装完成" + else + die "依赖安装失败,请手动安装: $uniq" + fi + else + die "缺少依赖且非 apt 系统,请手动安装: $uniq" + fi +} + +# 设置设备权限 +setup_device_permissions() { + # KVM 权限 + if [[ -e /dev/kvm ]]; then + chmod 666 /dev/kvm 2>/dev/null || warn "无法设置 /dev/kvm 权限" + fi + + # vhost-net:先尝试加载内核模块,再放开权限。新机器上 vhost_net 常未自动 + # 加载,缺少 modprobe 会导致 /dev/vhost-net 不存在、vhost 场景失败。 + if [[ ! -e /dev/vhost-net ]]; then + modprobe vhost_net 2>/dev/null || warn "无法加载 vhost_net 模块(vhost 场景将不可用)" + fi + if [[ -e /dev/vhost-net ]]; then + chmod 666 /dev/vhost-net 2>/dev/null || warn "无法设置 /dev/vhost-net 权限" + fi +} + +# 初始化状态文件 +init_state() { + if [[ ! -f "$STATE_FILE" ]]; then + cat > "$STATE_FILE" < "$tmp_file" + mv "$tmp_file" "$STATE_FILE" +} + +# 记录启动的进程 +record_process() { + local pid="$1" + local cmd="$2" + + local tmp_file=$(mktemp) + jq ".processes += [{\"pid\": $pid, \"cmd\": \"$cmd\"}]" "$STATE_FILE" > "$tmp_file" + mv "$tmp_file" "$STATE_FILE" +} + +# 检查端口是否被占用 +check_port() { + local port="$1" + if ss -tuln | grep -q ":${port} "; then + return 0 # 端口被占用 + else + return 1 # 端口空闲 + fi +} + +# 检查 bridge 是否存在 +bridge_exists() { + ip link show "$BRIDGE" >/dev/null 2>&1 +} + +# 检查 TAP 是否存在 +tap_exists() { + ip link show "$TAP_DEVICE" >/dev/null 2>&1 +} + +# 创建 bridge +setup_bridge() { + if bridge_exists; then + warn "Bridge $BRIDGE 已存在,跳过创建" + return 0 + fi + + info "创建 bridge $BRIDGE" + ip link add "$BRIDGE" type bridge || die "创建 bridge 失败" + ip link set "$BRIDGE" up || die "启动 bridge 失败" + ip addr add "$BRIDGE_IP" dev "$BRIDGE" || die "配置 bridge IP 失败" + + record_resource "bridge" "$BRIDGE" "$BRIDGE_IP" + info "Bridge $BRIDGE 创建成功: $BRIDGE_IP" +} + +# 创建 TAP 设备 +setup_tap() { + if tap_exists; then + warn "TAP 设备 $TAP_DEVICE 已存在,跳过创建" + # 确保 TAP 挂载到 bridge + if ! brctl show "$BRIDGE" | grep -q "$TAP_DEVICE"; then + info "将 $TAP_DEVICE 挂载到 $BRIDGE" + ip link set "$TAP_DEVICE" master "$BRIDGE" + fi + return 0 + fi + + info "创建 TAP 设备 $TAP_DEVICE" + ip tuntap add mode tap "$TAP_DEVICE" || die "创建 TAP 设备失败" + ip link set "$TAP_DEVICE" up || die "启动 TAP 设备失败" + ip link set "$TAP_DEVICE" master "$BRIDGE" || die "挂载 TAP 到 bridge 失败" + + record_resource "tap" "$TAP_DEVICE" "master=$BRIDGE" + info "TAP 设备 $TAP_DEVICE 创建成功" +} + +# 启动 DHCP 服务器(dnsmasq) +setup_dhcp() { + # 检查 dnsmasq 是否已运行在 bridge 上 + if pgrep -f "dnsmasq.*--interface=${BRIDGE}" >/dev/null; then + info "dnsmasq 已在 $BRIDGE 上运行,跳过启动" + return 0 + fi + + # 检查 dnsmasq 命令是否存在 + if ! command -v dnsmasq >/dev/null 2>&1; then + error "dnsmasq 未安装,但 TAP/vhost 场景的 guest 只支持 DHCP 获取地址。" + error "安装后重试: sudo apt-get install -y dnsmasq" + return 1 + fi + + info "启动 DHCP 服务器 (dnsmasq): $DHCP_RANGE" + # 重定向所有 fd 并 setsid 脱离控制终端,避免后台 daemon 持有调用方管道。 + setsid dnsmasq \ + --keep-in-foreground \ + --interface="$BRIDGE" \ + --bind-interfaces \ + --dhcp-range="$DHCP_RANGE" \ + --port=0 /dev/null 2>&1 & + + local dnsmasq_pid=$! + sleep 1 + + if kill -0 "$dnsmasq_pid" 2>/dev/null; then + record_process "$dnsmasq_pid" "dnsmasq --interface=$BRIDGE --dhcp-range=$DHCP_RANGE" + info "DHCP 服务器启动成功 (PID: $dnsmasq_pid)" + disown # 让进程在后台继续运行 + else + error "dnsmasq 启动失败" + return 1 + fi +} + +# 主函数 +main() { + check_root + + info "=== net-bench 通用环境配置 ===" + + # 新机器上先补齐主机依赖(iperf3/ip/brctl/jq/dnsmasq)。 + check_dependencies + + init_state + + # 设置设备权限(含 vhost_net 模块加载) + setup_device_permissions + + # 配置网络 + setup_bridge + setup_tap + + # 启动服务 + # 注意:不在此启动 iperf3 服务端。iperf3 的生命周期由测试入口 + # (run.sh / run-with-perf.sh 的 nb_start_iperf3/nb_stop_iperf3)自管, + # 这里若启动会与之争用端口 5201(端到端测试已验证此冲突)。 + setup_dhcp + + # 显示配置摘要 + info "" + info "=== 配置完成 ===" + info "Bridge: $BRIDGE ($BRIDGE_IP)" + info "TAP: $TAP_DEVICE" + info "DHCP 范围: $DHCP_RANGE" + info "iperf3: 由测试入口自管(run.sh),未在此启动" + info "" + info "验证配置:" + info " ip addr show $BRIDGE" + info " brctl show $BRIDGE" + info " ss -ulnp | grep :67 # DHCP 服务端" + info "" + info "运行测试:" + info " bash apps/starry/net-bench/run.sh --scenario vhost --arch x86_64" +} + +main "$@" diff --git a/apps/starry/net-bench/env/teardown.sh b/apps/starry/net-bench/env/teardown.sh new file mode 100755 index 0000000000..fd0ffa5c20 --- /dev/null +++ b/apps/starry/net-bench/env/teardown.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +# env/teardown.sh - 自动回退清理所有配置 +# +# 功能: +# 1. 读取状态文件 .bench-state.json +# 2. 停止所有启动的进程 +# 3. 删除所有创建的网络资源 +# 4. 清理状态文件 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +STATE_FILE="${SCRIPT_DIR}/../.bench-state.json" + +# 颜色输出 +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# 检查 root 权限 +check_root() { + if [[ $EUID -ne 0 ]]; then + error "需要 root 权限(使用 sudo)" + exit 1 + fi +} + +# 检查状态文件是否存在 +check_state_file() { + if [[ ! -f "$STATE_FILE" ]]; then + warn "状态文件不存在: $STATE_FILE" + warn "没有需要清理的资源" + return 1 + fi + return 0 +} + +# 停止进程 +stop_processes() { + info "停止启动的进程..." + + local process_count + process_count=$(jq '.processes | length' "$STATE_FILE" 2>/dev/null || echo 0) + + if [[ "$process_count" -eq 0 ]]; then + info " 没有需要停止的进程" + return 0 + fi + + for i in $(seq 0 $((process_count - 1))); do + local pid cmd + pid=$(jq -r ".processes[$i].pid" "$STATE_FILE") + cmd=$(jq -r ".processes[$i].cmd" "$STATE_FILE") + + if kill -0 "$pid" 2>/dev/null; then + info " 停止进程 $pid: $cmd" + kill "$pid" 2>/dev/null || warn " 无法停止进程 $pid" + sleep 0.5 + + # 如果进程仍在运行,强制终止 + if kill -0 "$pid" 2>/dev/null; then + warn " 进程 $pid 未响应,强制终止" + kill -9 "$pid" 2>/dev/null || true + fi + else + info " 进程 $pid 已不存在" + fi + done +} + +# 删除网络资源 +cleanup_resources() { + info "清理网络资源..." + + local resource_count + resource_count=$(jq '.created_resources | length' "$STATE_FILE" 2>/dev/null || echo 0) + + if [[ "$resource_count" -eq 0 ]]; then + info " 没有需要清理的资源" + return 0 + fi + + # 按逆序删除(先删除依赖资源) + for i in $(seq $((resource_count - 1)) -1 0); do + local type name details + type=$(jq -r ".created_resources[$i].type" "$STATE_FILE") + name=$(jq -r ".created_resources[$i].name" "$STATE_FILE") + details=$(jq -r ".created_resources[$i].details" "$STATE_FILE") + + case "$type" in + tap) + if ip link show "$name" >/dev/null 2>&1; then + info " 删除 TAP 设备: $name" + ip link set "$name" down 2>/dev/null || true + ip link delete "$name" 2>/dev/null || warn " 无法删除 $name" + else + info " TAP 设备 $name 已不存在" + fi + ;; + bridge) + if ip link show "$name" >/dev/null 2>&1; then + info " 删除 bridge: $name" + ip link set "$name" down 2>/dev/null || true + ip link delete "$name" 2>/dev/null || warn " 无法删除 $name" + else + info " Bridge $name 已不存在" + fi + ;; + *) + warn " 未知资源类型: $type ($name)" + ;; + esac + done +} + +# 清理状态文件 +cleanup_state_file() { + if [[ -f "$STATE_FILE" ]]; then + info "清理状态文件: $STATE_FILE" + rm -f "$STATE_FILE" + fi +} + +# 显示当前状态 +show_status() { + if ! check_state_file; then + return 0 + fi + + info "" + info "=== 当前配置状态 ===" + + local process_count resource_count + process_count=$(jq '.processes | length' "$STATE_FILE" 2>/dev/null || echo 0) + resource_count=$(jq '.created_resources | length' "$STATE_FILE" 2>/dev/null || echo 0) + + info "进程数: $process_count" + if [[ "$process_count" -gt 0 ]]; then + jq -r '.processes[] | " PID \(.pid): \(.cmd)"' "$STATE_FILE" + fi + + info "网络资源数: $resource_count" + if [[ "$resource_count" -gt 0 ]]; then + jq -r '.created_resources[] | " \(.type): \(.name) (\(.details))"' "$STATE_FILE" + fi + + info "" +} + +# 主函数 +main() { + local action="${1:-teardown}" + + if [[ "$action" == "status" ]]; then + show_status + exit 0 + fi + + check_root + + info "=== net-bench 环境清理 ===" + + if ! check_state_file; then + info "环境已清理或未配置" + exit 0 + fi + + # 显示将要清理的内容 + show_status + + # 执行清理 + stop_processes + cleanup_resources + cleanup_state_file + + info "" + info "=== 清理完成 ===" +} + +# 设置退出时清理(如果脚本被中断) +trap 'error "清理过程被中断"' INT TERM + +main "$@" diff --git a/apps/starry/net-bench/monitor-compilation.sh b/apps/starry/net-bench/monitor-compilation.sh new file mode 100755 index 0000000000..f6168c687a --- /dev/null +++ b/apps/starry/net-bench/monitor-compilation.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# monitor-compilation.sh — 监控 Starry 编译进度 + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LOG_FILE="/tmp/starry-vhost-test.log" +RESULT_DIR="$SCRIPT_DIR/results" + +echo "=== Monitoring Starry compilation and test ===" +echo "Log file: $LOG_FILE" +echo "" + +while true; do + # 检查编译进程 + if pgrep -f "cargo.*starryos" > /dev/null; then + echo "[$(date +%H:%M:%S)] Compilation in progress..." + + # 显示最新编译信息 + if [[ -f "$LOG_FILE" ]]; then + tail -3 "$LOG_FILE" | grep -E "Compiling|Finished" || echo " (still compiling dependencies...)" + fi + elif pgrep -f "qemu-system-aarch64" > /dev/null; then + echo "[$(date +%H:%M:%S)] QEMU running, test in progress..." + + # 检查是否有测试输出 + if [[ -f "$LOG_FILE" ]]; then + if grep -q "NET_BENCH_BEGIN" "$LOG_FILE"; then + echo " ✅ Test started!" + tail -5 "$LOG_FILE" | grep -E "NET_BENCH|iperf3" + fi + fi + else + echo "[$(date +%H:%M:%S)] No active processes found." + + # 检查是否有新结果 + LATEST_RESULT=$(ls -t "$RESULT_DIR"/starry-aarch64-vhost-*.txt 2>/dev/null | head -1) + if [[ -n "$LATEST_RESULT" ]]; then + echo "" + echo "Latest result: $LATEST_RESULT" + + # 检查测试是否完成 + if grep -q "NET_BENCH_PASSED" "$LATEST_RESULT"; then + echo " ✅ Test PASSED!" + exit 0 + elif grep -q "NET_BENCH_FAILED" "$LATEST_RESULT"; then + echo " ❌ Test FAILED!" + exit 1 + else + echo " ⏳ Test incomplete or in progress" + fi + fi + + echo "" + echo "Compilation may have completed. Check manually with:" + echo " tail -100 $LOG_FILE" + break + fi + + echo "" + sleep 30 +done diff --git a/apps/starry/net-bench/prebuild.sh b/apps/starry/net-bench/prebuild.sh new file mode 100755 index 0000000000..f6f630bf6e --- /dev/null +++ b/apps/starry/net-bench/prebuild.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -euo pipefail + +app_dir="${STARRY_APP_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" +rootfs="${STARRY_ROOTFS:-}" +staging_root="${STARRY_STAGING_ROOT:-}" +overlay_dir="${STARRY_OVERLAY_DIR:-}" +arch="${STARRY_ARCH:-}" +apk_cache="${STARRY_WORKSPACE:-$(cd "$app_dir/../../.." && pwd)}/target/net-bench-apk-cache" + +require_env() { [[ -n "$2" ]] || { echo "error: $1 is required" >&2; exit 1; }; } + +qemu_runner_candidates() { + case "$arch" in + aarch64) printf '%s\n' qemu-aarch64-static qemu-aarch64 ;; + riscv64) printf '%s\n' qemu-riscv64-static qemu-riscv64 ;; + x86_64) printf '%s\n' qemu-x86_64-static qemu-x86_64 ;; + loongarch64) printf '%s\n' qemu-loongarch64-static qemu-loongarch64 ;; + *) echo "error: unsupported arch: $arch" >&2; exit 1 ;; + esac +} + +find_qemu_runner() { + local c; while IFS= read -r c; do command -v "$c" >/dev/null 2>&1 && { command -v "$c"; return; }; done < <(qemu_runner_candidates) + echo "error: no qemu-user runner for $arch" >&2; exit 1 +} + +run_guest_apk_once() { + local qemu_runner; qemu_runner="$(find_qemu_runner)" + [[ -x "$staging_root/sbin/apk" ]] || { echo "error: missing apk in $staging_root" >&2; exit 1; } + mkdir -p "$apk_cache" + [[ -f /etc/resolv.conf ]] && cp /etc/resolv.conf "$staging_root/etc/resolv.conf" + QEMU_LD_PREFIX="$staging_root" LD_LIBRARY_PATH="$staging_root/lib:$staging_root/usr/lib" \ + "$qemu_runner" -L "$staging_root" "$staging_root/sbin/apk" \ + --root "$staging_root" \ + --repositories-file "$staging_root/etc/apk/repositories" \ + --keys-dir "$staging_root/etc/apk/keys" \ + --cache-dir "$apk_cache" \ + --update-cache --timeout 60 --no-interactive --force-no-chroot --scripts=no \ + "$@" +} + +run_guest_apk() { + local attempt; local max=4 + for attempt in $(seq 1 "$max"); do + run_guest_apk_once "$@" && return + [[ "$attempt" -eq "$max" ]] && return 1 + echo "apk retry ($attempt/$max)..." >&2; sleep $((attempt * 3)) + done +} + +find_library_path() { + local lib="$1" + for dir in lib usr/lib usr/local/lib; do + [[ -e "$staging_root/$dir/$lib" ]] && { printf '/%s/%s\n' "$dir" "$lib"; return; } + done + return 1 +} + +copy_file_to_overlay() { + local src="$staging_root$1" dst="$overlay_dir$1" + [[ -e "$src" ]] || { echo "error: missing $1 after apk install" >&2; exit 1; } + [[ -L "$src" ]] && src="$(readlink -f "$src")" + install -Dm"$2" "$src" "$dst" +} + +copy_runtime_deps() { + local pending=("$@") seen=" " guest_path library library_path + while [[ ${#pending[@]} -gt 0 ]]; do + guest_path="${pending[0]}"; pending=("${pending[@]:1}") + [[ "$seen" == *" $guest_path "* ]] && continue; seen+="$guest_path " + while IFS= read -r library; do + library_path="$(find_library_path "$library")" || continue + copy_file_to_overlay "$library_path" 0644 + pending+=("$library_path") + done < <(readelf -d "$staging_root$guest_path" 2>/dev/null | sed -n 's/.*Shared library: \[\(.*\)\].*/\1/p') + done +} + +require_env STARRY_ROOTFS "$rootfs" +require_env STARRY_STAGING_ROOT "$staging_root" +require_env STARRY_OVERLAY_DIR "$overlay_dir" +require_env STARRY_ARCH "$arch" +command -v debugfs >/dev/null 2>&1 || { echo "error: install e2fsprogs (debugfs)" >&2; exit 1; } +command -v readelf >/dev/null 2>&1 || { echo "error: install binutils (readelf)" >&2; exit 1; } + +debugfs -R "rdump / $staging_root" "$rootfs" +run_guest_apk add iperf3 + +copy_file_to_overlay /usr/bin/iperf3 0755 +copy_runtime_deps /usr/bin/iperf3 +# guest 侧基准脚本统一存放在 core/,安装到 rootfs 的 /usr/bin。 +install -Dm0755 "$app_dir/core/net-bench-common.sh" "$overlay_dir/usr/bin/net-bench-common.sh" +install -Dm0755 "$app_dir/core/net-bench.sh" "$overlay_dir/usr/bin/net-bench.sh" +install -Dm0755 "$app_dir/core/net-bench-tap.sh" "$overlay_dir/usr/bin/net-bench-tap.sh" diff --git a/apps/starry/net-bench/qemu/slirp-aarch64-kvm.toml b/apps/starry/net-bench/qemu/slirp-aarch64-kvm.toml new file mode 100644 index 0000000000..72578a138d --- /dev/null +++ b/apps/starry/net-bench/qemu/slirp-aarch64-kvm.toml @@ -0,0 +1,24 @@ +# QEMU 配置: slirp-aarch64-kvm +# SLIRP (usermode network) + KVM 加速 +# 适用于: aarch64 host + KVM 支持,仅功能冒烟测试 + +args = [ + "-nographic", + "-m", "512M", + "-accel", "kvm", + "-cpu", "host", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", + "-snapshot", + "-machine", "virt", +] + +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/slirp-aarch64-tcg.toml b/apps/starry/net-bench/qemu/slirp-aarch64-tcg.toml new file mode 100644 index 0000000000..1bc01e50d1 --- /dev/null +++ b/apps/starry/net-bench/qemu/slirp-aarch64-tcg.toml @@ -0,0 +1,24 @@ +# QEMU 配置: slirp-aarch64-tcg +# SLIRP (usermode network) + TCG 模拟 +# 适用于: aarch64 无 KVM 或跨架构测试,仅功能冒烟测试 + +args = [ + "-nographic", + "-m", "512M", + "-accel", "tcg", + "-cpu", "cortex-a57", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", + "-snapshot", + "-machine", "virt", +] + +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/slirp-x86_64-kvm.toml b/apps/starry/net-bench/qemu/slirp-x86_64-kvm.toml new file mode 100644 index 0000000000..5b042cf256 --- /dev/null +++ b/apps/starry/net-bench/qemu/slirp-x86_64-kvm.toml @@ -0,0 +1,24 @@ +# QEMU 配置: slirp-x86_64-kvm +# SLIRP (usermode network) + KVM 加速 +# 适用于: x86_64 host + KVM 支持,仅功能冒烟测试 +# SLIRP 不需要 host 网络配置,但性能较低 + +args = [ + "-nographic", + "-m", "512M", + "-accel", "kvm", + "-cpu", "host", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", + "-snapshot", +] + +uefi = true +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/slirp-x86_64-tcg.toml b/apps/starry/net-bench/qemu/slirp-x86_64-tcg.toml new file mode 100644 index 0000000000..02aa6369e7 --- /dev/null +++ b/apps/starry/net-bench/qemu/slirp-x86_64-tcg.toml @@ -0,0 +1,23 @@ +# QEMU 配置: slirp-x86_64-tcg +# SLIRP (usermode network) + TCG 模拟 +# 适用于: x86_64 无 KVM,仅功能冒烟测试 + +args = [ + "-nographic", + "-m", "512M", + "-accel", "tcg", + "-cpu", "max", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", + "-snapshot", +] + +uefi = true +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/tap-aarch64-kvm.toml b/apps/starry/net-bench/qemu/tap-aarch64-kvm.toml new file mode 100644 index 0000000000..2ac6840e40 --- /dev/null +++ b/apps/starry/net-bench/qemu/tap-aarch64-kvm.toml @@ -0,0 +1,24 @@ +# QEMU 配置: tap-aarch64-kvm +# TAP (无 vhost) + KVM 加速 +# 适用于: aarch64 host + KVM 支持,vhost-net 不可用时的降级模式 + +args = [ + "-nographic", + "-m", "1G", + "-accel", "kvm", + "-cpu", "host", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:56", + "-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no", + "-snapshot", + "-machine", "virt", +] + +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench-tap.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/tap-aarch64-tcg.toml b/apps/starry/net-bench/qemu/tap-aarch64-tcg.toml new file mode 100644 index 0000000000..ece6adb1c5 --- /dev/null +++ b/apps/starry/net-bench/qemu/tap-aarch64-tcg.toml @@ -0,0 +1,24 @@ +# QEMU 配置: tap-aarch64-tcg +# TAP (无 vhost) + TCG 模拟 +# 适用于: aarch64 无 KVM 或跨架构测试,vhost-net 不可用时的降级模式 + +args = [ + "-nographic", + "-m", "1G", + "-accel", "tcg", + "-cpu", "cortex-a57", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:56", + "-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no", + "-snapshot", + "-machine", "virt", +] + +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench-tap.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/tap-smp4-aarch64-kvm.toml b/apps/starry/net-bench/qemu/tap-smp4-aarch64-kvm.toml new file mode 100644 index 0000000000..41a90f2f70 --- /dev/null +++ b/apps/starry/net-bench/qemu/tap-smp4-aarch64-kvm.toml @@ -0,0 +1,24 @@ +# QEMU 配置: tap-smp4-aarch64-kvm +# TAP (无 vhost) + KVM 加速, smp=4(多核扩展兜底;vhost 不可用时使用) + +args = [ + "-nographic", + "-m", "1G", + "-smp", "4", + "-accel", "kvm", + "-cpu", "host", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:56", + "-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no", + "-snapshot", + "-machine", "virt", +] + +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench-tap.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/tap-smp4-aarch64-tcg.toml b/apps/starry/net-bench/qemu/tap-smp4-aarch64-tcg.toml new file mode 100644 index 0000000000..e239ce3c29 --- /dev/null +++ b/apps/starry/net-bench/qemu/tap-smp4-aarch64-tcg.toml @@ -0,0 +1,24 @@ +# QEMU 配置: tap-smp4-aarch64-tcg +# TAP (无 vhost) + TCG 模拟, smp=4(多核扩展兜底,跨架构 / 无 KVM) +# 注意: TCG 下性能数据仅供功能验证,不作为正式性能基线。 + +args = [ + "-nographic", + "-m", "1G", + "-smp", "4", + "-cpu", "cortex-a72", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:56", + "-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no", + "-snapshot", + "-machine", "virt", +] + +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench-tap.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/tap-smp4-x86_64-kvm.toml b/apps/starry/net-bench/qemu/tap-smp4-x86_64-kvm.toml new file mode 100644 index 0000000000..952456f302 --- /dev/null +++ b/apps/starry/net-bench/qemu/tap-smp4-x86_64-kvm.toml @@ -0,0 +1,23 @@ +# QEMU 配置: tap-smp4-x86_64-kvm +# TAP (无 vhost) + KVM 加速, smp=4(多核扩展兜底;vhost 不可用时使用) + +args = [ + "-nographic", + "-m", "2G", + "-smp", "4", + "-accel", "kvm", + "-cpu", "host", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:57", + "-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no", + "-snapshot", +] + +uefi = true +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench-tap.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/tap-smp4-x86_64-tcg.toml b/apps/starry/net-bench/qemu/tap-smp4-x86_64-tcg.toml new file mode 100644 index 0000000000..7a82aa16ec --- /dev/null +++ b/apps/starry/net-bench/qemu/tap-smp4-x86_64-tcg.toml @@ -0,0 +1,23 @@ +# QEMU 配置: tap-smp4-x86_64-tcg +# TAP (无 vhost) + TCG 模拟, smp=4(多核扩展兜底,无 KVM) +# 注意: TCG 下性能数据仅供功能验证,不作为正式性能基线。 + +args = [ + "-nographic", + "-m", "2G", + "-smp", "4", + "-cpu", "qemu64", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:57", + "-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no", + "-snapshot", +] + +uefi = true +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench-tap.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/tap-x86_64-kvm.toml b/apps/starry/net-bench/qemu/tap-x86_64-kvm.toml new file mode 100644 index 0000000000..fb0a753dfc --- /dev/null +++ b/apps/starry/net-bench/qemu/tap-x86_64-kvm.toml @@ -0,0 +1,23 @@ +# QEMU 配置: tap-x86_64-kvm +# TAP (无 vhost) + KVM 加速 +# 适用于: x86_64 host + KVM 支持,vhost-net 不可用时的降级模式 + +args = [ + "-nographic", + "-m", "2G", + "-accel", "kvm", + "-cpu", "host", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:57", + "-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no", + "-snapshot", +] + +uefi = true +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench-tap.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/tap-x86_64-tcg.toml b/apps/starry/net-bench/qemu/tap-x86_64-tcg.toml new file mode 100644 index 0000000000..2e9b7bc83b --- /dev/null +++ b/apps/starry/net-bench/qemu/tap-x86_64-tcg.toml @@ -0,0 +1,23 @@ +# QEMU 配置: tap-x86_64-tcg +# TAP (无 vhost) + TCG 模拟 +# 适用于: x86_64 无 KVM,vhost-net 不可用时的降级模式 + +args = [ + "-nographic", + "-m", "2G", + "-accel", "tcg", + "-cpu", "max", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:57", + "-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no", + "-snapshot", +] + +uefi = true +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench-tap.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/vhost-aarch64-kvm.toml b/apps/starry/net-bench/qemu/vhost-aarch64-kvm.toml new file mode 100644 index 0000000000..b1bfb60a03 --- /dev/null +++ b/apps/starry/net-bench/qemu/vhost-aarch64-kvm.toml @@ -0,0 +1,24 @@ +# QEMU 配置: vhost-aarch64-kvm +# TAP + vhost-net + KVM 加速 +# 适用于: aarch64 host + KVM 支持 + +args = [ + "-nographic", + "-m", "1G", + "-accel", "kvm", + "-cpu", "host", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:56", + "-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on", + "-snapshot", + "-machine", "virt", +] + +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench-tap.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/vhost-aarch64-tcg.toml b/apps/starry/net-bench/qemu/vhost-aarch64-tcg.toml new file mode 100644 index 0000000000..9e6d49dbdc --- /dev/null +++ b/apps/starry/net-bench/qemu/vhost-aarch64-tcg.toml @@ -0,0 +1,24 @@ +# QEMU 配置: vhost-aarch64-tcg +# TAP + vhost-net + TCG 模拟(无 KVM) +# 适用于: aarch64 无 KVM 或在 x86_64 host 上跨架构测试 +# 注意: TCG 下性能数据仅供功能验证,不作为正式性能基线 + +args = [ + "-nographic", + "-m", "1G", + "-cpu", "cortex-a72", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:56", + "-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on", + "-snapshot", + "-machine", "virt", +] + +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench-tap.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/vhost-smp4-aarch64-kvm.toml b/apps/starry/net-bench/qemu/vhost-smp4-aarch64-kvm.toml new file mode 100644 index 0000000000..09dbd83178 --- /dev/null +++ b/apps/starry/net-bench/qemu/vhost-smp4-aarch64-kvm.toml @@ -0,0 +1,27 @@ +# QEMU 配置: vhost-smp4-aarch64-kvm +# TAP + vhost-net + KVM 加速, smp=4(多核扩展测试) +# 对齐 methodology §1 "多核扩展比" 指标。 +# 注意: Starry 当前单队列驱动,smp4 用于验证全局锁/调度扩展,不依赖多队列 TAP。 +# 绑核建议(plan §3.3): taskset -c 4-7 ... + +args = [ + "-nographic", + "-m", "1G", + "-smp", "4", + "-accel", "kvm", + "-cpu", "host", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:56", + "-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on", + "-snapshot", + "-machine", "virt", +] + +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench-tap.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/vhost-smp4-aarch64-tcg.toml b/apps/starry/net-bench/qemu/vhost-smp4-aarch64-tcg.toml new file mode 100644 index 0000000000..833c3703d7 --- /dev/null +++ b/apps/starry/net-bench/qemu/vhost-smp4-aarch64-tcg.toml @@ -0,0 +1,24 @@ +# QEMU 配置: vhost-smp4-aarch64-tcg +# TAP + vhost-net + TCG 模拟, smp=4(多核扩展,跨架构 / 无 KVM) +# 注意: TCG 下性能数据仅供功能验证,不作为正式性能基线。 + +args = [ + "-nographic", + "-m", "1G", + "-smp", "4", + "-cpu", "cortex-a72", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:56", + "-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on", + "-snapshot", + "-machine", "virt", +] + +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench-tap.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/vhost-smp4-x86_64-kvm.toml b/apps/starry/net-bench/qemu/vhost-smp4-x86_64-kvm.toml new file mode 100644 index 0000000000..8aca9643db --- /dev/null +++ b/apps/starry/net-bench/qemu/vhost-smp4-x86_64-kvm.toml @@ -0,0 +1,26 @@ +# QEMU 配置: vhost-smp4-x86_64-kvm +# TAP + vhost-net + KVM 加速, smp=4(多核扩展测试) +# 对齐 methodology §1 "多核扩展比" 指标。 +# 注意: Starry 当前单队列驱动,smp4 用于验证全局锁/调度扩展,不依赖多队列 TAP。 +# 绑核建议(plan §3.3): taskset -c 4-7 ... + +args = [ + "-nographic", + "-m", "2G", + "-smp", "4", + "-accel", "kvm", + "-cpu", "host", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:57", + "-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on", + "-snapshot", +] + +uefi = true +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench-tap.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/vhost-smp4-x86_64-tcg.toml b/apps/starry/net-bench/qemu/vhost-smp4-x86_64-tcg.toml new file mode 100644 index 0000000000..67a2a19770 --- /dev/null +++ b/apps/starry/net-bench/qemu/vhost-smp4-x86_64-tcg.toml @@ -0,0 +1,23 @@ +# QEMU 配置: vhost-smp4-x86_64-tcg +# TAP + vhost-net + TCG 模拟, smp=4(多核扩展,无 KVM) +# 注意: TCG 下性能数据仅供功能验证,不作为正式性能基线。 + +args = [ + "-nographic", + "-m", "2G", + "-smp", "4", + "-cpu", "qemu64", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:57", + "-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on", + "-snapshot", +] + +uefi = true +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench-tap.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/vhost-x86_64-kvm.toml b/apps/starry/net-bench/qemu/vhost-x86_64-kvm.toml new file mode 100644 index 0000000000..6645e62aba --- /dev/null +++ b/apps/starry/net-bench/qemu/vhost-x86_64-kvm.toml @@ -0,0 +1,23 @@ +# QEMU 配置: vhost-x86_64-kvm +# TAP + vhost-net + KVM 加速 +# 适用于: x86_64 host + KVM 支持(最常见,推荐配置) + +args = [ + "-nographic", + "-m", "2G", + "-accel", "kvm", + "-cpu", "host", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:57", + "-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on", + "-snapshot", +] + +uefi = true +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench-tap.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/qemu/vhost-x86_64-tcg.toml b/apps/starry/net-bench/qemu/vhost-x86_64-tcg.toml new file mode 100644 index 0000000000..5d6b9d020d --- /dev/null +++ b/apps/starry/net-bench/qemu/vhost-x86_64-tcg.toml @@ -0,0 +1,22 @@ +# QEMU 配置: vhost-x86_64-tcg +# TAP + vhost-net + TCG 模拟(无 KVM) +# 适用于: x86_64 host 但 KVM 不可用 + +args = [ + "-nographic", + "-m", "2G", + "-cpu", "qemu64", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:57", + "-netdev", "tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on", + "-snapshot", +] + +uefi = true +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/net-bench-tap.sh" +success_regex = ["(?m)^NET_BENCH_PASSED\\s*$"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', "(?m)^NET_BENCH_FAILED:"] +timeout = 300 diff --git a/apps/starry/net-bench/results/README.md b/apps/starry/net-bench/results/README.md new file mode 100644 index 0000000000..22cb2109f2 --- /dev/null +++ b/apps/starry/net-bench/results/README.md @@ -0,0 +1,65 @@ +# net-bench baseline results + +本目录用于保存 StarryOS 网络基线测试结果。 + +## 文件约定 + +- `baseline-*.txt`:人工整理后的基线汇总,可提交,用于记录环境、命令和结果摘要。 +- `starry-*.txt`:`apps/starry/net-bench/run.sh` 生成的单次运行日志,默认不提交。 +- `iperf3-server-*.log`:host 侧 iperf3 server 日志,默认不提交。 + +## 推荐流程 + +```sh +# 默认 SLIRP/smp=1 +bash apps/starry/net-bench/run.sh --scenario slirp --arch aarch64 + +# vhost 多核扩展 +bash apps/starry/net-bench/run.sh --scenario vhost-smp4 --arch aarch64 + +# TAP/smp=1,需提前用 bin/setup 配置 tap0 +bash apps/starry/net-bench/run.sh --scenario tap --arch aarch64 +``` + +若 host 已用 `bin/setup` 配好 TAP + vhost 网络,也可以运行主力性能场景: + +```sh +bash apps/starry/net-bench/run.sh --scenario vhost --arch aarch64 +``` + +## 汇总模板 + +~~~md +# StarryOS 网络基线汇总 - YYYY-MM-DD + +## 环境 + +- Host OS: +- QEMU: +- iperf3: +- Arch: +- Rootfs: +- Commit: + +## 命令 + +```sh +bash apps/starry/net-bench/run.sh --scenario slirp --arch aarch64 +bash apps/starry/net-bench/run.sh --scenario vhost-smp4 --arch aarch64 +bash apps/starry/net-bench/run.sh --scenario tap --arch aarch64 +``` + +## 结果 + +| 场景 | TCP 1流 | TCP 4流 | UDP (target 1G) | 通过 | +|------|---------|---------|-----------------|------| +| slirp | | | | | +| vhost-smp4 | | | | | +| tap | | | | | + +## 备注 + +- 是否出现 `NET_BENCH_PASSED`: +- 是否有 host 侧端口/监听地址问题: +- TAP host 配置: +~~~ diff --git a/apps/starry/net-bench/results/baseline-all-2026-06-17.txt b/apps/starry/net-bench/results/baseline-all-2026-06-17.txt new file mode 100644 index 0000000000..1781e27ec0 --- /dev/null +++ b/apps/starry/net-bench/results/baseline-all-2026-06-17.txt @@ -0,0 +1,67 @@ +# StarryOS + Linux 网络性能基线汇总 — 2026-06-17 +# 环境:WSL2 (Linux 5.15.167.4-microsoft-standard-WSL2), QEMU aarch64 (cortex-a53) +# iperf3 3.19.1 (Alpine apk), 测试方向:Starry guest client → host server + +## 1. Starry smp=1, usermode 网络 (SLIRP, 10.0.2.2) + +| 指标 | 结果 | +|------|------| +| TCP 1-stream | 93.1 Mbit/s | +| TCP 4-stream | 93.9 Mbit/s | +| UDP (target 1G) | 70.9 Mbit/s, 60867 pkts, lost=0% | +| host CPU | ~100% (system 199%) | +| guest CPU | 4.0% | + +## 2. Starry smp=4, usermode 网络 (SLIRP) + +| 指标 | 结果 | +|------|------| +| TCP 1-stream | 70.1 Mbit/s (-25% vs smp=1) | +| TCP 4-stream | 80.1 Mbit/s | +| UDP (target 1G) | 62.9 Mbit/s, 54382 pkts, lost=0% | +| host CPU | ~100% | +| guest CPU | 3.2% / 2.0% | + +注:smp=4 TCP 吞吐比 smp=1 下降,说明多核调度开销 > 并发收益;锁竞争更严重。 + +## 3. Starry smp=1, TAP 网络 (192.168.100.x) + +| 指标 | 结果 | +|------|------| +| TCP 1-stream | 96.3 Mbit/s (+3% vs usermode) | +| TCP 4-stream | 91.0 Mbit/s | +| UDP (target 1G) | 62.7 Mbit/s, 54291 pkts, lost=0% | +| host CPU | ~100% | +| guest CPU | 3.3% / 2.2% | + +注:TAP 去掉 SLIRP 后 TCP 单流略有提升,但该场景尚未开启 vhost, + 也没有同拓扑 Linux guest 基线;当前只能作为 smoke baseline 和初步趋势观察。 + +## 4. Linux WSL2 loopback 基线(无 QEMU,参考上限) + +| 指标 | 结果 | +|------|------| +| TCP 1-stream | 174,837 Mbit/s (~174 Gbit/s) | +| TCP 4-stream | 506,842 Mbit/s (~507 Gbit/s) | +| UDP (target 1G) | 1,000 Mbit/s (达到目标上限) | + +注:loopback 走内核零拷贝,不代表真实网络,仅作 host 能力参考。 + +## 初步观察 + +1. **SLIRP smoke 数据约 70-93 Mbit/s**,只能用于确认 workflow,不用于正式性能结论 +2. **TAP(no vhost) TCP 单流约 96 Mbit/s**,可作为下一步 vhost/TAP baseline 的对照起点 +3. **smp=4 没有带来吞吐提升**,和全局 SERVICE/SOCKET_SET 锁、多核串行化的已知风险一致 +4. **host CPU 跑满(~100%)而 guest 只用 3-4%**,说明当前测量路径仍需进一步拆分 QEMU/host/guest 开销 +5. **UDP 实际吞吐约 63-71 Mbit/s**(目标 1G),提示需要补充小包 PPS、drop/retrans 和 cycles/packet 观测 + +这些数字尚未满足完整 benchmark 方法论要求:缺少 QEMU+TAP+vhost、同拓扑 Linux 基线、 +多次重复统计、环境指纹和 CPU/packet 归因数据。因此本文件记录的是 smoke baseline, +不是最终性能归因报告。 + +## 优化方向(按优先级) + +1. 消除 ax-net 全局大锁(SERVICE/SOCKET_SET)→ 预期多核扩展 + 单核吞吐提升 +2. RX 路径减少内存拷贝(smoltcp buffer → 用户缓冲区目前 4+ 次拷贝) +3. 增大 smoltcp 接收环(当前 64 包即满即丢) +4. 实现 virtio-net vhost-net + 多队列(绕过 QEMU 数据面) diff --git a/apps/starry/net-bench/results/baseline-starry-aarch64-2026-06-17.txt b/apps/starry/net-bench/results/baseline-starry-aarch64-2026-06-17.txt new file mode 100644 index 0000000000..9b1e04cebc --- /dev/null +++ b/apps/starry/net-bench/results/baseline-starry-aarch64-2026-06-17.txt @@ -0,0 +1,29 @@ +# StarryOS 网络性能基线 — 2026-06-17 +# 环境:WSL2 (Linux 5.15.167.4-microsoft-standard-WSL2), QEMU aarch64 (cortex-a53), usermode 网络 +# StarryOS: feat/net-enhance 分支, smp=1, iperf3 3.19.1 (Alpine apk) +# 测试:Starry guest 作为 iperf3 client → host iperf3 server (10.0.2.2:5201) + +## TCP 单流 (1 stream, 10s) +sender: 93.1 Mbit/s (118.8 MB in 10.2s) +receiver: 93.1 Mbit/s (118.8 MB in 10.2s) +retransmits: 0 +host CPU: ~100% (user 0.9% + system 199%) ← smoltcp 单核跑满 +guest CPU: 4.0% + +## TCP 4流 (4 streams, 10s) +sum_sent: 93.9 Mbit/s (210.2 MB in 17.9s) +sum_received: 93.9 Mbit/s (210.4 MB in 17.9s) +retransmits: 0 +备注: 4流之间严重不均衡 (stream0: 29.8M, stream1: 37.7M, stream2: 18.1M, stream3: 8.2M) + 总吞吐与单流几乎相同,证实单核/大锁瓶颈 + +## UDP (target 1Gbps, 10s) +sender bitrate: 70.9 Mbit/s (88.9 MB, 60867 packets) +lost: 0 / 60867 (0%) +jitter: 0.031 ms +备注: 目标 1Gbps,实际只达到 70.9 Mbps,受 SLIRP 用户模式网络栈限制 + +## 结论 +- QEMU usermode 网络上限约 93 Mbit/s (TCP) / 71 Mbit/s (UDP) +- 4流不展示多核扩展效果(smp=1,且锁竞争严重) +- 下一步:(1) smp=4 对比; (2) TAP 直连拓扑去除 SLIRP 瓶颈; (3) Linux guest 同拓扑基线 diff --git a/apps/starry/net-bench/run-linux-baseline.sh b/apps/starry/net-bench/run-linux-baseline.sh new file mode 100755 index 0000000000..92e1ff99fc --- /dev/null +++ b/apps/starry/net-bench/run-linux-baseline.sh @@ -0,0 +1,465 @@ +#!/usr/bin/env bash +# apps/starry/net-bench/run-linux-baseline.sh — Linux 基线性能测试 +# +# 用法: bash apps/starry/net-bench/run-linux-baseline.sh [arch] [scenario] [--repeat N] +# +# 目标:在与 Starry 完全相同的 QEMU+vhost 拓扑下运行 Linux guest, +# 以建立公平的性能基线对比。遵循 methodology §4.1 和 qemu-plan §6.1 的纪律。 +# +# 前置条件: +# 1. br0 + tap0 已配置(sudo bash apps/starry/net-bench/bin/setup) +# 2. 需要一个 Linux aarch64 内核和 rootfs(使用 Alpine Linux initramfs) +set -euo pipefail + +ARCH="aarch64" +SCENARIO="vhost" +REPEAT=1 +REBUILD_ROOTFS=0 + +# 解析参数 +positional=() +while [[ $# -gt 0 ]]; do + case "$1" in + --repeat) + REPEAT="${2:-}"; shift 2 + [[ "$REPEAT" =~ ^[1-9][0-9]*$ ]] || { echo "error: --repeat needs a positive integer" >&2; exit 1; } + ;; + --repeat=*) + REPEAT="${1#*=}"; shift + [[ "$REPEAT" =~ ^[1-9][0-9]*$ ]] || { echo "error: --repeat needs a positive integer" >&2; exit 1; } + ;; + --rebuild-rootfs) + REBUILD_ROOTFS=1; shift ;; + -h|--help|help) + positional+=("help"); shift ;; + *) + positional+=("$1"); shift ;; + esac +done +[[ ${#positional[@]} -ge 1 ]] && ARCH="${positional[0]}" +[[ ${#positional[@]} -ge 2 ]] && SCENARIO="${positional[1]}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKSPACE="$(cd "$SCRIPT_DIR/../../.." && pwd)" +RESULTS_DIR="$SCRIPT_DIR/results" +LINUX_DIR="$SCRIPT_DIR/linux-baseline" +TIMESTAMP="$(date +%Y%m%d-%H%M%S)" +TAP_HOST_IP="${TAP_HOST_IP:-192.168.100.1}" +TAP_GUEST_IP="${TAP_GUEST_IP:-192.168.100.2}" + +mkdir -p "$RESULTS_DIR" "$LINUX_DIR" + +usage() { + cat >&2 <&2 + exit 1 + fi + if [[ ! -e /dev/vhost-net ]]; then + echo "error: /dev/vhost-net not found. Run: sudo modprobe vhost_net" >&2 + exit 1 + fi + + # 检查网络配置 + if ! ip addr show br0 &>/dev/null; then + echo "error: br0 not found. Run: sudo bash apps/starry/net-bench/bin/setup" >&2 + exit 1 + fi + + # 检查 Linux 内核 + if [[ ! -f "$LINUX_DIR/vmlinuz" ]]; then + echo "info: Linux kernel not found, will use host kernel if available" >&2 + fi +} + +# 校验 initramfs 是否为完整可用的 gzip+cpio 归档。 +# 返回 0 表示有效,非 0 表示缺失或损坏(调用方据此决定是否重建)。 +validate_initramfs() { + local rootfs="$1" + [[ -f "$rootfs" ]] || return 1 + # gzip 完整性 + gzip -t "$rootfs" 2>/dev/null || return 1 + # cpio 归档可完整列出,并且包含 init 入口 + local listing + listing="$(gzip -dc "$rootfs" 2>/dev/null | cpio -it 2>/dev/null)" || return 1 + [[ -n "$listing" ]] || return 1 + grep -qx "./init\|init" <<<"$listing" || return 1 + return 0 +} + +# 定位与 Starry 同源的受管 Alpine ext4 rootfs 镜像(内含 busybox/iperf3/ip/nc)。 +# 兼容平铺 (rootfs/) 与嵌套 (rootfs//) 两种受管布局; +# 缺失时尝试通过 xtask 拉取;仍失败则明确报错。 +locate_alpine_image() { + local image_name="rootfs-${ARCH}-alpine.img" + local flat="$WORKSPACE/tmp/axbuild/rootfs/$image_name" + local nested="$WORKSPACE/tmp/axbuild/rootfs/$image_name/$image_name" + + if [[ ! -f "$flat" && ! -f "$nested" ]]; then + echo "=== Alpine rootfs image missing, fetching via xtask ===" >&2 + ( cd "$WORKSPACE" && cargo xtask starry rootfs --arch "$ARCH" ) >&2 || { + echo "error: failed to ensure Alpine rootfs via 'cargo xtask starry rootfs --arch $ARCH'" >&2 + return 1 + } + fi + local img + for img in "$flat" "$nested"; do + if [[ -f "$img" ]]; then + printf '%s\n' "$img" + return 0 + fi + done + echo "error: managed Alpine rootfs not found at $flat (or $nested)" >&2 + return 1 +} + +# 准备 Linux guest initramfs:从受管 Alpine rootfs 提取真实的 +# busybox/iperf3/ip/nc 及依赖库,注入 init 脚本后打包为 cpio.gz。 +# 这样得到的 initramfs 真实可启动,且与 Starry 测试使用同一镜像来源,可复现。 +prepare_linux_rootfs() { + local rootfs="$LINUX_DIR/initramfs.cpio.gz" + + if [[ "$REBUILD_ROOTFS" -eq 0 ]] && validate_initramfs "$rootfs"; then + echo "=== Reusing valid Linux baseline initramfs: $rootfs ===" + return 0 + fi + + if [[ -f "$rootfs" ]] && [[ "$REBUILD_ROOTFS" -eq 0 ]]; then + echo "warning: existing initramfs is missing/corrupt, rebuilding: $rootfs" >&2 + fi + rm -f "$rootfs" + + command -v debugfs >/dev/null 2>&1 || { echo "error: install e2fsprogs (debugfs)" >&2; exit 1; } + command -v cpio >/dev/null 2>&1 || { echo "error: install cpio" >&2; exit 1; } + + local alpine_img + alpine_img="$(locate_alpine_image)" || exit 1 + + echo "=== Building Linux baseline initramfs from $alpine_img ===" + + local tmpdir="$LINUX_DIR/rootfs-build" + rm -rf "$tmpdir" + mkdir -p "$tmpdir" + + # 从 Alpine ext4 镜像完整提取根文件系统(含 busybox/iperf3/ip/nc 及 musl 库)。 + debugfs -R "rdump / $tmpdir" "$alpine_img" >/dev/null 2>&1 || { + echo "error: debugfs rdump failed on $alpine_img" >&2 + exit 1 + } + + # 校验关键依赖确实存在,避免打包出无法运行测试的 initramfs。 + local missing=0 dep + for dep in bin/busybox usr/bin/iperf3; do + [[ -e "$tmpdir/$dep" ]] || { echo "error: $dep missing in extracted rootfs" >&2; missing=1; } + done + [[ "$missing" -eq 0 ]] || exit 1 + + # 注入 init 脚本(busybox ash 兼容:无 brace 展开,循环用 seq)。 + cat > "$tmpdir/init" <<'INIT_SCRIPT' +#!/bin/sh +# Linux baseline init script (busybox ash compatible) + +mount -t proc none /proc 2>/dev/null +mount -t sysfs none /sys 2>/dev/null +mount -t devtmpfs none /dev 2>/dev/null + +# 配置网络(拓扑对齐 Starry:192.168.100.2/24,网关 .1) +ip link set lo up +ip link set eth0 up +ip addr add 192.168.100.2/24 dev eth0 +ip route add default via 192.168.100.1 + +echo "=== Linux baseline: starting net-bench tests ===" +HOST_IP="192.168.100.1" +PORT=5201 +DURATION=5 + +# Mirror net-bench-common.sh run_test — keep in sync (duration/port/loads). +run_test() { + test_id="$1" + shift + local warmup_iters=1 + local measured_iters=5 + local iter=0 + local total=$((warmup_iters + measured_iters)) + while [ "$iter" -lt "$total" ]; do + if [ "$iter" -lt "$warmup_iters" ]; then + warm=1 + else + warm=0 + fi + echo "NET_BENCH_BEGIN test=$test_id iter=$iter warmup=$warm" + echo "NET_STATS_BEGIN warmup=$warm" + cat /proc/net/dev + echo "NET_STATS_END" + if iperf3 -c "$HOST_IP" -p "$PORT" -t "$DURATION" -J "$@"; then + echo "NET_BENCH_END test=$test_id iter=$iter" + else + echo "NET_BENCH_END test=$test_id iter=$iter" + if [ "$warm" -eq 0 ]; then + echo "NET_BENCH_FAILED: $test_id iteration $iter" + exit 1 + fi + echo "NET_BENCH_WARN: $test_id warmup iteration $iter failed (ignored)" + fi + echo "NET_STATS_BEGIN warmup=$warm" + cat /proc/net/dev + echo "NET_STATS_END" + iter=$((iter + 1)) + done +} + +# 等待 host iperf3 server 就绪 +for i in $(seq 1 15); do + if nc -z "$HOST_IP" 5201 2>/dev/null; then + echo "=== iperf3 server ready ===" + break + fi + sleep 1 +done + +run_test tcp1 +run_test tcp4 -P 4 +run_test tcp1r -R +run_test udp1g -u -b 1G +run_test udp64 -u -l 64 -b 100M + +echo "NET_BENCH_PASSED" +poweroff -f +INIT_SCRIPT + + chmod +x "$tmpdir/init" + + # 仅打包基准必需的最小集合。完整 Alpine 测试 rootfs(含 /opt、/guest 等 + # 测试资产,>1.5G)会超出 initramfs 解包内存预算:-m 2G 下内核报 + # "Initramfs unpacking failed: write error" 并 panic。最小集合 = + # busybox(+applet 符号链接) + iperf3 及其共享库 + /etc 基础配置。 + local staging="$LINUX_DIR/initramfs-staging" + rm -rf "$staging" + mkdir -p "$staging"/{dev,proc,sys,tmp,run,root,var,usr/bin,usr/lib} + cp -a "$tmpdir/bin" "$tmpdir/sbin" "$tmpdir/lib" "$tmpdir/etc" "$staging/" + cp -a "$tmpdir/usr/bin/iperf3" "$staging/usr/bin/" + [[ -e "$tmpdir/usr/bin/nc" ]] && cp -a "$tmpdir/usr/bin/nc" "$staging/usr/bin/" + # libiperf.so.0 的 NEEDED 依赖链:libcrypto.so.3 + musl(musl 已随 /lib 拷贝)。 + cp -a "$tmpdir"/usr/lib/libiperf.so* "$tmpdir"/usr/lib/libcrypto.so* "$staging/usr/lib/" + cp -a "$tmpdir/init" "$staging/init" + + # 打包成 cpio.gz + ( cd "$staging" && find . | cpio -o -H newc 2>/dev/null | gzip ) > "$rootfs" + + # 自校验:确保新生成的 initramfs 真实可用 + validate_initramfs "$rootfs" || { + echo "error: freshly built initramfs failed validation: $rootfs" >&2 + exit 1 + } + + echo "=== Linux initramfs created: $rootfs ($(du -h "$rootfs" | cut -f1)) ===" +} + +# 确保 x86_64 guest 内核可用:优先 $LINUX_DIR/vmlinuz;缺失时通过 +# apt-get download 拉取 Ubuntu generic 内核并解包(该内核 virtio_net/ +# virtio_pci/failover 均为 built-in,无需向 initramfs 注入模块)。 +ensure_x86_kernel() { + local vmlinuz="$LINUX_DIR/vmlinuz" + [[ -f "$vmlinuz" ]] && return 0 + + command -v apt-get >/dev/null 2>&1 || { echo "error: apt-get unavailable; place an x86_64 kernel at $vmlinuz" >&2; return 1; } + command -v dpkg-deb >/dev/null 2>&1 || { echo "error: dpkg-deb unavailable; place an x86_64 kernel at $vmlinuz" >&2; return 1; } + + local pkg + pkg="$(apt-cache search --names-only '^linux-image-unsigned-[0-9.-]+-generic$' \ + | awk '{print $1}' | sort -V | tail -1)" + [[ -n "$pkg" ]] || { echo "error: no linux-image-unsigned-*-generic candidate found" >&2; return 1; } + + echo "=== Fetching x86_64 guest kernel: $pkg ===" >&2 + local dl_dir="$LINUX_DIR/kernel-pkg" + rm -rf "$dl_dir" + mkdir -p "$dl_dir" + ( cd "$dl_dir" && apt-get download "$pkg" >&2 && dpkg-deb -x "$pkg"_*.deb extract/ ) || { + echo "error: failed to download/extract $pkg" >&2 + return 1 + } + local bz + bz="$(find "$dl_dir/extract/boot" -maxdepth 1 -name 'vmlinuz-*' | head -1)" + [[ -n "$bz" ]] || { echo "error: vmlinuz not found inside $pkg" >&2; return 1; } + cp "$bz" "$vmlinuz" + basename "$bz" | sed 's/^vmlinuz-//' > "$LINUX_DIR/kernel-version" + rm -rf "$dl_dir" + echo "=== x86_64 guest kernel ready: $vmlinuz ($(cat "$LINUX_DIR/kernel-version")) ===" >&2 +} + +# 运行 Linux baseline 测试 +run_linux_test() { + local repeat_id="$1" + local smp="${2:-1}" + local log_file="$RESULTS_DIR/linux-baseline-${ARCH}-${SCENARIO}-${TIMESTAMP}-r${repeat_id}.txt" + + echo "=== Linux baseline test (repeat $repeat_id/$REPEAT, smp=$smp) ===" | tee "$log_file" + + # 解析 Linux 内核镜像:优先使用 baseline 目录内显式放置的内核, + # 否则在 host 架构与目标架构一致时回退到 host 内核;不一致则明确报错。 + local kernel_img="" + if [[ -f "$LINUX_DIR/vmlinuz" ]]; then + kernel_img="$LINUX_DIR/vmlinuz" + elif [[ "$(uname -m)" == "$ARCH" && -f "/boot/vmlinuz-$(uname -r)" ]]; then + kernel_img="/boot/vmlinuz-$(uname -r)" + fi + if [[ -z "$kernel_img" ]]; then + echo "error: no usable $ARCH Linux kernel found" >&2 + echo "hint: place a $ARCH kernel at $LINUX_DIR/vmlinuz, or run on a $ARCH host" >&2 + return 1 + fi + echo "=== Using kernel: $kernel_img ===" | tee -a "$log_file" + + # QEMU 参数:网络设备/后端与 Starry 对应 vhost--kvm.toml 完全一致 + # (plain virtio-net-pci + tap,vhost=on,不额外附加 mq/vectors/offload)。 + local qemu_cmd=( + "qemu-system-$ARCH" + -cpu host + -accel kvm + -m 2G + -smp "$smp" + -kernel "$kernel_img" + -initrd "$LINUX_DIR/initramfs.cpio.gz" + -device "virtio-net-pci,netdev=net0,mac=52:54:00:12:34:57" + -netdev "tap,id=net0,ifname=tap0,script=no,downscript=no,vhost=on" + -nographic + -serial mon:stdio + -no-reboot + ) + # panic=-1: 内核 panic(如 init 失败退出)立即触发重启请求,配合 + # -no-reboot 让 QEMU 直接退出,避免失败场景空耗 300s 超时。 + case "$ARCH" in + aarch64) + qemu_cmd+=(-machine virt -append "console=ttyAMA0 quiet panic=-1") + ;; + x86_64) + qemu_cmd+=(-append "console=ttyS0 quiet panic=-1") + ;; + esac + + # 启动 iperf3 server + echo "=== Starting iperf3 server on $TAP_HOST_IP:5201 ===" | tee -a "$log_file" + iperf3 -s -p 5201 -B "$TAP_HOST_IP" & + local iperf_pid=$! + trap "kill $iperf_pid 2>/dev/null || true" EXIT + + sleep 2 + + # 运行 QEMU + echo "=== Running Linux guest ===" | tee -a "$log_file" + timeout 300 "${qemu_cmd[@]}" 2>&1 | tee -a "$log_file" || { + local ret=$? + if [[ $ret -eq 124 ]]; then + echo "error: QEMU timeout after 300s" >&2 + fi + kill $iperf_pid 2>/dev/null || true + return 1 + } + + kill $iperf_pid 2>/dev/null || true + trap - EXIT + + echo "=== Test complete: $log_file ===" | tee -a "$log_file" +} + +# 主流程 +main() { + if [[ "${positional[0]:-}" == "help" ]]; then + usage + exit 0 + fi + + case "$ARCH" in + aarch64|x86_64) ;; + *) + echo "error: unsupported arch '$ARCH' (aarch64|x86_64)" >&2 + exit 1 + ;; + esac + + check_prereq + if [[ "$ARCH" == "x86_64" ]]; then + ensure_x86_kernel || exit 1 + fi + prepare_linux_rootfs + + # 确定 smp 参数 + local smp=1 + if [[ "$SCENARIO" == "vhost-smp4" ]]; then + smp=4 + fi + + # 写环境指纹 + local fingerprint="$RESULTS_DIR/fingerprint-linux-baseline-${ARCH}-${SCENARIO}-${TIMESTAMP}.txt" + { + echo "# Linux baseline environment fingerprint" + echo "timestamp : $TIMESTAMP" + echo "arch : $ARCH" + echo "scenario : $SCENARIO (Linux guest)" + echo "repeat : $REPEAT" + echo "smp : $smp" + echo "host_uname : $(uname -a)" + echo "host_nproc : $(nproc 2>/dev/null || echo '?')" + echo "qemu : $(qemu-system-"$ARCH" --version 2>/dev/null | head -1)" + echo "iperf3 : $(iperf3 --version 2>/dev/null | head -1)" + echo "guest_kernel: $(cat "$LINUX_DIR/kernel-version" 2>/dev/null || echo 'external (see run log)')" + echo "kvm : present" + echo "vhost_net : present" + echo "topology : same as Starry (vhost-net + KVM + virtio-net-pci)" + } > "$fingerprint" + echo "=== Linux baseline fingerprint -> $fingerprint ===" + cat "$fingerprint" + + # 运行测试 + for r in $(seq 1 "$REPEAT"); do + run_linux_test "$r" "$smp" + done + + # 汇总结果 + echo "=== Summarizing Linux baseline results ===" + python3 "$SCRIPT_DIR/core/summarize.py" "$RESULTS_DIR"/linux-baseline-${ARCH}-${SCENARIO}-${TIMESTAMP}-r*.txt \ + > "$RESULTS_DIR/summary-linux-baseline-${ARCH}-${SCENARIO}-${TIMESTAMP}.txt" + + cat "$RESULTS_DIR/summary-linux-baseline-${ARCH}-${SCENARIO}-${TIMESTAMP}.txt" + + echo "" + echo "=== Linux baseline test complete ===" + echo "Results: $RESULTS_DIR/summary-linux-baseline-${ARCH}-${SCENARIO}-${TIMESTAMP}.txt" + echo "" + echo "Next: Compare with Starry baseline using the comparison script" +} + +main "$@" diff --git a/apps/starry/net-bench/run-with-perf.sh b/apps/starry/net-bench/run-with-perf.sh new file mode 100755 index 0000000000..bfa9db3b51 --- /dev/null +++ b/apps/starry/net-bench/run-with-perf.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# apps/starry/net-bench/run-with-perf.sh — 集成 perf stat 采集 cycles/instructions +# +# ⚠️ DEPRECATED: 请使用 `run.sh --with-perf` 代替。 +# 本脚本将继续保留以支持额外的 LLC-load-misses 计数器,但新的开发应 +# 优先使用 `run.sh --with-perf`(该入口将 perf 输出传给 summarize.py +# 统一渲染 CPU Efficiency 章节,并提供 IPC 和 cache-miss-rate)。 +# +# 用法: bash run-with-perf.sh [--arch A] [--scenario S] [--accel A] +# +# 目标:在运行 net-bench 的同时,用 perf stat 采集 CPU 性能计数器, +# 计算 cycles/byte 和 cycles/packet(methodology §1 核心 KPI)。 +# +# 说明:这是显式入口 run.sh 的 perf 包裹变体,复用 core/lib.sh 公共流程, +# 保证 iperf3 生命周期、前置检查、配置矩阵、汇总与 run.sh 完全一致。 +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=core/lib.sh +. "$SCRIPT_DIR/core/lib.sh" + +ARCH="aarch64" +SCENARIO="vhost" +ACCEL="" + +usage() { + cat >&2 <--.txt + - results/summary---.txt +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --arch) ARCH="${2:-}"; shift 2 ;; + --scenario) SCENARIO="${2:-}"; shift 2 ;; + --accel) ACCEL="${2:-}"; shift 2 ;; + -h|--help|help) usage; exit 0 ;; + *) nb_error "未知选项: $1"; usage; exit 1 ;; + esac +done + +nb_validate_arch "$ARCH" || exit 1 +nb_validate_scenario "$SCENARIO" || exit 1 +[[ -z "$ACCEL" ]] && ACCEL="$(nb_default_accel "$ARCH")" + +nb_require_cmd perf "apt-get install linux-tools-generic" +nb_require_cmd iperf3 +nb_require_cmd qemu-system-"$ARCH" + +TIMESTAMP="$(date +%Y%m%d-%H%M%S)" +mkdir -p "$NB_RESULTS_DIR" + +QEMU_CONFIG="$(nb_qemu_config "$SCENARIO" "$ARCH" "$ACCEL")" +[[ -f "$QEMU_CONFIG" ]] || nb_die "QEMU 配置不存在: $QEMU_CONFIG" +nb_check_scenario_prereq "$SCENARIO" "$ACCEL" + +BIND_ADDR="" +nb_scenario_needs_tap "$SCENARIO" && BIND_ADDR="$NB_TAP_HOST_IP" + +PERF_OUT="$NB_RESULTS_DIR/perf-stat-${ARCH}-${SCENARIO}-${TIMESTAMP}.txt" +RESULT_FILE="$NB_RESULTS_DIR/starry-${ARCH}-${SCENARIO}-${TIMESTAMP}.txt" +SERVER_LOG="$NB_RESULTS_DIR/iperf3-server-${ARCH}-${SCENARIO}-${TIMESTAMP}.log" + +nb_section "net-bench + perf stat (arch=$ARCH, scenario=$SCENARIO, accel=$ACCEL)" +nb_start_iperf3 "$BIND_ADDR" "$SERVER_LOG" +trap 'nb_stop_iperf3' EXIT + +nb_info "perf 输出: $PERF_OUT" +# guest 走 DHCP 获取地址,无需注入 AX_* 环境变量(见 lib.sh nb_guest_env_vars)。 +(cd "$NB_WORKSPACE" && \ + perf stat -o "$PERF_OUT" \ + -e cycles,instructions,cache-references,cache-misses,LLC-load-misses \ + -- cargo xtask starry app qemu --test-case net-bench --arch "$ARCH" \ + --qemu-config "$QEMU_CONFIG") 2>&1 | tee "$RESULT_FILE" + +nb_stop_iperf3 +trap - EXIT + +echo "" +nb_section "perf stat 结果" +cat "$PERF_OUT" + +# 提取 cycles / instructions,给出 IPC(cycles/byte 需结合 summary 总字节手算)。 +CYCLES=$(grep -E '^\s*[0-9,]+\s+cycles' "$PERF_OUT" | awk '{gsub(/,/,"",$1); print $1}') +INSTRUCTIONS=$(grep -E '^\s*[0-9,]+\s+instructions' "$PERF_OUT" | awk '{gsub(/,/,"",$1); print $1}') +if [[ -n "$CYCLES" && -n "$INSTRUCTIONS" ]] && command -v bc >/dev/null 2>&1; then + IPC=$(echo "scale=3; $INSTRUCTIONS / $CYCLES" | bc -l) + echo "" + nb_info "Total cycles: $CYCLES" + nb_info "Total instructions: $INSTRUCTIONS" + nb_info "IPC: $IPC" + nb_info "cycles/byte = cycles / (summary 中 throughput*时长 推算的总字节)" +else + nb_warn "未能从 perf 输出提取 cycles/instructions(或缺少 bc)" +fi + +echo "" +nb_summarize "$NB_RESULTS_DIR/summary-${ARCH}-${SCENARIO}-${TIMESTAMP}.txt" "$RESULT_FILE" + +echo "" +nb_section "完成" +nb_info "测试汇总: $NB_RESULTS_DIR/summary-${ARCH}-${SCENARIO}-${TIMESTAMP}.txt" +nb_info "perf stat: $PERF_OUT" diff --git a/apps/starry/net-bench/run.sh b/apps/starry/net-bench/run.sh new file mode 100755 index 0000000000..f0adfd6423 --- /dev/null +++ b/apps/starry/net-bench/run.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# apps/starry/net-bench/run.sh — StarryOS 网络性能测试的唯一严肃入口 +# +# 设计:本入口"参数明确"——架构、场景、加速器都显式指定(或用明确默认值), +# 不做隐式环境探测。智能/自动检测属于实验性能力,单独放在 bin/bench, +# 默认不参与严肃测试流程(见 README "智能入口(实验性)"一节)。 +# +# 用法: +# bash apps/starry/net-bench/run.sh [options] +# +# options: +# --scenario S slirp|tap|vhost|vhost-smp4|tap-smp4(默认 vhost) +# --arch A aarch64|x86_64(默认 aarch64) +# --accel A kvm|tcg(默认:同架构且 /dev/kvm 可用时 kvm,否则 tcg) +# --repeat N 每个场景重启 QEMU 跑 N 次,汇总跨启动方差(默认 1) +# --no-summary 跳过 summarize.py 汇总 +# --with-perf 在 QEMU 外侧采集 perf stat CPU 计数器 +# -h, --help 显示帮助 +# +# 兼容旧用法:前两个位置参数仍按 [arch] [scenario] 解析。 +# +# 每次 QEMU 启动内部,guest 脚本跑 warmup + ITERS 次迭代(见 net-bench-common.sh), +# 单次 --repeat 已给出 within-boot 的 mean/stddev;--repeat>1 额外覆盖 cross-boot 方差。 +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=core/lib.sh +. "$SCRIPT_DIR/core/lib.sh" + +ARCH="aarch64" +SCENARIO="vhost" +ACCEL="" +REPEAT=1 +DO_SUMMARY=true +WITH_PERF=false + +usage() { + cat >&2 </dev/null 2>&1; then + (cd "$NB_WORKSPACE" && \ + perf stat -e cycles,instructions,cache-references,cache-misses \ + -o "$perf_log" -- \ + cargo xtask starry app qemu --test-case net-bench --arch "$ARCH" \ + --qemu-config "$qemu_config") 2>&1 | tee "$result_file" + else + (cd "$NB_WORKSPACE" && \ + cargo xtask starry app qemu --test-case net-bench --arch "$ARCH" \ + --qemu-config "$qemu_config") 2>&1 | tee "$result_file" + fi + + nb_stop_iperf3 + run_logs+=("$result_file") + nb_info "结果已保存: $result_file" + done + + if [[ "$DO_SUMMARY" == "true" ]]; then + local perf_args=() + if [[ "$WITH_PERF" == "true" ]]; then + for ((rep = 1; rep <= REPEAT; rep++)); do + local pf="$NB_RESULTS_DIR/perf-$ARCH-$scenario-$TIMESTAMP-r${rep}.txt" + [[ -f "$pf" ]] && perf_args+=(--perf "$pf") + done + fi + nb_summarize \ + "$NB_RESULTS_DIR/summary-$ARCH-$scenario-$TIMESTAMP.txt" \ + "${run_logs[@]}" "${perf_args[@]}" + fi +} + +run_one "$SCENARIO" diff --git a/apps/starry/net-bench/wslconfig-example.txt b/apps/starry/net-bench/wslconfig-example.txt new file mode 100644 index 0000000000..16b42ba8ad --- /dev/null +++ b/apps/starry/net-bench/wslconfig-example.txt @@ -0,0 +1,80 @@ +# WSL2 降噪配置样例 +# +# 文件位置: %USERPROFILE%\.wslconfig (Windows 用户目录) +# 例如: C:\Users\YourName\.wslconfig +# +# 修改后需重启 WSL: wsl --shutdown +# +# 这个配置的目标是压制 WSL2/Windows 环境的结构性噪声源: +# - 固定 CPU/内存,避免动态分配抖动 +# - 关闭内存回收上报,减少周期性停顿 +# - 启用嵌套虚拟化(必须,确保 /dev/kvm 可用) +# - 可选:隔离部分核专供 QEMU + +[wsl2] +# 固定 vCPU 数量(必须 >= QEMU 要用的核数 + host 工具核数) +# 例如: QEMU smp=4 + host iperf3/vhost 线程 = 至少 6-8 核 +processors=8 + +# 固定内存,避免动态回收停顿(根据物理机调整,建议 >= 16GB) +memory=16GB + +# 关闭 swap 或给足且固定(避免换页尖刺) +# 选项 1(推荐): 关闭 swap +swap=0 +# 选项 2: 固定 swap 大小 +# swap=8GB +# swapFile=C:\\temp\\wsl-swap.vhdx + +# 关闭内存回收上报(减少周期性停顿) +pageReporting=false + +# 启用嵌套虚拟化(必须,否则 /dev/kvm 不可用) +nestedVirtualization=true + +# 可选: 通过内核参数隔离部分核专供 QEMU(高级) +# 例如: 8 核机器,隔离 4-7 核给 QEMU,0-3 核给 WSL2 系统任务 +# 需配合 taskset 使用: taskset -c 4-7 cargo xtask ... +# kernelCommandLine=isolcpus=4-7 + +# 可选: 禁用 Windows 侧的 Hyper-V 动态内存(进一步降噪) +# 需在 Windows 侧设置,WSL2 无法直接控制 +# 参考: Hyper-V Manager -> WSL 虚拟机 -> Settings -> Memory -> 取消勾选 "Enable Dynamic Memory" + +# 可选: 固定 WSL2 虚拟机的优先级(Windows 侧) +# 在任务管理器中找到 vmmem 进程,右键 -> Set priority -> High + +# ============================================================ +# 降噪纪律检查清单 +# ============================================================ +# Windows 宿主层: +# [ ] 电源计划设为"高性能/卓越性能" +# [ ] 关闭睡眠与 CPU 降频(BIOS C-state/Turbo 设置) +# [ ] 测试期间关闭杀毒实时扫描、Windows Update +# [ ] 关闭 Windows 内存压缩(可选) +# +# WSL2 层(本文件): +# [ ] processors 固定且 >= 测试需求 +# [ ] memory 固定且充足(不吃紧物理内存) +# [ ] swap 关闭或固定 +# [ ] pageReporting=false +# [ ] nestedVirtualization=true +# +# QEMU / guest 绑核(脚本侧): +# [ ] QEMU 用 taskset -c 绑到隔离核 +# [ ] -smp 与绑核数一致 +# [ ] guest 内用 taskset 把 iperf3/netperf 绑固定核 +# [ ] vhost-net 内核线程(vhost-$pid)绑独立核 +# +# 测量纪律: +# [ ] 每个数据点 >= 5 次,取均值 + 标准差 +# [ ] warmup(丢弃前 1-2 次结果) +# [ ] 固定测试时长(如 iperf3 -t 30) +# [ ] 固定 MTU(先 1500) +# [ ] 记录环境指纹(WSL 内核版本、QEMU 版本、commit、offload 状态) +# [ ] 标准差 > 10% 判为不可信,需排查噪声后重测 +# +# 可比性红线: +# [ ] Starry 与 Linux 基线在相同 .wslconfig 下背靠背测试 +# [ ] 相同绑核、相同 QEMU 参数、相同 host 负载 +# [ ] 中间不重启 Windows、不改电源计划 diff --git a/net/ax-net/src/router.rs b/net/ax-net/src/router.rs index 18f2b7e86a..093d55a969 100644 --- a/net/ax-net/src/router.rs +++ b/net/ax-net/src/router.rs @@ -315,6 +315,21 @@ impl DeviceHandle { /// `rx_packets` is incremented for every call regardless of `len`. Callers /// must ensure `len > 0` when counting a real reception; a zero `len` only /// makes sense for testing or diagnostic paths. + /// + /// Kept out-of-line (`#[inline(never)]`) so that eBPF kprobe-based + /// observers (e.g. `net_stats`) can attach to this function and see every + /// increment of the RX byte/packet counters at a stable symbol. + /// + /// # Performance note + /// + /// `#[inline(never)]` adds a call/ret boundary (~2 instructions) around + /// two `fetch_add` operations that would otherwise be inlined directly + /// into each call site. At the current target throughput (< 100K packets + /// per second) the overhead is negligible. If higher throughput becomes a + /// concern, consider feature-gating the annotation behind an + /// `ebpf_observability` cfg flag so production builds can opt into full + /// inlining. + #[inline(never)] fn count_rx(&self, len: usize) { // Relaxed ordering is sufficient: only the owning device worker writes // each counter, and /proc/net/dev readers tolerate slight staleness. @@ -326,6 +341,21 @@ impl DeviceHandle { /// /// `tx_packets` is incremented for every call regardless of `len`. Callers /// must ensure `len > 0` when counting a real transmission. + /// + /// Kept out-of-line (`#[inline(never)]`) so that eBPF kprobe-based + /// observers (e.g. `net_stats`) can attach to this function and see every + /// increment of the TX byte/packet counters at a stable symbol. + /// + /// # Performance note + /// + /// `#[inline(never)]` adds a call/ret boundary (~2 instructions) around + /// two `fetch_add` operations that would otherwise be inlined directly + /// into each call site. At the current target throughput (< 100K packets + /// per second) the overhead is negligible. If higher throughput becomes a + /// concern, consider feature-gating the annotation behind an + /// `ebpf_observability` cfg flag so production builds can opt into full + /// inlining. + #[inline(never)] fn count_tx(&self, len: usize) { self.tx_bytes.fetch_add(len as u64, Ordering::Relaxed); self.tx_packets.fetch_add(1, Ordering::Relaxed); diff --git a/scripts/axbuild/src/rootfs/qemu.rs b/scripts/axbuild/src/rootfs/qemu.rs index 43ceeabe6b..b9afebfc9f 100644 --- a/scripts/axbuild/src/rootfs/qemu.rs +++ b/scripts/axbuild/src/rootfs/qemu.rs @@ -56,7 +56,9 @@ impl RootfsQemuWiring { } fn net_device_matches(self, value: &str) -> bool { - self.net_devices.contains(&value) + self.net_devices + .iter() + .any(|dev| value == *dev || value.starts_with(&format!("{},", dev))) } fn netdev_arg(self) -> String {