From e5b529bd2ba6ce1aee1282804683e767865cc860 Mon Sep 17 00:00:00 2001 From: Janakarajan Natarajan Date: Thu, 12 Dec 2024 01:39:24 +0000 Subject: [PATCH] Aperf: Allow to use custom PMU Allow the user to specify a custom PMU file to be used during aperf record. This custom PMU file must be in the expected format. To aid in the creation of the custom PMU file, add a sub-command. 'aperf custom-pmu' - an interactive sub-command to create custom PMU. This allows the user to create a file from scratch or modify an existing PMU config aperf already knows about. The custom file generated can be used by specifying '--pmu-config' with 'aperf record'. --- Cargo.lock | 113 ++++++++++++++++ Cargo.toml | 1 + src/bin/aperf.rs | 5 + src/data.rs | 3 + src/data/perf_stat.rs | 20 ++- src/lib.rs | 6 + src/pmu.rs | 303 ++++++++++++++++++++++++++++++++++++++++++ src/record.rs | 9 +- tests/test_aperf.rs | 2 + 9 files changed, 457 insertions(+), 5 deletions(-) create mode 100644 src/pmu.rs diff --git a/Cargo.lock b/Cargo.lock index ceb075fa..207ed962 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,7 @@ dependencies = [ "indexmap", "infer", "inferno", + "inquire", "lazy_static", "libc", "log", @@ -714,6 +715,31 @@ dependencies = [ "once_cell", ] +[[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", + "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.6" @@ -828,6 +854,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "either" version = "1.8.0" @@ -988,6 +1020,24 @@ 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" @@ -1206,6 +1256,23 @@ 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.4.0", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "instant" version = "0.1.12" @@ -1384,6 +1451,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[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" @@ -2042,6 +2118,27 @@ dependencies = [ "digest", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -2238,6 +2335,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.36" @@ -2435,6 +2542,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.1.11" diff --git a/Cargo.toml b/Cargo.toml index 872142aa..73304d46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ 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" diff --git a/src/bin/aperf.rs b/src/bin/aperf.rs index fc14e144..bb093c76 100644 --- a/src/bin/aperf.rs +++ b/src/bin/aperf.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use aperf::pmu::{custom_pmu, CustomPMU}; use aperf::record::{record, Record}; use aperf::report::{report, Report}; use aperf::{PDError, APERF_RUNLOG, APERF_TMP}; @@ -39,6 +40,9 @@ enum Commands { /// Generate an HTML report based on the data collected. Report(Report), + + /// Create a custom PMU configuration file for use with Aperf record. + CustomPMU(CustomPMU), } fn init_logger(verbose: u8, runlog: &PathBuf) -> Result<()> { @@ -98,6 +102,7 @@ fn main() -> Result<()> { match cli.command { Commands::Record(r) => record(&r, &tmp_dir_path_buf, &runlog), Commands::Report(r) => report(&r, &tmp_dir_path_buf), + Commands::CustomPMU(r) => custom_pmu(&r), }?; fs::remove_dir_all(tmp_dir_path_buf)?; Ok(()) diff --git a/src/data.rs b/src/data.rs index d15d6de0..0c66358f 100644 --- a/src/data.rs +++ b/src/data.rs @@ -56,6 +56,7 @@ pub struct CollectorParams { pub tmp_dir: PathBuf, pub signal: Signal, pub runlog: PathBuf, + pub pmu_config: Option, } impl CollectorParams { @@ -70,6 +71,7 @@ impl CollectorParams { tmp_dir: PathBuf::new(), signal: signal::SIGTERM, runlog: PathBuf::new(), + pmu_config: Option::None, } } } @@ -129,6 +131,7 @@ impl DataType { self.collector_params.profile = param.profile.clone(); self.collector_params.tmp_dir = param.tmp_dir.clone(); self.collector_params.runlog = param.runlog.clone(); + self.collector_params.pmu_config = param.pmu_config.clone(); self.file_handle = Some( OpenOptions::new() diff --git a/src/data/perf_stat.rs b/src/data/perf_stat.rs index 1e03f328..1f35b062 100644 --- a/src/data/perf_stat.rs +++ b/src/data/perf_stat.rs @@ -6,7 +6,7 @@ use crate::{PDError, PERFORMANCE_DATA, VISUALIZATION_DATA}; use anyhow::Result; use chrono::prelude::*; use ctor::ctor; -use log::{trace, warn}; +use log::{error, info, trace, warn}; use perf_event::events::{Raw, Software}; use perf_event::{Builder, Counter, Group, ReadFormat}; use serde::{Deserialize, Serialize}; @@ -144,7 +144,7 @@ pub fn to_events(data: &[u8]) -> Result> { } impl CollectData for PerfStatRaw { - fn prepare_data_collector(&mut self, _params: &CollectorParams) -> Result<()> { + 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."); @@ -156,7 +156,7 @@ impl CollectData for PerfStatRaw { cfg_if::cfg_if! { if #[cfg(target_arch = "aarch64")] { - let perf_list = to_events(arm64::perf_list::GRV_EVENTS)?; + 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::utils::get_cpu_info()?; let platform_specific_counter: &[u8]; @@ -186,11 +186,23 @@ impl CollectData for PerfStatRaw { return Err(PDError::CollectorPerfUnsupportedCPU.into()); } - let perf_list = form_events_map(base, platform_specific_counter)?; + 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 = std::fs::File::open(custom_file)?; + let user_provided_list: Vec = serde_json::from_reader(&f)?; + if user_provided_list.is_empty() { + error!( + "User provided PMU configuration is invalid. Falling back to default events." + ); + } else { + info!("Using custom PMU configuration provided by user."); + perf_list = user_provided_list; + } + } for cpu in 0..num_cpus { for named_ctr in &perf_list { let perf_group = Builder::new(Software::DUMMY) diff --git a/src/lib.rs b/src/lib.rs index 566b9efd..b38b5c50 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ extern crate lazy_static; pub mod data; +pub mod pmu; pub mod record; pub mod report; pub mod visualizer; @@ -94,6 +95,9 @@ pub enum PDError { #[error("File not found {}", .0)] VisualizerFileNotFound(String), + #[error("Custom PMU config file not provided.")] + PMUCustomFileNotFound, + #[error("Run data not available")] InvalidRunData, @@ -530,6 +534,7 @@ pub struct InitParams { pub dir_name: String, pub period: u64, pub profile: HashMap, + pub pmu_config: Option, pub interval: u64, pub run_name: String, pub collector_version: String, @@ -568,6 +573,7 @@ impl InitParams { dir_name, period: 0, profile: HashMap::new(), + pmu_config: Option::None, interval: 0, run_name, collector_version, diff --git a/src/pmu.rs b/src/pmu.rs new file mode 100644 index 00000000..f4b997f1 --- /dev/null +++ b/src/pmu.rs @@ -0,0 +1,303 @@ +#[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 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(short, long, value_parser)] + pub pmu_file: Option, +} + +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; + } + } + Ok(()) +} + +pub fn custom_pmu(cpmu: &CustomPMU) -> Result<()> { + 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/record.rs b/src/record.rs index 696e12a1..949cefd3 100644 --- a/src/record.rs +++ b/src/record.rs @@ -2,7 +2,7 @@ use crate::{data, InitParams, PERFORMANCE_DATA}; use anyhow::Result; use clap::Args; use log::{debug, error, info}; -use std::path::Path; +use std::path::{Path, PathBuf}; #[derive(Args, Debug)] pub struct Record { @@ -25,6 +25,10 @@ pub struct Record { /// Profile JVMs using async-profiler. Specify args using comma separated values. Profiles all JVMs if no args are provided. #[clap(long, value_parser, default_missing_value = Some("jps"), value_names = &["PID/Name>,,...,, + + /// Custom PMU config file to use. + #[clap(long, value_parser)] + pub pmu_config: Option, } fn prepare_data_collectors() -> Result<()> { @@ -64,6 +68,9 @@ pub fn record(record: &Record, tmp_dir: &Path, runlog: &Path) -> Result<()> { params.interval = record.interval; params.tmp_dir = tmp_dir.to_path_buf(); params.runlog = runlog.to_path_buf(); + if let Some(p) = &record.pmu_config { + params.pmu_config = Some(PathBuf::from(p)); + } match &record.profile_java { Some(j) => { diff --git a/tests/test_aperf.rs b/tests/test_aperf.rs index ad7f5529..44d3df6f 100644 --- a/tests/test_aperf.rs +++ b/tests/test_aperf.rs @@ -41,6 +41,7 @@ fn test_record() { period: 2, profile: false, profile_java: None, + pmu_config: None, }; let runlog = tempdir.join(APERF_RUNLOG); fs::File::create(&runlog).unwrap(); @@ -70,6 +71,7 @@ fn test_report() { period: 2, profile: false, profile_java: None, + pmu_config: None, }; let runlog = tempdir.join(APERF_RUNLOG); fs::File::create(&runlog).unwrap();