diff --git a/Cargo.lock b/Cargo.lock index 3bb7248fd..9b153cf1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -555,7 +555,7 @@ dependencies = [ [[package]] name = "clarinet" -version = "0.15.1" +version = "0.15.2" dependencies = [ "aes", "atty", @@ -613,9 +613,9 @@ dependencies = [ [[package]] name = "clarity-repl" -version = "0.14.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b300d996728da074dd5d11e04ec45c5e3334165aa0017ecfcaa869b4d892177b" +checksum = "cab9a91d7ef8169adf9cf5dfd0e58907453d67e3180bb2a481864b8fc2cb63fa" dependencies = [ "ansi_term 0.12.1", "atty", diff --git a/Cargo.toml b/Cargo.toml index 5553fda1c..eaeaf9d4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "clarinet" -version = "0.15.1" +version = "0.15.2" authors = ["Ludo Galabru "] edition = "2018" description = "Clarinet is a clarity runtime packaged as a command line tool, designed to facilitate smart contract understanding, development, testing and deployment." @@ -26,7 +26,7 @@ deno_core = { path = "./vendor/deno/core" } deno_runtime = { path = "./vendor/deno/runtime" } deno = { path = "./vendor/deno/cli" } # clarity_repl = { package = "clarity-repl", path = "../../clarity-repl", features = ["cli"] } -clarity_repl = { package = "clarity-repl", version = "=0.14.2" } +clarity_repl = { package = "clarity-repl", version = "=0.16.0" } bip39 = { version = "1.0.1", default-features = false } aes = "0.6.0" base64 = "0.13.0" diff --git a/deno/ext/stacksjs-helper-generator.ts b/deno/ext/stacksjs-helper-generator.ts index f00e0d42e..50acd5dbf 100644 --- a/deno/ext/stacksjs-helper-generator.ts +++ b/deno/ext/stacksjs-helper-generator.ts @@ -43,7 +43,7 @@ // ... // } -import { Clarinet, Contract, Account, StacksNode } from './index'; +import { Clarinet, Contract, Account, StacksNode } from '../index.ts'; Clarinet.run({ async fn(accounts: Map, contracts: Map, node: StacksNode) { diff --git a/deno/index.ts b/deno/index.ts index f77c8ee78..a5d96fe2c 100644 --- a/deno/index.ts +++ b/deno/index.ts @@ -327,8 +327,10 @@ export namespace types { return `u"${val}"`; } - export function buff(val: ArrayBuffer) { - const buff = new Uint8Array(val); + export function buff(val: ArrayBuffer | string) { + + const buff = typeof val == "string" ? new TextEncoder().encode(val) : new Uint8Array(val); + const hexOctets = new Array(buff.length); for (let i = 0; i < buff.length; ++i) { diff --git a/examples/counter/contracts/counter.clar b/examples/counter/contracts/counter.clar index d72f65674..3db550bed 100644 --- a/examples/counter/contracts/counter.clar +++ b/examples/counter/contracts/counter.clar @@ -12,8 +12,8 @@ (define-public (decrement (step uint)) (let ((new-val (- step (var-get counter)))) (var-set counter new-val) - (print { object: "counter", action: "incremented", value: new-val }) + (print { object: "counter", action: "decremented", value: new-val }) (ok new-val))) (define-read-only (read-counter) - (ok (var-get counter))) \ No newline at end of file + (ok (var-get counter))) diff --git a/src/frontend/cli.rs b/src/frontend/cli.rs index 29f70e1c7..ec6d76c9f 100644 --- a/src/frontend/cli.rs +++ b/src/frontend/cli.rs @@ -11,7 +11,7 @@ use crate::generate::{ use crate::integrate::{self, DevnetOrchestrator}; use crate::poke::load_session; use crate::publish::{publish_all_contracts, Network}; -use crate::test::run_scripts; +use crate::runnner::run_scripts; use crate::types::{MainConfig, MainConfigFile, RequirementConfig}; use clarity_repl::repl; @@ -126,6 +126,9 @@ struct Test { /// Generate coverage #[clap(long = "coverage")] pub coverage: bool, + /// Generate costs report + #[clap(long = "costs")] + pub costs_report: bool, /// Path to Clarinet.toml #[clap(long = "manifest-path")] pub manifest_path: Option, @@ -311,6 +314,7 @@ pub fn main() { run_scripts( cmd.files, cmd.coverage, + cmd.costs_report, cmd.watch, true, false, @@ -333,6 +337,7 @@ pub fn main() { vec![cmd.script], false, false, + false, cmd.allow_wallets, cmd.allow_disk_write, manifest_path, diff --git a/src/generate/project.rs b/src/generate/project.rs index 3f2fa19fa..bfff365ac 100644 --- a/src/generate/project.rs +++ b/src/generate/project.rs @@ -121,9 +121,9 @@ history.txt [project] name = "{}" -[contracts] - -[notebooks] +# [contracts.counter] +# path = "contracts/counter.clar" +# depends_on = [] "#, self.project_name ); diff --git a/src/integrate/events_observer.rs b/src/integrate/events_observer.rs index b30de74e4..20d90f100 100644 --- a/src/integrate/events_observer.rs +++ b/src/integrate/events_observer.rs @@ -2,7 +2,7 @@ use super::{DevnetEvent, NodeObserverEvent}; use crate::integrate::{BlockData, MempoolAdmissionData, ServiceStatusData, Status, Transaction}; use crate::poke::load_session; use crate::publish::{publish_contract, Network}; -use crate::test::deno; +use crate::runnner::deno; use crate::types::{self, AccountConfig, DevnetConfig}; use crate::utils; use crate::utils::stacks::{transactions, StacksRpc}; @@ -109,6 +109,7 @@ impl EventObserverConfig { vec![cmd.script.clone()], false, false, + false, cmd.allow_wallets, cmd.allow_write, self.manifest_path.clone(), diff --git a/src/main.rs b/src/main.rs index 22e89ce50..d3b348949 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ mod generate; mod integrate; mod poke; mod publish; -mod test; +mod runnner; mod types; mod utils; diff --git a/src/poke/mod.rs b/src/poke/mod.rs index 8e2ec5be6..c6d51eca3 100644 --- a/src/poke/mod.rs +++ b/src/poke/mod.rs @@ -93,9 +93,14 @@ pub fn load_session( }); } - settings.include_boot_contracts = - vec!["pox".to_string(), "costs".to_string(), "bns".to_string()]; + settings.include_boot_contracts = vec![ + "pox".to_string(), + "costs-v1".to_string(), + "costs-v2".to_string(), + "bns".to_string(), + ]; settings.initial_deployer = initial_deployer; + settings.costs_version = project_config.project.costs_version; let session = if start_repl { let mut terminal = Terminal::new(settings.clone()); @@ -105,7 +110,7 @@ pub fn load_session( let mut session = repl::Session::new(settings.clone()); match session.start() { Err(message) => { - println!("Error: {}", message); + println!("{}", message); std::process::exit(1); } _ => {} diff --git a/src/test/deno.rs b/src/runnner/deno.rs similarity index 69% rename from src/test/deno.rs rename to src/runnner/deno.rs index 184cd9852..5829474a9 100644 --- a/src/test/deno.rs +++ b/src/runnner/deno.rs @@ -1,4 +1,6 @@ use clarity_repl::clarity::coverage::CoverageReporter; +use clarity_repl::prettytable::{color, format, Attr, Cell, Row, Table}; +use clarity_repl::repl::session::CostsReport; use clarity_repl::repl::Session; use deno::ast; use deno::colors; @@ -30,9 +32,10 @@ use deno_runtime::permissions::Permissions; use regex::Regex; use serde::de::DeserializeOwned; use serde::Serialize; -use std::collections::BTreeMap; use std::collections::HashSet; +use std::collections::{btree_map::Entry, BTreeMap}; use std::convert::TryFrom; +use std::ops::Index; use std::path::Path; use std::path::PathBuf; use std::rc::Rc; @@ -61,6 +64,11 @@ mod sessions { pub static ref SESSION_TEMPLATE: Mutex> = Mutex::new(vec![]); } + pub fn reset() { + SESSION_TEMPLATE.lock().unwrap().clear(); + SESSIONS.lock().unwrap().clear(); + } + pub fn handle_setup_chain( manifest_path: &PathBuf, name: String, @@ -145,8 +153,13 @@ mod sessions { }); } settings.initial_deployer = initial_deployer; - settings.include_boot_contracts = - vec!["pox".to_string(), "costs".to_string(), "bns".to_string()]; + settings.costs_version = project_config.project.costs_version; + settings.include_boot_contracts = vec![ + "pox".to_string(), + "costs-v1".to_string(), + "costs-v2".to_string(), + "bns".to_string(), + ]; let mut session = Session::new(settings.clone()); let (_, contracts) = match session.start() { Ok(res) => res, @@ -176,7 +189,7 @@ mod sessions { match sessions.get_mut(&session_id) { None => { println!("Error: unable to retrieve session"); - unreachable!() + panic!() } Some((name, ref mut session)) => handler(name.as_str(), session), } @@ -186,6 +199,7 @@ mod sessions { pub async fn do_run_scripts( include: Vec, include_coverage: bool, + include_costs_report: bool, watch: bool, allow_wallets: bool, allow_disk_write: bool, @@ -207,8 +221,8 @@ pub async fn do_run_scripts( let mut project_path = manifest_path.clone(); project_path.pop(); let cwd = Path::new(&project_path); - let include = if include.is_empty() { - vec![".".into()] + let mut include = if include.is_empty() { + vec!["tests".into()] } else { include.clone() }; @@ -232,6 +246,8 @@ pub async fn do_run_scripts( Permissions::allow_all(), )?)); + include.push("contracts".into()); + let paths_to_watch: Vec<_> = include.iter().map(PathBuf::from).collect(); let resolver = |changed: Option>| { @@ -358,6 +374,10 @@ pub async fn do_run_scripts( file_watcher::watch_func( resolver, |modules_to_reload| { + // Clear the screen + print!("{esc}c", esc = 27 as char); + // Clear eventual previous sessions + sessions::reset(); run_scripts( program_state.clone(), permissions.clone(), @@ -372,9 +392,14 @@ pub async fn do_run_scripts( concurrent_jobs, manifest_path.clone(), allow_wallets, - session.clone(), + None, ) - .map(|res| res.map(|_| ())) + .map(|res| { + if include_costs_report { + display_costs_report() + } + res.map(|_| ()) + }) }, "Test", ) @@ -429,9 +454,306 @@ pub async fn do_run_scripts( coverage_reporter.write_lcov_file("coverage.lcov"); } + if include_costs_report { + display_costs_report() + } + Ok(true) } +#[derive(Clone)] +enum Bottleneck { + Unknown, + Runtime(u64, u64), + ReadCount(u64, u64), + ReadLength(u64, u64), + WriteCount(u64, u64), + WriteLength(u64, u64), +} + +fn display_costs_report() { + let mut consolidated: BTreeMap>> = BTreeMap::new(); + let sessions = sessions::SESSIONS.lock().unwrap(); + let mut mins: BTreeMap<(&String, &String), (f32, CostsReport, Bottleneck)> = BTreeMap::new(); + let mut maxs: BTreeMap<(&String, &String), (f32, CostsReport, Bottleneck)> = BTreeMap::new(); + + for (session_id, (name, session)) in sessions.iter() { + for report in session.costs_reports.iter() { + let key = report.contract_id.to_string(); + match consolidated.entry(key) { + Entry::Occupied(ref mut entry) => { + match entry.get_mut().entry(report.method.to_string()) { + Entry::Occupied(entry) => entry.into_mut().push(report.clone()), + Entry::Vacant(entry) => { + let mut reports = Vec::new(); + reports.push(report.clone()); + entry.insert(reports); + } + } + } + Entry::Vacant(entry) => { + let mut reports = Vec::new(); + reports.push(report.clone()); + let mut methods = BTreeMap::new(); + methods.insert(report.method.to_string(), reports); + entry.insert(methods); + } + }; + + // Look for the bounding factor + let ratios = vec![ + ( + report.cost_result.total.runtime, + report.cost_result.limit.runtime, + ), + ( + report.cost_result.total.read_count, + report.cost_result.limit.read_count, + ), + ( + report.cost_result.total.read_length, + report.cost_result.limit.read_length, + ), + ( + report.cost_result.total.write_count, + report.cost_result.limit.write_count, + ), + ( + report.cost_result.total.write_length, + report.cost_result.limit.write_length, + ), + ]; + let (bottleneck, mut max) = ratios.iter().enumerate().fold( + (Bottleneck::Unknown, 0 as f32), + |(bottleneck, max), (index, (cost, limit))| { + let ratio = (*cost as f32) / (*limit as f32); + if ratio > max { + ( + match index { + 0 => Bottleneck::Runtime(*cost, *limit), + 1 => Bottleneck::ReadCount(*cost, *limit), + 2 => Bottleneck::ReadLength(*cost, *limit), + 3 => Bottleneck::WriteCount(*cost, *limit), + 4 => Bottleneck::WriteLength(*cost, *limit), + _ => Bottleneck::Unknown, + }, + ratio, + ) + } else { + (bottleneck, max) + } + }, + ); + + let key = (&report.contract_id, &report.method); + + mins.entry(key) + .and_modify(|(cur_min, min_report, cur_bottleneck)| { + if &mut max < cur_min { + *cur_min = max; + *min_report = report.clone(); + *cur_bottleneck = bottleneck.clone(); + } + }) + .or_insert((max, report.clone(), bottleneck.clone())); + maxs.entry(key) + .and_modify(|(cur_max, max_report, cur_bottleneck)| { + if &mut max > cur_max { + *cur_max = max; + *max_report = report.clone(); + *cur_bottleneck = bottleneck.clone(); + } + }) + .or_insert((max, report.clone(), bottleneck.clone())); + } + } + + println!("\nContract calls cost synthesis"); + let mut table = Table::new(); + let headers = vec![ + "".to_string(), + "Runtime (units)".to_string(), + "Read Count".to_string(), + "Read Length (bytes)".to_string(), + "Write Count".to_string(), + "Write Length (bytes)".to_string(), + "Tx per Block".to_string(), + ]; + let mut headers_cells = vec![]; + for header in headers.iter() { + headers_cells.push(Cell::new(&header)); + } + table.add_row(Row::new(headers_cells.clone())); + + for (contract_id, methods) in consolidated.iter() { + for (method, reports) in methods.iter() { + let (min, min_report, min_bottleneck) = mins.get(&(contract_id, method)).unwrap(); + let (max, max_report, max_bottleneck) = mins.get(&(contract_id, method)).unwrap(); + + // Not displaying the min row for now - probably not so interesting atm. + // if min != max { + // table.add_row(Row::new(formatted_cost_cells( + // "Min", + // &min_report, + // &min_bottleneck, + // ))); + // } + + let contract_name = contract_id.split(".").last().unwrap(); + table.add_row(Row::new(formatted_cost_cells( + &format!("{}::{}", contract_name, method), + &max_report, + &max_bottleneck, + ))); + } + } + + if let Some((_, (_, report, _))) = maxs.iter().next() { + let limit = &report.cost_result.limit; + table.add_row(Row::new(vec![Cell::new_align( + &format!(""), + format::Alignment::LEFT, + ) + .with_hspan(7)])); + + table.add_row(Row::new(vec![ + Cell::new("Mainnet Block Limits (Stacks 2.0)"), + Cell::new_align( + &format!("{}", &limit.runtime.to_string()), + format::Alignment::RIGHT, + ), + Cell::new_align(&limit.read_count.to_string(), format::Alignment::RIGHT), + Cell::new_align(&format!("{}", limit.read_length), format::Alignment::RIGHT), + Cell::new_align(&limit.write_count.to_string(), format::Alignment::RIGHT), + Cell::new_align(&format!("{}", limit.write_length), format::Alignment::RIGHT), + Cell::new_align("/", format::Alignment::RIGHT), + ])); + } + + table.printstd(); + println!(""); +} + +fn formatted_cost_cells(title: &str, report: &CostsReport, bottleneck: &Bottleneck) -> Vec { + let mut runtime_style = Attr::ForegroundColor(color::BRIGHT_BLACK); + let mut read_count_style = Attr::ForegroundColor(color::BRIGHT_BLACK); + let mut read_len_style = Attr::ForegroundColor(color::BRIGHT_BLACK); + let mut write_count_style = Attr::ForegroundColor(color::BRIGHT_BLACK); + let mut write_len_style = Attr::ForegroundColor(color::BRIGHT_BLACK); + + let tx_per_block = match bottleneck { + Bottleneck::Runtime(cost, limit) => { + runtime_style = Attr::ForegroundColor(color::BRIGHT_WHITE); + limit / cost + } + Bottleneck::ReadCount(cost, limit) => { + read_count_style = Attr::ForegroundColor(color::BRIGHT_WHITE); + limit / cost + } + Bottleneck::ReadLength(cost, limit) => { + read_len_style = Attr::ForegroundColor(color::BRIGHT_WHITE); + limit / cost + } + Bottleneck::WriteCount(cost, limit) => { + write_count_style = Attr::ForegroundColor(color::BRIGHT_WHITE); + limit / cost + } + Bottleneck::WriteLength(cost, limit) => { + write_len_style = Attr::ForegroundColor(color::BRIGHT_WHITE); + limit / cost + } + _ => 0, + }; + + let block_style = if tx_per_block < 100 { + Attr::ForegroundColor(color::RED) + } else if tx_per_block < 500 { + Attr::ForegroundColor(color::YELLOW) + } else { + Attr::ForegroundColor(color::GREEN) + }; + + let ratios = vec![ + ( + report.cost_result.total.runtime, + report.cost_result.limit.runtime, + ), + ( + report.cost_result.total.read_count, + report.cost_result.limit.read_count, + ), + ( + report.cost_result.total.read_length, + report.cost_result.limit.read_length, + ), + ( + report.cost_result.total.write_count, + report.cost_result.limit.write_count, + ), + ( + report.cost_result.total.write_length, + report.cost_result.limit.write_length, + ), + ]; + + let annotations = ratios + .iter() + .map(|(value, limit)| { + if *value == 0 { + "".to_string() + } else { + format!(" ({:.2}%)", 100.0 * *value as f32 / *limit as f32) + } + }) + .collect::>(); + + vec![ + Cell::new(title), + Cell::new_align( + &format!( + "{}{}", + report.cost_result.total.runtime.to_string(), + annotations[0] + ), + format::Alignment::RIGHT, + ) + .with_style(runtime_style), + Cell::new_align( + &format!( + "{}{}", + report.cost_result.total.read_count.to_string(), + annotations[1] + ), + format::Alignment::RIGHT, + ) + .with_style(read_count_style), + Cell::new_align( + &format!("{}{}", report.cost_result.total.read_length, annotations[2]), + format::Alignment::RIGHT, + ) + .with_style(read_len_style), + Cell::new_align( + &format!( + "{}{}", + report.cost_result.total.write_count.to_string(), + annotations[3] + ), + format::Alignment::RIGHT, + ) + .with_style(write_count_style), + Cell::new_align( + &format!( + "{}{}", + report.cost_result.total.write_length, annotations[4] + ), + format::Alignment::RIGHT, + ) + .with_style(write_len_style), + Cell::new_align(&format!("{}", tx_per_block), format::Alignment::RIGHT) + .with_style(block_style), + ] +} + pub fn is_supported_ext(path: &Path) -> bool { if let Some(ext) = fs_util::get_extension(path) { matches!(ext.as_str(), "ts" | "js" | "clar") @@ -867,56 +1189,56 @@ fn mine_block(state: &mut OpState, args: Value, _: ()) -> Result res, + Err((_, _, err)) => { + if let Some(e) = err { + // todo(ludo): if CLARINET_BACKTRACE=1 + // Retrieve the AST (penultimate entry), and the expression id (last entry) + println!( + "Runtime error: {}::{}({}) -> {:?}", + args.contract, + args.method, + args.args.join(", "), + e + ); + } + continue; + } }; - let execution = session - .interpret(snippet, None, true, Some(name.into())) - .unwrap(); // todo(ludo) - receipts.push((execution.result, execution.events)); - } - - if let Some(ref args) = tx.deploy_contract { - let execution = session - .interpret( - args.code.clone(), - Some(args.name.clone()), - true, - Some(name.into()), - ) - .unwrap(); // todo(ludo) - receipts.push((execution.result, execution.events)); - } - - if let Some(ref args) = tx.transfer_stx { - let snippet = format!( - "(stx-transfer? u{} tx-sender '{})", - args.amount, args.recipient - ); - let execution = session - .interpret(snippet, None, true, Some(name.into())) - .unwrap(); // todo(ludo) receipts.push((execution.result, execution.events)); + } else { + session.set_tx_sender(tx.sender.clone()); + if let Some(ref args) = tx.deploy_contract { + let execution = session + .interpret( + args.code.clone(), + Some(args.name.clone()), + true, + Some(name.into()), + ) + .unwrap(); // todo(ludo) + receipts.push((execution.result, execution.events)); + } else if let Some(ref args) = tx.transfer_stx { + let snippet = format!( + "(stx-transfer? u{} tx-sender '{})", + args.amount, args.recipient + ); + let execution = session + .interpret(snippet, None, true, Some(name.into())) + .unwrap(); // todo(ludo) + receipts.push((execution.result, execution.events)); + } + session.set_tx_sender(initial_tx_sender.clone()); } } - session.set_tx_sender(initial_tx_sender); let block_height = session.advance_chain_tip(1); Ok((block_height, receipts)) })?; @@ -971,32 +1293,16 @@ fn call_read_only_fn(state: &mut OpState, args: Value, _: ()) -> Result, include_coverage: bool, + include_costs_report: bool, watch: bool, allow_wallets: bool, allow_disk_write: bool, @@ -25,6 +26,7 @@ pub fn run_scripts( match block_on(deno::do_run_scripts( files, include_coverage, + include_costs_report, watch, allow_wallets, allow_disk_write, diff --git a/src/types/project_config.rs b/src/types/project_config.rs index 13d2d9fd2..de55ee17c 100644 --- a/src/types/project_config.rs +++ b/src/types/project_config.rs @@ -16,6 +16,7 @@ pub struct MainConfigFile { pub struct ProjectConfigFile { name: String, requirements: Option, + costs_version: Option, } #[derive(Serialize, Deserialize, Debug)] @@ -29,6 +30,7 @@ pub struct MainConfig { pub struct ProjectConfig { pub name: String, pub requirements: Option>, + pub costs_version: u32, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] @@ -126,6 +128,7 @@ impl MainConfig { let project = ProjectConfig { name: config_file.project.name.clone(), requirements: None, + costs_version: config_file.project.costs_version.unwrap_or(1), }; let mut config = MainConfig {