diff --git a/Cargo.lock b/Cargo.lock index 0c0faa343c..64b1a125bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2853,7 +2853,7 @@ version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ - "encode_unicode", + "encode_unicode 0.3.6", "lazy_static", "libc", "unicode-width", @@ -3127,6 +3127,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + [[package]] name = "ctor" version = "0.2.6" @@ -3880,6 +3901,12 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -9151,6 +9178,20 @@ dependencies = [ "syn 2.0.55", ] +[[package]] +name = "prettytable-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" +dependencies = [ + "csv", + "encode_unicode 1.0.0", + "is-terminal", + "lazy_static", + "term", + "unicode-width", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -11090,6 +11131,7 @@ dependencies = [ "katana-runner", "notify", "notify-debouncer-mini", + "prettytable-rs", "scarb", "scarb-ui", "semver 1.0.22", diff --git a/bin/sozo/Cargo.toml b/bin/sozo/Cargo.toml index 254734d9e3..cb3d0f0def 100644 --- a/bin/sozo/Cargo.toml +++ b/bin/sozo/Cargo.toml @@ -6,6 +6,7 @@ version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +prettytable-rs = "0.10.0" anyhow.workspace = true async-trait.workspace = true cairo-lang-compiler.workspace = true diff --git a/bin/sozo/src/commands/build.rs b/bin/sozo/src/commands/build.rs index f56ba04e51..075268c40d 100644 --- a/bin/sozo/src/commands/build.rs +++ b/bin/sozo/src/commands/build.rs @@ -1,9 +1,12 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use clap::Args; use dojo_bindgen::{BuiltinPlugins, PluginManager}; use dojo_lang::scarb_internal::compile_workspace; +use prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE; +use prettytable::{format, Cell, Row, Table}; use scarb::core::{Config, TargetKind}; use scarb::ops::CompileOpts; +use sozo_ops::statistics::{get_contract_statistics_for_dir, ContractStatistics}; #[derive(Debug, Args)] pub struct BuildArgs { @@ -22,6 +25,9 @@ pub struct BuildArgs { #[arg(long)] #[arg(help = "Output directory.", default_value = "bindings")] pub bindings_output: String, + + #[arg(long, help = "Display statistics about the compiled contracts")] + pub stats: bool, } impl BuildArgs { @@ -44,6 +50,14 @@ impl BuildArgs { builtin_plugins.push(BuiltinPlugins::Unity); } + if self.stats { + let target_dir = &compile_info.target_dir; + let contracts_statistics = get_contract_statistics_for_dir(target_dir) + .context(format!("Error getting contracts stats"))?; + let table = create_stats_table(contracts_statistics); + table.printstd() + } + // Custom plugins are always empty for now. let bindgen = PluginManager { profile_name: compile_info.profile_name, @@ -65,11 +79,41 @@ impl BuildArgs { } } +fn create_stats_table(contracts_statistics: Vec) -> Table { + let mut table = Table::new(); + table.set_format(*FORMAT_NO_LINESEP_WITH_TITLE); + + // Add table headers + table.set_titles(Row::new(vec![ + Cell::new_align("Contract", format::Alignment::CENTER), + Cell::new_align("Bytecode size (felts)", format::Alignment::CENTER), + Cell::new_align("Class size (bytes)", format::Alignment::CENTER), + ])); + + for contract_stats in contracts_statistics { + // Add table rows + let contract_name = contract_stats.contract_name; + let number_felts = contract_stats.number_felts; + let file_size = contract_stats.file_size; + + table.add_row(Row::new(vec![ + Cell::new_align(&contract_name, format::Alignment::LEFT), + Cell::new_align(format!("{}", number_felts).as_str(), format::Alignment::RIGHT), + Cell::new_align(format!("{}", file_size).as_str(), format::Alignment::RIGHT), + ])); + } + + table +} + #[cfg(test)] mod tests { use dojo_test_utils::compiler::build_test_config; + use prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE; + use prettytable::{format, Cell, Row, Table}; + use sozo_ops::statistics::ContractStatistics; - use super::BuildArgs; + use super::{create_stats_table, BuildArgs}; #[test] fn build_example_with_typescript_and_unity_bindings() { @@ -80,8 +124,60 @@ mod tests { typescript: true, unity: true, typescript_v2: true, + stats: true, }; let result = build_args.run(&config); assert!(result.is_ok()); } + + #[test] + fn test_create_stats_table() { + // Arrange + let contracts_statistics = vec![ + ContractStatistics { + contract_name: "Test1".to_string(), + number_felts: 33, + file_size: 33, + }, + ContractStatistics { + contract_name: "Test2".to_string(), + number_felts: 43, + file_size: 24, + }, + ContractStatistics { + contract_name: "Test3".to_string(), + number_felts: 36, + file_size: 12, + }, + ]; + + let mut expected_table = Table::new(); + expected_table.set_format(*FORMAT_NO_LINESEP_WITH_TITLE); + expected_table.set_titles(Row::new(vec![ + Cell::new_align("Contract", format::Alignment::CENTER), + Cell::new_align("Bytecode size (felts)", format::Alignment::CENTER), + Cell::new_align("Class size (bytes)", format::Alignment::CENTER), + ])); + expected_table.add_row(Row::new(vec![ + Cell::new_align("Test1", format::Alignment::LEFT), + Cell::new_align(format!("{}", 33).as_str(), format::Alignment::RIGHT), + Cell::new_align(format!("{}", 33).as_str(), format::Alignment::RIGHT), + ])); + expected_table.add_row(Row::new(vec![ + Cell::new_align("Test2", format::Alignment::LEFT), + Cell::new_align(format!("{}", 43).as_str(), format::Alignment::RIGHT), + Cell::new_align(format!("{}", 24).as_str(), format::Alignment::RIGHT), + ])); + expected_table.add_row(Row::new(vec![ + Cell::new_align("Test3", format::Alignment::LEFT), + Cell::new_align(format!("{}", 36).as_str(), format::Alignment::RIGHT), + Cell::new_align(format!("{}", 12).as_str(), format::Alignment::RIGHT), + ])); + + // Act + let table = create_stats_table(contracts_statistics); + + // Assert + assert_eq!(table, expected_table, "Tables mismatch") + } } diff --git a/bin/sozo/tests/test_data/compiled_contracts/test_contract.json b/bin/sozo/tests/test_data/compiled_contracts/test_contract.json new file mode 120000 index 0000000000..c7a135aa79 --- /dev/null +++ b/bin/sozo/tests/test_data/compiled_contracts/test_contract.json @@ -0,0 +1 @@ +../../../../../crates/katana/contracts/compiled/cairo1_contract.json \ No newline at end of file diff --git a/crates/sozo/ops/src/lib.rs b/crates/sozo/ops/src/lib.rs index 3d1b69ce11..96e366bdf2 100644 --- a/crates/sozo/ops/src/lib.rs +++ b/crates/sozo/ops/src/lib.rs @@ -5,6 +5,7 @@ pub mod execute; pub mod migration; pub mod model; pub mod register; +pub mod statistics; pub mod utils; #[cfg(test)] diff --git a/crates/sozo/ops/src/statistics.rs b/crates/sozo/ops/src/statistics.rs new file mode 100644 index 0000000000..741cd61dee --- /dev/null +++ b/crates/sozo/ops/src/statistics.rs @@ -0,0 +1,166 @@ +use std::fs::{self, File}; +use std::io::{self, BufReader}; +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use camino::Utf8PathBuf; +use starknet::core::types::contract::SierraClass; +use starknet::core::types::FlattenedSierraClass; + +#[derive(Debug, PartialEq)] +pub struct ContractStatistics { + pub contract_name: String, + pub number_felts: u64, + pub file_size: u64, +} + +fn read_sierra_json_program(file: &File) -> Result { + let contract_artifact: SierraClass = serde_json::from_reader(BufReader::new(file))?; + let contract_artifact: FlattenedSierraClass = contract_artifact.flatten()?; + + Ok(contract_artifact) +} + +fn get_sierra_byte_code_size(contract_artifact: FlattenedSierraClass) -> u64 { + contract_artifact.sierra_program.len() as u64 +} + +fn get_file_size(file: &File) -> Result { + file.metadata().map(|metadata| metadata.len()) +} + +fn get_contract_statistics_for_file( + contract_name: String, + sierra_json_file: File, + contract_artifact: FlattenedSierraClass, +) -> Result { + let file_size = get_file_size(&sierra_json_file).context(format!("Error getting file size"))?; + let number_felts = get_sierra_byte_code_size(contract_artifact); + Ok(ContractStatistics { file_size, contract_name, number_felts }) +} + +pub fn get_contract_statistics_for_dir( + target_directory: &Utf8PathBuf, +) -> Result> { + let mut contract_statistics = Vec::new(); + let target_directory = target_directory.as_str(); + let dir: fs::ReadDir = fs::read_dir(target_directory)?; + for entry in dir { + let path: PathBuf = entry?.path(); + + if path.is_dir() { + continue; + } + + let contract_name: String = + path.file_stem().context("Error getting file name")?.to_string_lossy().to_string(); + + let sierra_json_file: File = + File::open(&path).context(format!("Error opening file: {}", path.to_string_lossy()))?; + + let contract_artifact: FlattenedSierraClass = read_sierra_json_program(&sierra_json_file) + .context(format!( + "Error parsing Sierra class artifact: {}", + path.to_string_lossy() + ))?; + + contract_statistics.push(get_contract_statistics_for_file( + contract_name, + sierra_json_file, + contract_artifact, + )?); + } + Ok(contract_statistics) +} + +#[cfg(test)] +mod tests { + use std::fs::File; + use std::path::Path; + + use camino::Utf8PathBuf; + + use super::{ + get_contract_statistics_for_dir, get_contract_statistics_for_file, get_file_size, + get_sierra_byte_code_size, read_sierra_json_program, ContractStatistics, + }; + + const TEST_SIERRA_JSON_CONTRACT: &str = + "../../../bin/sozo/tests/test_data/compiled_contracts/test_contract.json"; + const TEST_SIERRA_FOLDER_CONTRACTS: &str = + "../../../bin/sozo/tests/test_data/compiled_contracts/"; + + #[test] + fn get_sierra_byte_code_size_returns_correct_size() { + let sierra_json_file = File::open(TEST_SIERRA_JSON_CONTRACT) + .unwrap_or_else(|err| panic!("Failed to open file: {}", err)); + let flattened_sierra_class = read_sierra_json_program(&sierra_json_file) + .unwrap_or_else(|err| panic!("Failed to read JSON program: {}", err)); + const EXPECTED_NUMBER_OF_FELTS: u64 = 2175; + + let number_of_felts = get_sierra_byte_code_size(flattened_sierra_class); + + assert_eq!( + number_of_felts, EXPECTED_NUMBER_OF_FELTS, + "Number of felts mismatch. Expected {}, got {}", + EXPECTED_NUMBER_OF_FELTS, number_of_felts + ); + } + + #[test] + fn get_contract_statistics_for_file_returns_correct_statistics() { + let sierra_json_file = File::open(TEST_SIERRA_JSON_CONTRACT) + .unwrap_or_else(|err| panic!("Failed to open file: {}", err)); + let contract_artifact = read_sierra_json_program(&sierra_json_file) + .unwrap_or_else(|err| panic!("Failed to read JSON program: {}", err)); + let filename = Path::new(TEST_SIERRA_JSON_CONTRACT) + .file_stem() + .expect("Error getting file name") + .to_string_lossy() + .to_string(); + let expected_contract_statistics: ContractStatistics = ContractStatistics { + contract_name: String::from("test_contract"), + number_felts: 2175, + file_size: 114925, + }; + + let statistics = + get_contract_statistics_for_file(filename.clone(), sierra_json_file, contract_artifact) + .expect("Error getting contract statistics for file"); + + assert_eq!(statistics, expected_contract_statistics); + } + + #[test] + fn get_contract_statistics_for_dir_returns_correct_statistics() { + let target_dir = Utf8PathBuf::from(TEST_SIERRA_FOLDER_CONTRACTS); + + let contract_statistics = get_contract_statistics_for_dir(&target_dir) + .expect(format!("Error getting contracts in dir {target_dir}").as_str()); + + assert_eq!(contract_statistics.len(), 1, "Mismatch number of contract statistics"); + } + + #[test] + fn get_file_size_returns_correct_size() { + let sierra_json_file = File::open(TEST_SIERRA_JSON_CONTRACT) + .unwrap_or_else(|err| panic!("Failed to open test file: {}", err)); + const EXPECTED_SIZE: u64 = 114925; + + let file_size = get_file_size(&sierra_json_file) + .expect(format!("Error getting file size for test file").as_str()); + + assert_eq!(file_size, EXPECTED_SIZE, "File size mismatch"); + } + + #[test] + fn read_sierra_json_program_returns_ok_when_successful() { + // Arrange + let sierra_json_file = File::open(TEST_SIERRA_JSON_CONTRACT) + .unwrap_or_else(|err| panic!("Failed to open test file: {}", err)); + + let result = read_sierra_json_program(&sierra_json_file); + + assert!(result.is_ok(), "Expected Ok result"); + } +}