diff --git a/.aztec-sync-commit b/.aztec-sync-commit index ec0df2d1b06..485ef5a8949 100644 --- a/.aztec-sync-commit +++ b/.aztec-sync-commit @@ -1 +1 @@ -eb9e9f6f2b3952760822faaacb7e851e936e0800 +df3b27b8c603845598bf966100be3a21e8e442db diff --git a/Cargo.lock b/Cargo.lock index 37376ad7c80..589c3d179d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2747,13 +2747,11 @@ version = "0.31.0" dependencies = [ "acir", "clap", - "codespan-reporting", "color-eyre", "const_format", "fm", "im", "inferno", - "nargo", "noirc_abi", "noirc_artifacts", "noirc_driver", diff --git a/tooling/profiler/Cargo.toml b/tooling/profiler/Cargo.toml index d33e99f1a4c..0ccd56b791f 100644 --- a/tooling/profiler/Cargo.toml +++ b/tooling/profiler/Cargo.toml @@ -18,12 +18,10 @@ path = "src/main.rs" color-eyre.workspace = true clap.workspace = true noirc_artifacts.workspace = true -nargo.workspace = true const_format.workspace = true serde.workspace = true serde_json.workspace = true fm.workspace = true -codespan-reporting.workspace = true inferno = "0.11.19" im.workspace = true acir.workspace = true diff --git a/tooling/profiler/src/cli/gates_flamegraph_cmd.rs b/tooling/profiler/src/cli/gates_flamegraph_cmd.rs index 38e7fff5f42..154ac38f4bb 100644 --- a/tooling/profiler/src/cli/gates_flamegraph_cmd.rs +++ b/tooling/profiler/src/cli/gates_flamegraph_cmd.rs @@ -1,19 +1,13 @@ -use std::collections::BTreeMap; -use std::io::BufWriter; use std::path::{Path, PathBuf}; -use std::process::Command; use clap::Args; -use codespan_reporting::files::Files; use color_eyre::eyre::{self, Context}; -use inferno::flamegraph::{from_lines, Options}; -use serde::{Deserialize, Serialize}; -use acir::circuit::OpcodeLocation; -use nargo::errors::Location; use noirc_artifacts::debug::DebugArtifact; -use noirc_artifacts::program::ProgramArtifact; -use noirc_errors::reporter::line_and_column_from_span; + +use crate::flamegraph::{FlamegraphGenerator, InfernoFlamegraphGenerator}; +use crate::fs::read_program_from_file; +use crate::gates_provider::{BackendGatesProvider, GatesProvider}; #[derive(Debug, Clone, Args)] pub(crate) struct GatesFlamegraphCommand { @@ -30,85 +24,11 @@ pub(crate) struct GatesFlamegraphCommand { output: String, } -trait GatesProvider { - fn get_gates(&self, artifact_path: &Path) -> eyre::Result<BackendGatesResponse>; -} - -struct BackendGatesProvider { - backend_path: PathBuf, -} - -impl GatesProvider for BackendGatesProvider { - fn get_gates(&self, artifact_path: &Path) -> eyre::Result<BackendGatesResponse> { - let backend_gates_response = - Command::new(&self.backend_path).arg("gates").arg("-b").arg(artifact_path).output()?; - - // Parse the backend gates command stdout as json - let backend_gates_response: BackendGatesResponse = - serde_json::from_slice(&backend_gates_response.stdout)?; - Ok(backend_gates_response) - } -} - -trait FlamegraphGenerator { - fn generate_flamegraph<'lines, I: IntoIterator<Item = &'lines str>>( - &self, - folded_lines: I, - artifact_name: &str, - function_name: &str, - output_path: &Path, - ) -> eyre::Result<()>; -} - -struct InfernoFlamegraphGenerator {} - -impl FlamegraphGenerator for InfernoFlamegraphGenerator { - fn generate_flamegraph<'lines, I: IntoIterator<Item = &'lines str>>( - &self, - folded_lines: I, - artifact_name: &str, - function_name: &str, - output_path: &Path, - ) -> eyre::Result<()> { - let flamegraph_file = std::fs::File::create(output_path)?; - let flamegraph_writer = BufWriter::new(flamegraph_file); - - let mut options = Options::default(); - options.hash = true; - options.deterministic = true; - options.title = format!("{}-{}", artifact_name, function_name); - options.subtitle = Some("Sample = Gate".to_string()); - options.frame_height = 24; - options.color_diffusion = true; - - from_lines(&mut options, folded_lines, flamegraph_writer)?; - - Ok(()) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct BackendGatesReport { - acir_opcodes: usize, - circuit_size: usize, - gates_per_opcode: Vec<usize>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct BackendGatesResponse { - functions: Vec<BackendGatesReport>, -} - -struct FoldedStackItem { - total_gates: usize, - nested_items: BTreeMap<String, FoldedStackItem>, -} - pub(crate) fn run(args: GatesFlamegraphCommand) -> eyre::Result<()> { run_with_provider( &PathBuf::from(args.artifact_path), &BackendGatesProvider { backend_path: PathBuf::from(args.backend_path) }, - &InfernoFlamegraphGenerator {}, + &InfernoFlamegraphGenerator { count_name: "gates".to_string() }, &PathBuf::from(args.output), ) } @@ -119,7 +39,7 @@ fn run_with_provider<Provider: GatesProvider, Generator: FlamegraphGenerator>( flamegraph_generator: &Generator, output_path: &Path, ) -> eyre::Result<()> { - let program = + let mut program = read_program_from_file(artifact_path).context("Error reading program from file")?; let backend_gates_response = @@ -127,10 +47,16 @@ fn run_with_provider<Provider: GatesProvider, Generator: FlamegraphGenerator>( let function_names = program.names.clone(); + let bytecode = std::mem::take(&mut program.bytecode); + let debug_artifact: DebugArtifact = program.into(); - for (func_idx, (func_gates, func_name)) in - backend_gates_response.functions.into_iter().zip(function_names).enumerate() + for (func_idx, ((func_gates, func_name), bytecode)) in backend_gates_response + .functions + .into_iter() + .zip(function_names) + .zip(bytecode.functions) + .enumerate() { println!( "Opcode count: {}, Total gates by opcodes: {}, Circuit size: {}", @@ -139,143 +65,33 @@ fn run_with_provider<Provider: GatesProvider, Generator: FlamegraphGenerator>( func_gates.circuit_size ); - // Create a nested hashmap with the stack items, folding the gates for all the callsites that are equal - let mut folded_stack_items = BTreeMap::new(); - - func_gates.gates_per_opcode.into_iter().enumerate().for_each(|(opcode_index, gates)| { - let call_stack = &debug_artifact.debug_symbols[func_idx] - .locations - .get(&OpcodeLocation::Acir(opcode_index)); - let location_names = if let Some(call_stack) = call_stack { - call_stack - .iter() - .map(|location| location_to_callsite_label(*location, &debug_artifact)) - .collect::<Vec<String>>() - } else { - vec!["unknown".to_string()] - }; - - add_locations_to_folded_stack_items(&mut folded_stack_items, location_names, gates); - }); - let folded_lines = to_folded_sorted_lines(&folded_stack_items, Default::default()); - flamegraph_generator.generate_flamegraph( - folded_lines.iter().map(|as_string| as_string.as_str()), + func_gates.gates_per_opcode, + bytecode.opcodes, + &debug_artifact.debug_symbols[func_idx], + &debug_artifact, artifact_path.to_str().unwrap(), &func_name, - &Path::new(&output_path).join(Path::new(&format!("{}.svg", &func_name))), + &Path::new(&output_path).join(Path::new(&format!("{}_gates.svg", &func_name))), )?; } Ok(()) } -pub(crate) fn read_program_from_file<P: AsRef<Path>>( - circuit_path: P, -) -> eyre::Result<ProgramArtifact> { - let file_path = circuit_path.as_ref().with_extension("json"); - - let input_string = std::fs::read(file_path)?; - let program = serde_json::from_slice(&input_string)?; - - Ok(program) -} - -fn location_to_callsite_label<'files>( - location: Location, - files: &'files impl Files<'files, FileId = fm::FileId>, -) -> String { - let filename = - Path::new(&files.name(location.file).expect("should have a file path").to_string()) - .file_name() - .map(|os_str| os_str.to_string_lossy().to_string()) - .unwrap_or("invalid_path".to_string()); - let source = files.source(location.file).expect("should have a file source"); - - let code_slice = source - .as_ref() - .chars() - .skip(location.span.start() as usize) - .take(location.span.end() as usize - location.span.start() as usize) - .collect::<String>(); - - // ";" is used for frame separation, and is not allowed by inferno - // Check code slice for ";" and replace it with 'GREEK QUESTION MARK' (U+037E) - let code_slice = code_slice.replace(';', "\u{037E}"); - - let (line, column) = line_and_column_from_span(source.as_ref(), &location.span); - - format!("{}:{}:{}::{}", filename, line, column, code_slice) -} - -fn add_locations_to_folded_stack_items( - stack_items: &mut BTreeMap<String, FoldedStackItem>, - locations: Vec<String>, - gates: usize, -) { - let mut child_map = stack_items; - for (index, location) in locations.iter().enumerate() { - let current_item = child_map - .entry(location.clone()) - .or_insert(FoldedStackItem { total_gates: 0, nested_items: BTreeMap::new() }); - - child_map = &mut current_item.nested_items; - - if index == locations.len() - 1 { - current_item.total_gates += gates; - } - } -} - -/// Creates a vector of lines in the format that inferno expects from a nested hashmap of stack items -/// The lines have to be sorted in the following way, exploring the graph in a depth-first manner: -/// main 100 -/// main::foo 0 -/// main::foo::bar 200 -/// main::baz 27 -/// main::baz::qux 800 -fn to_folded_sorted_lines( - folded_stack_items: &BTreeMap<String, FoldedStackItem>, - parent_stacks: im::Vector<String>, -) -> Vec<String> { - folded_stack_items - .iter() - .flat_map(move |(location, folded_stack_item)| { - let frame_list: Vec<String> = - parent_stacks.iter().cloned().chain(std::iter::once(location.clone())).collect(); - let line: String = - format!("{} {}", frame_list.join(";"), folded_stack_item.total_gates); - - let mut new_parent_stacks = parent_stacks.clone(); - new_parent_stacks.push_back(location.clone()); - - let child_lines: Vec<String> = - to_folded_sorted_lines(&folded_stack_item.nested_items, new_parent_stacks); - - std::iter::once(line).chain(child_lines) - }) - .collect() -} - #[cfg(test)] mod tests { - use acir::circuit::{OpcodeLocation, Program}; + use acir::circuit::{Circuit, Opcode, Program}; use color_eyre::eyre::{self}; - use fm::{FileId, FileManager}; + use fm::codespan_files::Files; use noirc_artifacts::program::ProgramArtifact; - use noirc_driver::DebugFile; - use noirc_errors::{ - debug_info::{DebugInfo, ProgramDebugInfo}, - Location, Span, - }; + use noirc_errors::debug_info::{DebugInfo, ProgramDebugInfo}; use std::{ - cell::RefCell, collections::{BTreeMap, HashMap}, path::{Path, PathBuf}, }; - use tempfile::TempDir; - use super::{BackendGatesReport, BackendGatesResponse, GatesProvider}; + use crate::gates_provider::{BackendGatesReport, BackendGatesResponse, GatesProvider}; struct TestGateProvider { mock_responses: HashMap<PathBuf, BackendGatesResponse>, @@ -293,194 +109,63 @@ mod tests { } #[derive(Default)] - struct TestFlamegraphGenerator { - lines_received: RefCell<Vec<Vec<String>>>, - } + struct TestFlamegraphGenerator {} impl super::FlamegraphGenerator for TestFlamegraphGenerator { - fn generate_flamegraph<'lines, I: IntoIterator<Item = &'lines str>>( + fn generate_flamegraph<'files, F>( &self, - folded_lines: I, + _samples_per_opcode: Vec<usize>, + _opcodes: Vec<Opcode<F>>, + _debug_symbols: &DebugInfo, + _files: &'files impl Files<'files, FileId = fm::FileId>, _artifact_name: &str, _function_name: &str, - _output_path: &std::path::Path, + output_path: &Path, ) -> eyre::Result<()> { - let lines = folded_lines.into_iter().map(|line| line.to_string()).collect(); - self.lines_received.borrow_mut().push(lines); - Ok(()) - } - } + let output_file = std::fs::File::create(output_path).unwrap(); + std::io::Write::write_all(&mut std::io::BufWriter::new(output_file), b"success") + .unwrap(); - fn find_spans_for(source: &str, needle: &str) -> Vec<Span> { - let mut spans = Vec::new(); - let mut start = 0; - while let Some(start_idx) = source[start..].find(needle) { - let start_idx = start + start_idx; - let end_idx = start_idx + needle.len(); - spans.push(Span::inclusive(start_idx as u32, end_idx as u32 - 1)); - start = end_idx; + Ok(()) } - spans } - struct TestCase { - expected_folded_sorted_lines: Vec<Vec<String>>, - debug_symbols: ProgramDebugInfo, - file_map: BTreeMap<FileId, DebugFile>, - gates_report: BackendGatesResponse, - } + #[test] + fn smoke_test() { + let temp_dir = tempfile::tempdir().unwrap(); - fn simple_test_case(temp_dir: &TempDir) -> TestCase { - let source_code = r##" - fn main() { - foo(); - bar(); - whatever(); - } - fn foo() { - baz(); - } - fn bar () { - whatever() - } - fn baz () { - whatever() - } - "##; + let artifact_path = temp_dir.path().join("test.json"); - let source_file_name = Path::new("main.nr"); - let mut fm = FileManager::new(temp_dir.path()); - let file_id = fm.add_file_with_source(source_file_name, source_code.to_string()).unwrap(); + let artifact = ProgramArtifact { + noir_version: "0.0.0".to_string(), + hash: 27, + abi: noirc_abi::Abi::default(), + bytecode: Program { functions: vec![Circuit::default()], ..Program::default() }, + debug_symbols: ProgramDebugInfo { debug_infos: vec![DebugInfo::default()] }, + file_map: BTreeMap::default(), + names: vec!["main".to_string()], + }; - let main_declaration_location = - Location::new(find_spans_for(source_code, "fn main()")[0], file_id); - let main_foo_call_location = - Location::new(find_spans_for(source_code, "foo()")[0], file_id); - let main_bar_call_location = - Location::new(find_spans_for(source_code, "bar()")[0], file_id); - let main_whatever_call_location = - Location::new(find_spans_for(source_code, "whatever()")[0], file_id); - let foo_baz_call_location = Location::new(find_spans_for(source_code, "baz()")[0], file_id); - let bar_whatever_call_location = - Location::new(find_spans_for(source_code, "whatever()")[1], file_id); - let baz_whatever_call_location = - Location::new(find_spans_for(source_code, "whatever()")[2], file_id); + // Write the artifact to a file + let artifact_file = std::fs::File::create(&artifact_path).unwrap(); + serde_json::to_writer(artifact_file, &artifact).unwrap(); - let mut opcode_locations = BTreeMap::<OpcodeLocation, Vec<Location>>::new(); - // main::foo::baz::whatever - opcode_locations.insert( - OpcodeLocation::Acir(0), - vec![ - main_declaration_location, - main_foo_call_location, - foo_baz_call_location, - baz_whatever_call_location, + let mock_gates_response = BackendGatesResponse { + functions: vec![ + (BackendGatesReport { acir_opcodes: 0, gates_per_opcode: vec![], circuit_size: 0 }), ], - ); - - // main::bar::whatever - opcode_locations.insert( - OpcodeLocation::Acir(1), - vec![main_declaration_location, main_bar_call_location, bar_whatever_call_location], - ); - // main::whatever - opcode_locations.insert( - OpcodeLocation::Acir(2), - vec![main_declaration_location, main_whatever_call_location], - ); - - let file_map = BTreeMap::from_iter(vec![( - file_id, - DebugFile { source: source_code.to_string(), path: source_file_name.to_path_buf() }, - )]); - - let debug_symbols = ProgramDebugInfo { - debug_infos: vec![DebugInfo::new( - opcode_locations, - BTreeMap::default(), - BTreeMap::default(), - BTreeMap::default(), - )], }; - let backend_gates_response = BackendGatesResponse { - functions: vec![BackendGatesReport { - acir_opcodes: 3, - circuit_size: 100, - gates_per_opcode: vec![10, 20, 30], - }], + let provider = TestGateProvider { + mock_responses: HashMap::from([(artifact_path.clone(), mock_gates_response)]), }; + let flamegraph_generator = TestFlamegraphGenerator::default(); - let expected_folded_sorted_lines = vec![ - "main.nr:2:9::fn main() 0".to_string(), - "main.nr:2:9::fn main();main.nr:3:13::foo() 0".to_string(), - "main.nr:2:9::fn main();main.nr:3:13::foo();main.nr:8:13::baz() 0".to_string(), - "main.nr:2:9::fn main();main.nr:3:13::foo();main.nr:8:13::baz();main.nr:14:13::whatever() 10".to_string(), - "main.nr:2:9::fn main();main.nr:4:13::bar() 0".to_string(), - "main.nr:2:9::fn main();main.nr:4:13::bar();main.nr:11:13::whatever() 20".to_string(), - "main.nr:2:9::fn main();main.nr:5:13::whatever() 30".to_string(), - ]; - - TestCase { - expected_folded_sorted_lines: vec![expected_folded_sorted_lines], - debug_symbols, - file_map, - gates_report: backend_gates_response, - } - } - - #[test] - fn test_flamegraph() { - let temp_dir = tempfile::tempdir().unwrap(); - - let test_cases = vec![simple_test_case(&temp_dir)]; - let artifact_names: Vec<_> = - test_cases.iter().enumerate().map(|(idx, _)| format!("test{}.json", idx)).collect(); - - let test_cases_with_names: Vec<_> = test_cases.into_iter().zip(artifact_names).collect(); - - let mut mock_responses: HashMap<PathBuf, BackendGatesResponse> = HashMap::new(); - // Collect mock responses - for (test_case, artifact_name) in test_cases_with_names.iter() { - mock_responses.insert( - temp_dir.path().join(artifact_name.clone()), - test_case.gates_report.clone(), - ); - } - - let provider = TestGateProvider { mock_responses }; - - for (test_case, artifact_name) in test_cases_with_names.iter() { - let artifact_path = temp_dir.path().join(artifact_name.clone()); - - let artifact = ProgramArtifact { - noir_version: "0.0.0".to_string(), - hash: 27, - abi: noirc_abi::Abi::default(), - bytecode: Program::default(), - debug_symbols: test_case.debug_symbols.clone(), - file_map: test_case.file_map.clone(), - names: vec!["main".to_string()], - }; - - // Write the artifact to a file - let artifact_file = std::fs::File::create(&artifact_path).unwrap(); - serde_json::to_writer(artifact_file, &artifact).unwrap(); - - let flamegraph_generator = TestFlamegraphGenerator::default(); - - super::run_with_provider( - &artifact_path, - &provider, - &flamegraph_generator, - temp_dir.path(), - ) + super::run_with_provider(&artifact_path, &provider, &flamegraph_generator, temp_dir.path()) .expect("should run without errors"); - // Check that the flamegraph generator was called with the correct folded sorted lines - let calls_received = flamegraph_generator.lines_received.borrow().clone(); - - assert_eq!(calls_received, test_case.expected_folded_sorted_lines); - } + // Check that the output file was written to + let output_file = temp_dir.path().join("main_gates.svg"); + assert!(output_file.exists()); } } diff --git a/tooling/profiler/src/cli/mod.rs b/tooling/profiler/src/cli/mod.rs index d54a3f6167c..4c2503fbe4f 100644 --- a/tooling/profiler/src/cli/mod.rs +++ b/tooling/profiler/src/cli/mod.rs @@ -3,6 +3,7 @@ use color_eyre::eyre; use const_format::formatcp; mod gates_flamegraph_cmd; +mod opcodes_flamegraph_cmd; const PROFILER_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -12,20 +13,22 @@ static VERSION_STRING: &str = formatcp!("version = {}\n", PROFILER_VERSION,); #[command(name="Noir profiler", author, version=VERSION_STRING, about, long_about = None)] struct ProfilerCli { #[command(subcommand)] - command: GatesFlamegraphCommand, + command: ProfilerCommand, } #[non_exhaustive] #[derive(Subcommand, Clone, Debug)] -enum GatesFlamegraphCommand { +enum ProfilerCommand { GatesFlamegraph(gates_flamegraph_cmd::GatesFlamegraphCommand), + OpcodesFlamegraph(opcodes_flamegraph_cmd::OpcodesFlamegraphCommand), } pub(crate) fn start_cli() -> eyre::Result<()> { let ProfilerCli { command } = ProfilerCli::parse(); match command { - GatesFlamegraphCommand::GatesFlamegraph(args) => gates_flamegraph_cmd::run(args), + ProfilerCommand::GatesFlamegraph(args) => gates_flamegraph_cmd::run(args), + ProfilerCommand::OpcodesFlamegraph(args) => opcodes_flamegraph_cmd::run(args), } .map_err(|err| eyre::eyre!("{}", err))?; diff --git a/tooling/profiler/src/cli/opcodes_flamegraph_cmd.rs b/tooling/profiler/src/cli/opcodes_flamegraph_cmd.rs new file mode 100644 index 00000000000..e1dc1464f6f --- /dev/null +++ b/tooling/profiler/src/cli/opcodes_flamegraph_cmd.rs @@ -0,0 +1,123 @@ +use std::path::{Path, PathBuf}; + +use clap::Args; +use color_eyre::eyre::{self, Context}; + +use noirc_artifacts::debug::DebugArtifact; + +use crate::flamegraph::{FlamegraphGenerator, InfernoFlamegraphGenerator}; +use crate::fs::read_program_from_file; + +#[derive(Debug, Clone, Args)] +pub(crate) struct OpcodesFlamegraphCommand { + /// The path to the artifact JSON file + #[clap(long, short)] + artifact_path: String, + + /// The output folder for the flamegraph svg files + #[clap(long, short)] + output: String, +} + +pub(crate) fn run(args: OpcodesFlamegraphCommand) -> eyre::Result<()> { + run_with_generator( + &PathBuf::from(args.artifact_path), + &InfernoFlamegraphGenerator { count_name: "opcodes".to_string() }, + &PathBuf::from(args.output), + ) +} + +fn run_with_generator<Generator: FlamegraphGenerator>( + artifact_path: &Path, + flamegraph_generator: &Generator, + output_path: &Path, +) -> eyre::Result<()> { + let mut program = + read_program_from_file(artifact_path).context("Error reading program from file")?; + + let function_names = program.names.clone(); + + let bytecode = std::mem::take(&mut program.bytecode); + + let debug_artifact: DebugArtifact = program.into(); + + for (func_idx, (func_name, bytecode)) in + function_names.into_iter().zip(bytecode.functions).enumerate() + { + println!("Opcode count: {}", bytecode.opcodes.len()); + + flamegraph_generator.generate_flamegraph( + bytecode.opcodes.iter().map(|_op| 1).collect(), + bytecode.opcodes, + &debug_artifact.debug_symbols[func_idx], + &debug_artifact, + artifact_path.to_str().unwrap(), + &func_name, + &Path::new(&output_path).join(Path::new(&format!("{}_opcodes.svg", &func_name))), + )?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use acir::circuit::{Circuit, Opcode, Program}; + use color_eyre::eyre::{self}; + use fm::codespan_files::Files; + use noirc_artifacts::program::ProgramArtifact; + use noirc_errors::debug_info::{DebugInfo, ProgramDebugInfo}; + use std::{collections::BTreeMap, path::Path}; + + #[derive(Default)] + struct TestFlamegraphGenerator {} + + impl super::FlamegraphGenerator for TestFlamegraphGenerator { + fn generate_flamegraph<'files, F>( + &self, + _samples_per_opcode: Vec<usize>, + _opcodes: Vec<Opcode<F>>, + _debug_symbols: &DebugInfo, + _files: &'files impl Files<'files, FileId = fm::FileId>, + _artifact_name: &str, + _function_name: &str, + output_path: &Path, + ) -> eyre::Result<()> { + let output_file = std::fs::File::create(output_path).unwrap(); + std::io::Write::write_all(&mut std::io::BufWriter::new(output_file), b"success") + .unwrap(); + + Ok(()) + } + } + + #[test] + fn smoke_test() { + let temp_dir = tempfile::tempdir().unwrap(); + + let artifact_path = temp_dir.path().join("test.json"); + + let artifact = ProgramArtifact { + noir_version: "0.0.0".to_string(), + hash: 27, + abi: noirc_abi::Abi::default(), + bytecode: Program { functions: vec![Circuit::default()], ..Program::default() }, + debug_symbols: ProgramDebugInfo { debug_infos: vec![DebugInfo::default()] }, + file_map: BTreeMap::default(), + names: vec!["main".to_string()], + }; + + // Write the artifact to a file + let artifact_file = std::fs::File::create(&artifact_path).unwrap(); + serde_json::to_writer(artifact_file, &artifact).unwrap(); + + let flamegraph_generator = TestFlamegraphGenerator::default(); + + super::run_with_generator(&artifact_path, &flamegraph_generator, temp_dir.path()) + .expect("should run without errors"); + + // Check that the output file was written to + let output_file = temp_dir.path().join("main_opcodes.svg"); + assert!(output_file.exists()); + } +} diff --git a/tooling/profiler/src/flamegraph.rs b/tooling/profiler/src/flamegraph.rs new file mode 100644 index 00000000000..70de6e743b2 --- /dev/null +++ b/tooling/profiler/src/flamegraph.rs @@ -0,0 +1,300 @@ +use std::path::Path; +use std::{collections::BTreeMap, io::BufWriter}; + +use acir::circuit::{Opcode, OpcodeLocation}; +use color_eyre::eyre::{self}; +use fm::codespan_files::Files; +use inferno::flamegraph::{from_lines, Options}; +use noirc_errors::debug_info::DebugInfo; +use noirc_errors::reporter::line_and_column_from_span; +use noirc_errors::Location; + +use super::opcode_formatter::format_opcode; + +#[derive(Debug, Default)] +pub(crate) struct FoldedStackItem { + pub(crate) total_samples: usize, + pub(crate) nested_items: BTreeMap<String, FoldedStackItem>, +} + +pub(crate) trait FlamegraphGenerator { + #[allow(clippy::too_many_arguments)] + fn generate_flamegraph<'files, F>( + &self, + samples_per_opcode: Vec<usize>, + opcodes: Vec<Opcode<F>>, + debug_symbols: &DebugInfo, + files: &'files impl Files<'files, FileId = fm::FileId>, + artifact_name: &str, + function_name: &str, + output_path: &Path, + ) -> eyre::Result<()>; +} + +pub(crate) struct InfernoFlamegraphGenerator { + pub(crate) count_name: String, +} + +impl FlamegraphGenerator for InfernoFlamegraphGenerator { + fn generate_flamegraph<'files, F>( + &self, + samples_per_opcode: Vec<usize>, + opcodes: Vec<Opcode<F>>, + debug_symbols: &DebugInfo, + files: &'files impl Files<'files, FileId = fm::FileId>, + artifact_name: &str, + function_name: &str, + output_path: &Path, + ) -> eyre::Result<()> { + let folded_lines = + generate_folded_sorted_lines(samples_per_opcode, opcodes, debug_symbols, files); + + let flamegraph_file = std::fs::File::create(output_path)?; + let flamegraph_writer = BufWriter::new(flamegraph_file); + + let mut options = Options::default(); + options.hash = true; + options.deterministic = true; + options.title = format!("{}-{}", artifact_name, function_name); + options.frame_height = 24; + options.color_diffusion = true; + options.min_width = 0.0; + options.count_name = self.count_name.clone(); + + from_lines( + &mut options, + folded_lines.iter().map(|as_string| as_string.as_str()), + flamegraph_writer, + )?; + + Ok(()) + } +} + +fn generate_folded_sorted_lines<'files, F>( + samples_per_opcode: Vec<usize>, + opcodes: Vec<Opcode<F>>, + debug_symbols: &DebugInfo, + files: &'files impl Files<'files, FileId = fm::FileId>, +) -> Vec<String> { + // Create a nested hashmap with the stack items, folding the gates for all the callsites that are equal + let mut folded_stack_items = BTreeMap::new(); + + samples_per_opcode.into_iter().enumerate().for_each(|(opcode_index, gates)| { + let call_stack = debug_symbols.locations.get(&OpcodeLocation::Acir(opcode_index)); + let location_names = if let Some(call_stack) = call_stack { + call_stack + .iter() + .map(|location| location_to_callsite_label(*location, files)) + .chain(std::iter::once(format_opcode(&opcodes[opcode_index]))) + .collect::<Vec<String>>() + } else { + vec!["unknown".to_string()] + }; + + add_locations_to_folded_stack_items(&mut folded_stack_items, location_names, gates); + }); + + to_folded_sorted_lines(&folded_stack_items, Default::default()) +} + +fn location_to_callsite_label<'files>( + location: Location, + files: &'files impl Files<'files, FileId = fm::FileId>, +) -> String { + let filename = + Path::new(&files.name(location.file).expect("should have a file path").to_string()) + .file_name() + .map(|os_str| os_str.to_string_lossy().to_string()) + .unwrap_or("invalid_path".to_string()); + let source = files.source(location.file).expect("should have a file source"); + + let code_slice = source + .as_ref() + .chars() + .skip(location.span.start() as usize) + .take(location.span.end() as usize - location.span.start() as usize) + .collect::<String>(); + + // ";" is used for frame separation, and is not allowed by inferno + // Check code slice for ";" and replace it with 'GREEK QUESTION MARK' (U+037E) + let code_slice = code_slice.replace(';', "\u{037E}"); + + let (line, column) = line_and_column_from_span(source.as_ref(), &location.span); + + format!("{}:{}:{}::{}", filename, line, column, code_slice) +} + +fn add_locations_to_folded_stack_items( + stack_items: &mut BTreeMap<String, FoldedStackItem>, + locations: Vec<String>, + gates: usize, +) { + let mut child_map = stack_items; + for (index, location) in locations.iter().enumerate() { + let current_item = child_map.entry(location.clone()).or_default(); + + child_map = &mut current_item.nested_items; + + if index == locations.len() - 1 { + current_item.total_samples += gates; + } + } +} + +/// Creates a vector of lines in the format that inferno expects from a nested hashmap of stack items +/// The lines have to be sorted in the following way, exploring the graph in a depth-first manner: +/// main 100 +/// main::foo 0 +/// main::foo::bar 200 +/// main::baz 27 +/// main::baz::qux 800 +fn to_folded_sorted_lines( + folded_stack_items: &BTreeMap<String, FoldedStackItem>, + parent_stacks: im::Vector<String>, +) -> Vec<String> { + let mut result_vector = Vec::with_capacity(folded_stack_items.len()); + + for (location, folded_stack_item) in folded_stack_items.iter() { + if folded_stack_item.total_samples > 0 { + let frame_list: Vec<String> = + parent_stacks.iter().cloned().chain(std::iter::once(location.clone())).collect(); + let line: String = + format!("{} {}", frame_list.join(";"), folded_stack_item.total_samples); + + result_vector.push(line); + }; + + let mut new_parent_stacks = parent_stacks.clone(); + new_parent_stacks.push_back(location.clone()); + let child_lines = + to_folded_sorted_lines(&folded_stack_item.nested_items, new_parent_stacks); + + result_vector.extend(child_lines); + } + + result_vector +} + +#[cfg(test)] +mod tests { + use acir::{ + circuit::{opcodes::BlockId, Opcode, OpcodeLocation}, + native_types::Expression, + FieldElement, + }; + use fm::FileManager; + use noirc_errors::{debug_info::DebugInfo, Location, Span}; + use std::{collections::BTreeMap, path::Path}; + + use super::generate_folded_sorted_lines; + + fn find_spans_for(source: &str, needle: &str) -> Vec<Span> { + let mut spans = Vec::new(); + let mut start = 0; + while let Some(start_idx) = source[start..].find(needle) { + let start_idx = start + start_idx; + let end_idx = start_idx + needle.len(); + spans.push(Span::inclusive(start_idx as u32, end_idx as u32 - 1)); + start = end_idx; + } + spans + } + + #[test] + fn simple_test_case() { + let source_code = r##" + fn main() { + foo(); + bar(); + whatever(); + } + fn foo() { + baz(); + } + fn bar () { + whatever() + } + fn baz () { + whatever() + } + "##; + + let source_file_name = Path::new("main.nr"); + let temp_dir = tempfile::tempdir().unwrap(); + + let mut fm = FileManager::new(temp_dir.path()); + let file_id = fm.add_file_with_source(source_file_name, source_code.to_string()).unwrap(); + + let main_declaration_location = + Location::new(find_spans_for(source_code, "fn main()")[0], file_id); + let main_foo_call_location = + Location::new(find_spans_for(source_code, "foo()")[0], file_id); + let main_bar_call_location = + Location::new(find_spans_for(source_code, "bar()")[0], file_id); + let main_whatever_call_location = + Location::new(find_spans_for(source_code, "whatever()")[0], file_id); + let foo_baz_call_location = Location::new(find_spans_for(source_code, "baz()")[0], file_id); + let bar_whatever_call_location = + Location::new(find_spans_for(source_code, "whatever()")[1], file_id); + let baz_whatever_call_location = + Location::new(find_spans_for(source_code, "whatever()")[2], file_id); + + let mut opcode_locations = BTreeMap::<OpcodeLocation, Vec<Location>>::new(); + // main::foo::baz::whatever + opcode_locations.insert( + OpcodeLocation::Acir(0), + vec![ + main_declaration_location, + main_foo_call_location, + foo_baz_call_location, + baz_whatever_call_location, + ], + ); + + // main::bar::whatever + opcode_locations.insert( + OpcodeLocation::Acir(1), + vec![main_declaration_location, main_bar_call_location, bar_whatever_call_location], + ); + // main::whatever + opcode_locations.insert( + OpcodeLocation::Acir(2), + vec![main_declaration_location, main_whatever_call_location], + ); + + let debug_info = DebugInfo::new( + opcode_locations, + BTreeMap::default(), + BTreeMap::default(), + BTreeMap::default(), + ); + + let samples_per_opcode = vec![10, 20, 30]; + + let expected_folded_sorted_lines = vec![ + "main.nr:2:9::fn main();main.nr:3:13::foo();main.nr:8:13::baz();main.nr:14:13::whatever();opcode::arithmetic 10".to_string(), + "main.nr:2:9::fn main();main.nr:4:13::bar();main.nr:11:13::whatever();opcode::arithmetic 20".to_string(), + "main.nr:2:9::fn main();main.nr:5:13::whatever();opcode::memory::init 30".to_string(), + ]; + + let opcodes: Vec<Opcode<FieldElement>> = vec![ + Opcode::AssertZero(Expression::default()), + Opcode::AssertZero(Expression::default()), + Opcode::MemoryInit { + block_id: BlockId(0), + init: vec![], + block_type: acir::circuit::opcodes::BlockType::Memory, + }, + ]; + + let actual_folded_sorted_lines = generate_folded_sorted_lines( + samples_per_opcode, + opcodes, + &debug_info, + fm.as_file_map(), + ); + + assert_eq!(expected_folded_sorted_lines, actual_folded_sorted_lines); + } +} diff --git a/tooling/profiler/src/fs.rs b/tooling/profiler/src/fs.rs new file mode 100644 index 00000000000..e8eec2cbb14 --- /dev/null +++ b/tooling/profiler/src/fs.rs @@ -0,0 +1,15 @@ +use std::path::Path; + +use color_eyre::eyre; +use noirc_artifacts::program::ProgramArtifact; + +pub(crate) fn read_program_from_file<P: AsRef<Path>>( + circuit_path: P, +) -> eyre::Result<ProgramArtifact> { + let file_path = circuit_path.as_ref().with_extension("json"); + + let input_string = std::fs::read(file_path)?; + let program = serde_json::from_slice(&input_string)?; + + Ok(program) +} diff --git a/tooling/profiler/src/gates_provider.rs b/tooling/profiler/src/gates_provider.rs new file mode 100644 index 00000000000..caed2666426 --- /dev/null +++ b/tooling/profiler/src/gates_provider.rs @@ -0,0 +1,37 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; + +use color_eyre::eyre::{self}; +use serde::{Deserialize, Serialize}; + +pub(crate) trait GatesProvider { + fn get_gates(&self, artifact_path: &Path) -> eyre::Result<BackendGatesResponse>; +} + +pub(crate) struct BackendGatesProvider { + pub(crate) backend_path: PathBuf, +} + +impl GatesProvider for BackendGatesProvider { + fn get_gates(&self, artifact_path: &Path) -> eyre::Result<BackendGatesResponse> { + let backend_gates_response = + Command::new(&self.backend_path).arg("gates").arg("-b").arg(artifact_path).output()?; + + // Parse the backend gates command stdout as json + let backend_gates_response: BackendGatesResponse = + serde_json::from_slice(&backend_gates_response.stdout)?; + Ok(backend_gates_response) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct BackendGatesReport { + pub(crate) acir_opcodes: usize, + pub(crate) circuit_size: usize, + pub(crate) gates_per_opcode: Vec<usize>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct BackendGatesResponse { + pub(crate) functions: Vec<BackendGatesReport>, +} diff --git a/tooling/profiler/src/main.rs b/tooling/profiler/src/main.rs index 8e08644de23..215feb0a4e7 100644 --- a/tooling/profiler/src/main.rs +++ b/tooling/profiler/src/main.rs @@ -4,6 +4,10 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies, unused_extern_crates))] mod cli; +mod flamegraph; +mod fs; +mod gates_provider; +mod opcode_formatter; use std::env; diff --git a/tooling/profiler/src/opcode_formatter.rs b/tooling/profiler/src/opcode_formatter.rs new file mode 100644 index 00000000000..aba92c95d85 --- /dev/null +++ b/tooling/profiler/src/opcode_formatter.rs @@ -0,0 +1,53 @@ +use acir::circuit::{directives::Directive, opcodes::BlackBoxFuncCall, Opcode}; + +fn format_blackbox_function(call: &BlackBoxFuncCall) -> String { + match call { + BlackBoxFuncCall::AES128Encrypt { .. } => "aes128_encrypt".to_string(), + BlackBoxFuncCall::AND { .. } => "and".to_string(), + BlackBoxFuncCall::XOR { .. } => "xor".to_string(), + BlackBoxFuncCall::RANGE { .. } => "range".to_string(), + BlackBoxFuncCall::SHA256 { .. } => "sha256".to_string(), + BlackBoxFuncCall::Blake2s { .. } => "blake2s".to_string(), + BlackBoxFuncCall::Blake3 { .. } => "blake3".to_string(), + BlackBoxFuncCall::SchnorrVerify { .. } => "schnorr_verify".to_string(), + BlackBoxFuncCall::PedersenCommitment { .. } => "pedersen_commitment".to_string(), + BlackBoxFuncCall::PedersenHash { .. } => "pedersen_hash".to_string(), + BlackBoxFuncCall::EcdsaSecp256k1 { .. } => "ecdsa_secp256k1".to_string(), + BlackBoxFuncCall::EcdsaSecp256r1 { .. } => "ecdsa_secp256r1".to_string(), + BlackBoxFuncCall::MultiScalarMul { .. } => "multi_scalar_mul".to_string(), + BlackBoxFuncCall::EmbeddedCurveAdd { .. } => "embedded_curve_add".to_string(), + BlackBoxFuncCall::Keccak256 { .. } => "keccak256".to_string(), + BlackBoxFuncCall::Keccakf1600 { .. } => "keccakf1600".to_string(), + BlackBoxFuncCall::RecursiveAggregation { .. } => "recursive_aggregation".to_string(), + BlackBoxFuncCall::BigIntAdd { .. } => "big_int_add".to_string(), + BlackBoxFuncCall::BigIntSub { .. } => "big_int_sub".to_string(), + BlackBoxFuncCall::BigIntMul { .. } => "big_int_mul".to_string(), + BlackBoxFuncCall::BigIntDiv { .. } => "big_int_div".to_string(), + BlackBoxFuncCall::BigIntFromLeBytes { .. } => "big_int_from_le_bytes".to_string(), + BlackBoxFuncCall::BigIntToLeBytes { .. } => "big_int_to_le_bytes".to_string(), + BlackBoxFuncCall::Poseidon2Permutation { .. } => "poseidon2_permutation".to_string(), + BlackBoxFuncCall::Sha256Compression { .. } => "sha256_compression".to_string(), + } +} + +fn format_directive_kind<F>(directive: &Directive<F>) -> String { + match directive { + Directive::ToLeRadix { .. } => "to_le_radix".to_string(), + } +} + +fn format_opcode_kind<F>(opcode: &Opcode<F>) -> String { + match opcode { + Opcode::AssertZero(_) => "arithmetic".to_string(), + Opcode::BlackBoxFuncCall(call) => format!("blackbox::{}", format_blackbox_function(call)), + Opcode::MemoryOp { .. } => "memory::op".to_string(), + Opcode::MemoryInit { .. } => "memory::init".to_string(), + Opcode::Directive(directive) => format!("directive::{}", format_directive_kind(directive)), + Opcode::BrilligCall { .. } => "brillig_call".to_string(), + Opcode::Call { .. } => "acir_call".to_string(), + } +} + +pub(crate) fn format_opcode<F>(opcode: &Opcode<F>) -> String { + format!("opcode::{}", format_opcode_kind(opcode)) +}