use std::collections::BTreeMap;
use comfy_table::{
modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, Attribute, Cell, Color, Table,
};
use kythera_lib::{DeployedActor, ExecutionEvent, Method, Payload, TestResult, TestResultType};
#[derive(Default, Debug)]
pub struct GasReport {
reports: BTreeMap<DeployedActor, ActorInfo>,
}
#[derive(Debug, Default)]
pub struct ActorInfo {
methods: BTreeMap<Method, Vec<u64>>,
}
struct MethodCost {
gas_cost: u64,
num: u64,
at: u64,
}
impl GasReport {
pub fn analyze_method(&mut self, actor: &DeployedActor, method: Method, cost: u64) {
let (actor, mut info) = match self.reports.remove_entry(actor) {
Some((actor, info)) => (actor, info),
None => (actor.clone(), ActorInfo::default()),
};
let mut gas_info = match info.methods.remove(&method) {
Some(gi) => gi,
None => vec![],
};
gas_info.push(cost);
info.methods.insert(method.clone(), gas_info);
self.reports.insert(actor, info);
}
pub fn analyze_results(&mut self, actor: &DeployedActor, test_results: &[TestResult]) {
let (actor, mut info) = match self.reports.remove_entry(actor) {
Some((actor, info)) => (actor, info),
None => (actor.clone(), ActorInfo::default()),
};
let actor_id = match actor.address().payload() {
Payload::ID(id) => id,
_ => panic!("DeployedActor address payload should be an Id"),
};
for result in test_results {
let apply_ret = match result.ret() {
TestResultType::Passed(apply_ret) | TestResultType::Failed(apply_ret) => apply_ret,
TestResultType::Erred(_) => {
continue;
}
};
let mut stack: Vec<MethodCost> = vec![];
for trace in &apply_ret.exec_trace {
match trace {
ExecutionEvent::GasCharge(gas_charge) => {
let method = match stack.last_mut() {
Some(e) => e,
None => continue,
};
method.gas_cost += gas_charge.compute_gas.as_milligas();
}
ExecutionEvent::Call { method, to, .. } => {
let to_id = match to.payload() {
Payload::ID(id) => id,
_ => panic!("Call to address payload should be an Id"),
};
stack.push(MethodCost {
gas_cost: 0,
num: *method,
at: *to_id,
});
}
ExecutionEvent::CallReturn(_, _) | ExecutionEvent::CallError(_) => {
let method_return = stack.pop().expect("A CallReturn should match a Call");
if let Some(previous) = stack.last_mut() {
previous.gas_cost += method_return.gas_cost;
}
let Some(method) = actor
.abi()
.methods()
.iter()
.find(|a| a.number() == method_return.num && method_return.at == *actor_id) else {
continue;
};
let mut gas_info = match info.methods.remove(method) {
Some(gi) => gi,
None => vec![],
};
gas_info.push(method_return.gas_cost);
info.methods.insert(method.clone(), gas_info);
}
_ => {}
}
}
}
self.reports.insert(actor, info);
}
pub fn finalize(self) -> Vec<Table> {
let mut tables = vec![];
for (actor, contract_info) in self.reports {
let mut table = Table::new();
table.load_preset(UTF8_FULL);
table.apply_modifier(UTF8_ROUND_CORNERS);
table.set_header(vec![Cell::new(format!("{} contract", actor.name()))
.add_attribute(Attribute::Bold)
.fg(Color::Green)]);
table.add_row(vec![
Cell::new("Function Name")
.add_attribute(Attribute::Bold)
.fg(Color::Magenta),
Cell::new("min")
.add_attribute(Attribute::Bold)
.fg(Color::Green),
Cell::new("max")
.add_attribute(Attribute::Bold)
.fg(Color::Red),
Cell::new("avg")
.add_attribute(Attribute::Bold)
.fg(Color::Yellow),
Cell::new("median")
.add_attribute(Attribute::Bold)
.fg(Color::Yellow),
Cell::new("# calls").add_attribute(Attribute::Bold),
]);
for (method, mut calls) in contract_info.methods {
calls.sort_unstable();
let min = calls.first().copied().unwrap_or_default();
let max = calls.last().copied().unwrap_or_default();
let mean = {
if calls.is_empty() {
0f64
} else {
calls.iter().copied().sum::<u64>() as f64 / calls.len() as f64
}
};
let median = {
if calls.is_empty() {
0u64
} else {
let len = calls.len();
let mid = len / 2;
if len % 2 == 0 {
(calls[mid - 1] + calls[mid]) / 2u64
} else {
calls[mid]
}
}
};
table.add_row(vec![
Cell::new(method.name()).add_attribute(Attribute::Bold),
Cell::new(min.to_string()).fg(Color::Green),
Cell::new(mean.to_string()).fg(Color::Yellow),
Cell::new(median.to_string()).fg(Color::Yellow),
Cell::new(max.to_string()).fg(Color::Red),
Cell::new(calls.len().to_string()),
]);
}
tables.push(table);
}
tables
}
}
#[cfg(test)]
mod tests {
use kythera_lib::{
Address, ApplyRet, ErrorNumber, ExitCode, Gas, GasCharge, RawBytes, Receipt, TokenAmount,
WasmActor, Zero,
};
use super::*;
const TARGET_ACTOR_ADDRESS: u64 = 44;
#[test]
fn analyzes_gas_consumption() {
let constructor = Method::new_from_name("Constructor").unwrap();
let c_number = constructor.number();
let m1 = Method::new_from_name("Method1").unwrap();
let m1_number = m1.number();
let m2 = Method::new_from_name("Method2").unwrap();
let m2_number = m2.number();
let target = WasmActor::new(
"Target".into(),
vec![],
kythera_lib::Abi {
constructor: None,
set_up: None,
methods: vec![m1, m2],
},
)
.deploy(Address::new_id(44));
let mut gr = GasReport::default();
gr.analyze_method(&target, constructor, 40);
let result = TestResult::new(
Method::new_from_name("TestMethod").unwrap(),
TestResultType::Passed(ApplyRet {
msg_receipt: Receipt {
exit_code: ExitCode::new(0),
return_data: RawBytes::default(),
gas_used: 0,
events_root: None,
},
penalty: TokenAmount::zero(),
miner_tip: TokenAmount::zero(),
base_fee_burn: TokenAmount::zero(),
over_estimation_burn: TokenAmount::zero(),
refund: TokenAmount::zero(),
gas_refund: 0,
gas_burned: 0,
failure_info: None,
exec_trace: vec![
ExecutionEvent::Call {
from: 0,
to: Address::new_id(TARGET_ACTOR_ADDRESS),
method: m1_number,
params: None,
value: TokenAmount::zero(),
},
ExecutionEvent::Call {
from: 0,
to: Address::new_id(TARGET_ACTOR_ADDRESS),
method: m2_number,
params: None,
value: TokenAmount::zero(),
},
ExecutionEvent::GasCharge(GasCharge::new(
"",
Gas::from_milligas(10),
Gas::from_milligas(0),
)),
ExecutionEvent::CallError(kythera_lib::SyscallError(
"error".into(),
ErrorNumber::Forbidden,
)),
ExecutionEvent::GasCharge(GasCharge::new(
"",
Gas::from_milligas(20),
Gas::from_milligas(0),
)),
ExecutionEvent::CallReturn(ExitCode::new(0), None),
],
events: vec![],
}),
);
gr.analyze_results(&target, &[result]);
let report = gr.reports.get(&target).unwrap();
assert_eq!(report.methods.len(), 3);
let cm = report.methods.get(&c_number).unwrap();
assert_eq!(cm.len(), 1);
assert_eq!(cm[0], 40);
let m1m = report.methods.get(&m1_number).unwrap();
assert_eq!(m1m.len(), 1);
assert_eq!(m1m[0], 30);
let m2m = report.methods.get(&m2_number).unwrap();
assert_eq!(m2m.len(), 1);
assert_eq!(m2m[0], 10);
}
#[test]
fn calculates_totals() {
let m1 = Method::new_from_name("Method1").unwrap();
let m1_number = m1.number();
let m2 = Method::new_from_name("Method2").unwrap();
let m2_number = m2.number();
let target = WasmActor::new(
"Target.wasm".into(),
vec![],
kythera_lib::Abi {
constructor: None,
set_up: None,
methods: vec![m1, m2],
},
)
.deploy(Address::new_id(44));
let mut gr = GasReport::default();
let result1 = TestResult::new(
Method::new_from_name("TestMethod1").unwrap(),
TestResultType::Passed(ApplyRet {
msg_receipt: Receipt {
exit_code: ExitCode::new(0),
return_data: RawBytes::default(),
gas_used: 0,
events_root: None,
},
penalty: TokenAmount::zero(),
miner_tip: TokenAmount::zero(),
base_fee_burn: TokenAmount::zero(),
over_estimation_burn: TokenAmount::zero(),
refund: TokenAmount::zero(),
gas_refund: 0,
gas_burned: 0,
failure_info: None,
exec_trace: vec![
ExecutionEvent::Call {
from: 0,
to: Address::new_id(TARGET_ACTOR_ADDRESS),
method: m1_number,
params: None,
value: TokenAmount::zero(),
},
ExecutionEvent::Call {
from: 0,
to: Address::new_id(TARGET_ACTOR_ADDRESS),
method: m2_number,
params: None,
value: TokenAmount::zero(),
},
ExecutionEvent::GasCharge(GasCharge::new(
"",
Gas::from_milligas(10),
Gas::from_milligas(0),
)),
ExecutionEvent::CallError(kythera_lib::SyscallError(
"error".into(),
ErrorNumber::Forbidden,
)),
ExecutionEvent::GasCharge(GasCharge::new(
"",
Gas::from_milligas(20),
Gas::from_milligas(0),
)),
ExecutionEvent::CallReturn(ExitCode::new(0), None),
],
events: vec![],
}),
);
let result2 = TestResult::new(
Method::new_from_name("TestMethod2").unwrap(),
TestResultType::Passed(ApplyRet {
msg_receipt: Receipt {
exit_code: ExitCode::new(0),
return_data: RawBytes::default(),
gas_used: 0,
events_root: None,
},
penalty: TokenAmount::zero(),
miner_tip: TokenAmount::zero(),
base_fee_burn: TokenAmount::zero(),
over_estimation_burn: TokenAmount::zero(),
refund: TokenAmount::zero(),
gas_refund: 0,
gas_burned: 0,
failure_info: None,
exec_trace: vec![
ExecutionEvent::Call {
from: 0,
to: Address::new_id(TARGET_ACTOR_ADDRESS),
method: m1_number,
params: None,
value: TokenAmount::zero(),
},
ExecutionEvent::Call {
from: 0,
to: Address::new_id(TARGET_ACTOR_ADDRESS),
method: m2_number,
params: None,
value: TokenAmount::zero(),
},
ExecutionEvent::GasCharge(GasCharge::new(
"",
Gas::from_milligas(20),
Gas::from_milligas(0),
)),
ExecutionEvent::CallError(kythera_lib::SyscallError(
"error".into(),
ErrorNumber::Forbidden,
)),
ExecutionEvent::GasCharge(GasCharge::new(
"",
Gas::from_milligas(10),
Gas::from_milligas(0),
)),
ExecutionEvent::CallReturn(ExitCode::new(0), None),
],
events: vec![],
}),
);
let result3 = TestResult::new(
Method::new_from_name("TestMethod3").unwrap(),
TestResultType::Passed(ApplyRet {
msg_receipt: Receipt {
exit_code: ExitCode::new(0),
return_data: RawBytes::default(),
gas_used: 0,
events_root: None,
},
penalty: TokenAmount::zero(),
miner_tip: TokenAmount::zero(),
base_fee_burn: TokenAmount::zero(),
over_estimation_burn: TokenAmount::zero(),
refund: TokenAmount::zero(),
gas_refund: 0,
gas_burned: 0,
failure_info: None,
exec_trace: vec![
ExecutionEvent::Call {
from: 0,
to: Address::new_id(TARGET_ACTOR_ADDRESS),
method: m1_number,
params: None,
value: TokenAmount::zero(),
},
ExecutionEvent::Call {
from: 0,
to: Address::new_id(TARGET_ACTOR_ADDRESS),
method: m2_number,
params: None,
value: TokenAmount::zero(),
},
ExecutionEvent::GasCharge(GasCharge::new(
"",
Gas::from_milligas(0),
Gas::from_milligas(0),
)),
ExecutionEvent::CallError(kythera_lib::SyscallError(
"error".into(),
ErrorNumber::Forbidden,
)),
ExecutionEvent::GasCharge(GasCharge::new(
"",
Gas::from_milligas(30),
Gas::from_milligas(0),
)),
ExecutionEvent::CallReturn(ExitCode::new(0), None),
],
events: vec![],
}),
);
gr.analyze_results(&target, &[result1, result2, result3]);
let table = gr.finalize().pop().unwrap();
let header = table.header().unwrap();
assert_eq!(
header.cell_iter().next().unwrap().content(),
"Target.wasm contract"
);
let mut names = table.row(0).unwrap().cell_iter();
assert_eq!(names.next().unwrap().content(), "Function Name");
assert_eq!(names.next().unwrap().content(), "min");
assert_eq!(names.next().unwrap().content(), "max");
assert_eq!(names.next().unwrap().content(), "avg");
assert_eq!(names.next().unwrap().content(), "median");
assert_eq!(names.next().unwrap().content(), "# calls");
let mut method2 = table.row(1).unwrap().cell_iter();
assert_eq!(method2.next().unwrap().content(), "Method2");
assert_eq!(method2.next().unwrap().content(), "0");
assert_eq!(method2.next().unwrap().content(), "10");
assert_eq!(method2.next().unwrap().content(), "10");
assert_eq!(method2.next().unwrap().content(), "20");
assert_eq!(method2.next().unwrap().content(), "3");
let mut method1 = table.row(2).unwrap().cell_iter();
assert_eq!(method1.next().unwrap().content(), "Method1");
assert_eq!(method1.next().unwrap().content(), "30");
assert_eq!(method1.next().unwrap().content(), "30");
assert_eq!(method1.next().unwrap().content(), "30");
assert_eq!(method1.next().unwrap().content(), "30");
assert_eq!(method1.next().unwrap().content(), "3");
}
}