From 71205073a7e2f481553bce2dd18cbdd844662991 Mon Sep 17 00:00:00 2001 From: CongkaiTan Date: Fri, 26 Jun 2026 17:47:02 -0500 Subject: [PATCH] Introduce new PMU config format and ungrouped collection mode --- Cargo.lock | 337 +---- Cargo.toml | 16 +- src/data.rs | 7 +- src/data/common/processed_data_accessor.rs | 22 +- src/data/common/time_series_data_processor.rs | 16 +- src/data/common/utils.rs | 175 ++- src/data/perf_stat.rs | 1338 ++++++++++++----- src/lib.rs | 81 +- src/pmu.rs | 321 +--- src/pmu_configs/amd_genoa_ctrs.json | 20 - src/pmu_configs/amd_genoa_pmu_config.json | 9 + src/pmu_configs/amd_milan_ctrs.json | 25 - src/pmu_configs/amd_milan_pmu_config.json | 10 + src/pmu_configs/amd_perf_list.json | 200 --- src/pmu_configs/amd_pmu_config.json | 29 + src/pmu_configs/grv_5_pmu_config.json | 8 + src/pmu_configs/grv_perf_list.json | 254 ---- src/pmu_configs/grv_pmu_config.json | 35 + src/pmu_configs/intel_icelake_ctrs.json | 38 - src/pmu_configs/intel_icelake_pmu_config.json | 12 + src/pmu_configs/intel_perf_list.json | 254 ---- src/pmu_configs/intel_pmu_config.json | 35 + .../intel_sapphire_rapids_ctrs.json | 164 -- .../intel_sapphire_rapids_pmu_config.json | 27 + src/record.rs | 14 +- src/report.rs | 45 +- src/visualizer.rs | 7 + tests/test_aperf.rs | 2 + tests/test_aperf_stats.rs | 1 + tests/test_java_profile.rs | 1 + tests/test_perf_stat.rs | 894 +++++++---- 31 files changed, 2025 insertions(+), 2372 deletions(-) delete mode 100644 src/pmu_configs/amd_genoa_ctrs.json create mode 100644 src/pmu_configs/amd_genoa_pmu_config.json delete mode 100644 src/pmu_configs/amd_milan_ctrs.json create mode 100644 src/pmu_configs/amd_milan_pmu_config.json delete mode 100644 src/pmu_configs/amd_perf_list.json create mode 100644 src/pmu_configs/amd_pmu_config.json create mode 100644 src/pmu_configs/grv_5_pmu_config.json delete mode 100644 src/pmu_configs/grv_perf_list.json create mode 100644 src/pmu_configs/grv_pmu_config.json delete mode 100644 src/pmu_configs/intel_icelake_ctrs.json create mode 100644 src/pmu_configs/intel_icelake_pmu_config.json delete mode 100644 src/pmu_configs/intel_perf_list.json create mode 100644 src/pmu_configs/intel_pmu_config.json delete mode 100644 src/pmu_configs/intel_sapphire_rapids_ctrs.json create mode 100644 src/pmu_configs/intel_sapphire_rapids_pmu_config.json diff --git a/Cargo.lock b/Cargo.lock index 1cfa135b..97f34f6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,27 +97,25 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "aperf" -version = "1.2.3" +version = "1.3.0" dependencies = [ "anyhow", "aws-config", "bincode", "cc", - "cfg-if", "chrono", "clap", "clap_complete", "cpp_demangle", - "csv", "csv-to-html", "ctor", + "exmex", "flate2", "gimli", - "include_directory", + "include_dir", "indexmap", "infer", "inferno", - "inquire", "itoa", "lazy_static", "libc", @@ -129,9 +127,11 @@ dependencies = [ "nix", "numeric-sort", "object", + "perf-event-open-sys2", "perf-event2", "procfs", "regex", + "rlimit", "rmcp", "rustc-demangle", "rustix 0.38.44", @@ -139,15 +139,11 @@ dependencies = [ "schemars 0.8.22", "serde", "serde_json", - "serde_urlencoded", - "serde_yaml", - "serial_test", "strum", "strum_macros", "sysctl", "sysinfo", "tar", - "tdigest", "tempfile", "thiserror 1.0.69", "timerfd", @@ -904,31 +900,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crossterm" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" -dependencies = [ - "bitflags 1.3.2", - "crossterm_winapi", - "libc", - "mio 0.8.11", - "parking_lot", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - [[package]] name = "crypto-common" version = "0.1.7" @@ -1182,6 +1153,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "exmex" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9a53dccfc387af263e69c1eb53d13a4e9b26a7134ffdd38398a208374cbf851" +dependencies = [ + "regex", + "smallvec", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -1329,24 +1310,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fuzzy-matcher" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" -dependencies = [ - "thread_local", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1751,23 +1714,20 @@ dependencies = [ ] [[package]] -name = "include_directory" -version = "0.1.1" +name = "include_dir" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc51bf21d9c8c76d0d55b3926add7fde9b595719ee0d5710d46f8ee66131cca9" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" dependencies = [ - "include_directory_macros", - "mime", + "include_dir_macros", ] [[package]] -name = "include_directory_macros" -version = "0.1.0" +name = "include_dir_macros" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35011b5de7391d94ea631aa584d09a88b2e877505a614e4952970214f2fd1b90" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" dependencies = [ - "mime", - "new_mime_guess", "proc-macro2", "quote", ] @@ -1816,23 +1776,6 @@ dependencies = [ "str_stack", ] -[[package]] -name = "inquire" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" -dependencies = [ - "bitflags 2.11.1", - "crossterm", - "dyn-clone", - "fuzzy-matcher", - "fxhash", - "newline-converter", - "once_cell", - "unicode-segmentation", - "unicode-width", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -2049,12 +1992,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2065,18 +2002,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - [[package]] name = "mio" version = "1.2.0" @@ -2094,25 +2019,6 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dce6dd36094cac388f119d2e9dc82dc730ef91c32a6222170d630e5414b956e6" -[[package]] -name = "new_mime_guess" -version = "4.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02a2dfb3559d53e90b709376af1c379462f7fb3085a0177deb73e6ea0d99eff4" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "newline-converter" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "nix" version = "0.29.0" @@ -2555,6 +2461,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rlimit" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f35ee2729c56bb610f6dba436bf78135f728b7373bdffae2ec815b2d3eb98cc3" +dependencies = [ + "libc", +] + [[package]] name = "rmcp" version = "1.7.0" @@ -2708,15 +2623,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - [[package]] name = "schannel" version = "0.1.29" @@ -2782,12 +2688,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - [[package]] name = "security-framework" version = "3.7.0" @@ -2881,18 +2781,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -2906,32 +2794,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "serial_test" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" -dependencies = [ - "futures-executor", - "futures-util", - "log", - "once_cell", - "parking_lot", - "scc", - "serial_test_derive", -] - -[[package]] -name = "serial_test_derive" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "sha1" version = "0.10.6" @@ -2960,27 +2822,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" -dependencies = [ - "libc", - "mio 0.8.11", - "signal-hook", -] - [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -3141,15 +2982,6 @@ dependencies = [ "xattr", ] -[[package]] -name = "tdigest" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c45d225a08ebccf0e0c7d46db4066ea8ab05b29d3750ecc1a04f1675978bf3c8" -dependencies = [ - "ordered-float", -] - [[package]] name = "tempfile" version = "3.27.0" @@ -3213,15 +3045,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - [[package]] name = "time" version = "0.3.47" @@ -3282,7 +3105,7 @@ checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", - "mio 1.2.0", + "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -3409,12 +3232,6 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - [[package]] name = "unicode-ident" version = "1.0.24" @@ -3427,12 +3244,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-xid" version = "0.2.6" @@ -3736,22 +3547,13 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -3760,7 +3562,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -3772,67 +3574,34 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[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.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[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.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3845,48 +3614,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[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.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[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.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[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.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 38a8a36d..40f11d7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aperf" -version = "1.2.3" +version = "1.3.0" edition = "2021" publish = false @@ -32,13 +32,11 @@ clap_complete = "4.2.3" serde = { version = "1.0", features = ["derive"] } chrono = { version = "0.4", features = ["serde"] } rustix = { version = "0.38.28", features = ["system"] } -serde_yaml = "0.9" thiserror = "1.0" log = "0.4.21" lazy_static = "1.4.0" ctor = "0.2.6" anyhow = "1.0" -serde_urlencoded = "0.7" serde_json = "1.0" itoa = "1" ryu = "1" @@ -50,29 +48,27 @@ libc = "0.2" flate2 = "1.0.30" tar = "0.4.40" infer = "0.13.0" -inquire = "0.7.5" bincode = "1.3.3" inferno = "0.11.19" -indexmap = "2.1.0" -include_directory = "0.1.1" -cfg-if = "1.0" +indexmap = { version = "2.1.0", features = ["serde"] } +include_dir = "0.7.4" tempfile = "3" -serial_test = "3.1.1" log4rs = "1.3.0" -tdigest = "0.2" -csv = "1.2" csv-to-html = "0.5.10" numeric-sort = "0.1" regex = "1" rmcp = { version = "1.5", features = ["server", "transport-io"], optional = true } schemars = { version = "0.8", optional = true } +exmex = "0.21" [target.'cfg(target_os = "linux")'.dependencies] sysinfo = "0.26.2" sysctl = "*" perf-event2 = "0.7.2" +perf-event-open-sys2 = "5.0.6" procfs = "0.12.0" timerfd = "1.6.0" +rlimit = "0.11" linux-perf-data = "0.12" linux-perf-event-reader = "0.10" object = { version = "0.36", features = ["read"] } diff --git a/src/data.rs b/src/data.rs index 29416a6e..c55e1605 100644 --- a/src/data.rs +++ b/src/data.rs @@ -33,7 +33,7 @@ use diskstats::{Diskstats, DiskstatsRaw}; use efa_stat::{EfaStat, EfaStatRaw}; use ena_stat::{EnaStat, EnaStatRaw}; use hotline::{Hotline, HotlineRaw}; -use include_directory::{include_directory, Dir}; +use include_dir::{include_dir, Dir}; use interrupts::{InterruptData, InterruptDataRaw}; use java_profile::{JavaProfile, JavaProfileRaw}; use kernel_config::KernelConfig; @@ -74,6 +74,7 @@ pub struct CollectorParams { pub signal: Signal, pub runlog: PathBuf, pub pmu_config: Option, + pub pmu_counter_mode: String, pub perf_frequency: u32, pub save_profile_events: bool, pub hotline_frequency: u32, @@ -95,6 +96,7 @@ impl CollectorParams { signal: signal::SIGTERM, runlog: PathBuf::new(), pmu_config: None, + pmu_counter_mode: String::new(), perf_frequency: 99, save_profile_events: false, hotline_frequency: 1000, @@ -156,6 +158,7 @@ impl DataType { self.collector_params.perf_frequency = param.perf_frequency; self.collector_params.save_profile_events = param.save_profile_events; self.collector_params.pmu_config = param.pmu_config.clone(); + self.collector_params.pmu_counter_mode = param.pmu_counter_mode.clone(); self.collector_params.interval = param.interval; self.collector_params.hotline_frequency = param.hotline_frequency; self.collector_params.num_to_report = param.num_to_report; @@ -329,7 +332,7 @@ macro_rules! data { /// creates the VisualizationData object and invokes the function. macro_rules! report_data { ( $( $report_data:ident ),* ) => { - pub static JS_DIR: Dir<'_> = include_directory!("$JS_DIR"); + pub static JS_DIR: Dir<'_> = include_dir!("$JS_DIR"); #[derive(Clone, Debug, Deserialize, Serialize)] pub enum ReportData { diff --git a/src/data/common/processed_data_accessor.rs b/src/data/common/processed_data_accessor.rs index 7e0ed1ba..628111ea 100644 --- a/src/data/common/processed_data_accessor.rs +++ b/src/data/common/processed_data_accessor.rs @@ -528,7 +528,13 @@ fn write_series_json_string( if i > 0 { buf.push(','); } - buf.push_str(ryu_buf.format(f64_to_fixed_2(v))); + if v.is_finite() { + buf.push_str(ryu_buf.format(f64_to_fixed_2(v))); + } else { + // ryu formats non-finite floats as bare `NaN`/`inf`/`-inf` tokens, which + // are invalid JSON. Emit `null` instead (matching serde_json's behavior). + buf.push_str("null"); + } } buf.push(']'); @@ -1189,6 +1195,20 @@ mod tests { assert_eq!(v["is_aggregate"], true); } + #[test] + fn test_write_series_non_finite_values_are_null() { + let series = make_series( + "total", + vec![0, 10, 20, 30, 40], + vec![1.5, f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 2.5], + ); + let mut buf = String::new(); + write_series_json_string(&mut buf, &series, None, None, None); + // The whole line must be valid JSON. + let v: serde_json::Value = serde_json::from_str(&buf).unwrap(); + assert_eq!(v["values"], serde_json::json!([1.5, null, null, null, 2.5])); + } + // ---- time_series_metric_json_string tests ---- #[test] diff --git a/src/data/common/time_series_data_processor.rs b/src/data/common/time_series_data_processor.rs index d2308b27..0c854832 100644 --- a/src/data/common/time_series_data_processor.rs +++ b/src/data/common/time_series_data_processor.rs @@ -42,6 +42,9 @@ pub struct TimeSeriesDataProcessor { time_zero: Option, // The current time_diff to be added to every series cur_time_diff: u64, + // Whether the first accumulative value, where there is no previous value to compute the delta, + // should be ignored (the delta is 0) or used (the delta is the value itself). + ignore_first_accumulative_value: bool, // Map - used to count the // number of unexpected decreases of accumulative data, which are to be logged as warning // at the end of collection @@ -67,11 +70,18 @@ impl TimeSeriesDataProcessor { per_metric_sum_count: HashMap::new(), time_zero, cur_time_diff: 0, + ignore_first_accumulative_value: true, decreasing_accumulative_data: HashMap::new(), fixed_value_range: None, } } + /// Force to use the accumulative value itself, instead of 0, when there is no + /// previous value to compute the delta. + pub fn use_first_accumulative_value(&mut self) { + self.ignore_first_accumulative_value = false; + } + /// Override the name of the auto-generated aggregate series pub fn set_aggregate_series_name(&mut self, aggregate_series_name: &'static str) { self.aggregate_series_name = Some(aggregate_series_name); @@ -210,7 +220,11 @@ impl TimeSeriesDataProcessor { } Some(data_value - prev_value) } - None => Some(0.0), + None => Some(if self.ignore_first_accumulative_value { + 0.0 + } else { + data_value + }), } } diff --git a/src/data/common/utils.rs b/src/data/common/utils.rs index 3a72bb0a..effc86a8 100644 --- a/src/data/common/utils.rs +++ b/src/data/common/utils.rs @@ -6,6 +6,8 @@ use std::fs; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; +#[cfg(target_os = "linux")] +use {anyhow::Context, log::debug}; use crate::data::common::data_formats::{Graph, GraphData}; @@ -31,43 +33,137 @@ pub fn get_data_name_from_type() -> &'static str { #[cfg(target_os = "linux")] #[derive(Clone, Debug)] pub struct CpuInfo { - pub vendor: String, - pub model_name: String, + pub part: Option, + pub vendor_id: Option, + pub model_name: Option, } #[cfg(target_os = "linux")] impl CpuInfo { - fn new() -> Self { - CpuInfo { - vendor: String::new(), - model_name: String::new(), + pub fn new() -> Result { + let cpu_info_file = File::open("/proc/cpuinfo")?; + let cpu_info_reader = BufReader::new(cpu_info_file); + let mut part = None; + let mut vendor_id = None; + let mut model_name = None; + for line in cpu_info_reader.lines() { + let info_line = line?; + if info_line.is_empty() { + break; + } + let key_value: Vec<&str> = info_line.split(':').collect(); + if key_value.len() < 2 { + continue; + } + let key = key_value[0].trim().to_string(); + let value = key_value[1].trim().to_string(); + match key.as_str() { + "CPU part" => part = Some(value), + "vendor_id" => vendor_id = Some(value), + "model name" => model_name = Some(value), + _ => continue, + } } + + Ok(Self { + part, + vendor_id, + model_name, + }) + } + + pub fn is_graviton(&self) -> bool { + self.part.is_some() + } + + pub fn is_graviton_5(&self) -> bool { + self.part.as_ref().map_or(false, |part| part == "0xd84") + } + + pub fn is_intel(&self) -> bool { + self.vendor_id + .as_ref() + .map_or(false, |vendor_id| vendor_id == "GenuineIntel") + } + + pub fn is_intel_icelake(&self) -> bool { + self.model_name.as_ref().map_or(false, |model_name| { + model_name == "Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz" + }) + } + + pub fn is_intel_sapphire_rapids(&self) -> bool { + self.model_name.as_ref().map_or(false, |model_name| { + model_name == "Intel(R) Xeon(R) Platinum 8488C" + }) + } + + pub fn is_amd(&self) -> bool { + self.vendor_id + .as_ref() + .map_or(false, |vendor_id| vendor_id == "AuthenticAMD") + } + + pub fn is_amd_genoa(&self) -> bool { + self.model_name + .as_ref() + .map_or(false, |model_name| model_name == "AMD EPYC 9R14") + } + + pub fn is_amd_milan(&self) -> bool { + self.model_name + .as_ref() + .map_or(false, |model_name| model_name == "AMD EPYC 7R13") } } #[cfg(target_os = "linux")] -pub fn get_cpu_info() -> Result { - let file = File::open("/proc/cpuinfo")?; - let proc_cpuinfo = BufReader::new(file); - let mut cpu_info = CpuInfo::new(); - for line in proc_cpuinfo.lines() { - let info_line = line?; - if info_line.is_empty() { - break; - } - let key_value: Vec<&str> = info_line.split(':').collect(); - if key_value.len() < 2 { +/// Return the IDs of all online CPUs by parsing /sys/devices/system/cpu/online, +/// a comma-separated list of single CPUs and inclusive ranges, e.g. "0-1,3-5,7". +pub fn get_online_cpu_ids() -> Result> { + let mut ids = Vec::new(); + let cpu_list = fs::read_to_string("/sys/devices/system/cpu/online")?; + let cpu_list = cpu_list.trim(); + if cpu_list.is_empty() { + return Ok(ids); + } + for part in cpu_list.split(',') { + let part = part.trim(); + if part.is_empty() { continue; } - let key = key_value[0].trim().to_string(); - let value = key_value[1].trim().to_string(); - match key.as_str() { - "vendor_id" => cpu_info.vendor = value, - "model name" => cpu_info.model_name = value, - _ => {} + match part.split_once('-') { + Some((low, high)) => { + let low: usize = low.trim().parse()?; + let high: usize = high.trim().parse()?; + if high < low { + bail!("invalid CPU range '{part}' in cpu list '{cpu_list}'"); + } + ids.extend(low..=high); + } + None => ids.push(part.parse()?), + } + } + ids.sort_unstable(); + ids.dedup(); + Ok(ids) +} + +/// Check the current fd limit and raise it if the number of required fd is larger. +#[cfg(target_os = "linux")] +pub fn raise_fd_limit(num_required_fds: u64) -> Result<()> { + let (cur_fd_limit, max_fd_limit) = rlimit::Resource::NOFILE + .get() + .context("Failed to read fd limit")?; + if num_required_fds > cur_fd_limit { + if num_required_fds >= max_fd_limit { + bail!("The number of required fds ({num_required_fds}) is larger than the max fds ({max_fd_limit}).") } + debug!("Increasing fd limit from {cur_fd_limit} to {num_required_fds}"); + rlimit::increase_nofile_limit(num_required_fds) + .with_context(|| format!("Failed to increase the fd limit to {num_required_fds}"))?; } - Ok(cpu_info) + Ok(()) } pub fn no_tar_gz_file_name(path: &PathBuf) -> Option { @@ -272,6 +368,37 @@ mod utils_test { use std::path::PathBuf; use tempfile::TempDir; + #[cfg(target_os = "linux")] + #[test] + fn test_get_online_cpu_ids() { + use super::get_online_cpu_ids; + let ids = get_online_cpu_ids().expect("should read /sys/devices/system/cpu/online"); + // At least CPU 0 is always online. + assert!(!ids.is_empty(), "expected at least one online CPU"); + assert!(ids.contains(&0), "CPU 0 should be online"); + // Result is sorted and de-duplicated. + let mut sorted = ids.clone(); + sorted.sort_unstable(); + sorted.dedup(); + assert_eq!(ids, sorted, "ids should be sorted and unique"); + // Count should match sysconf(_SC_NPROCESSORS_ONLN) on a normal system. + let nproc = unsafe { libc::sysconf(libc::_SC_NPROCESSORS_ONLN as libc::c_int) } as usize; + assert_eq!(ids.len(), nproc, "online CPU count should match sysconf"); + } + + #[cfg(target_os = "linux")] + #[test] + fn test_cpu_info() { + use super::CpuInfo; + + let cpu_info = CpuInfo::new().expect("Should read and parse /proc/cpuinfo"); + + assert!( + cpu_info.is_graviton() || cpu_info.is_intel() || cpu_info.is_amd(), + "The CPU should be recognized as one of Graviton, Intel, and AMD" + ); + } + #[test] fn test_find_file_prefix_match() { let dir = TempDir::new().unwrap(); diff --git a/src/data/perf_stat.rs b/src/data/perf_stat.rs index 7d240b36..1762c6a5 100644 --- a/src/data/perf_stat.rs +++ b/src/data/perf_stat.rs @@ -1,359 +1,701 @@ use crate::data::common::data_formats::AperfData; -use crate::data::common::time_series_data_processor::time_series_data_processor_with_custom_aggregate; +use crate::data::common::time_series_data_processor::{ + time_series_data_processor_with_custom_aggregate, TimeSeriesDataProcessor, +}; use crate::data::common::utils::{get_aggregate_series_name, get_cpu_series_name}; use crate::data::{Data, ProcessData, TimeEnum}; use crate::visualizer::ReportParams; -use anyhow::Result; -use chrono::prelude::*; -use log::error; +use crate::UNGROUPED_PMU_MODE; +use anyhow::{Context, Result}; +use exmex::{Express, FlatEx}; +use include_dir::{include_dir, Dir}; +use indexmap::IndexMap; +use log::{debug, error, warn}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; +use std::fmt::{Debug, Write}; +use std::fs; +use std::path::{Path, PathBuf}; #[cfg(target_os = "linux")] use { + crate::data::common::utils::{get_online_cpu_ids, raise_fd_limit, CpuInfo}, crate::data::{CollectData, CollectorParams}, - crate::PDError, - log::{info, warn}, - perf_event::events::{Raw, Software}, - perf_event::{Builder, Counter, Group, ReadFormat}, - std::fs::File, - std::io::ErrorKind, - std::path::PathBuf, - std::sync::Mutex, + anyhow::{anyhow, bail}, + chrono::prelude::*, + log::info, }; -#[cfg(target_arch = "aarch64")] -pub mod arm64_perf_list { - pub static GRV_EVENTS: &[u8] = include_bytes!("../pmu_configs/grv_perf_list.json"); +static DEFAULT_PMU_CONFIG_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/src/pmu_configs"); + +/// Help build a PMU counter or group with unified error handling logic. +#[cfg(target_os = "linux")] +macro_rules! build_pmu_counter { + ($build:expr, $on_unsupported:expr) => { + match $build { + Ok(counter) => counter, + Err(e) => match e.raw_os_error() { + Some(libc::EACCES) | Some(libc::EPERM) => { + error!("kernel.perf_event_paranoid is not <=0. Run `sudo sysctl -w kernel.perf_event_paranoid=-1`"); + return Err(e.into()); + } + Some(libc::ENOENT) | Some(libc::ENODEV) | Some(libc::EOPNOTSUPP) + | Some(libc::EINVAL) => $on_unsupported, + _ => bail!("Failed to create PMU counter: {:?}", e), + }, + } + }; } -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -pub mod x86_perf_list { - /// Intel+ events. - pub static INTEL_EVENTS: &[u8] = include_bytes!("../pmu_configs/intel_perf_list.json"); - pub static ICX_CTRS: &[u8] = include_bytes!("../pmu_configs/intel_icelake_ctrs.json"); - pub static SPR_CTRS: &[u8] = include_bytes!("../pmu_configs/intel_sapphire_rapids_ctrs.json"); - - /// AMD+ events. - pub static AMD_EVENTS: &[u8] = include_bytes!("../pmu_configs/amd_perf_list.json"); - pub static GENOA_CTRS: &[u8] = include_bytes!("../pmu_configs/amd_genoa_ctrs.json"); - pub static MILAN_CTRS: &[u8] = include_bytes!("../pmu_configs/amd_milan_ctrs.json"); + +/// Get the path to PMU config saved in the run dir. +fn get_saved_pmu_config_path(run_dir: &Path) -> PathBuf { + PathBuf::from(run_dir).join("pmu_config.json") } +/// Custom event type used to build the counter. It essentially duplicates perf-event2's +/// DynamicBuilder and Event::Dynamic, which, unfortunately, fails to parse a config +/// with multiple non-contiguous bit ranges (the format used on AMD). The implementations +/// below fix the issue. #[cfg(target_os = "linux")] -#[derive(Copy, Clone, Debug, Deserialize, Serialize)] -pub enum PerfType { - RAW = 4, +struct PmuConfigEvent { + pmu_type: u32, + /// A format file could have config/config1/config2 mapping to + /// 3 different 64-bit fields. + config: [u64; 3], } #[cfg(target_os = "linux")] -lazy_static! { - pub static ref CPU_CTR_GROUPS: Mutex> = Mutex::new(Vec::new()); +impl perf_event::events::Event for PmuConfigEvent { + fn update_attrs(self, attr: &mut perf_event_open_sys::bindings::perf_event_attr) { + attr.type_ = self.pmu_type; + attr.config = self.config[0]; + attr.__bindgen_anon_3.config1 = self.config[1]; + attr.__bindgen_anon_4.config2 = self.config[2]; + } } #[cfg(target_os = "linux")] -#[derive(Debug)] -pub struct Ctr { - pub perf_type: u64, - pub name: String, - pub config: u64, - pub counter: Counter, +impl PmuConfigEvent { + /// Parse "pmu/field=val,field=val,.../" and build the event by reading the + /// PMU type and building the config bit map from each field's config. + fn from_event_string(event_string: &str) -> Result { + let (pmu_name, fields_str) = event_string + .split_once('/') + .ok_or_else(|| anyhow!("Missing '/' in the PMU event string {event_string}"))?; + // Drop trailing '/' + let fields_str = fields_str.strip_suffix('/').unwrap_or(fields_str); + // Parse the fields string into Vec<(field_key, field_value)>. + let mut fields = Vec::new(); + for field_str in fields_str.split(',') { + let field_str = field_str.trim(); + if field_str.is_empty() { + continue; + } + let (field_key, field_val_str) = field_str + .split_once('=') + .ok_or_else(|| anyhow!("Invalid PMU event string: {event_string}"))?; + // values are hex (0x..) or decimal + let field_val = field_val_str.trim(); + let field_val = if let Some(hex) = field_val.strip_prefix("0x") { + u64::from_str_radix(hex, 16)? + } else { + field_val.parse::()? + }; + fields.push((field_key.trim().to_string(), field_val)); + } + + let pmu_device_path = PathBuf::from("/sys/bus/event_source/devices").join(pmu_name); + let pmu_type = fs::read_to_string(pmu_device_path.join("type"))? + .trim() + .parse::()?; + + // For each field=value, the layout of the 3 64-bit fields (config, config1, and config2) + // comes from the kernel's sysfs format file /sys/bus/event_source/devices//format/, + // e.g. confi1g:0-7, config2:1-8, or AMD's non-contiguous config:0-7,32-35. + let mut config = [0u64; 3]; + for (field, value) in fields { + let mut shift = 0; + + let format_str = fs::read_to_string(pmu_device_path.join("format").join(&field))?; + let (config_idx, ranges_str) = + if let Some(ranges_str) = format_str.strip_prefix("config:") { + (0, ranges_str) + } else if let Some(ranges_str) = format_str.strip_prefix("config1:") { + (1, ranges_str) + } else if let Some(ranges_str) = format_str.strip_prefix("config2:") { + (2, ranges_str) + } else { + bail!("Unexpected PMU format {format_str} for field {field}"); + }; + + for range_str in ranges_str.trim().split(',') { + let (low, high) = match range_str.split_once('-') { + Some((low_str, high_str)) => { + (low_str.parse::()?, high_str.parse::()?) + } + None => (range_str.parse::()?, range_str.parse::()?), + }; + let width = high - low + 1; + let mask = (1u64 << width) - 1; + config[config_idx] |= ((value >> shift) & mask) << low; + shift += width; + } + } + + Ok(Self { pmu_type, config }) + } +} + +/// Maps the format of the PMU config, for simpler deserialization/parsing of the config file. +#[derive(Deserialize, Serialize)] +struct PmuConfig { + pub events: IndexMap, + pub metrics: IndexMap, } +impl PmuConfig { + /// Parse the PMU config JSON located at the path. + pub fn from_file(path: &Path) -> Result { + let bytes = fs::read(path)?; + Self::from_bytes(&bytes) + } + + /// Save to a PMU config JSON file at the path. + pub fn save_to_file(&self, path: &Path) -> Result<()> { + let file = fs::File::create(path)?; + serde_json::to_writer_pretty(file, &self) + .with_context(|| format!("Failed to save PMU config to {}", path.display()))?; + Ok(()) + } + + /// Parse the byte content of a PMU config JSON. + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(serde_json::from_slice(bytes).context("Failed to parse PMU config")?) + } + + /// Parse one of the default PMU config JSONs. + #[cfg(target_os = "linux")] + pub fn from_default(name: &str) -> Result { + let bytes = DEFAULT_PMU_CONFIG_DIR + .get_file(name) + .with_context(|| format!("Failed to read default PMU config file content for {name}"))? + .contents(); + Self::from_bytes(&bytes) + } + + /// Extend the current config by another default PMU config JSON. + #[cfg(target_os = "linux")] + pub fn extend_from_default(&mut self, name: &str) -> Result<()> { + let extent_pmu_config = Self::from_default(name)?; + self.events.extend(extent_pmu_config.events); + self.metrics.extend(extent_pmu_config.metrics); + Ok(()) + } + + /// Grouped mode: for each defined metric, create a counter group that contains all the + /// event counters used by the metric definition. Grouped counters are scheduled together + /// on CPU, so the metric computation is guaranteed to be correct. However, the same event + /// across different groups creates one counter per group. Therfore, this mode puts more + /// loads on the collection multiplexing, and each event is collected for less time. + #[cfg(target_os = "linux")] + pub fn create_metric_counter_groups(&self) -> Result> { + let online_cpu_ids = get_online_cpu_ids()?; + let metric_expressions = self.get_metric_expressions()?; + + // Add some buffers to the expected fd requirement. + let num_required_fds: u64 = 50 + + online_cpu_ids.len() as u64 + * metric_expressions + .values() + .map(|metric_expression| metric_expression.var_names().len() as u64 + 1) + .sum::(); + debug!( + "Require {num_required_fds} fds for the collection of {} PMU metrics over {} CPUs.", + metric_expressions.len(), + online_cpu_ids.len() + ); + raise_fd_limit(num_required_fds)?; + + let mut metric_counter_groups = Vec::new(); + + // For each defined metric on each online CPU, create a group that contains all the PMU event counters + // used by the metric. + 'outer: for (metric_name, metric_expression) in metric_expressions { + // The event names are in alphabetical order, and their values need to be passed to the + // expression in the exactly same order for evaluation. Therefore, the order needs to + // be maintained between collection and report generation. + let event_names = metric_expression.var_names(); + + for &cpu_id in &online_cpu_ids { + let mut group = build_pmu_counter!( + perf_event::Builder::new(perf_event::events::Software::DUMMY) + .read_format( + perf_event::ReadFormat::GROUP + | perf_event::ReadFormat::TOTAL_TIME_ENABLED + | perf_event::ReadFormat::TOTAL_TIME_RUNNING + | perf_event::ReadFormat::ID, + ) + .one_cpu(cpu_id) + .any_pid() + .build_group(), + unreachable!("Group leader is a software event and cannot be unsupported") + ); + let mut counters = Vec::new(); + + for event_name in event_names { + let event_string = match self.events.get(event_name) { + Some(event_string) => event_string, + None => { + error!("Skipping metric {metric_name} due to unrecognized event name {event_name}"); + continue 'outer; + } + }; + let event = PmuConfigEvent::from_event_string(event_string).with_context(|| { + format!("Failed to create event {event_name} from definition {event_string}") + })?; + + let counter = build_pmu_counter!( + perf_event::Builder::new(event) + .one_cpu(cpu_id) + .any_pid() + .include_kernel() + .build_with_group(&mut group), + { + warn!("Skipping metric {metric_name}, as PMU event {event_name} is not supported."); + continue 'outer; + } + ); + counters.push(counter); + } + + metric_counter_groups.push(PmuCollector::Grouped(PmuMetricCounterGroup { + cpu_id: cpu_id, + metric_name: metric_name.clone(), + counters, + group, + })); + } + } + + Ok(metric_counter_groups) + } + + /// Ungrouped mode: create a counter for each unique event, without any groups. All counters + /// time-share the PMU registers for collection. This mode puts less load on multiplexing, + /// and each counter is collected for a longer time, but the counters used to compute a + /// metric value are not guaranteed to be collected at the same time, unless all counters + /// can fit in available PMU registers (typically 4-8 depeding on the CPU type). + #[cfg(target_os = "linux")] + pub fn create_event_counters(&self) -> Result> { + let online_cpu_ids = get_online_cpu_ids()?; + + // Add some buffers to the expected fd requirement. + let num_required_fds: u64 = (50 + online_cpu_ids.len() * self.events.len()) as u64; + debug!( + "Require {num_required_fds} fds for the collection of {} PMU eventd over {} CPUs.", + self.events.len(), + online_cpu_ids.len() + ); + raise_fd_limit(num_required_fds)?; + + let mut event_counters = Vec::new(); + + 'outer: for (event_name, event_string) in &self.events { + for &cpu_id in &online_cpu_ids { + let event = PmuConfigEvent::from_event_string(event_string).with_context(|| { + format!("Failed to create event {event_name} from definition {event_string}") + })?; + let counter = build_pmu_counter!( + perf_event::Builder::new(event) + .read_format( + perf_event::ReadFormat::TOTAL_TIME_ENABLED + | perf_event::ReadFormat::TOTAL_TIME_RUNNING, + ) + .one_cpu(cpu_id) + .any_pid() + .include_kernel() + .build(), + { + warn!("Skipping PMU event {event_name} as it is not supported."); + continue 'outer; + } + ); + + event_counters.push(PmuCollector::Ungrouped(PmuEventCounter { + cpu_id, + event_name: event_name.clone(), + counter, + })); + } + } + + Ok(event_counters) + } + + /// Parse each PMU metric expression into exmes object, to retrieve the identifiers + /// or to evaluate the expression with identifier values. + pub fn get_metric_expressions(&self) -> Result>> { + let mut metric_expressions: HashMap> = HashMap::new(); + for (metric_name, metric_expression_str) in &self.metrics { + metric_expressions.insert( + metric_name.clone(), + exmex::parse::(metric_expression_str).with_context(|| { + format!("Failed to parse metric expression for {metric_name} = {metric_expression_str}") + })?, + ); + } + Ok(metric_expressions) + } +} + +/// Provides unified interface for grouped and ungrouped collectors. #[cfg(target_os = "linux")] -impl Ctr { - fn new( - perf_type: u64, - name: String, - cpu: usize, - config: u64, - group: &mut Group, - ) -> Result { - let raw_config = Raw::new(config); - Ok(Ctr { - perf_type, - name, - config, - counter: Builder::new(raw_config) - .one_cpu(cpu) - .any_pid() - .include_kernel() - .build_with_group(group)?, - }) +pub enum PmuCollector { + Grouped(PmuMetricCounterGroup), + Ungrouped(PmuEventCounter), +} + +#[cfg(target_os = "linux")] +impl PmuCollector { + pub fn enable(&mut self) -> Result<()> { + match self { + PmuCollector::Grouped(pmu_metric_counter_group) => pmu_metric_counter_group.enable(), + PmuCollector::Ungrouped(pmu_event_counter) => pmu_event_counter.enable(), + } + } + + pub fn collect(&mut self) -> Result { + match self { + PmuCollector::Grouped(pmu_metric_counter_group) => pmu_metric_counter_group.collect(), + PmuCollector::Ungrouped(pmu_event_counter) => pmu_event_counter.collect(), + } } } +/// Grouped mode: contains all PMU event counters used by a metric, which are placed +/// in the same group so that they will be scheduled by the CPU atomically. The struct +/// is used for the collection of the grouped event counter values. #[cfg(target_os = "linux")] -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct NamedCtr { - pub name: String, - pub nrs: Vec, - pub drs: Vec, - pub scale: u64, +pub struct PmuMetricCounterGroup { + pub cpu_id: usize, + pub metric_name: String, + pub counters: Vec, + pub group: perf_event::Group, } #[cfg(target_os = "linux")] -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct NamedTypeCtr { - pub perf_type: PerfType, - pub name: String, - pub config: u64, +impl PmuMetricCounterGroup { + /// Enable the group for collection. + pub fn enable(&mut self) -> Result<()> { + self.group.reset().with_context(|| { + format!( + "Failed to reset PMU counter group for metric {} on CPU {}", + self.metric_name, self.cpu_id + ) + })?; + self.group.enable().with_context(|| { + format!( + "Failed to enable PMU counter group for metric {} on CPU {}", + self.metric_name, self.cpu_id + ) + })?; + Ok(()) + } + + /// Collect the value of all counters within this group. + pub fn collect(&mut self) -> Result { + let group_data = self.group.read().with_context(|| { + format!( + "Failed to read PMU counter group for metric {} on CPU {}", + self.metric_name, self.cpu_id + ) + })?; + + let mut counter_values: Vec = Vec::with_capacity(self.counters.len()); + for counter in &self.counters { + counter_values.push(group_data[counter]); + } + let time_enabled = group_data + .time_enabled() + .with_context(|| { + format!( + "Failed to read time_enabled for metric {} on CPU {}", + self.metric_name, self.cpu_id + ) + })? + .as_secs_f64(); + let time_running = group_data + .time_running() + .with_context(|| { + format!( + "Failed to read time_running for metric {} on CPU {}", + self.metric_name, self.cpu_id + ) + })? + .as_secs_f64(); + + self.group.reset()?; + + Ok(PmuCollectedData { + cpu_id: self.cpu_id, + identifier: self.metric_name.clone(), + counter_values, + time_enabled, + time_running, + }) + } } +/// Ungrouped mode: contains the PMU counter of a defined event. The struct is used +/// for the collection of the event counter value. #[cfg(target_os = "linux")] -pub struct CpuCtrGroup { - pub cpu: u64, - pub name: String, - pub nr_ctrs: Vec, - pub dr_ctrs: Vec, - pub scale: u64, - pub group: Group, +pub struct PmuEventCounter { + pub cpu_id: usize, + pub event_name: String, + pub counter: perf_event::Counter, } #[cfg(target_os = "linux")] -impl CpuCtrGroup { - fn nr_ctr_add(&mut self, ctr: Ctr) { - self.nr_ctrs.push(ctr); +impl PmuEventCounter { + /// Enable the counter for collection. + pub fn enable(&mut self) -> Result<()> { + self.counter.reset().with_context(|| { + format!( + "Failed to reset PMU counter for event {} on CPU {}", + self.event_name, self.cpu_id + ) + })?; + self.counter.enable().with_context(|| { + format!( + "Failed to enable PMU counter for event {} on CPU {}", + self.event_name, self.cpu_id + ) + })?; + Ok(()) } - fn dr_ctr_add(&mut self, ctr: Ctr) { - self.dr_ctrs.push(ctr); + + /// Collect the value of the event counter. + pub fn collect(&mut self) -> Result { + let counter_data = self.counter.read_full().with_context(|| { + format!( + "Failed to read PMU counter for event {} on CPU {}", + self.event_name, self.cpu_id + ) + })?; + + let counter_value = counter_data.count(); + let time_enabled = counter_data + .time_enabled() + .with_context(|| { + format!( + "Failed to read time_enabled for event {} on CPU {}", + self.event_name, self.cpu_id + ) + })? + .as_secs_f64(); + let time_running = counter_data + .time_running() + .with_context(|| { + format!( + "Failed to read time_running for event {} on CPU {}", + self.event_name, self.cpu_id + ) + })? + .as_secs_f64(); + + self.counter.reset()?; + + Ok(PmuCollectedData { + cpu_id: self.cpu_id, + identifier: self.event_name.clone(), + counter_values: vec![counter_value], + time_enabled, + time_running, + }) } } -#[derive(Serialize, Deserialize, Debug, Clone)] +/// Contains the collected values of either a PmuMetricCounterGroup or PmuEventCounterData. +/// In group mode, the identifier is the metric name, and counter_values contains the +/// collected value of all counters within the group; in ungrouped mode, the identifier is +/// the event name, and counter_values contains the collected value of the one event counter. +pub struct PmuCollectedData { + pub cpu_id: usize, + pub identifier: String, + pub counter_values: Vec, + pub time_enabled: f64, + pub time_running: f64, +} + +impl PmuCollectedData { + /// Parse (deserialize) the data from the string format as defined below. + pub fn from_string(data_string: &str) -> Option { + let parts: Vec<&str> = data_string.split(';').collect(); + // All fields, including at least one counter value, need to be present. + if parts.len() < 5 { + return None; + } + let cpu_id = parts[0].parse::().ok()?; + let identifier = parts[1].to_string(); + let mut counter_values: Vec = Vec::new(); + for i in 2..(parts.len() - 2) { + counter_values.push(parts[i].parse::().ok()?); + } + let time_enabled = parts[parts.len() - 2].parse::().ok()?; + let time_running = parts[parts.len() - 1].parse::().ok()?; + + Some(Self { + cpu_id, + identifier, + counter_values, + time_enabled, + time_running, + }) + } + + /// Serialize the data to string in the format of: + /// cpu_id;identifier;;;...;time_enabled;time_running + pub fn to_string(self) -> String { + let mut data_string = String::new(); + write!(&mut data_string, "{};{};", self.cpu_id, self.identifier).unwrap(); + self.counter_values + .into_iter() + .for_each(|value| write!(&mut data_string, "{value};").unwrap()); + write!( + &mut data_string, + "{};{}", + self.time_enabled, self.time_running + ) + .unwrap(); + + data_string + } +} + +#[derive(Serialize, Deserialize)] pub struct PerfStatRaw { + #[cfg(target_os = "linux")] + #[serde(skip)] + pub pmu_collectors: Vec, pub time: TimeEnum, pub data: String, } +/// Skip Debug for pmu_metrics since they are not implemented for Counter and Group +impl Debug for PerfStatRaw { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PerfStatRaw") + .field("time", &self.time) + .field("data", &self.data) + .finish() + } +} + +#[cfg(target_os = "linux")] impl PerfStatRaw { pub fn new() -> Self { PerfStatRaw { + pmu_collectors: Vec::new(), time: TimeEnum::DateTime(Utc::now()), data: String::new(), } } } -#[cfg(target_os = "linux")] -pub fn form_events_map(base: &[u8], plat_counters: &[u8]) -> Result> { - let mut events_map = indexmap::IndexMap::new(); - for event in &to_events(base)? { - events_map.insert(event.name.clone(), event.clone()); - } - - if plat_counters != [0; 1] { - for event in to_events(plat_counters)? { - if let Some(ctr) = events_map.get_mut(&event.name) { - ctr.nrs = event.nrs; - ctr.drs = event.drs; - ctr.scale = event.scale; - } else { - events_map.insert(event.name.clone(), event); - } - } - } - Ok(events_map.into_values().collect()) -} - -#[cfg(target_os = "linux")] -pub fn to_events(data: &[u8]) -> Result> { - Ok(serde_json::from_slice(data)?) -} - #[cfg(target_os = "linux")] impl CollectData for PerfStatRaw { fn prepare_data_collector(&mut self, params: &CollectorParams) -> Result<()> { - let num_cpus = match unsafe { libc::sysconf(libc::_SC_NPROCESSORS_ONLN as libc::c_int) } { - -1 => { - warn!("Could not get the number of cpus in the system with sysconf."); - return Err(PDError::CollectorPMUCPUError.into()); - } - ret => ret as usize, - }; - let mut cpu_groups: Vec = Vec::new(); - - cfg_if::cfg_if! { - if #[cfg(target_arch = "aarch64")] { - let mut perf_list = to_events(arm64_perf_list::GRV_EVENTS)?; - } else if #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] { - let cpu_info = crate::data::common::utils::get_cpu_info()?; - let platform_specific_counter: &[u8]; - let base: &[u8]; - - /* Get Vendor Specific Perf events */ - if cpu_info.vendor == "GenuineIntel" { - base = x86_perf_list::INTEL_EVENTS; - - /* Get Model specific events */ - platform_specific_counter = match cpu_info.model_name.as_str() { - "Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz" => x86_perf_list::ICX_CTRS, - "Intel(R) Xeon(R) Platinum 8488C" => x86_perf_list::SPR_CTRS, - _ => &[0; 1], - }; - } else if cpu_info.vendor == "AuthenticAMD" { - warn!("Event multiplexing may result in bad PMU data."); //TODO: mitigate bad PMU data on AMD instances - base = x86_perf_list::AMD_EVENTS; - - /* Get Model specific events */ - platform_specific_counter = match cpu_info.model_name.get(..13).unwrap_or_default() { - "AMD EPYC 9R14" => x86_perf_list::GENOA_CTRS, - "AMD EPYC 7R13" => x86_perf_list::MILAN_CTRS, - _ => &[0; 1], - }; - } else { - return Err(PDError::CollectorPerfUnsupportedCPU.into()); - } - - let mut perf_list = form_events_map(base, platform_specific_counter)?; - } else { - return Err(PDError::CollectorPerfUnsupportedCPU.into()); - } - } - if let Some(custom_file) = ¶ms.pmu_config { - let f = File::open(custom_file)?; - let user_provided_list: Result, serde_json::Error> = - serde_json::from_reader(&f); - match user_provided_list { - Ok(ul) => { - info!("Using custom PMU configuration provided by user."); - perf_list = ul; + // Read and parse the PMU config. If user did not provide a PMU config file, use the default one + // based on the CPU type. + let pmu_config = if let Some(custom_pmu_config_path) = ¶ms.pmu_config { + match PmuConfig::from_file(custom_pmu_config_path) { + Ok(custom_pmu_config) => { + info!( + "Using custom PMU configuration {}", + custom_pmu_config_path.display() + ); + custom_pmu_config } - Err(_) => { - error!("User provided PMU configuration is invalid. Aperf exiting..."); + Err(e) => { + error!( + "Custom PMU configuration {} is invalid: {:?}", + custom_pmu_config_path.display(), + e + ); std::process::exit(1); } } - } - /* Write the pmu_config being used to the recorded data */ - let perf_list_pathbuf = PathBuf::from(¶ms.data_dir).join("pmu_config.json"); - let f = File::create(&perf_list_pathbuf)?; - serde_json::to_writer_pretty(f, &perf_list)?; - for cpu in 0..num_cpus { - for named_ctr in &perf_list { - let perf_group = Builder::new(Software::DUMMY) - .read_format( - ReadFormat::GROUP - | ReadFormat::TOTAL_TIME_ENABLED - | ReadFormat::TOTAL_TIME_RUNNING - | ReadFormat::ID, - ) - .any_pid() - .one_cpu(cpu) - .build_group(); - - let group = match perf_group { - Err(e) => { - match e.kind() { - ErrorKind::PermissionDenied => { - warn!("kernel.perf_event_paranoid is not <=0. Run `sudo sysctl -w kernel.perf_event_paranoid=-1`") - } - ErrorKind::NotFound => warn!("PMU counters not available on this instance type. Refer to APerf documentation for supported instances"), - _ => match e.raw_os_error().unwrap() { - libc::EMFILE => warn!("Too many open files. Increase limit by running `ulimit -n 65536`"), - _ => warn!("Unknown error when trying to use Perf API"), - } - } - return Err(e.into()); - } - Ok(g) => g, - }; - let mut cpu_group = CpuCtrGroup { - cpu: cpu as u64, - name: named_ctr.name.to_string(), - nr_ctrs: Vec::new(), - dr_ctrs: Vec::new(), - scale: named_ctr.scale, - group, - }; - for nr in &named_ctr.nrs { - let nr_ctr = Ctr::new( - nr.perf_type as u64, - nr.name.to_string(), - cpu, - nr.config, - &mut cpu_group.group, - ); - match nr_ctr { - Err(e) => { - if let Some(os_error) = e.downcast_ref::() { - match os_error.kind() { - ErrorKind::NotFound => { - warn!("PMU counters not available on this instance type. Refer to APerf documentation for supported instances") - } - _ => match os_error.raw_os_error().unwrap() { - libc::EMFILE => warn!( - "Too many open files. Increase limit with by running `ulimit -n 65536`" - ), - _ => warn!("Unknown error when trying to use Perf API."), - }, - } - return Err(e); - } - } - Ok(v) => cpu_group.nr_ctr_add(v), - } + } else { + let cpu_info = CpuInfo::new().context("Failed to obtain CPU info")?; + + if cpu_info.is_graviton() { + let mut pmu_config = PmuConfig::from_default("grv_pmu_config.json")?; + + if cpu_info.is_graviton_5() { + pmu_config.extend_from_default("grv_5_pmu_config.json")?; } - for dr in &named_ctr.drs { - let dr_ctr = Ctr::new( - dr.perf_type as u64, - dr.name.to_string(), - cpu, - dr.config, - &mut cpu_group.group, - ); - match dr_ctr { - Err(e) => { - if let Some(os_error) = e.downcast_ref::() { - match os_error.kind() { - ErrorKind::NotFound => { - warn!("PMU counters not available on this instance type. Refer to APerf documentation for supported instances") - } - _ => match os_error.raw_os_error().unwrap() { - libc::EMFILE => warn!( - "Too many open files. Increase limit with by running `ulimit -n 65536`" - ), - _ => warn!("Unknown error when trying to use Perf API."), - }, - } - return Err(e); - } - } - Ok(v) => cpu_group.dr_ctr_add(v), - } + + pmu_config + } else if cpu_info.is_intel() { + let mut pmu_config = PmuConfig::from_default("intel_pmu_config.json")?; + + if cpu_info.is_intel_icelake() { + pmu_config.extend_from_default("intel_icelake_pmu_config.json")?; + } else if cpu_info.is_intel_sapphire_rapids() { + pmu_config.extend_from_default("intel_sapphire_rapids_pmu_config.json")?; + } + + pmu_config + } else if cpu_info.is_amd() { + let mut pmu_config = PmuConfig::from_default("amd_pmu_config.json")?; + + if cpu_info.is_amd_genoa() { + pmu_config.extend_from_default("amd_genoa_pmu_config.json")?; + } else if cpu_info.is_amd_milan() { + pmu_config.extend_from_default("amd_milan_pmu_config.json")?; } - cpu_groups.push(cpu_group); + + pmu_config + } else { + bail!( + "Unsupported CPU type for PMU config selection: {:?}", + cpu_info + ); } + }; + + // Write the selected PMU config to the run archive. + pmu_config.save_to_file(&get_saved_pmu_config_path(¶ms.data_dir))?; + + // Depending on the counter mode, either creates ungrouped event counters or per-metric + // counter groups for collection. + let mut pmu_collectors = if params.pmu_counter_mode == UNGROUPED_PMU_MODE { + pmu_config + .create_event_counters() + .context("Failed to create PMU event counters")? + } else { + pmu_config + .create_metric_counter_groups() + .context("Failed to create PMU metric counter groups")? + }; + + for pmu_collector in &mut pmu_collectors { + pmu_collector.enable()?; } - for cpu_group in &mut *cpu_groups { - cpu_group.group.reset()?; - cpu_group.group.enable()?; - } - CPU_CTR_GROUPS.lock().unwrap().append(&mut cpu_groups); + + self.pmu_collectors = pmu_collectors; + Ok(()) } fn collect_data(&mut self, _params: &CollectorParams) -> Result<()> { self.time = TimeEnum::DateTime(Utc::now()); self.data = String::new(); - let cpu_groups = &mut *CPU_CTR_GROUPS.lock().unwrap(); - for cpu_group in &mut *cpu_groups { - let count = cpu_group.group.read()?; - let mut group_data = format!("{} {};", cpu_group.cpu, cpu_group.name.clone()); - for nr in &cpu_group.nr_ctrs { - let nr_string = format!(" {}", count[&nr.counter]); - group_data.push_str(&nr_string); - } - group_data.push(';'); - for dr in &cpu_group.dr_ctrs { - let dr_string = format!(" {}", count[&dr.counter]); - group_data.push_str(&dr_string); - } - group_data.push(';'); - let scale_string = format!("{}", cpu_group.scale); - group_data.push_str(&scale_string); - group_data.push('\n'); - cpu_group.group.reset()?; - self.data.push_str(&group_data); + + for pmu_metric_counter_group in &mut self.pmu_collectors { + let group_data = pmu_metric_counter_group.collect()?; + write!(&mut self.data, "{}\n", group_data.to_string())?; } + Ok(()) } } @@ -367,9 +709,9 @@ impl PerfStat { } } -/// Parse the single-line raw PMU stat collected during APerf record into +/// For backward compatibility, parse the single-line legacy raw PMU stat collected during APerf record into /// (cpu number, stat name, numerator, denominator, scale) -fn parse_raw_pmu_stat(raw_pmu_stat: &str) -> Result<(usize, String, f64, f64, f64), String> { +fn parse_legacy_raw_pmu_stat(raw_pmu_stat: &str) -> Result<(usize, String, f64, f64, f64), String> { let mut raw_items = raw_pmu_stat.split(";"); let header = raw_items @@ -427,91 +769,371 @@ fn parse_raw_pmu_stat(raw_pmu_stat: &str) -> Result<(usize, String, f64, f64, f6 )) } -impl ProcessData for PerfStat { - fn process_raw_data(&mut self, params: ReportParams, raw_data: Vec) -> Result { - let mut time_series_data_processor = - time_series_data_processor_with_custom_aggregate!(params.collection_start); +/// For backward compatibility, process the raw PMU stat in the legacy (nr/dr-based) format. +fn process_legacy_raw_pmu_stat_data( + mut time_series_data_processor: TimeSeriesDataProcessor, + raw_data: &Vec, +) -> Result { + for buffer in raw_data { + let raw_value = match buffer { + Data::PerfStatRaw(ref value) => value, + _ => panic!("Invalid Data type in raw file"), + }; + time_series_data_processor.proceed_to_time(raw_value.time); - for buffer in raw_data { - let raw_value = match buffer { - Data::PerfStatRaw(ref value) => value, - _ => panic!("Invalid Data type in raw file"), + // To count the sum of every PMU stat's numerator and denominator across all CPUs, + // for the computation of the aggregate PMU stats, which is + // / * scale + let mut per_pmu_stat_numerator_sums: HashMap = HashMap::new(); + let mut per_pmu_stat_denominator_sums: HashMap = HashMap::new(); + + for raw_pmu_stat in raw_value.data.lines() { + let (cpu, pmu_stat_name, numerator, denominator, scale) = + match parse_legacy_raw_pmu_stat(raw_pmu_stat) { + Ok(parsed_pmu_stat) => parsed_pmu_stat, + Err(message) => { + error!("{}", message); + continue; + } + }; + if denominator == 0.0 { + continue; + } + let pmu_stat_value = numerator / denominator * scale; + time_series_data_processor.add_data_point( + &pmu_stat_name, + &get_cpu_series_name(cpu), + pmu_stat_value, + ); + + // For the computation of aggregate PMU stats + per_pmu_stat_numerator_sums + .entry(pmu_stat_name.clone()) + .and_modify(|numerator_sum| *numerator_sum += numerator * scale) + .or_insert(numerator * scale); + per_pmu_stat_denominator_sums + .entry(pmu_stat_name.clone()) + .and_modify(|denominator_sum| *denominator_sum += denominator) + .or_insert(denominator); + } + + // Insert average values into aggregate series + for (pmu_stat_name, numerator_sum) in per_pmu_stat_numerator_sums { + let denominator_sum = match per_pmu_stat_denominator_sums.get(&pmu_stat_name) { + Some(denominator_sum) => *denominator_sum, + None => continue, }; - time_series_data_processor.proceed_to_time(raw_value.time); - - // To count the sum of every PMU stat's numerator and denominator across all CPUs, - // for the computation of the aggregate PMU stats, which is - // / * scale - let mut per_pmu_stat_numerator_sums: HashMap = HashMap::new(); - let mut per_pmu_stat_denominator_sums: HashMap = HashMap::new(); - - for raw_pmu_stat in raw_value.data.lines() { - let (cpu, pmu_stat_name, numerator, denominator, scale) = - match parse_raw_pmu_stat(raw_pmu_stat) { - Ok(parsed_pmu_stat) => parsed_pmu_stat, - Err(message) => { - error!("{}", message); - continue; - } - }; - if denominator == 0.0 { - continue; - } - let pmu_stat_value = numerator / denominator * scale; + time_series_data_processor.add_aggregate_data_point( + &pmu_stat_name, + &get_aggregate_series_name(), + numerator_sum / denominator_sum, + ); + } + } + + // The metric order is defined by top down debug method https://github.com/aws/aws-graviton-getting-started/blob/main/perfrunbook/debug_hw_perf.md#how-to-collect-pmu-counters + let top_down_order = vec![ + "ipc", + "stall-frontend-pkc", + "stall-backend-pkc", + "branch-mpki", + "inst-l1-mpki", + "inst-tlb-mpki", + "inst-tlb-tw-pki", + "inst-tlb-tw-mpki", + "code-sparsity", + "data-l1-mpki", + "l2-mpki", + "l3-mpki", + "data-tlb-mpki", + "data-tlb-tw-pki", + "data-st-tlb-mpki", + "data-st-tlb-tw-pki", + "data-rd-tlb-mpki", + "data-rd-tlb-tw-pki", + ]; + let time_series_data = + time_series_data_processor.get_time_series_data_with_metric_name_order(top_down_order); + + Ok(AperfData::TimeSeries(time_series_data)) +} + +/// Process a snapshot of raw PMU counters that were collected using groups. +fn process_single_raw_pmu_metric_counter_group_data( + time_series_data_processor: &mut TimeSeriesDataProcessor, + raw_data: &str, + pmu_metric_expressions: &HashMap>, + zero_time_running_metrics: &mut HashSet, +) { + // For every metric, store the sum of every counter used by that metric across + // all CPUs, to compute the aggregate metric value. The order of the counter + // sums is in the same alphabetical order. + let mut per_metric_counter_value_sums: HashMap> = HashMap::new(); + + for pmu_data_string in raw_data.lines() { + let metric_group_data = match PmuCollectedData::from_string(pmu_data_string) { + Some(data) => data, + None => continue, + }; + let metric_name = metric_group_data.identifier; + let metric_expression = match pmu_metric_expressions.get(&metric_name) { + Some(expression) => expression, + None => continue, + }; + let cpu_series_name = get_cpu_series_name(metric_group_data.cpu_id); + + // All collected data (including time_enabled and time_running) are accumulative. + let time_enabled = match time_series_data_processor.get_delta_and_set_previous_value( + &format!("{metric_name}_time_enabled"), + &cpu_series_name, + metric_group_data.time_enabled, + ) { + Some(time_enabled) => time_enabled, + None => continue, + }; + let time_running = match time_series_data_processor.get_delta_and_set_previous_value( + &format!("{metric_name}_time_running"), + &cpu_series_name, + metric_group_data.time_running, + ) { + Some(time_running) => time_running, + None => continue, + }; + if time_running == 0.0 { + zero_time_running_metrics.insert(metric_name.clone()); + continue; + } + + // Use time_enabled and time_running to scale the value, as when more events need to be + // collected than the hardware limitation, the PMU will multiplex (time-share) all the + // counters and they will be scheduled to run for different times. + let scaled_counter_values: Vec = metric_group_data + .counter_values + .iter() + .map(|&value| (value as f64) * time_enabled / time_running) + .collect(); + + // Use the counter value and metric expression to compute the metric value. + match metric_expression.eval(&scaled_counter_values) { + Ok(metric_value) if metric_value.is_finite() => { time_series_data_processor.add_data_point( - &pmu_stat_name, - &get_cpu_series_name(cpu), - pmu_stat_value, + &metric_name, + &get_cpu_series_name(metric_group_data.cpu_id), + metric_value, ); + } + Err(e) => { + debug!( + "Failed to evaluate PMU metric {metric_name} on CPU {}: {:?}", + metric_group_data.cpu_id, e + ); + continue; + } + _ => continue, + } - // For the computation of aggregate PMU stats - per_pmu_stat_numerator_sums - .entry(pmu_stat_name.clone()) - .and_modify(|numerator_sum| *numerator_sum += numerator * scale) - .or_insert(numerator * scale); - per_pmu_stat_denominator_sums - .entry(pmu_stat_name.clone()) - .and_modify(|denominator_sum| *denominator_sum += denominator) - .or_insert(denominator); + // Sum up the counter values within the metric across all CPUs. + let counter_value_sums = per_metric_counter_value_sums + .entry(metric_name) + .or_insert_with(|| Vec::new()); + for (index, counter_value) in scaled_counter_values.iter().enumerate() { + if index >= counter_value_sums.len() { + counter_value_sums.push(*counter_value); + } else { + counter_value_sums[index] += *counter_value; } + } + } - // Insert average values into aggregate series - for (pmu_stat_name, numerator_sum) in per_pmu_stat_numerator_sums { - let denominator_sum = match per_pmu_stat_denominator_sums.get(&pmu_stat_name) { - Some(denominator_sum) => *denominator_sum, - None => continue, - }; + // Compute and add aggregate series data points. + for (metric_name, counter_value_sums) in per_metric_counter_value_sums { + let metric_expression = match pmu_metric_expressions.get(&metric_name) { + Some(expression) => expression, + None => continue, + }; + match metric_expression.eval_vec(counter_value_sums) { + Ok(metric_value) if metric_value.is_finite() => { time_series_data_processor.add_aggregate_data_point( - &pmu_stat_name, + &metric_name, &get_aggregate_series_name(), - numerator_sum / denominator_sum, + metric_value, ); } + Err(e) => debug!( + "Failed to evaluate the aggregate series of PMU metric {metric_name}: {:?}", + e + ), + _ => continue, + } + } +} + +/// Process a snapshot of raw PMU counters that were collected without using groups. +fn process_single_raw_pmu_event_counter_data( + time_series_data_processor: &mut TimeSeriesDataProcessor, + raw_data: &str, + pmu_metric_expressions: &HashMap>, +) { + // Store the delta value of an event counter across all CPUs. + let mut per_cpu_event_counter_values: HashMap> = HashMap::new(); + // Store the sum of every counter value across all CPUs, used to compute the aggregate + // series value for the metric. + let mut event_counter_value_sums: HashMap = HashMap::new(); + + for pmu_data_string in raw_data.lines() { + let event_counter_data = match PmuCollectedData::from_string(pmu_data_string) { + Some(data) => data, + None => continue, + }; + let event_name = event_counter_data.identifier; + let cpu_id = event_counter_data.cpu_id; + let cpu_series_name = get_cpu_series_name(cpu_id); + + // All collected data (including time_enabled and time_running) are accumulative. + let counter_value = event_counter_data.counter_values[0] as f64; + let time_enabled = match time_series_data_processor.get_delta_and_set_previous_value( + &format!("{event_name}_time_enabled"), + &cpu_series_name, + event_counter_data.time_enabled, + ) { + Some(time_enabled) => time_enabled, + None => continue, + }; + let time_running = match time_series_data_processor.get_delta_and_set_previous_value( + &format!("{event_name}_time_running"), + &cpu_series_name, + event_counter_data.time_running, + ) { + Some(time_running) => time_running, + None => continue, + }; + if time_running == 0.0 { + continue; + } + + // Use time_enabled and time_running to scale the value, as when more events need to be + // collected than the hardware limitation, the PMU will multiplex (time-share) all the + // counters and they will be scheduled to run for different times. + let scaled_counter_value = counter_value * time_enabled / time_running; + per_cpu_event_counter_values + .entry(cpu_id) + .or_insert_with(|| HashMap::new()) + .insert(event_name.clone(), scaled_counter_value); + *(event_counter_value_sums.entry(event_name).or_insert(0.0)) += scaled_counter_value; + } + + for (metric_name, metric_expression) in pmu_metric_expressions { + // The list of identifiers (event names) in the metric expression. + let event_names = metric_expression.var_names().to_vec(); + + for (&cpu_id, event_counter_values) in &per_cpu_event_counter_values { + let expression_values: Vec = event_names + .iter() + .map(|event_name| event_counter_values.get(event_name).copied()) + .flatten() + .collect(); + + match metric_expression.eval_vec(expression_values) { + Ok(metric_value) if metric_value.is_finite() => { + time_series_data_processor.add_data_point( + metric_name, + &get_cpu_series_name(cpu_id), + metric_value, + ); + } + Err(e) => { + debug!( + "Failed to evaluate PMU metric {metric_name} on CPU {}: {:?}", + cpu_id, e + ); + } + _ => continue, + } + } + + // Compute aggregate series value using computed sums + let aggregate_expression_values: Vec = event_names + .iter() + .map(|event_name| event_counter_value_sums.get(event_name).copied()) + .flatten() + .collect(); + match metric_expression.eval_vec(aggregate_expression_values) { + Ok(metric_value) if metric_value.is_finite() => { + time_series_data_processor.add_aggregate_data_point( + metric_name, + &get_aggregate_series_name(), + metric_value, + ); + } + Err(e) => { + debug!( + "Failed to evaluate the aggregate series of PMU metric {metric_name}: {:?}", + e + ); + } + _ => continue, + } + } +} + +impl ProcessData for PerfStat { + fn process_raw_data(&mut self, params: ReportParams, raw_data: Vec) -> Result { + let mut time_series_data_processor = + time_series_data_processor_with_custom_aggregate!(params.collection_start); + + if params.pmu_counter_mode.is_empty() { + return process_legacy_raw_pmu_stat_data(time_series_data_processor, &raw_data); + } + + // Since the counters accumulates from 0, we should use what has already + // been collected at the first collection. + time_series_data_processor.use_first_accumulative_value(); + + let pmu_config = PmuConfig::from_file(&get_saved_pmu_config_path(¶ms.data_dir)) + .with_context(|| { + format!( + "Failed to open saved PMU config for run {}", + params.run_name + ) + })?; + let pmu_metric_expressions = pmu_config.get_metric_expressions()?; + + if params.pmu_counter_mode == UNGROUPED_PMU_MODE { + for buffer in raw_data { + let raw_value = match buffer { + Data::PerfStatRaw(ref value) => value, + _ => panic!("Invalid Data type in raw file"), + }; + time_series_data_processor.proceed_to_time(raw_value.time); + process_single_raw_pmu_event_counter_data( + &mut time_series_data_processor, + &raw_value.data, + &pmu_metric_expressions, + ); + } + } else { + let mut zero_time_running_metrics = HashSet::new(); + for buffer in raw_data { + let raw_value = match buffer { + Data::PerfStatRaw(ref value) => value, + _ => panic!("Invalid Data type in raw file"), + }; + time_series_data_processor.proceed_to_time(raw_value.time); + process_single_raw_pmu_metric_counter_group_data( + &mut time_series_data_processor, + &raw_value.data, + &pmu_metric_expressions, + &mut zero_time_running_metrics, + ); + } + for metric in zero_time_running_metrics { + warn!("PMU metric {metric} might contain too many events to be scheduled for collection. Please reduce the number of events in it or use --ungroup-pmu-events."); + } } - // The metric order is defined by top down debug method https://github.com/aws/aws-graviton-getting-started/blob/main/perfrunbook/debug_hw_perf.md#how-to-collect-pmu-counters - let top_down_order = vec![ - "ipc", - "stall-frontend-pkc", - "stall-backend-pkc", - "branch-mpki", - "inst-l1-mpki", - "inst-tlb-mpki", - "inst-tlb-tw-pki", - "inst-tlb-tw-mpki", - "code-sparsity", - "data-l1-mpki", - "l2-mpki", - "l3-mpki", - "data-tlb-mpki", - "data-tlb-tw-pki", - "data-st-tlb-mpki", - "data-st-tlb-tw-pki", - "data-rd-tlb-mpki", - "data-rd-tlb-tw-pki", - ]; - let time_series_data = - time_series_data_processor.get_time_series_data_with_metric_name_order(top_down_order); + let all_metric_names: Vec<&str> = pmu_config.metrics.keys().map(String::as_str).collect(); + let time_series_data = time_series_data_processor + .get_time_series_data_with_metric_name_order(all_metric_names); Ok(AperfData::TimeSeries(time_series_data)) } diff --git a/src/lib.rs b/src/lib.rs index 459ebdfa..c10d148c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,51 +61,12 @@ lazy_static! { #[derive(Error, Debug)] pub enum PDError { - #[error("Error initializing logger")] - LoggerInitError, - - #[error("Error getting JavaScript file for {}", .0)] - VisualizerJSFileGetError(String), - - #[error("Error getting HashMap entry for {}", .0)] - VisualizerHashMapEntryError(String), - - #[error("Error getting run values for {}", .0)] - VisualizerRunValueGetError(String), - - #[error("Error getting Vmstat value for {}", .0)] - VisualizerVmstatValueGetError(String), - - #[error("Error getting interrupt line count for CPU {}", .0)] - VisualizerInterruptLineCPUCountError(String), - - #[error("Error getting Netstat value for {}", .0)] - VisualizerNetstatValueGetError(String), - - #[error("{} data is not available for run {}", .0, .1)] - DataUnavailableError(String, String), - #[error("Error getting Line Name Error")] CollectorLineNameError, #[error("Error getting Line Value Error")] CollectorLineValueError, - #[error("Error getting value from Option")] - ProcessorOptionExtractError, - - #[error("Unsupported CPU")] - CollectorPerfUnsupportedCPU, - - #[error("Unsupported API")] - VisualizerUnsupportedAPI, - - #[error("Visualizer Init error")] - VisualizerInitError, - - #[error("Multiple runs with the same name: {0}")] - DuplicateRunNames(String), - #[error("The run {0:?} does not exist.")] RunNotFound(PathBuf), @@ -127,24 +88,6 @@ pub enum PDError { #[error("Invalid time-range option: {}", .0)] InvalidRunTimeRangeOption(String), - #[error("All processes collection error")] - CollectorAllProcessError, - - #[error("Could not get the total number of online CPUs with sysconf")] - CollectorPMUCPUError, - - #[error("Generating report from other reports. Name must be given.")] - VisualizerReportFromReportNoNameError, - - #[error("File not found {}", .0)] - VisualizerFileNotFound(String), - - #[error("Custom PMU config file not provided.")] - PMUCustomFileNotFound, - - #[error("PMU config file is invalid.")] - PMUFileInvalid, - #[error("Failed to detect network interfaces: {}", .0)] NetworkInterfaceDetectionFailure(String), @@ -154,9 +97,6 @@ pub enum PDError { #[error("Run data not available")] InvalidRunData, - #[error("Error getting Meminfo values for {}", .0)] - VisualizerMeminfoValueGetError(String), - #[error("Dependency error: {}", .0)] DependencyError(String), @@ -240,7 +180,7 @@ impl PerformanceData { process::exit(1); } let msg = format!( - "Excluding {} from collection, data preparation failed. Error msg: {}", + "Excluding {} from collection, data preparation failed: {:?}", name, e ); if matches!( @@ -437,6 +377,7 @@ impl VisualizationData { tmp_dir: &Path, report_dir: &Path, collection_start: Option, + pmu_counter_mode: String, ) -> Result<()> { let visualizers_len = self.visualizers.len(); let mut error_count = 0; @@ -448,6 +389,7 @@ impl VisualizationData { tmp_dir, report_dir, collection_start, + pmu_counter_mode.clone(), ) { debug!("{:#?}", e); error_count += 1; @@ -511,16 +453,26 @@ impl VisualizationData { } } +pub const GROUPED_PMU_MODE: &str = "grouped"; +pub const UNGROUPED_PMU_MODE: &str = "ungrouped"; + #[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(default)] pub struct InitParams { pub dir_name: String, pub period: u64, pub profile: HashMap, pub pmu_config: Option, + /// Whether the collection of PMU counters is "grouped" or + /// "ungrouped". An empty string means a legacy run before + /// PMU config revamp. + pub pmu_counter_mode: String, pub interval: u64, pub run_name: String, + /// The version of APerf that performed the collection. pub collector_version: String, - pub commit_sha_short: String, + /// The short commit SHA of APerf that performed the collection. + pub collector_commit_sha: String, pub tmp_dir: PathBuf, pub runlog: PathBuf, pub perf_frequency: u32, @@ -560,17 +512,18 @@ impl InitParams { ); } let collector_version = env!("CARGO_PKG_VERSION").to_string(); - let commit_sha_short = env!("VERGEN_GIT_SHA").to_string(); + let collector_commit_sha = env!("VERGEN_GIT_SHA").to_string(); InitParams { dir_name, period: 0, profile: HashMap::new(), pmu_config: Option::None, + pmu_counter_mode: GROUPED_PMU_MODE.to_string(), interval: 0, run_name, collector_version, - commit_sha_short, + collector_commit_sha, tmp_dir: PathBuf::from(APERF_TMP), runlog: PathBuf::new(), perf_frequency: 99, diff --git a/src/pmu.rs b/src/pmu.rs index 90e15313..8742e0bd 100644 --- a/src/pmu.rs +++ b/src/pmu.rs @@ -1,323 +1,10 @@ -#![cfg(target_os = "linux")] - -#[cfg(target_arch = "aarch64")] -use crate::data::perf_stat::arm64_perf_list; -#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -use crate::data::perf_stat::{form_events_map, x86_perf_list}; -use crate::data::perf_stat::{to_events, NamedCtr, NamedTypeCtr, PerfType}; -use crate::PDError; - -use anyhow::Result; +use anyhow::{Ok, Result}; use clap::Args; -use inquire::{ - list_option::ListOption, required, validator::Validation, Confirm, MultiSelect, Select, Text, -}; -use std::path::PathBuf; #[derive(Args, Debug)] -pub struct CustomPMU { - /// Name of the file for the custom PMU configuration. - #[clap(help_heading = "PMU Options", short, long, value_parser)] - pub pmu_file: Option, +pub struct CustomPMU {} - /// Verify the supplied pmu file. - #[clap(help_heading = "PMU Options", long, value_parser)] - pub verify: bool, -} - -pub fn get_ctrs(opt_str: &str) -> Result> { - println!(" \"{opt_str}\": ["); - let mut ret: Vec = Vec::new(); - loop { - println!(" {{"); - println!(" \"perf_type\": RAW"); - let perf_type = PerfType::RAW; - let name = Text::new(" \"name\":") - .with_validator(|s: &str| { - if s.chars().all(char::is_alphanumeric) { - Ok(Validation::Valid) - } else { - Ok(Validation::Invalid( - "Name must contain only alphanumeric characters.".into(), - )) - } - }) - .with_validator(required!()) - .prompt()?; - let raw_config = Text::new(" \"config\":") - .with_validator(|s: &str| { - if s.starts_with("0x") { - Ok(Validation::Valid) - } else { - Ok(Validation::Invalid( - "Config must be hexadecimal and start with 0x.".into(), - )) - } - }) - .with_validator(|s: &str| { - let no_prefix = s.trim_start_matches("0x"); - match u64::from_str_radix(no_prefix, 16) { - Ok(_) => Ok(Validation::Valid), - Err(_) => Ok(Validation::Invalid("Invalid hexadecimal value.".into())), - } - }) - .prompt()?; - let no_prefix = raw_config.trim_start_matches("0x"); - let config = u64::from_str_radix(no_prefix, 16)?; - println!(" }}"); - ret.push(NamedTypeCtr { - perf_type, - name, - config, - }); - if !Confirm::new(format!("Add more {opt_str}:").as_str()) - .with_default(false) - .prompt()? - { - println!("\n ]"); - break; - } - } - Ok(ret) -} - -pub fn add_events(existing_events: Vec) -> Result> { - let mut events: Vec = Vec::new(); - let mut event_names = Vec::new(); - for event in existing_events { - event_names.push(event.name); - } - loop { - println!("{{"); - let event_names_tmp = event_names.clone(); - let name = Text::new(" \"name\":") - .with_validator(|s: &str| { - if s.chars().all(char::is_alphanumeric) { - Ok(Validation::Valid) - } else { - Ok(Validation::Invalid( - "Name must contain only alphanumeric characters.".into(), - )) - } - }) - .with_validator(move |s: &str| { - if !event_names_tmp.contains(&s.to_string()) { - Ok(Validation::Valid) - } else { - Ok(Validation::Invalid( - "Event with the same name exists.".into(), - )) - } - }) - .with_validator(required!()) - .prompt()?; - event_names.push(name.clone()); - let nrs = get_ctrs("nrs")?; - let drs = get_ctrs("drs")?; - let scale_text = Text::new(" \"scale\":") - .with_validator(required!()) - .with_validator(|s: &str| match s.parse::() { - Ok(v) => { - if v == 0 { - Ok(Validation::Invalid("Scaling value cannot be 0.".into())) - } else { - Ok(Validation::Valid) - } - } - Err(_) => Ok(Validation::Invalid( - "Scaling value should be a valid number.".into(), - )), - }) - .prompt()?; - let scale = scale_text.parse::()?; - events.push(NamedCtr { - name, - nrs, - drs, - scale, - }); - println!(" }}"); - if !Confirm::new("Add more events:") - .with_default(false) - .prompt()? - { - break; - } - } - Ok(events) -} - -pub fn delete_events(mut perf_list: Vec) -> Result> { - loop { - let mut ev_list = Vec::new(); - for event in &perf_list { - ev_list.push(event.name.clone()); - } - if ev_list.is_empty() { - println!("Cannot delete any more events."); - return Ok(Vec::new()); - } - let delete_list = MultiSelect::new("Select event(s) to delete:", ev_list) - .with_validator(|a: &[ListOption<&String>]| { - if a.is_empty() { - Ok(Validation::Invalid("Must choose at least 1 option.".into())) - } else { - Ok(Validation::Valid) - } - }) - .prompt()?; - for name in delete_list { - let index = perf_list.iter().position(|ev| ev.name == name).unwrap(); - let out = format!( - "\n{}\nDelete?", - serde_json::to_string_pretty(&perf_list[index])? - ); - if Confirm::new(&out).with_default(true).prompt()? { - perf_list.remove(index); - } - } - if !Confirm::new("Remove more?").with_default(false).prompt()? { - break; - } - } - Ok(perf_list.to_vec()) -} - -pub fn create_pmu_config(cpmu: &CustomPMU) -> Result<()> { - let mut pmu_file = PathBuf::from("aperf_custom_pmu.json"); - if let Some(f) = &cpmu.pmu_file { - pmu_file = PathBuf::from(f); - } - #[cfg(target_arch = "aarch64")] - let events: Vec = serde_json::from_slice(arm64_perf_list::GRV_EVENTS)?; - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - let events: Vec = serde_json::from_slice(x86_perf_list::INTEL_EVENTS)?; - #[cfg(target_arch = "aarch64")] - let platform = "Graviton"; - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - let platform = "Intel"; - println!( - "\nAperf PMU Event structure (Ex: {})\n{}", - platform, - serde_json::to_string_pretty(&events[0])? - ); - println!( - "\nPlease enter your custom PMU details. Only hex values (0x) for 'config' will work.\n" - ); - let events = add_events(Vec::new())?; - let f = std::fs::File::create(&pmu_file)?; - serde_json::to_writer_pretty(f, &events)?; - println!( - "\nCustom PMU config generated at: {:?}. Use this with 'aperf record --pmu-file {:?}'.", - pmu_file, pmu_file - ); - Ok(()) -} - -pub fn get_config(choice: &str, cpmu: &CustomPMU) -> Result> { - if choice == "User provided" { - if let Some(f) = &cpmu.pmu_file { - let file = std::fs::File::open(PathBuf::from(f))?; - return Ok(serde_json::from_reader(&file)?); - } else { - println!("No custom config file provided."); - return Err(PDError::PMUCustomFileNotFound.into()); - } - }; - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - match choice { - "Intel" => to_events(x86_perf_list::INTEL_EVENTS), - "Intel Sapphire Rapids" => { - form_events_map(x86_perf_list::INTEL_EVENTS, x86_perf_list::SPR_CTRS) - } - "Intel Icelake" => form_events_map(x86_perf_list::INTEL_EVENTS, x86_perf_list::ICX_CTRS), - "AMD" => to_events(x86_perf_list::AMD_EVENTS), - "AMD Genoa" => form_events_map(x86_perf_list::AMD_EVENTS, x86_perf_list::GENOA_CTRS), - "AMD Milan" => form_events_map(x86_perf_list::AMD_EVENTS, x86_perf_list::MILAN_CTRS), - _ => Ok(Vec::new()), - } - #[cfg(target_arch = "aarch64")] - match choice { - "Graviton" => to_events(arm64_perf_list::GRV_EVENTS), - _ => Ok(Vec::new()), - } -} - -pub fn modify_existing_config(cpmu: &CustomPMU) -> Result<()> { - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - let config = Select::new( - "Select a config:", - vec![ - "User provided", - "Intel", - "Intel Sapphire Rapids", - "Intel Icelake", - "AMD", - "AMD Genoa", - "AMD Milan", - ], - ) - .prompt()?; - #[cfg(target_arch = "aarch64")] - let config = Select::new("Select a config:", vec!["User provided", "Graviton"]).prompt()?; - let mut perf_list = get_config(config, cpmu)?; - loop { - let option = Select::new("Select action:", vec!["Add", "Delete", "Done"]).prompt()?; - if option == "Add" { - if perf_list.is_empty() { - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] - let example: Vec = to_events(x86_perf_list::INTEL_EVENTS)?; - #[cfg(target_arch = "aarch64")] - let example: Vec = to_events(arm64_perf_list::GRV_EVENTS)?; - println!( - "\nAperf PMU Event structure (Example)\n{}", - serde_json::to_string_pretty(&example[0])? - ); - } else { - println!( - "\nAperf PMU Event structure (Ex: {})\n{}", - config, - serde_json::to_string_pretty(&perf_list[0])? - ); - } - perf_list.append(&mut add_events(perf_list.clone())?); - } else if option == "Delete" { - perf_list = delete_events(perf_list.clone())?; - } else if option == "Done" { - let f = std::fs::File::create("aperf_existing_modified.json")?; - serde_json::to_writer_pretty(f, &perf_list)?; - println!( - "\nCustom PMU config generated at: aperf_existing_modified.json. Use this with 'aperf record --pmu-file aperf_existing_modified.json'.", - ); - break; - } - } +pub fn custom_pmu(_custom_pmu: &CustomPMU) -> Result<()> { + print!("Please refer to src/pmu_configs to create a custom PMU config file."); Ok(()) } - -pub fn custom_pmu(cpmu: &CustomPMU) -> Result<()> { - if cpmu.verify { - if let Some(custom_file) = &cpmu.pmu_file { - let f = std::fs::File::open(custom_file)?; - let check_format: Result, serde_json::Error> = - serde_json::from_reader(&f); - if check_format.is_ok() { - println!("Custom PMU file is valid."); - return Ok(()); - } else { - return Err(PDError::PMUFileInvalid.into()); - } - } - return Err(PDError::PMUCustomFileNotFound.into()); - } - let choice = Select::new( - "Aperf Custom PMU config:", - vec!["Create from scratch", "Modify existing config"], - ) - .prompt()?; - if choice == "Create from scratch" { - create_pmu_config(cpmu) - } else { - modify_existing_config(cpmu) - } -} diff --git a/src/pmu_configs/amd_genoa_ctrs.json b/src/pmu_configs/amd_genoa_ctrs.json deleted file mode 100644 index ea952331..00000000 --- a/src/pmu_configs/amd_genoa_ctrs.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "name": "stall-backend-pkc", - "nrs": [ - { - "perf_type": "RAW", - "name": "Backend-Stalls", - "config": 4294975136 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Cycles", - "config": 118 - } - ], - "scale": 167 - } -] \ No newline at end of file diff --git a/src/pmu_configs/amd_genoa_pmu_config.json b/src/pmu_configs/amd_genoa_pmu_config.json new file mode 100644 index 00000000..bf18e805 --- /dev/null +++ b/src/pmu_configs/amd_genoa_pmu_config.json @@ -0,0 +1,9 @@ +{ + "events": { + "Backend_Stalls": "cpu/event=0x1a0,umask=0x1e/", + "Cycles": "cpu/event=0x76,umask=0x0/" + }, + "metrics": { + "stall-backend-pkc": "Backend_Stalls / Cycles * 167" + } +} diff --git a/src/pmu_configs/amd_milan_ctrs.json b/src/pmu_configs/amd_milan_ctrs.json deleted file mode 100644 index d5cba8ec..00000000 --- a/src/pmu_configs/amd_milan_ctrs.json +++ /dev/null @@ -1,25 +0,0 @@ -[ - { - "name": "stall-backend-pkc", - "nrs": [ - { - "perf_type": "RAW", - "name": "Backend-Stalls-1", - "config": 63406 - }, - { - "perf_type": "RAW", - "name": "Backend-Stalls-2", - "config": 10159 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Cycles", - "config": 118 - } - ], - "scale": 1000 - } -] \ No newline at end of file diff --git a/src/pmu_configs/amd_milan_pmu_config.json b/src/pmu_configs/amd_milan_pmu_config.json new file mode 100644 index 00000000..d7fb6820 --- /dev/null +++ b/src/pmu_configs/amd_milan_pmu_config.json @@ -0,0 +1,10 @@ +{ + "events": { + "Backend_Stalls_1": "cpu/event=0xae,umask=0xf7/", + "Backend_Stalls_2": "cpu/event=0xaf,umask=0x27/", + "Cycles": "cpu/event=0x76,umask=0x0/" + }, + "metrics": { + "stall-backend-pkc": "(Backend_Stalls_1 + Backend_Stalls_2) / Cycles * 1000" + } +} diff --git a/src/pmu_configs/amd_perf_list.json b/src/pmu_configs/amd_perf_list.json deleted file mode 100644 index 09aca0aa..00000000 --- a/src/pmu_configs/amd_perf_list.json +++ /dev/null @@ -1,200 +0,0 @@ -[ - { - "name": "ipc", - "nrs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Cycles", - "config": 118 - } - ], - "scale": 1 - }, - { - "name": "branch-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Branch-Mispredictions", - "config": 195 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "data-l1-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "L1-Data-Fills", - "config": 65348 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "inst-l1-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "L1-Instruction-Misses", - "config": 4192 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "l2-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "L2-Demand-Misses", - "config": 2404 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "l3-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "L1-Any-Fills-DRAM", - "config": 2116 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "stall-frontend-pkc", - "nrs": [ - { - "perf_type": "RAW", - "name": "Frontend-Stalls", - "config": 169 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Cycles", - "config": 118 - } - ], - "scale": 1000 - }, - { - "name": "inst-tlb-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Instruction-TLB-Misses", - "config": 132 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "inst-tlb-tw-pki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Instruction-TLB-TW-Misses", - "config": 3973 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "data-tlb-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Data-TLB-Misses", - "config": 65349 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "data-tlb-tw-pki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Data-TLB-TW-Misses", - "config": 61509 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - } -] \ No newline at end of file diff --git a/src/pmu_configs/amd_pmu_config.json b/src/pmu_configs/amd_pmu_config.json new file mode 100644 index 00000000..0c1d1941 --- /dev/null +++ b/src/pmu_configs/amd_pmu_config.json @@ -0,0 +1,29 @@ +{ + "events": { + "Instructions": "cpu/event=0xc0,umask=0x0/", + "Cycles": "cpu/event=0x76,umask=0x0/", + "Branch_Mispredictions": "cpu/event=0xc3,umask=0x0/", + "L1_Data_Fills": "cpu/event=0x44,umask=0xff/", + "L1_Instruction_Misses": "cpu/event=0x60,umask=0x10/", + "L2_Demand_Misses": "cpu/event=0x64,umask=0x9/", + "L1_Any_Fills_DRAM": "cpu/event=0x44,umask=0x8/", + "Frontend_Stalls": "cpu/event=0xa9,umask=0x0/", + "Instruction_TLB_Misses": "cpu/event=0x84,umask=0x0/", + "Instruction_TLB_TW_Misses": "cpu/event=0x85,umask=0xf/", + "Data_TLB_Misses": "cpu/event=0x45,umask=0xff/", + "Data_TLB_TW_Misses": "cpu/event=0x45,umask=0xf0/" + }, + "metrics": { + "ipc": "Instructions / Cycles", + "stall-frontend-pkc": "Frontend_Stalls / Cycles * 1000", + "branch-mpki": "Branch_Mispredictions / Instructions * 1000", + "inst-l1-mpki": "L1_Instruction_Misses / Instructions * 1000", + "inst-tlb-mpki": "Instruction_TLB_Misses / Instructions * 1000", + "inst-tlb-tw-pki": "Instruction_TLB_TW_Misses / Instructions * 1000", + "data-l1-mpki": "L1_Data_Fills / Instructions * 1000", + "l2-mpki": "L2_Demand_Misses / Instructions * 1000", + "l3-mpki": "L1_Any_Fills_DRAM / Instructions * 1000", + "data-tlb-mpki": "Data_TLB_Misses / Instructions * 1000", + "data-tlb-tw-pki": "Data_TLB_TW_Misses / Instructions * 1000" + } +} diff --git a/src/pmu_configs/grv_5_pmu_config.json b/src/pmu_configs/grv_5_pmu_config.json new file mode 100644 index 00000000..39daf215 --- /dev/null +++ b/src/pmu_configs/grv_5_pmu_config.json @@ -0,0 +1,8 @@ +{ + "events": { + "L3_READ": "armv8_pmuv3_0/event=0x36/" + }, + "metrics": { + "l3-mpki": "(L3 / L3_READ * L2) / Instructions * 1000" + } +} \ No newline at end of file diff --git a/src/pmu_configs/grv_perf_list.json b/src/pmu_configs/grv_perf_list.json deleted file mode 100644 index ed2e6f4e..00000000 --- a/src/pmu_configs/grv_perf_list.json +++ /dev/null @@ -1,254 +0,0 @@ -[ - { - "name": "ipc", - "nrs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 8 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Cycles", - "config": 17 - } - ], - "scale": 1 - }, - { - "name": "stall-frontend-pkc", - "nrs": [ - { - "perf_type": "RAW", - "name": "Frontend-Stalls", - "config": 35 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Cycles", - "config": 17 - } - ], - "scale": 1000 - }, - { - "name": "branch-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Branches", - "config": 16 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 8 - } - ], - "scale": 1000 - }, - { - "name": "code-sparsity", - "nrs": [ - { - "perf_type": "RAW", - "name": "Code-Sparsity", - "config": 284 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 8 - } - ], - "scale": 1000 - }, - { - "name": "inst-tlb-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Instruction-TLB", - "config": 2 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 8 - } - ], - "scale": 1000 - }, - { - "name": "inst-tlb-tw-pki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Instruction-TLB-TW", - "config": 53 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 8 - } - ], - "scale": 1000 - }, - { - "name": "inst-l1-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "L1-Instructions", - "config": 1 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 8 - } - ], - "scale": 1000 - }, - { - "name": "stall-backend-pkc", - "nrs": [ - { - "perf_type": "RAW", - "name": "Backend-Stalls", - "config": 36 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Cycles", - "config": 17 - } - ], - "scale": 1000 - }, - { - "name": "l3-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "L3", - "config": 55 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 8 - } - ], - "scale": 1000 - }, - { - "name": "l2-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "L2", - "config": 23 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 8 - } - ], - "scale": 1000 - }, - { - "name": "data-tlb-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Data-TLB", - "config": 5 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 8 - } - ], - "scale": 1000 - }, - { - "name": "data-tlb-tw-pki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Data-TLB-TW", - "config": 52 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 8 - } - ], - "scale": 1000 - }, - { - "name": "data-l1-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "L1-Data", - "config": 3 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 8 - } - ], - "scale": 1000 - }, - { - "name": "strex-spec-pki", - "nrs": [ - { - "perf_type": "RAW", - "name": "STREX-SPEC", - "config": 111 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 8 - } - ], - "scale": 1000 - } -] \ No newline at end of file diff --git a/src/pmu_configs/grv_pmu_config.json b/src/pmu_configs/grv_pmu_config.json new file mode 100644 index 00000000..cb8a4b94 --- /dev/null +++ b/src/pmu_configs/grv_pmu_config.json @@ -0,0 +1,35 @@ +{ + "events": { + "Instructions": "armv8_pmuv3_0/event=0x8/", + "Cycles": "armv8_pmuv3_0/event=0x11/", + "Frontend_Stalls": "armv8_pmuv3_0/event=0x23/", + "Backend_Stalls": "armv8_pmuv3_0/event=0x24/", + "Branches": "armv8_pmuv3_0/event=0x10/", + "Code_Sparsity": "armv8_pmuv3_0/event=0x11c/", + "Instruction_TLB": "armv8_pmuv3_0/event=0x2/", + "Instruction_TLB_TW": "armv8_pmuv3_0/event=0x35/", + "L1_Instructions": "armv8_pmuv3_0/event=0x1/", + "L1_Data": "armv8_pmuv3_0/event=0x3/", + "L2": "armv8_pmuv3_0/event=0x17/", + "L3": "armv8_pmuv3_0/event=0x37/", + "Data_TLB": "armv8_pmuv3_0/event=0x5/", + "Data_TLB_TW": "armv8_pmuv3_0/event=0x34/", + "STREX_SPEC": "armv8_pmuv3_0/event=0x6f/" + }, + "metrics": { + "ipc": "Instructions / Cycles", + "stall-frontend-pkc": "Frontend_Stalls / Cycles * 1000", + "stall-backend-pkc": "Backend_Stalls / Cycles * 1000", + "branch-mpki": "Branches / Instructions * 1000", + "inst-l1-mpki": "L1_Instructions / Instructions * 1000", + "inst-tlb-mpki": "Instruction_TLB / Instructions * 1000", + "inst-tlb-tw-pki": "Instruction_TLB_TW / Instructions * 1000", + "code-sparsity": "Code_Sparsity / Instructions * 1000", + "data-l1-mpki": "L1_Data / Instructions * 1000", + "l2-mpki": "L2 / Instructions * 1000", + "l3-mpki": "L3 / Instructions * 1000", + "data-tlb-mpki": "Data_TLB / Instructions * 1000", + "data-tlb-tw-pki": "Data_TLB_TW / Instructions * 1000", + "strex-spec-pki": "STREX_SPEC / Instructions * 1000" + } +} diff --git a/src/pmu_configs/intel_icelake_ctrs.json b/src/pmu_configs/intel_icelake_ctrs.json deleted file mode 100644 index 3dad47f1..00000000 --- a/src/pmu_configs/intel_icelake_ctrs.json +++ /dev/null @@ -1,38 +0,0 @@ -[ - { - "name": "stall-frontend-pkc", - "nrs": [ - { - "perf_type": "RAW", - "name": "Frontend-Stalls", - "config": 83886492 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Cycles", - "config": 60 - } - ], - "scale": 1000 - }, - { - "name": "stall-backend-pkc", - "nrs": [ - { - "perf_type": "RAW", - "name": "Backend-Stalls", - "config": 676 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Slots", - "config": 420 - } - ], - "scale": 1000 - } -] \ No newline at end of file diff --git a/src/pmu_configs/intel_icelake_pmu_config.json b/src/pmu_configs/intel_icelake_pmu_config.json new file mode 100644 index 00000000..0a55b353 --- /dev/null +++ b/src/pmu_configs/intel_icelake_pmu_config.json @@ -0,0 +1,12 @@ +{ + "events": { + "Frontend_Stalls": "cpu/event=0x9c,umask=0x1,cmask=0x5/", + "Cycles": "cpu/event=0x3c,umask=0x0/", + "Backend_Stalls": "cpu/event=0xa4,umask=0x2/", + "Slots": "cpu/event=0xa4,umask=0x1/" + }, + "metrics": { + "stall-frontend-pkc": "Frontend_Stalls / Cycles * 1000", + "stall-backend-pkc": "Backend_Stalls / Slots * 1000" + } +} diff --git a/src/pmu_configs/intel_perf_list.json b/src/pmu_configs/intel_perf_list.json deleted file mode 100644 index ad692790..00000000 --- a/src/pmu_configs/intel_perf_list.json +++ /dev/null @@ -1,254 +0,0 @@ -[ - { - "name": "ipc", - "nrs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Cycles", - "config": 60 - } - ], - "scale": 1 - }, - { - "name": "stall-frontend-pkc", - "nrs": [ - { - "perf_type": "RAW", - "name": "Frontend-Stalls", - "config": 67109276 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Cycles", - "config": 60 - } - ], - "scale": 1000 - }, - { - "name": "branch-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Branches", - "config": 197 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "inst-tlb-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Instruction-TLB", - "config": 8325 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "inst-tlb-tw-pki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Instruction-TLB-TW", - "config": 389 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "inst-l1-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "L1-Instructions", - "config": 58404 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "stall-backend-pkc", - "nrs": [ - { - "perf_type": "RAW", - "name": "Backend-Stalls", - "config": 418 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Cycles", - "config": 60 - } - ], - "scale": 1000 - }, - { - "name": "l3-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "L3", - "config": 16686 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "l2-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "L2", - "config": 8177 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "data-tlb-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Data-TLB", - "config": 8200 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "data-tlb-tw-pki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Data-TLB-TW", - "config": 264 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "data-l1-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "L1-Data", - "config": 337 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "data-st-tlb-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Data-ST-TLB", - "config": 8265 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "data-st-tlb-tw-pki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Data-ST-TLB-TW", - "config": 329 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - } -] \ No newline at end of file diff --git a/src/pmu_configs/intel_pmu_config.json b/src/pmu_configs/intel_pmu_config.json new file mode 100644 index 00000000..3c5647de --- /dev/null +++ b/src/pmu_configs/intel_pmu_config.json @@ -0,0 +1,35 @@ +{ + "events": { + "Instructions": "cpu/event=0xc0,umask=0x0/", + "Cycles": "cpu/event=0x3c,umask=0x0/", + "Frontend_Stalls": "cpu/event=0x9c,umask=0x1,cmask=0x4/", + "Branches": "cpu/event=0xc5,umask=0x0/", + "Instruction_TLB": "cpu/event=0x85,umask=0x20/", + "Instruction_TLB_TW": "cpu/event=0x85,umask=0x1/", + "L1_Instructions": "cpu/event=0x24,umask=0xe4/", + "Backend_Stalls": "cpu/event=0xa2,umask=0x1/", + "L3": "cpu/event=0x2e,umask=0x41/", + "L2": "cpu/event=0xf1,umask=0x1f/", + "Data_TLB": "cpu/event=0x8,umask=0x20/", + "Data_TLB_TW": "cpu/event=0x8,umask=0x1/", + "L1_Data": "cpu/event=0x51,umask=0x1/", + "Data_ST_TLB": "cpu/event=0x49,umask=0x20/", + "Data_ST_TLB_TW": "cpu/event=0x49,umask=0x1/" + }, + "metrics": { + "ipc": "Instructions / Cycles", + "stall-frontend-pkc": "Frontend_Stalls / Cycles * 1000", + "stall-backend-pkc": "Backend_Stalls / Cycles * 1000", + "branch-mpki": "Branches / Instructions * 1000", + "inst-l1-mpki": "L1_Instructions / Instructions * 1000", + "inst-tlb-mpki": "Instruction_TLB / Instructions * 1000", + "inst-tlb-tw-pki": "Instruction_TLB_TW / Instructions * 1000", + "data-l1-mpki": "L1_Data / Instructions * 1000", + "l2-mpki": "L2 / Instructions * 1000", + "l3-mpki": "L3 / Instructions * 1000", + "data-tlb-mpki": "Data_TLB / Instructions * 1000", + "data-tlb-tw-pki": "Data_TLB_TW / Instructions * 1000", + "data-st-tlb-mpki": "Data_ST_TLB / Instructions * 1000", + "data-st-tlb-tw-pki": "Data_ST_TLB_TW / Instructions * 1000" + } +} diff --git a/src/pmu_configs/intel_sapphire_rapids_ctrs.json b/src/pmu_configs/intel_sapphire_rapids_ctrs.json deleted file mode 100644 index c9b094b6..00000000 --- a/src/pmu_configs/intel_sapphire_rapids_ctrs.json +++ /dev/null @@ -1,164 +0,0 @@ -[ - { - "name": "l2-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "L2", - "config": 7973 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "inst-tlb-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Instruction-TLB", - "config": 8209 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "inst-tlb-tw-pki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Instruction-TLB-TW", - "config": 3601 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "data-rd-tlb-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Data-RD-TLB", - "config": 8210 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "data-st-tlb-mpki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Data-ST-TLB", - "config": 8211 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "data-rd-tlb-tw-pki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Data-RD-TLB-TW", - "config": 3602 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "data-st-tlb-tw-pki", - "nrs": [ - { - "perf_type": "RAW", - "name": "Data-ST-TLB-TW", - "config": 3603 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Instructions", - "config": 192 - } - ], - "scale": 1000 - }, - { - "name": "stall-frontend-pkc", - "nrs": [ - { - "perf_type": "RAW", - "name": "Frontend-Stalls", - "config": 100663708 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Cycles", - "config": 60 - } - ], - "scale": 1000 - }, - { - "name": "stall-backend-pkc", - "nrs": [ - { - "perf_type": "RAW", - "name": "Backend-Stalls", - "config": 676 - } - ], - "drs": [ - { - "perf_type": "RAW", - "name": "Slots", - "config": 420 - } - ], - "scale": 1000 - } -] \ No newline at end of file diff --git a/src/pmu_configs/intel_sapphire_rapids_pmu_config.json b/src/pmu_configs/intel_sapphire_rapids_pmu_config.json new file mode 100644 index 00000000..ed6362cf --- /dev/null +++ b/src/pmu_configs/intel_sapphire_rapids_pmu_config.json @@ -0,0 +1,27 @@ +{ + "events": { + "L2": "cpu/event=0x25,umask=0x1f/", + "Instructions": "cpu/event=0xc0,umask=0x0/", + "Instruction_TLB": "cpu/event=0x11,umask=0x20/", + "Instruction_TLB_TW": "cpu/event=0x11,umask=0xe/", + "Data_RD_TLB": "cpu/event=0x12,umask=0x20/", + "Data_ST_TLB": "cpu/event=0x13,umask=0x20/", + "Data_RD_TLB_TW": "cpu/event=0x12,umask=0xe/", + "Data_ST_TLB_TW": "cpu/event=0x13,umask=0xe/", + "Frontend_Stalls": "cpu/event=0x9c,umask=0x1,cmask=0x6/", + "Cycles": "cpu/event=0x3c,umask=0x0/", + "Backend_Stalls": "cpu/event=0xa4,umask=0x2/", + "Slots": "cpu/event=0xa4,umask=0x1/" + }, + "metrics": { + "stall-frontend-pkc": "Frontend_Stalls / Cycles * 1000", + "stall-backend-pkc": "Backend_Stalls / Slots * 1000", + "inst-tlb-mpki": "Instruction_TLB / Instructions * 1000", + "inst-tlb-tw-pki": "Instruction_TLB_TW / Instructions * 1000", + "l2-mpki": "L2 / Instructions * 1000", + "data-st-tlb-mpki": "Data_ST_TLB / Instructions * 1000", + "data-st-tlb-tw-pki": "Data_ST_TLB_TW / Instructions * 1000", + "data-rd-tlb-mpki": "Data_RD_TLB / Instructions * 1000", + "data-rd-tlb-tw-pki": "Data_RD_TLB_TW / Instructions * 1000" + } +} diff --git a/src/record.rs b/src/record.rs index 5e48e652..8bcd44e7 100644 --- a/src/record.rs +++ b/src/record.rs @@ -2,7 +2,7 @@ use crate::data::common::utils::get_data_name_from_type; use crate::data::java_profile::JavaProfile; -use crate::{data, InitParams, PerformanceData}; +use crate::{data, InitParams, PerformanceData, UNGROUPED_PMU_MODE}; use anyhow::anyhow; use anyhow::Result; use clap::{builder::PossibleValuesParser, ArgGroup, Args}; @@ -91,6 +91,15 @@ pub struct Record { #[clap(help_heading = "PMU Options", long, value_parser)] pub pmu_config: Option, + /// Avoid creating a PMU counter group for each metric defined in the + /// PMU config. It reduces multiplexing and file descriptor usage, but + /// counters used by a metric are no longer guaranteed to be collected + /// together. Recommend to use the option when the total number of + /// counters (events) to be collected is less than or equal to that of + /// the PMU counter registers. + #[clap(help_heading = "PMU Options", long, value_parser, verbatim_doc_comment)] + pub ungroup_pmu_events: bool, + #[cfg(feature = "hotline")] /// SPE sampling frequency, defaulted to 1kHz on Grv4. #[clap( @@ -140,6 +149,9 @@ pub fn record(record: &Record, tmp_dir: &Path, runlog: &Path) -> Result<()> { if let Some(p) = &record.pmu_config { params.pmu_config = Some(PathBuf::from(p)); } + if record.ungroup_pmu_events { + params.pmu_counter_mode = UNGROUPED_PMU_MODE.to_string(); + } #[cfg(feature = "hotline")] { diff --git a/src/report.rs b/src/report.rs index 320e8805..42da11fb 100644 --- a/src/report.rs +++ b/src/report.rs @@ -38,17 +38,6 @@ impl VersionInfo { } } -/// Reads `meta_data.bin` from a run's data directory and returns the run's -/// wall-clock collection start and end times. Returns `None` if the file is -/// missing, fails to deserialize, or was produced by an older aperf that did -/// not stamp collection start/end. -fn read_collection_times(run_dir_path: &PathBuf) -> Option<(TimeEnum, TimeEnum)> { - let meta_data_path = run_dir_path.join(format!("meta_data.{}", APERF_FILE_FORMAT)); - let bytes = fs::read(&meta_data_path).ok()?; - let params: InitParams = bincode::deserialize(&bytes).ok()?; - Some((params.collection_start?, params.collection_end?)) -} - /// Stores the information of all runs to be included in the report #[derive(Default)] struct RunsInfo { @@ -70,6 +59,9 @@ struct RunsInfo { per_run_start_time: HashMap, /// Wall-clock end of `collect_data_serial`. per_run_end_time: HashMap, + /// Whether the PMU counters were collected with or without groups. + /// Empty string means legacy runs before PMU config revamp. + per_run_pmu_counter_mode: HashMap, } impl RunsInfo { @@ -87,11 +79,24 @@ impl RunsInfo { self.run_names.push(deduped_run_name.clone()); self.run_archive_paths .insert(deduped_run_name.clone(), run_archive_path); - if let Some((start, end)) = read_collection_times(&run_dir_path) { - self.per_run_start_time - .insert(deduped_run_name.clone(), start); - self.per_run_end_time.insert(deduped_run_name.clone(), end); + + // Reads meta_data.bin from a run's data directory for info about the collector. + let meta_data_path = run_dir_path.join(format!("meta_data.{}", APERF_FILE_FORMAT)); + if let Ok(meta_data_bytes) = fs::read(&meta_data_path) { + if let Ok(meta_data) = bincode::deserialize::(&meta_data_bytes) { + self.per_run_pmu_counter_mode + .insert(deduped_run_name.clone(), meta_data.pmu_counter_mode); + if let Some(collection_start) = meta_data.collection_start { + self.per_run_start_time + .insert(deduped_run_name.clone(), collection_start); + } + if let Some(collection_end) = meta_data.collection_end { + self.per_run_end_time + .insert(deduped_run_name.clone(), collection_end); + } + } } + self.run_dir_paths.insert(deduped_run_name, run_dir_path); Ok(()) @@ -441,9 +446,15 @@ fn generate_report_files(report_dir: PathBuf, runs_info: RunsInfo, tmp_dir: &Pat info!("Processing collected data..."); let mut visualization_data = VisualizationData::new(); data::add_all_visualization_data(&mut visualization_data); - /* Init visualizers */ + for (run_name, run_data_dir) in runs_info.run_dir_paths { let collection_start = runs_info.per_run_start_time.get(&run_name).copied(); + let pmu_counter_mode = runs_info + .per_run_pmu_counter_mode + .get(&run_name) + .cloned() + .unwrap_or_default(); + visualization_data .init_visualizers( run_name.clone(), @@ -451,8 +462,10 @@ fn generate_report_files(report_dir: PathBuf, runs_info: RunsInfo, tmp_dir: &Pat tmp_dir, &report_dir, collection_start, + pmu_counter_mode, ) .unwrap(); + visualization_data.process_raw_data().unwrap(); } diff --git a/src/visualizer.rs b/src/visualizer.rs index 82f8fcb2..d29b369e 100644 --- a/src/visualizer.rs +++ b/src/visualizer.rs @@ -19,6 +19,9 @@ pub struct ReportParams { /// Wall-clock start of `collect_data_serial`. Used to anchor time_diff=0 in /// time-series data to the actual collection start rather than the first sample time. pub collection_start: Option, + /// Whether the collection of PMU counters is "grouped" or "ungrouped". An empty \ + /// string means a legacy run before PMU config revamp. + pub pmu_counter_mode: String, } impl ReportParams { @@ -30,6 +33,7 @@ impl ReportParams { run_name: String::new(), data_file_path: PathBuf::new(), collection_start: None, + pmu_counter_mode: String::new(), } } } @@ -62,9 +66,12 @@ impl DataVisualizer { tmp_dir: &Path, report_dir: &Path, collection_start: Option, + pmu_counter_mode: String, ) -> Result<()> { self.report_params.run_name = run_name.clone(); self.report_params.collection_start = collection_start; + self.report_params.pmu_counter_mode = pmu_counter_mode; + let file_path = find_file( &run_data_dir, &format!("^{}", regex::escape(self.data_name)), diff --git a/tests/test_aperf.rs b/tests/test_aperf.rs index 88cd2c8d..30f5b733 100644 --- a/tests/test_aperf.rs +++ b/tests/test_aperf.rs @@ -915,6 +915,7 @@ fn record_with_name( save_profile_events: false, profile_java: None, pmu_config: None, + ungroup_pmu_events: false, hotline_frequency: 1000, num_to_report: 5000, }; @@ -931,6 +932,7 @@ fn record_with_name( save_profile_events: false, profile_java: None, pmu_config: None, + ungroup_pmu_events: false, }; let runlog = work_dir.join(*APERF_RUNLOG); diff --git a/tests/test_aperf_stats.rs b/tests/test_aperf_stats.rs index 02023d33..7f3ce616 100644 --- a/tests/test_aperf_stats.rs +++ b/tests/test_aperf_stats.rs @@ -29,6 +29,7 @@ mod aperf_stats_tests { run_name: String::new(), data_file_path: PathBuf::from(data_file_path), collection_start: None, + pmu_counter_mode: String::new(), } } diff --git a/tests/test_java_profile.rs b/tests/test_java_profile.rs index ec88da1d..f50ab993 100644 --- a/tests/test_java_profile.rs +++ b/tests/test_java_profile.rs @@ -21,6 +21,7 @@ fn setup_test_env() -> (TempDir, PathBuf, PathBuf, ReportParams) { run_name: "test_run".to_string(), data_file_path: PathBuf::new(), collection_start: None, + pmu_counter_mode: String::new(), }; (temp_dir, data_dir, report_dir, params) diff --git a/tests/test_perf_stat.rs b/tests/test_perf_stat.rs index 92f7dced..fd266f9e 100644 --- a/tests/test_perf_stat.rs +++ b/tests/test_perf_stat.rs @@ -1,12 +1,45 @@ -use aperf::data::common::data_formats::AperfData; +// PMU perf_stat processing tests. +// +// The PMU feature is Linux-only: `PerfStatRaw` carries a Linux-only +// `pmu_collectors` field and is constructed via `PerfStatRaw::new()`, so this +// whole test file only compiles/runs on Linux (matches the rest of the perf +// collection code, which is gated the same way). +#![cfg(target_os = "linux")] + +use aperf::data::common::data_formats::{AperfData, Series, TimeSeriesMetric}; use aperf::data::perf_stat::{PerfStat, PerfStatRaw}; -use aperf::data::ProcessData; -use aperf::data::{Data, TimeEnum}; +use aperf::data::{Data, ProcessData, TimeEnum}; use aperf::visualizer::ReportParams; +use aperf::{GROUPED_PMU_MODE, UNGROUPED_PMU_MODE}; use chrono::prelude::*; use std::collections::HashMap; +use std::path::Path; +use tempfile::TempDir; -/// Expected PMU statistics for a single CPU and stat name +const EPS: f64 = 1e-6; + +fn base_time(secs: i64) -> TimeEnum { + TimeEnum::DateTime( + Utc.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap() + chrono::Duration::seconds(secs), + ) +} + +fn approx(actual: f64, expected: f64, ctx: &str) { + assert!( + (actual - expected).abs() < EPS, + "{ctx}: expected {expected}, got {actual}" + ); +} + +// =========================================================================== +// Legacy (pre-revamp) format: " ; ; ;" +// per-CPU value = sum(nr) / sum(dr) * scale +// aggregate value = sum(nr * scale) / sum(dr) (over all CPUs) +// One comprehensive test covers the legacy path; the rest of this file covers +// the new (events + metric-formula) grouped/ungrouped flows. +// =========================================================================== + +/// Expected PMU statistics for a single CPU and stat name (legacy format). #[derive(Debug, Clone)] struct ExpectedPmuStats { pub numerators: Vec, @@ -30,18 +63,16 @@ impl ExpectedPmuStats { } } -/// Generate fake PMU stat raw data in the format: " ; ; ;" -fn generate_pmu_stat_raw_data( - expected_per_sample_per_cpu_per_stat: &Vec>>, // [sample][cpu][stat_name] +/// Generate legacy-format raw PMU data: " ; ; ;". +fn generate_legacy_raw_data( + expected_per_sample_per_cpu_per_stat: &Vec>>, interval_seconds: u64, ) -> Vec { let mut raw_data = Vec::new(); - let base_time = Utc.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap(); for (sample_idx, per_cpu_per_stat) in expected_per_sample_per_cpu_per_stat.iter().enumerate() { let mut data_lines = Vec::new(); - // Sort CPUs to ensure consistent ordering let mut sorted_cpus: Vec<_> = per_cpu_per_stat.keys().collect(); sorted_cpus.sort(); @@ -60,401 +91,610 @@ fn generate_pmu_stat_raw_data( .map(|d| d.to_string()) .collect::>() .join(" "); - - let line = format!( + data_lines.push(format!( "{} {}; {}; {};{}", cpu, stat_name, numerators_str, denominators_str, expected_stats.scale - ); - data_lines.push(line); + )); } } - let time = TimeEnum::DateTime( - base_time + chrono::Duration::seconds((sample_idx as i64) * (interval_seconds as i64)), - ); - let raw_stat = PerfStatRaw { - time, - data: data_lines.join("\n"), - }; - raw_data.push(Data::PerfStatRaw(raw_stat)); + let mut raw = PerfStatRaw::new(); + raw.time = base_time((sample_idx as i64) * (interval_seconds as i64)); + raw.data = data_lines.join("\n"); + raw_data.push(Data::PerfStatRaw(raw)); } raw_data } #[test] -fn test_process_pmu_stat_raw_data_complex() { +fn test_legacy_format_comprehensive() { + // 100 samples x 4 CPUs x 3 stats, exercising multiple numerators/denominators, + // distinct scales, per-CPU values, the aggregate series, time diffs, the + // top-down metric ordering, and aggregate-series marking — all in the legacy + // nr/dr format (empty pmu_counter_mode). let mut expected_data = Vec::new(); - - // Generate 100 samples with 4 CPUs and 3 PMU stats for sample in 0usize..100 { let mut per_cpu_per_stat = HashMap::new(); - for cpu in 0usize..4 { let mut per_stat = HashMap::new(); - // IPC stat with varying patterns - let ipc_numerators = vec![ - (1000 + sample * 10 + cpu * 5) as u64, - (2000 + sample * 15 + cpu * 7) as u64, - ]; - let ipc_denominators = vec![ - (5000 + sample * 50 + cpu * 25) as u64, - (10000 + sample * 75 + cpu * 35) as u64, - ]; + // ipc: two numerators + two denominators, scale 1. per_stat.insert( "ipc".to_string(), - ExpectedPmuStats::new(ipc_numerators, ipc_denominators, 1), + ExpectedPmuStats::new( + vec![ + (1000 + sample * 10 + cpu * 5) as u64, + (2000 + sample * 15 + cpu * 7) as u64, + ], + vec![ + (5000 + sample * 50 + cpu * 25) as u64, + (10000 + sample * 75 + cpu * 35) as u64, + ], + 1, + ), ); - - // Cache miss rate with different scale - let cache_numerators = vec![(100 + sample * 2 + cpu) as u64]; - let cache_denominators = vec![(10000 + sample * 100 + cpu * 50) as u64]; + // l3-mpki: single nr/dr, scale 100. per_stat.insert( "l3-mpki".to_string(), - ExpectedPmuStats::new(cache_numerators, cache_denominators, 100), + ExpectedPmuStats::new( + vec![(100 + sample * 2 + cpu) as u64], + vec![(10000 + sample * 100 + cpu * 50) as u64], + 100, + ), ); - - // Branch prediction accuracy - let branch_numerators = vec![(9000 + sample * 5 + cpu * 2) as u64]; - let branch_denominators = vec![(10000 + sample * 10 + cpu * 3) as u64]; + // branch-mpki: single nr/dr, scale 1000. per_stat.insert( "branch-mpki".to_string(), - ExpectedPmuStats::new(branch_numerators, branch_denominators, 1000), + ExpectedPmuStats::new( + vec![(9000 + sample * 5 + cpu * 2) as u64], + vec![(10000 + sample * 10 + cpu * 3) as u64], + 1000, + ), ); per_cpu_per_stat.insert(cpu, per_stat); } - expected_data.push(per_cpu_per_stat); } - let raw_data = generate_pmu_stat_raw_data(&expected_data, 1); + let raw_data = generate_legacy_raw_data(&expected_data, 1); + // Legacy path: pmu_counter_mode is empty. let result = PerfStat::new() .process_raw_data(ReportParams::new(), raw_data) .unwrap(); - if let AperfData::TimeSeries(time_series_data) = result { - // Validate structure: 3 metrics (branch_accuracy, cache_miss_rate, ipc - sorted by guidance) - assert_eq!(time_series_data.metrics.len(), 3); - assert_eq!(time_series_data.sorted_metric_names.len(), 3); - assert_eq!( - time_series_data.sorted_metric_names, - vec!["ipc", "branch-mpki", "l3-mpki"] - ); + let ts = match result { + AperfData::TimeSeries(ts) => ts, + _ => panic!("Expected TimeSeries data"), + }; - // Validate each metric has 5 series (4 CPUs + 1 aggregate) - for metric in time_series_data.metrics.values() { - assert_eq!(metric.series.len(), 5); - // Each series should have 100 data points - for series in &metric.series { - assert_eq!(series.values.len(), 100); - assert_eq!(series.time_diff.len(), 100); - } + // Structure: 3 metrics, ordered per the top-down debug guidance. + assert_eq!(ts.metrics.len(), 3); + assert_eq!( + ts.sorted_metric_names, + vec!["ipc", "branch-mpki", "l3-mpki"] + ); + + // Each metric: 4 CPU series + 1 aggregate, each with 100 points. + for metric in ts.metrics.values() { + assert_eq!(metric.series.len(), 5); + for series in &metric.series { + assert_eq!(series.values.len(), 100); + assert_eq!(series.time_diff.len(), 100); } + } - // Validate ALL values for ALL metrics and ALL CPUs - for (stat_name, metric) in &time_series_data.metrics { - for sample in 0..100 { - let mut aggregate_numerator_sum = 0.0; - let mut aggregate_denominator_sum = 0.0; - - // Check each CPU series value - for cpu in 0..4 { - let cpu_series = &metric.series[cpu + 1]; - let expected_stats = &expected_data[sample][&cpu][stat_name]; - let expected_value = expected_stats.calculate_value(); - - assert!( - (cpu_series.values[sample] - expected_value).abs() < 1e-5, - "Sample {}, CPU {}, stat {}: expected {}, got {}", - sample, - cpu, - stat_name, - expected_value, - cpu_series.values[sample] - ); - - // Accumulate for aggregate calculation - let numerator_sum: u64 = expected_stats.numerators.iter().sum(); - let denominator_sum: u64 = expected_stats.denominators.iter().sum(); - aggregate_numerator_sum += - (numerator_sum as f64) * (expected_stats.scale as f64); - aggregate_denominator_sum += denominator_sum as f64; - } - - // Check aggregate series value - let aggregate_series = &metric.series[0]; - let expected_aggregate_value = aggregate_numerator_sum / aggregate_denominator_sum; - assert!( - (aggregate_series.values[sample] - expected_aggregate_value).abs() < 1e-5, - "Sample {}, stat {} aggregate: expected {}, got {}", - sample, - stat_name, - expected_aggregate_value, - aggregate_series.values[sample] + // Validate every per-CPU and aggregate value. + for (stat_name, metric) in &ts.metrics { + for sample in 0..100 { + let mut agg_numerator = 0.0; + let mut agg_denominator = 0.0; + for cpu in 0..4 { + // Series sort: "Aggregate" < "CPU0".."CPU3" → CPU{n} at index n+1. + let cpu_series = &metric.series[cpu + 1]; + let expected = &expected_data[sample][&cpu][stat_name]; + approx( + cpu_series.values[sample], + expected.calculate_value(), + &format!("legacy sample {sample} CPU {cpu} {stat_name}"), ); + let nr: u64 = expected.numerators.iter().sum(); + let dr: u64 = expected.denominators.iter().sum(); + agg_numerator += (nr as f64) * (expected.scale as f64); + agg_denominator += dr as f64; } - } - - // Validate aggregate series is marked correctly - for metric in time_series_data.metrics.values() { let aggregate_series = &metric.series[0]; assert!(aggregate_series.is_aggregate); assert_eq!(aggregate_series.series_name, "Aggregate"); + approx( + aggregate_series.values[sample], + agg_numerator / agg_denominator, + &format!("legacy sample {sample} {stat_name} aggregate"), + ); } + } - // Validate time differences - for metric in time_series_data.metrics.values() { - for series in &metric.series { - for sample in 0..100 { - assert_eq!(series.time_diff[sample], sample as u64); - } + // Time diffs are 1s intervals. + for metric in ts.metrics.values() { + for series in &metric.series { + for sample in 0..100 { + assert_eq!(series.time_diff[sample], sample as u64); } } - } else { - panic!("Expected TimeSeries data"); } } -#[test] -fn test_process_pmu_stat_raw_data_simple() { - let mut expected_data = Vec::new(); - - // Generate 3 samples with 2 CPUs and 1 PMU stat - for sample in 0usize..3 { - let mut per_cpu_per_stat = HashMap::new(); - - for cpu in 0usize..2 { - let mut per_stat = HashMap::new(); - let numerators = vec![(1000 + sample * 100 + cpu * 10) as u64]; - let denominators = vec![(5000 + sample * 200 + cpu * 20) as u64]; - per_stat.insert( - "simple_stat".to_string(), - ExpectedPmuStats::new(numerators, denominators, 1), - ); - per_cpu_per_stat.insert(cpu, per_stat); - } - - expected_data.push(per_cpu_per_stat); +// =========================================================================== +// New format helpers (events + metric-formula config, evaluated via exmex). +// +// The report path reads the PMU config that `record` saved next to the data +// (data_dir/pmu_config.json), so each new-format test writes one to a temp dir. +// +// IMPORTANT ordering contract: a metric's counters are serialized in exmex +// var_names() order, which is ALPHABETICAL by event name. eval() consumes them +// positionally in that same order. Test data below mirrors that exactly. +// =========================================================================== + +/// Write a v2 PMU config (events + metrics) to `/pmu_config.json`, +/// preserving the given key order (matters for metric display order). +fn write_pmu_config(dir: &Path, events: &[(&str, &str)], metrics: &[(&str, &str)]) { + fn obj(pairs: &[(&str, &str)]) -> String { + pairs + .iter() + .map(|(k, v)| format!(" {:?}: {:?}", k, v)) + .collect::>() + .join(",\n") } + let json = format!( + "{{\n \"events\": {{\n{}\n }},\n \"metrics\": {{\n{}\n }}\n}}", + obj(events), + obj(metrics) + ); + std::fs::write(dir.join("pmu_config.json"), json).unwrap(); +} - let raw_data = generate_pmu_stat_raw_data(&expected_data, 2); - let result = PerfStat::new() - .process_raw_data(ReportParams::new(), raw_data) - .unwrap(); - - if let AperfData::TimeSeries(time_series_data) = result { - assert_eq!(time_series_data.metrics.len(), 1); - assert_eq!(time_series_data.sorted_metric_names, vec!["simple_stat"]); - - let metric = &time_series_data.metrics["simple_stat"]; - assert_eq!(metric.series.len(), 3); // 2 CPUs + 1 aggregate - - // Validate ALL values for ALL samples and CPUs - for sample in 0..3 { - let mut aggregate_numerator_sum = 0.0; - let mut aggregate_denominator_sum = 0.0; - - // Check each CPU series value - for cpu in 0..2 { - let cpu_series = &metric.series[cpu + 1]; - let expected_stats = &expected_data[sample][&cpu]["simple_stat"]; - let expected_value = expected_stats.calculate_value(); - - assert!( - (cpu_series.values[sample] - expected_value).abs() < 1e-5, - "Sample {}, CPU {}: expected {}, got {}", - sample, - cpu, - expected_value, - cpu_series.values[sample] - ); - - // Accumulate for aggregate calculation - let numerator_sum: u64 = expected_stats.numerators.iter().sum(); - let denominator_sum: u64 = expected_stats.denominators.iter().sum(); - aggregate_numerator_sum += (numerator_sum as f64) * (expected_stats.scale as f64); - aggregate_denominator_sum += denominator_sum as f64; - } - - // Check aggregate series value - let aggregate_series = &metric.series[0]; - let expected_aggregate_value = aggregate_numerator_sum / aggregate_denominator_sum; - assert!( - (aggregate_series.values[sample] - expected_aggregate_value).abs() < 1e-5, - "Sample {} aggregate: expected {}, got {}", - sample, - expected_aggregate_value, - aggregate_series.values[sample] - ); - } +fn report_params(data_dir: &Path, mode: &str) -> ReportParams { + let mut params = ReportParams::new(); + params.data_dir = data_dir.to_path_buf(); + params.pmu_counter_mode = mode.to_string(); + params +} - // Validate time intervals - for series in &metric.series { - assert_eq!(series.time_diff, vec![0, 2, 4]); // 2-second intervals - } - } else { - panic!("Expected TimeSeries data"); +/// Grouped raw line: "cpu;metric;c1;c2;...;time_enabled;time_running". +/// `counters` MUST be in exmex var_names() (alphabetical) order. +fn grouped_line( + cpu: usize, + metric: &str, + counters: &[u64], + time_enabled: f64, + time_running: f64, +) -> String { + let mut line = format!("{cpu};{metric};"); + for c in counters { + line.push_str(&format!("{c};")); } + line.push_str(&format!("{time_enabled};{time_running}")); + line } -#[test] -fn test_process_pmu_stat_multiple_numerators_denominators() { - let mut expected_data = Vec::new(); - - // Generate 3 samples with multiple numerators and denominators - for sample in 0usize..3 { - let mut per_cpu_per_stat = HashMap::new(); +/// Ungrouped raw line: "cpu;event;value;time_enabled;time_running". +fn ungrouped_line( + cpu: usize, + event: &str, + value: u64, + time_enabled: f64, + time_running: f64, +) -> String { + format!("{cpu};{event};{value};{time_enabled};{time_running}") +} - for cpu in 0usize..2 { - let mut per_stat = HashMap::new(); - // Multiple numerators and denominators that should be summed - let numerators = vec![ - (100 + sample * 10) as u64, - (200 + sample * 20) as u64, - (300 + sample * 30) as u64, - ]; - let denominators = vec![(1000 + sample * 100) as u64, (2000 + sample * 200) as u64]; - per_stat.insert( - "multi_counter_stat".to_string(), - ExpectedPmuStats::new(numerators, denominators, 1), - ); - per_cpu_per_stat.insert(cpu, per_stat); - } +fn snapshot(secs: i64, lines: Vec) -> Data { + let mut raw = PerfStatRaw::new(); + raw.time = base_time(secs); + raw.data = lines.join("\n"); + Data::PerfStatRaw(raw) +} - expected_data.push(per_cpu_per_stat); +fn process( + params: ReportParams, + raw_data: Vec, +) -> aperf::data::common::data_formats::TimeSeriesData { + match PerfStat::new().process_raw_data(params, raw_data).unwrap() { + AperfData::TimeSeries(ts) => ts, + _ => panic!("Expected TimeSeries data"), } +} - let raw_data = generate_pmu_stat_raw_data(&expected_data, 1); - let result = PerfStat::new() - .process_raw_data(ReportParams::new(), raw_data) - .unwrap(); +fn series<'a>(metric: &'a TimeSeriesMetric, name: &str) -> &'a Series { + metric + .series + .iter() + .find(|s| s.series_name == name) + .unwrap_or_else(|| panic!("series {name} not found")) +} - if let AperfData::TimeSeries(time_series_data) = result { - let metric = &time_series_data.metrics["multi_counter_stat"]; - - // Validate ALL values for ALL samples and CPUs - for sample in 0..3 { - let mut aggregate_numerator_sum = 0.0; - let mut aggregate_denominator_sum = 0.0; - - // Check each CPU series value - for cpu in 0..2 { - let cpu_series = &metric.series[cpu]; - let expected_stats = &expected_data[sample][&cpu]["multi_counter_stat"]; - let expected_value = expected_stats.calculate_value(); - - assert!( - (cpu_series.values[sample] - expected_value).abs() < 1e-5, - "Sample {}, CPU {}: expected {}, got {}", - sample, - cpu, - expected_value, - cpu_series.values[sample] - ); +fn aggregate<'a>(metric: &'a TimeSeriesMetric) -> &'a Series { + metric + .series + .iter() + .find(|s| s.is_aggregate) + .expect("aggregate series not found") +} - // Accumulate for aggregate calculation - let numerator_sum: u64 = expected_stats.numerators.iter().sum(); - let denominator_sum: u64 = expected_stats.denominators.iter().sum(); - aggregate_numerator_sum += (numerator_sum as f64) * (expected_stats.scale as f64); - aggregate_denominator_sum += denominator_sum as f64; - } +// =========================================================================== +// New format — GROUPED collection +// =========================================================================== - // Check aggregate series value - let aggregate_series = &metric.series[2]; - let expected_aggregate_value = aggregate_numerator_sum / aggregate_denominator_sum; - assert!( - (aggregate_series.values[sample] - expected_aggregate_value).abs() < 1e-5, - "Sample {} aggregate: expected {}, got {}", - sample, - expected_aggregate_value, - aggregate_series.values[sample] - ); - } - } else { - panic!("Expected TimeSeries data"); - } +#[test] +fn test_grouped_single_metric_multi_cpu() { + // ipc = Instructions / Cycles ; var_names = [Cycles, Instructions] + // so each group line's counters are [Cycles, Instructions]. No multiplexing + // (time_enabled == time_running => scale 1). First sample uses the first + // accumulative value as its own delta. + let dir = TempDir::new().unwrap(); + write_pmu_config( + dir.path(), + &[ + ("Cycles", "armv8_pmuv3_0/event=0x11/"), + ("Instructions", "armv8_pmuv3_0/event=0x8/"), + ], + &[("ipc", "Instructions / Cycles")], + ); + + // Counters are PER-INTERVAL (collection resets the group each interval); + // only time_enabled/time_running are accumulative (the kernel cannot reset + // those). With te == tr each interval the multiplexing scale is 1, so the + // per-CPU value is just (Instructions / Cycles) of that interval. + // CPU0: (Cycles,Instr) -> (1000,2000) (2000,4000) (3000,4000) + // CPU1: (Cycles,Instr) -> ( 500,1500) (1000,2000) (1500,2000) + let raw = vec![ + snapshot( + 0, + vec![ + grouped_line(0, "ipc", &[1000, 2000], 1.0, 1.0), + grouped_line(1, "ipc", &[500, 1500], 1.0, 1.0), + ], + ), + snapshot( + 1, + vec![ + grouped_line(0, "ipc", &[2000, 4000], 2.0, 2.0), + grouped_line(1, "ipc", &[1000, 2000], 2.0, 2.0), + ], + ), + snapshot( + 2, + vec![ + grouped_line(0, "ipc", &[3000, 4000], 3.0, 3.0), + grouped_line(1, "ipc", &[1500, 2000], 3.0, 3.0), + ], + ), + ]; + + let ts = process(report_params(dir.path(), GROUPED_PMU_MODE), raw); + + assert_eq!(ts.sorted_metric_names, vec!["ipc"]); + let ipc = &ts.metrics["ipc"]; + // 2 CPU series + 1 aggregate. + assert_eq!(ipc.series.len(), 3); + + let cpu0 = series(ipc, "CPU0"); + let cpu1 = series(ipc, "CPU1"); + // CPU0: 2000/1000, 4000/2000, 4000/3000 + approx(cpu0.values[0], 2.0, "cpu0 s0"); + approx(cpu0.values[1], 2.0, "cpu0 s1"); + approx(cpu0.values[2], 4000.0 / 3000.0, "cpu0 s2"); + // CPU1: 1500/500, 2000/1000, 2000/1500 + approx(cpu1.values[0], 3.0, "cpu1 s0"); + approx(cpu1.values[1], 2.0, "cpu1 s1"); + approx(cpu1.values[2], 2000.0 / 1500.0, "cpu1 s2"); + + // Aggregate = sum(Instructions) / sum(Cycles) across CPUs, per sample. + let agg = aggregate(ipc); + approx(agg.values[0], 3500.0 / 1500.0, "agg s0"); + approx(agg.values[1], 6000.0 / 3000.0, "agg s1"); + approx(agg.values[2], 6000.0 / 4500.0, "agg s2"); + + assert_eq!(agg.time_diff, vec![0, 1, 2]); } #[test] -fn test_process_pmu_stat_empty_data() { - let raw_data = Vec::new(); - let result = PerfStat::new() - .process_raw_data(ReportParams::new(), raw_data) - .unwrap(); - - if let AperfData::TimeSeries(time_series_data) = result { - assert_eq!(time_series_data.metrics.len(), 0); - assert_eq!(time_series_data.sorted_metric_names.len(), 0); - } else { - panic!("Expected TimeSeries data"); - } +fn test_grouped_multiplexing_scale_cancels_for_ratio() { + // A group shares one (time_enabled, time_running) across all its counters, so + // every counter is scaled by the same factor and the factor cancels in a ratio + // metric. This guards against double-applying or mis-applying the scale: ipc + // must stay the pure Instructions/Cycles ratio regardless of the scale factor. + let dir = TempDir::new().unwrap(); + write_pmu_config( + dir.path(), + &[ + ("Cycles", "armv8_pmuv3_0/event=0x11/"), + ("Instructions", "armv8_pmuv3_0/event=0x8/"), + ], + &[("ipc", "Instructions / Cycles")], + ); + + // s0: delta (Cycles=1000, Instr=2000), te=2 tr=1 => scale x2 + // s1: delta (Cycles=1000, Instr=2000), te=4 tr=1 => scale x4 + let raw = vec![ + snapshot(0, vec![grouped_line(0, "ipc", &[1000, 2000], 2.0, 1.0)]), + snapshot(1, vec![grouped_line(0, "ipc", &[2000, 4000], 6.0, 2.0)]), + ]; + + let ts = process(report_params(dir.path(), GROUPED_PMU_MODE), raw); + let cpu0 = series(&ts.metrics["ipc"], "CPU0"); + // Despite scale x2 then x4, the ratio is unchanged. + approx(cpu0.values[0], 2.0, "scaled ratio s0"); + approx(cpu0.values[1], 2.0, "scaled ratio s1"); } #[test] -fn test_process_pmu_stat_zero_denominator_data_point_skipped() { - let mut raw_data = Vec::new(); - let base_time = Utc.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap(); - - // Create raw data with zero denominators that should be skipped - let raw_stat = PerfStatRaw { - time: TimeEnum::DateTime(base_time), - data: "0 some_stat; 1000 2000; 0 0;1\n1 some_stat; 3000 4000; 5000 5000;1".to_string(), - }; - raw_data.push(Data::PerfStatRaw(raw_stat)); - - let result = PerfStat::new() - .process_raw_data(ReportParams::new(), raw_data) - .unwrap(); - - if let AperfData::TimeSeries(time_series_data) = result { - // Should have no metrics since all denominators are zero - assert_eq!(time_series_data.metrics.len(), 1); - assert_eq!(time_series_data.sorted_metric_names.len(), 1); - let metric = &time_series_data.metrics["some_stat"]; - assert_eq!(metric.series.len(), 2); - assert_eq!(metric.series[0].values[0], 0.7); - assert_eq!(metric.series[1].values[0], 0.7); - } else { - panic!("Expected TimeSeries data"); - } +fn test_grouped_metric_order_follows_config() { + // sorted_metric_names must follow the config's metric insertion order, NOT + // alphabetical. Config order here is deliberately non-alphabetical. + let dir = TempDir::new().unwrap(); + write_pmu_config( + dir.path(), + &[ + ("Branches", "p/event=0x10/"), + ("Cycles", "p/event=0x11/"), + ("Instructions", "p/event=0x8/"), + ("L3", "p/event=0x37/"), + ], + &[ + ("l3-mpki", "L3 / Instructions * 1000"), + ("ipc", "Instructions / Cycles"), + ("branch-mpki", "Branches / Instructions * 1000"), + ], + ); + + // One sample each; counters in var_names (alphabetical) order: + // l3-mpki var_names [Instructions, L3] + // ipc var_names [Cycles, Instructions] + // branch-mpki var_names [Branches, Instructions] + let raw = vec![snapshot( + 0, + vec![ + grouped_line(0, "l3-mpki", &[1000, 50], 1.0, 1.0), + grouped_line(0, "ipc", &[1000, 2000], 1.0, 1.0), + grouped_line(0, "branch-mpki", &[100, 2000], 1.0, 1.0), + ], + )]; + + let ts = process(report_params(dir.path(), GROUPED_PMU_MODE), raw); + assert_eq!( + ts.sorted_metric_names, + vec!["l3-mpki", "ipc", "branch-mpki"] + ); + + approx( + series(&ts.metrics["l3-mpki"], "CPU0").values[0], + 50.0, + "l3-mpki", + ); + approx(series(&ts.metrics["ipc"], "CPU0").values[0], 2.0, "ipc"); + approx( + series(&ts.metrics["branch-mpki"], "CPU0").values[0], + 50.0, + "branch-mpki", + ); } #[test] -fn test_process_pmu_stat_zero_nonzero_metric_skipped() { - let mut raw_data = Vec::new(); - let base_time = Utc.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap(); - - // Create raw data with mixed zero and non-zero denominators - let raw_stat = PerfStatRaw { - time: TimeEnum::DateTime(base_time), - data: - "0 valid_stat; 1000; 2000;1\n0 zero_denom_stat; 1000; 0;1\n1 valid_stat; 3000; 4000;1" - .to_string(), - }; - raw_data.push(Data::PerfStatRaw(raw_stat)); +fn test_grouped_frozen_time_running_sample_skipped() { + // If time_running does not advance between snapshots (delta 0), that snapshot + // is dropped to avoid a divide-by-zero scale. + let dir = TempDir::new().unwrap(); + write_pmu_config( + dir.path(), + &[ + ("Cycles", "p/event=0x11/"), + ("Instructions", "p/event=0x8/"), + ], + &[("ipc", "Instructions / Cycles")], + ); + + let raw = vec![ + // s0: first sample, time_running delta = 5 (nonzero) -> processed. + snapshot(0, vec![grouped_line(0, "ipc", &[1000, 2000], 5.0, 5.0)]), + // s1: time_running stays at 5 -> delta 0 -> skipped. + snapshot(1, vec![grouped_line(0, "ipc", &[2000, 4000], 5.0, 5.0)]), + // s2: time_running advances again -> processed. + snapshot(2, vec![grouped_line(0, "ipc", &[3000, 6000], 7.0, 7.0)]), + ]; + + let ts = process(report_params(dir.path(), GROUPED_PMU_MODE), raw); + let cpu0 = series(&ts.metrics["ipc"], "CPU0"); + // Only s0 and s2 produced points (s1 dropped). + assert_eq!(cpu0.values.len(), 2); + assert_eq!(cpu0.time_diff, vec![0, 2]); + approx(cpu0.values[0], 2.0, "frozen-tr s0"); + approx(cpu0.values[1], 2.0, "frozen-tr s2"); +} - let result = PerfStat::new() - .process_raw_data(ReportParams::new(), raw_data) - .unwrap(); +// =========================================================================== +// New format — UNGROUPED collection +// =========================================================================== - if let AperfData::TimeSeries(time_series_data) = result { - // Should have only 1 metric (valid_stat), zero_denom_stat should be skipped - assert_eq!(time_series_data.metrics.len(), 1); - assert_eq!(time_series_data.sorted_metric_names, vec!["valid_stat"]); +#[test] +fn test_ungrouped_event_dedup_across_metrics() { + // Ungrouped collects each EVENT once (deduplicated across metrics) and feeds + // the same event value into every metric that references it. Here both ipc and + // branch-mpki use Instructions, collected from a single Instructions line. + // No multiplexing (te == tr => scale 1). + let dir = TempDir::new().unwrap(); + write_pmu_config( + dir.path(), + &[ + ("Branches", "p/event=0x10/"), + ("Cycles", "p/event=0x11/"), + ("Instructions", "p/event=0x8/"), + ], + &[ + ("ipc", "Instructions / Cycles"), + ("branch-mpki", "Branches / Instructions * 1000"), + ], + ); + + // Counter values are PER-INTERVAL (reset-based collection); only te/tr are + // accumulative. te == tr each interval => scale 1. + // CPU0: s0 (C1000,I2000,B200) s1 (C1000,I2000,B100) + // CPU1: s0 (C 500,I1500,B150) s1 (C1000,I2000,B250) + let raw = vec![ + snapshot( + 0, + vec![ + ungrouped_line(0, "Cycles", 1000, 1.0, 1.0), + ungrouped_line(0, "Instructions", 2000, 1.0, 1.0), + ungrouped_line(0, "Branches", 200, 1.0, 1.0), + ungrouped_line(1, "Cycles", 500, 1.0, 1.0), + ungrouped_line(1, "Instructions", 1500, 1.0, 1.0), + ungrouped_line(1, "Branches", 150, 1.0, 1.0), + ], + ), + snapshot( + 1, + vec![ + ungrouped_line(0, "Cycles", 1000, 2.0, 2.0), + ungrouped_line(0, "Instructions", 2000, 2.0, 2.0), + ungrouped_line(0, "Branches", 100, 2.0, 2.0), + ungrouped_line(1, "Cycles", 1000, 2.0, 2.0), + ungrouped_line(1, "Instructions", 2000, 2.0, 2.0), + ungrouped_line(1, "Branches", 250, 2.0, 2.0), + ], + ), + ]; + + let ts = process(report_params(dir.path(), UNGROUPED_PMU_MODE), raw); + + assert_eq!(ts.metrics.len(), 2); + assert_eq!(ts.sorted_metric_names, vec!["ipc", "branch-mpki"]); + + // ipc per CPU. + approx( + series(&ts.metrics["ipc"], "CPU0").values[0], + 2000.0 / 1000.0, + "ipc cpu0 s0", + ); + approx( + series(&ts.metrics["ipc"], "CPU1").values[0], + 1500.0 / 500.0, + "ipc cpu1 s0", + ); + approx( + series(&ts.metrics["ipc"], "CPU0").values[1], + 4000.0 / 2000.0, + "ipc cpu0 s1", + ); + approx( + series(&ts.metrics["ipc"], "CPU1").values[1], + 2000.0 / 1000.0, + "ipc cpu1 s1", + ); + + // branch-mpki = Branches / Instructions * 1000 per CPU. + approx( + series(&ts.metrics["branch-mpki"], "CPU0").values[0], + 200.0 / 2000.0 * 1000.0, + "bm cpu0 s0", + ); + approx( + series(&ts.metrics["branch-mpki"], "CPU1").values[0], + 150.0 / 1500.0 * 1000.0, + "bm cpu1 s0", + ); + approx( + series(&ts.metrics["branch-mpki"], "CPU0").values[1], + 100.0 / 2000.0 * 1000.0, + "bm cpu0 s1", + ); + approx( + series(&ts.metrics["branch-mpki"], "CPU1").values[1], + 250.0 / 2000.0 * 1000.0, + "bm cpu1 s1", + ); + + // Aggregate uses summed event values across CPUs. + // s1: ipc = (4000+2000)/(2000+1000) = 2.0 ; bm = (100+250)/(2000+2000)*1000 = 87.5 + approx( + aggregate(&ts.metrics["ipc"]).values[1], + 6000.0 / 3000.0, + "ipc agg s1", + ); + approx( + aggregate(&ts.metrics["branch-mpki"]).values[1], + 350.0 / 4000.0 * 1000.0, + "bm agg s1", + ); +} - let metric = &time_series_data.metrics["valid_stat"]; - // Should have 3 series: 2 CPUs + 1 aggregate - assert_eq!(metric.series.len(), 3); +#[test] +fn test_ungrouped_per_event_scaling_changes_result() { + // Unlike grouped, each ungrouped event carries its OWN (time_enabled, + // time_running), so the multiplexing scale does NOT cancel in a ratio when the + // two events are scaled differently. Here Cycles is scaled x2 (ran half the + // time) while Instructions is unscaled, turning a raw 2.0 ratio into 1.0. + let dir = TempDir::new().unwrap(); + write_pmu_config( + dir.path(), + &[ + ("Cycles", "p/event=0x11/"), + ("Instructions", "p/event=0x8/"), + ], + &[("ipc", "Instructions / Cycles")], + ); + + let raw = vec![ + // s0 baseline: both events te=1 tr=1. + snapshot( + 0, + vec![ + ungrouped_line(0, "Cycles", 1000, 1.0, 1.0), + ungrouped_line(0, "Instructions", 2000, 1.0, 1.0), + ], + ), + // s1 deltas: Cycles counter +1000 with te_delta=2, tr_delta=1 -> scale x2 -> 2000; + // Instructions counter +2000 with te_delta=2, tr_delta=2 -> scale x1 -> 2000. + // (time_running must advance, else the snapshot is skipped.) + // ipc = 2000 / 2000 = 1.0 (would be 2.0 without per-event scaling). + snapshot( + 1, + vec![ + ungrouped_line(0, "Cycles", 2000, 3.0, 2.0), + ungrouped_line(0, "Instructions", 4000, 3.0, 3.0), + ], + ), + ]; + + let ts = process(report_params(dir.path(), UNGROUPED_PMU_MODE), raw); + let cpu0 = series(&ts.metrics["ipc"], "CPU0"); + approx(cpu0.values[1], 1.0, "per-event scaled ipc s1"); +} - // Verify the values are calculated correctly for valid denominators - assert_eq!(metric.series[1].values[0], 0.5); // CPU 0: 1000/2000 * 1 = 0.5 - assert_eq!(metric.series[2].values[0], 0.75); // CPU 1: 3000/4000 * 1 = 0.75 +// =========================================================================== +// New format — shared edge cases +// =========================================================================== - // Aggregate: (1000*1 + 3000*1) / (2000 + 4000) = 4000/6000 = 0.6667 - assert!((metric.series[0].values[0] - (4000.0 / 6000.0)).abs() < 1e-5); - } else { - panic!("Expected TimeSeries data"); +#[test] +fn test_new_format_empty_data() { + // Empty raw data with a valid saved config yields no metrics (no panic). + let dir = TempDir::new().unwrap(); + write_pmu_config( + dir.path(), + &[ + ("Cycles", "p/event=0x11/"), + ("Instructions", "p/event=0x8/"), + ], + &[("ipc", "Instructions / Cycles")], + ); + + for mode in [GROUPED_PMU_MODE, UNGROUPED_PMU_MODE] { + let ts = process(report_params(dir.path(), mode), Vec::new()); + assert!(ts.metrics.is_empty(), "{mode}: metrics should be empty"); + assert!( + ts.sorted_metric_names.is_empty(), + "{mode}: names should be empty" + ); } }