diff --git a/Cargo.lock b/Cargo.lock index 0d2d424463b..37ac8ec619a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7675,6 +7675,7 @@ dependencies = [ "peg", "prettydiff 0.7.0", "rustc-hash 1.1.0", + "serde", "slotmap", "sway-features", "sway-ir-macros", diff --git a/forc-pkg/Cargo.toml b/forc-pkg/Cargo.toml index 7b1bc68587b..5735bbc05a6 100644 --- a/forc-pkg/Cargo.toml +++ b/forc-pkg/Cargo.toml @@ -45,3 +45,4 @@ regex = "^1.10.2" [target.'cfg(not(target_os = "macos"))'.dependencies] sysinfo = "0.29" + diff --git a/forc-pkg/src/manifest/build_profile.rs b/forc-pkg/src/manifest/build_profile.rs index af5822ec693..a196cd51068 100644 --- a/forc-pkg/src/manifest/build_profile.rs +++ b/forc-pkg/src/manifest/build_profile.rs @@ -24,6 +24,8 @@ pub struct BuildProfile { #[serde(default)] pub time_phases: bool, #[serde(default)] + pub profile: bool, + #[serde(default)] pub metrics_outfile: Option, #[serde(default)] pub include_tests: bool, @@ -52,6 +54,7 @@ impl BuildProfile { print_bytecode_spans: false, terse: false, time_phases: false, + profile: false, metrics_outfile: None, include_tests: false, error_on_warnings: false, @@ -72,6 +75,7 @@ impl BuildProfile { print_bytecode_spans: false, terse: false, time_phases: false, + profile: false, metrics_outfile: None, include_tests: false, error_on_warnings: false, @@ -140,6 +144,7 @@ mod tests { print_bytecode_spans: false, terse: true, time_phases: true, + profile: false, metrics_outfile: Some("metrics_outfile".into()), include_tests: true, error_on_warnings: true, diff --git a/forc-pkg/src/pkg.rs b/forc-pkg/src/pkg.rs index 50d8b24c703..fc510979ba4 100644 --- a/forc-pkg/src/pkg.rs +++ b/forc-pkg/src/pkg.rs @@ -300,6 +300,8 @@ pub struct BuildOpts { pub release: bool, /// Output the time elapsed over each part of the compilation process. pub time_phases: bool, + /// Profile the build process. + pub profile: bool, /// If set, outputs compilation metrics info in JSON format. pub metrics_outfile: Option, /// Warnings must be treated as compiler errors. @@ -1561,6 +1563,7 @@ pub fn sway_build_config( .with_print_ir(build_profile.print_ir.clone()) .with_include_tests(build_profile.include_tests) .with_time_phases(build_profile.time_phases) + .with_profile(build_profile.profile) .with_metrics(build_profile.metrics_outfile.clone()) .with_optimization_level(build_profile.optimization_level); Ok(build_config) @@ -1780,6 +1783,7 @@ pub fn compile( // First, compile to an AST. We'll update the namespace and check for JSON ABI output. let ast_res = time_expr!( + pkg.name, "compile to ast", "compile_to_ast", sway_core::compile_to_ast( @@ -1819,6 +1823,7 @@ pub fn compile( } let asm_res = time_expr!( + pkg.name, "compile ast to asm", "compile_ast_to_asm", sway_core::ast_to_asm( @@ -1839,6 +1844,7 @@ pub fn compile( let mut program_abi = match pkg.target { BuildTarget::Fuel => { let program_abi_res = time_expr!( + pkg.name, "generate JSON ABI program", "generate_json_abi", fuel_abi::generate_program_abi( @@ -1877,6 +1883,7 @@ pub fn compile( }; let abi = time_expr!( + pkg.name, "generate JSON ABI program", "generate_json_abi", evm_abi::generate_abi_program(typed_program, engines), @@ -1899,15 +1906,22 @@ pub fn compile( .map(|finalized_entry| PkgEntry::from_finalized_entry(finalized_entry, engines)) .collect::>()?; - let asm = match asm_res { + let mut asm = match asm_res { Err(_) => return fail(handler), Ok(asm) => asm, }; let bc_res = time_expr!( + pkg.name, "compile asm to bytecode", "compile_asm_to_bytecode", - sway_core::asm_to_bytecode(&handler, asm, source_map, engines.se(), &sway_build_config), + sway_core::asm_to_bytecode( + &handler, + &mut asm, + source_map, + engines.se(), + &sway_build_config + ), Some(sway_build_config.clone()), metrics ); @@ -1970,9 +1984,82 @@ pub fn compile( warnings, metrics, }; + if sway_build_config.profile { + report_assembly_information(&asm, &compiled_package); + } + Ok(compiled_package) } +/// Reports assembly information for a compiled package to an external `dyno` process through `stdout`. +fn report_assembly_information( + compiled_asm: &sway_core::CompiledAsm, + compiled_package: &CompiledPackage, +) { + // Get the bytes of the compiled package. + let mut bytes = compiled_package.bytecode.bytes.clone(); + + // Attempt to get the data section offset out of the compiled package bytes. + let data_offset = u64::from_be_bytes( + bytes + .iter() + .skip(8) + .take(8) + .cloned() + .collect::>() + .try_into() + .unwrap(), + ); + let data_section_size = bytes.len() as u64 - data_offset; + + // Remove the data section from the compiled package bytes. + bytes.truncate(data_offset as usize); + + // Calculate the unpadded size of each data section section. + // Implementation based directly on `sway_core::asm_generation::Entry::to_bytes`, referenced here: + // https://github.com/FuelLabs/sway/blob/afd6a6709e7cb11c676059a5004012cc466e653b/sway-core/src/asm_generation/fuel/data_section.rs#L147 + fn calculate_entry_size(entry: &sway_core::asm_generation::Entry) -> u64 { + match &entry.value { + sway_core::asm_generation::Datum::Byte(value) => std::mem::size_of_val(value) as u64, + + sway_core::asm_generation::Datum::Word(value) => std::mem::size_of_val(value) as u64, + + sway_core::asm_generation::Datum::ByteArray(bytes) + | sway_core::asm_generation::Datum::Slice(bytes) => { + if bytes.len() % 8 == 0 { + bytes.len() as u64 + } else { + ((bytes.len() + 7) & 0xfffffff8_usize) as u64 + } + } + + sway_core::asm_generation::Datum::Collection(items) => { + items.iter().map(calculate_entry_size).sum() + } + } + } + + // Merge the data section entry slots together for usage calculation. + let mut data_section_entries: Vec<_> = compiled_asm.0.data_section.non_configurables.clone(); + data_section_entries.extend(compiled_asm.0.data_section.configurables.clone()); + + // Compute the assembly information to be reported. + let asm_information = sway_core::asm_generation::AsmInformation { + bytecode_size: bytes.len() as _, + data_section: sway_core::asm_generation::DataSectionInformation { + size: data_section_size, + used: data_section_entries.iter().map(calculate_entry_size).sum(), + value_pairs: data_section_entries.clone(), + }, + }; + + // Report the assembly information to the `dyno` process through `stdout`. + println!( + "/dyno info {}", + serde_json::to_string(&asm_information).unwrap() + ); +} + impl PkgEntry { /// Returns whether this `PkgEntry` corresponds to a test. pub fn is_test(&self) -> bool { @@ -2075,6 +2162,7 @@ fn build_profile_from_opts( pkg, print, time_phases, + profile: profile_opt, build_profile, release, metrics_outfile, @@ -2115,6 +2203,7 @@ fn build_profile_from_opts( profile.print_bytecode_spans |= print.bytecode_spans; profile.terse |= pkg.terse; profile.time_phases |= time_phases; + profile.profile |= profile_opt; if profile.metrics_outfile.is_none() { profile.metrics_outfile.clone_from(metrics_outfile); } diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index ace89c3ddae..2d5ce4770d0 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -913,6 +913,7 @@ fn build_opts_from_cmd(cmd: &cmd::Deploy, member_filter: pkg::MemberFilter) -> p reverse_order: cmd.print.reverse_order, }, time_phases: cmd.print.time_phases, + profile: cmd.print.profile, metrics_outfile: cmd.print.metrics_outfile.clone(), minify: pkg::MinifyOpts { json_abi: cmd.minify.json_abi, diff --git a/forc-plugins/forc-client/src/op/run/mod.rs b/forc-plugins/forc-client/src/op/run/mod.rs index debf0579c19..7d21d439e8c 100644 --- a/forc-plugins/forc-client/src/op/run/mod.rs +++ b/forc-plugins/forc-client/src/op/run/mod.rs @@ -267,6 +267,7 @@ fn build_opts_from_cmd(cmd: &cmd::Run) -> pkg::BuildOpts { release: cmd.build_profile.release, error_on_warnings: cmd.build_profile.error_on_warnings, time_phases: cmd.print.time_phases, + profile: cmd.print.profile, metrics_outfile: cmd.print.metrics_outfile.clone(), binary_outfile: cmd.build_output.bin_file.clone(), debug_outfile: cmd.build_output.debug_file.clone(), diff --git a/forc-test/src/lib.rs b/forc-test/src/lib.rs index c7bd19e95fd..bccc4cc0209 100644 --- a/forc-test/src/lib.rs +++ b/forc-test/src/lib.rs @@ -151,6 +151,8 @@ pub struct TestOpts { pub error_on_warnings: bool, /// Output the time elapsed over each part of the compilation process. pub time_phases: bool, + /// Profile the compilation process. + pub profile: bool, /// Output compilation metrics into file. pub metrics_outfile: Option, /// Set of enabled experimental flags @@ -453,6 +455,7 @@ impl From for pkg::BuildOpts { release: val.release, error_on_warnings: val.error_on_warnings, time_phases: val.time_phases, + profile: val.profile, metrics_outfile: val.metrics_outfile, tests: true, member_filter: Default::default(), @@ -476,6 +479,7 @@ impl TestOpts { release: self.release, error_on_warnings: self.error_on_warnings, time_phases: self.time_phases, + profile: self.profile, metrics_outfile: self.metrics_outfile, tests: true, member_filter: Default::default(), diff --git a/forc/src/cli/commands/test.rs b/forc/src/cli/commands/test.rs index d556af6b790..96c9cb02f2b 100644 --- a/forc/src/cli/commands/test.rs +++ b/forc/src/cli/commands/test.rs @@ -243,6 +243,7 @@ fn opts_from_cmd(cmd: Command) -> forc_test::TestOpts { reverse_order: cmd.build.print.reverse_order, }, time_phases: cmd.build.print.time_phases, + profile: cmd.build.print.profile, metrics_outfile: cmd.build.print.metrics_outfile, minify: pkg::MinifyOpts { json_abi: cmd.build.minify.json_abi, diff --git a/forc/src/cli/shared.rs b/forc/src/cli/shared.rs index 28b38465df3..23ccdd59cb9 100644 --- a/forc/src/cli/shared.rs +++ b/forc/src/cli/shared.rs @@ -99,6 +99,9 @@ pub struct Print { /// Output the time elapsed over each part of the compilation process. #[clap(long)] pub time_phases: bool, + /// Profile the compilation process. + #[clap(long)] + pub profile: bool, /// Output build errors and warnings in reverse order. #[clap(long)] pub reverse_order: bool, diff --git a/forc/src/ops/forc_build.rs b/forc/src/ops/forc_build.rs index 1118bdb5d54..2ec40cca931 100644 --- a/forc/src/ops/forc_build.rs +++ b/forc/src/ops/forc_build.rs @@ -30,6 +30,7 @@ fn opts_from_cmd(cmd: BuildCommand) -> pkg::BuildOpts { reverse_order: cmd.build.print.reverse_order, }, time_phases: cmd.build.print.time_phases, + profile: cmd.build.print.profile, metrics_outfile: cmd.build.print.metrics_outfile, minify: pkg::MinifyOpts { json_abi: cmd.build.minify.json_abi, diff --git a/forc/src/ops/forc_contract_id.rs b/forc/src/ops/forc_contract_id.rs index f80f4f5262f..9cc18edcfeb 100644 --- a/forc/src/ops/forc_contract_id.rs +++ b/forc/src/ops/forc_contract_id.rs @@ -64,6 +64,7 @@ fn build_opts_from_cmd(cmd: &ContractIdCommand) -> pkg::BuildOpts { reverse_order: cmd.print.reverse_order, }, time_phases: cmd.print.time_phases, + profile: cmd.print.profile, metrics_outfile: cmd.print.metrics_outfile.clone(), minify: pkg::MinifyOpts { json_abi: cmd.minify.json_abi, diff --git a/forc/src/ops/forc_predicate_root.rs b/forc/src/ops/forc_predicate_root.rs index 34be42c4117..41b7c5020a8 100644 --- a/forc/src/ops/forc_predicate_root.rs +++ b/forc/src/ops/forc_predicate_root.rs @@ -33,6 +33,7 @@ fn build_opts_from_cmd(cmd: PredicateRootCommand) -> pkg::BuildOpts { reverse_order: cmd.print.reverse_order, }, time_phases: cmd.print.time_phases, + profile: cmd.print.profile, metrics_outfile: cmd.print.metrics_outfile, minify: pkg::MinifyOpts { json_abi: cmd.minify.json_abi, diff --git a/sway-core/src/asm_generation/finalized_asm.rs b/sway-core/src/asm_generation/finalized_asm.rs index c132d687855..8ed7bc9847a 100644 --- a/sway-core/src/asm_generation/finalized_asm.rs +++ b/sway-core/src/asm_generation/finalized_asm.rs @@ -18,6 +18,24 @@ use sway_types::SourceEngine; use std::{collections::BTreeMap, fmt}; +/// Represents an ASM set which has had register allocation, jump elimination, and optimization +/// applied to it +#[derive(Clone, serde::Serialize)] +pub struct AsmInformation { + pub bytecode_size: u64, + pub data_section: DataSectionInformation, +} + +#[derive(Default, Clone, Debug, serde::Serialize)] +pub struct DataSectionInformation { + /// The total size of the data section in bytes + pub size: u64, + /// The used size of the data section in bytes + pub used: u64, + /// The data to be put in the data section of the asm + pub value_pairs: Vec, +} + /// Represents an ASM set which has had register allocation, jump elimination, and optimization /// applied to it #[derive(Clone)] diff --git a/sway-core/src/asm_generation/fuel/data_section.rs b/sway-core/src/asm_generation/fuel/data_section.rs index e442dcf5ac1..926caffe37b 100644 --- a/sway-core/src/asm_generation/fuel/data_section.rs +++ b/sway-core/src/asm_generation/fuel/data_section.rs @@ -3,7 +3,7 @@ use sway_ir::{size_bytes_round_up_to_word_alignment, Constant, ConstantValue, Co use std::{fmt, iter::repeat}; -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] pub enum EntryName { NonConfigurable, Configurable(String), @@ -20,14 +20,14 @@ impl fmt::Display for EntryName { // An entry in the data section. It's important for the size to be correct, especially for unions // where the size could be larger than the represented value. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize)] pub struct Entry { pub value: Datum, pub padding: Padding, pub name: EntryName, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize)] pub enum Datum { Byte(u8), Word(u64), diff --git a/sway-core/src/asm_generation/mod.rs b/sway-core/src/asm_generation/mod.rs index 2cd0e7d0605..dc7be862366 100644 --- a/sway-core/src/asm_generation/mod.rs +++ b/sway-core/src/asm_generation/mod.rs @@ -8,7 +8,8 @@ pub mod fuel; pub mod instruction_set; mod finalized_asm; -pub use finalized_asm::{CompiledBytecode, FinalizedAsm, FinalizedEntry}; +pub use finalized_asm::*; +pub use fuel::data_section::{Datum, Entry}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ProgramKind { diff --git a/sway-core/src/build_config.rs b/sway-core/src/build_config.rs index 711ceb014d4..4f4ad896662 100644 --- a/sway-core/src/build_config.rs +++ b/sway-core/src/build_config.rs @@ -188,6 +188,7 @@ pub struct BuildConfig { pub(crate) include_tests: bool, pub(crate) optimization_level: OptLevel, pub time_phases: bool, + pub profile: bool, pub metrics_outfile: Option, pub lsp_mode: Option, } @@ -234,6 +235,7 @@ impl BuildConfig { print_ir: PrintIr::default(), include_tests: false, time_phases: false, + profile: false, metrics_outfile: None, optimization_level: OptLevel::Opt0, lsp_mode: None, @@ -280,6 +282,10 @@ impl BuildConfig { } } + pub fn with_profile(self, a: bool) -> Self { + Self { profile: a, ..self } + } + pub fn with_metrics(self, a: Option) -> Self { Self { metrics_outfile: a, diff --git a/sway-core/src/lib.rs b/sway-core/src/lib.rs index ac6b7d54d34..402a81d1242 100644 --- a/sway-core/src/lib.rs +++ b/sway-core/src/lib.rs @@ -740,6 +740,7 @@ pub fn compile_to_ast( // Parse the program to a concrete syntax tree (CST). let parse_program_opt = time_expr!( + package_name, "parse the program to a concrete syntax tree (CST)", "parse_cst", parse(input, handler, engines, build_config, experimental), @@ -764,6 +765,7 @@ pub fn compile_to_ast( // Type check (+ other static analysis) the CST to a typed AST. let typed_res = time_expr!( + package_name, "parse the concrete syntax tree (CST) to a typed AST", "parse_ast", parsed_to_ast( @@ -980,7 +982,7 @@ pub fn compile_to_bytecode( package_name: &str, experimental: ExperimentalFeatures, ) -> Result { - let asm_res = compile_to_asm( + let mut asm_res = compile_to_asm( handler, engines, input, @@ -989,7 +991,13 @@ pub fn compile_to_bytecode( package_name, experimental, )?; - asm_to_bytecode(handler, asm_res, source_map, engines.se(), build_config) + asm_to_bytecode( + handler, + &mut asm_res, + source_map, + engines.se(), + build_config, + ) } /// Size of the prelude's CONFIGURABLES_OFFSET section, in bytes. @@ -1017,7 +1025,7 @@ pub fn set_bytecode_configurables_offset( /// Given the assembly (opcodes), compile to [CompiledBytecode], containing the asm in bytecode form. pub fn asm_to_bytecode( handler: &Handler, - mut asm: CompiledAsm, + asm: &mut CompiledAsm, source_map: &mut SourceMap, source_engine: &SourceEngine, build_config: &BuildConfig, diff --git a/sway-ir/Cargo.toml b/sway-ir/Cargo.toml index 331302af16b..c4264f9281c 100644 --- a/sway-ir/Cargo.toml +++ b/sway-ir/Cargo.toml @@ -18,6 +18,7 @@ once_cell.workspace = true peg.workspace = true prettydiff.workspace = true rustc-hash.workspace = true +serde = { version = "1.0", features = ["derive"] } slotmap.workspace = true sway-features.workspace = true sway-ir-macros.workspace = true diff --git a/sway-ir/src/irtype.rs b/sway-ir/src/irtype.rs index 1771ca954bc..dc96427d56a 100644 --- a/sway-ir/src/irtype.rs +++ b/sway-ir/src/irtype.rs @@ -642,7 +642,7 @@ impl TypeSize { /// the value in aggregates. E.g., in an array of `u8`, each `u8` is "padded" /// to its size of one byte while as a struct field, it will be right padded /// to 8 bytes. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize)] pub enum Padding { Left { target_size: usize }, Right { target_size: usize }, diff --git a/sway-utils/src/performance.rs b/sway-utils/src/performance.rs index cbf53cc5510..479c229c5e3 100644 --- a/sway-utils/src/performance.rs +++ b/sway-utils/src/performance.rs @@ -14,12 +14,28 @@ pub struct PerformanceData { pub reused_programs: u64, } +#[derive(serde::Serialize, Clone)] +pub struct FunctionEntryPoint { + /// The original entry point function name. + pub fn_name: String, + /// The immediate instruction offset at which the entry function begins. + pub imm: u64, + /// The function selector (only `Some` for contract ABI methods). + pub selector: Option<[u8; 4]>, +} + #[macro_export] // Time the given expression and print/save the result. macro_rules! time_expr { - ($description:expr, $key:expr, $expression:expr, $build_config:expr, $data:expr) => {{ + ($pkg_name:expr, $description:expr, $key:expr, $expression:expr, $build_config:expr, $data:expr) => {{ + use std::io::{BufRead, Read, Write}; if let Some(cfg) = $build_config { - if cfg.time_phases || cfg.metrics_outfile.is_some() { + if cfg.profile { + println!("/dyno start {} {}", $pkg_name, $description); + let output = { $expression }; + println!("/dyno stop {} {}", $pkg_name, $description); + output + } else if cfg.time_phases || cfg.metrics_outfile.is_some() { let expr_start = std::time::Instant::now(); let output = { $expression }; let elapsed = expr_start.elapsed(); diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/forc/help/stdout.snap b/test/src/e2e_vm_tests/test_programs/should_pass/forc/help/stdout.snap index 8dd4a804aae..6baad19fd46 100644 --- a/test/src/e2e_vm_tests/test_programs/should_pass/forc/help/stdout.snap +++ b/test/src/e2e_vm_tests/test_programs/should_pass/forc/help/stdout.snap @@ -277,12 +277,9 @@ EXAMPLES: > forc test -h exit status: 0 output: -Run the Sway unit tests for the current project - -Usage: forc test [OPTIONS] [FILTER] +Compile the current or target project -Arguments: - [FILTER] When specified, only tests containing the given string will be executed +Usage: forc build [OPTIONS] Options: -p, --path @@ -311,20 +308,22 @@ Options: Print the generated Sway IR (Intermediate Representation). [possible values: initial, final, all, modified, inline, simplify-cfg, sroa, dce, fn-dce, fn-dedup-release, fn-dedup-debug, mem2reg, memcpyopt, const-folding, arg-demotion, const-demotion, ret-demotion, misc-demotion] --time-phases Output the time elapsed over each part of the compilation process + --profil + Profile the compilation process --reverse-order Output build errors and warnings in reverse order - --metrics-outfile - Output compilation metrics into the specified file -v, --verbose... Use verbose output - --json-abi - Minify JSON ABI files + --metrics-outfile + Output compilation metrics into the specified file -s, --silent Silence all output - --json-storage-slots - Minify JSON storage slot files + --json-abi + Minify JSON ABI files -L, --log-level Set the log level + --json-storage-slots + Minify JSON storage slot files -o, --output-bin Create a binary file at the provided path representing the final bytecode -g, --output-debug @@ -337,16 +336,8 @@ Options: Treat warnings as errors --build-target Build target to use for code generation [default: fuel] [possible values: fuel, evm] - --pretty - Pretty-print the logs emitted from tests - -l, --logs - Print `Log` and `LogData` receipts for tests - --raw-logs - Print the raw logs for tests - --filter-exact - When specified, only the test exactly matching the given string will be executed - --test-threads - Number of threads to utilize when running the tests. By default, this is the number of threads available in your system + --tests + Also build all tests within the project --experimental Comma separated list of all experimental features that will be enabled [possible values: new_encoding] --no-experimental @@ -357,17 +348,14 @@ Options: Print version EXAMPLES: - # Run test - forc test - - # Run test with a filter - forc test $filter + # Compile the current projectx + forc build - # Run test without any output - forc test --silent + # Compile the current project from a different path + forc build --path - # Run test without creating or update the lock file - forc test --locked + # Compile the current project without updating dependencies + forc build --path --locked > forc update -h exit status: 0