diff --git a/Cargo.lock b/Cargo.lock index e702136..1d1f1c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1399,7 +1399,6 @@ dependencies = [ "serde", "serde_json", "uplc", - "uuid", ] [[package]] @@ -1424,12 +1423,14 @@ dependencies = [ "dashmap", "figment", "gastronomy", + "pallas-codec", "serde", "serde_json", "tauri", "tauri-build", "tauri-plugin-store", "tokio", + "uuid", ] [[package]] diff --git a/gastronomy-ui/Cargo.toml b/gastronomy-ui/Cargo.toml index b04adfa..776e383 100644 --- a/gastronomy-ui/Cargo.toml +++ b/gastronomy-ui/Cargo.toml @@ -12,14 +12,16 @@ publish = false tauri-build = { version = "1.5.3", features = [] } [dependencies] -figment = { version = "0.10", features = ["env", "toml"] } dashmap = "6" +figment = { version = "0.10", features = ["env", "toml"] } gastronomy = { path = "../gastronomy" } +pallas-codec = "0.30" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } tauri = { version = "1.7.0", features = ["dialog-open"] } tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tokio = "1.40" +uuid = { version = "1", features = ["v4"] } [features] # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. diff --git a/gastronomy-ui/src/execution_trace.rs b/gastronomy-ui/src/execution_trace.rs new file mode 100644 index 0000000..2175cd6 --- /dev/null +++ b/gastronomy-ui/src/execution_trace.rs @@ -0,0 +1,113 @@ +use std::{collections::BTreeMap, fmt::Display}; + +use gastronomy::{ + execution_trace::{parse_context, parse_env, parse_raw_frames, parse_uplc_value, RawFrame}, + uplc::{self, LoadedProgram, Program}, + Frame, +}; +use pallas_codec::flat::Flat; +use tauri::InvokeError; +use tokio::sync::{mpsc, oneshot}; +use uuid::Uuid; + +pub struct ExecutionTrace { + pub identifier: String, + worker_channel: mpsc::Sender, +} + +impl ExecutionTrace { + pub fn from_program(program: LoadedProgram) -> Result { + let identifier = Uuid::new_v4().to_string(); + + // The Aiken uplc crate uses lots of Rc internally, so it's not Send. + // The string representation of a frame of execution can get HUGE, so we need to serialize it lazily. + // So, send the raw bytes to another thread, and interact with it over a channel. + let (worker_channel, requests) = mpsc::channel(16); + let worker = ExecutionTraceWorker { + raw_program: program.program.to_flat().map_err(to_invoke_error)?, + source_map: program.source_map, + requests, + }; + std::thread::Builder::new() + .name(identifier.clone()) + .stack_size(4 * 1024 * 1024) + .spawn(|| worker.run()) + .map_err(to_invoke_error)?; + + Ok(Self { + identifier, + worker_channel, + }) + } + pub async fn frame_count(&self) -> Result { + let (frame_count_sink, frame_count_source) = oneshot::channel(); + let request = WorkerRequest::FrameCount(frame_count_sink); + self.worker_channel + .send(request) + .await + .map_err(to_invoke_error)?; + frame_count_source.await.map_err(to_invoke_error)? + } + pub async fn get_frame(&self, frame: usize) -> Result { + let (frame_sink, frame_source) = oneshot::channel(); + let request = WorkerRequest::GetFrame(frame, frame_sink); + self.worker_channel + .send(request) + .await + .map_err(to_invoke_error)?; + frame_source.await.map_err(to_invoke_error)? + } +} + +fn to_invoke_error(err: T) -> InvokeError { + InvokeError::from(err.to_string()) +} + +type ResponseChannel = oneshot::Sender>; + +enum WorkerRequest { + FrameCount(ResponseChannel), + GetFrame(usize, ResponseChannel), +} + +struct ExecutionTraceWorker { + raw_program: Vec, + source_map: BTreeMap, + requests: mpsc::Receiver, +} + +impl ExecutionTraceWorker { + fn run(self) { + let program = Program::unflat(&self.raw_program).unwrap(); + let states = uplc::execute_program(program).unwrap(); + let frames = parse_raw_frames(&states, &self.source_map); + + let mut requests = self.requests; + while let Some(request) = requests.blocking_recv() { + match request { + WorkerRequest::FrameCount(res) => { + let _ = res.send(Ok(frames.len())); + } + WorkerRequest::GetFrame(index, res) => { + let _ = res.send(Self::get_frame(index, &frames)); + } + } + } + } + + fn get_frame(index: usize, frames: &[RawFrame<'_>]) -> Result { + let Some(raw) = frames.get(index) else { + return Err(InvokeError::from("Invalid frame index")); + }; + let frame = Frame { + label: raw.label.to_string(), + context: parse_context(raw.context), + env: parse_env(raw.env), + term: raw.term.to_string(), + ret_value: raw.ret_value.map(|v| parse_uplc_value(v.clone())), + location: raw.location.cloned(), + budget: raw.budget.clone(), + }; + Ok(frame) + } +} diff --git a/gastronomy-ui/src/main.rs b/gastronomy-ui/src/main.rs index 12cd39b..eada3dc 100644 --- a/gastronomy-ui/src/main.rs +++ b/gastronomy-ui/src/main.rs @@ -5,16 +5,17 @@ use std::path::{Path, PathBuf}; use api::{CreateTraceResponse, GetFrameResponse, GetTraceSummaryResponse}; use dashmap::DashMap; +use execution_trace::ExecutionTrace; use figment::providers::{Env, Serialized}; use gastronomy::{ chain_query::ChainQuery, config::{load_base_config, Config}, - ExecutionTrace, }; use tauri::{InvokeError, Manager, State, Wry}; use tauri_plugin_store::{with_store, StoreBuilder, StoreCollection}; mod api; +mod execution_trace; struct SessionState { traces: DashMap, @@ -54,11 +55,12 @@ async fn create_traces<'a>( ChainQuery::None }; - let mut traces = gastronomy::trace_executions(file, ¶meters, query) + let mut programs = gastronomy::execution_trace::load_file(file, ¶meters, query) .await .map_err(InvokeError::from_anyhow)?; let mut identifiers = vec![]; - for trace in traces.drain(..) { + for program in programs.drain(..) { + let trace = ExecutionTrace::from_program(program)?; let identifier = trace.identifier.clone(); state.traces.insert(identifier.clone(), trace); identifiers.push(identifier); @@ -67,35 +69,30 @@ async fn create_traces<'a>( } #[tauri::command] -fn get_trace_summary( +async fn get_trace_summary( identifier: &str, - state: State, + state: State<'_, SessionState>, ) -> Result { println!("Getting summary"); let Some(trace) = state.traces.get(identifier) else { return Err(InvokeError::from("Trace not found")); }; - Ok(GetTraceSummaryResponse { - frame_count: trace.frames.len(), - }) + let frame_count = trace.frame_count().await?; + Ok(GetTraceSummaryResponse { frame_count }) } #[tauri::command] -fn get_frame( +async fn get_frame( identifier: &str, frame: usize, - state: State, + state: State<'_, SessionState>, ) -> Result { println!("Getting frame"); let Some(trace) = state.traces.get(identifier) else { return Err(InvokeError::from("Trace not found")); }; - let Some(frame) = trace.frames.get(frame) else { - return Err(InvokeError::from("Frame not found")); - }; - Ok(GetFrameResponse { - frame: frame.clone(), - }) + let frame = trace.get_frame(frame).await?; + Ok(GetFrameResponse { frame }) } const STACK_SIZE: usize = 4 * 1024 * 1024; diff --git a/gastronomy/Cargo.toml b/gastronomy/Cargo.toml index 4d5765b..385bfee 100644 --- a/gastronomy/Cargo.toml +++ b/gastronomy/Cargo.toml @@ -22,4 +22,3 @@ serde = "1" serde_json = "1" uplc = { git = "https://github.com/SundaeSwap-finance/aiken.git", rev = "3c2ae7c" } # uplc = { path = "../../aiken/crates/uplc" } -uuid = { version = "1", features = ["v4"] } diff --git a/gastronomy/src/execution_trace.rs b/gastronomy/src/execution_trace.rs index dbbbdee..7500d2f 100644 --- a/gastronomy/src/execution_trace.rs +++ b/gastronomy/src/execution_trace.rs @@ -1,22 +1,16 @@ -use std::{collections::BTreeMap, path::Path}; +use std::{collections::BTreeMap, path::Path, rc::Rc}; use anyhow::Result; use serde::Serialize; -use uplc::machine::{Context, MachineState}; -use uuid::Uuid; +use uplc::{ + ast::NamedDeBruijn, + machine::{indexed_term::IndexedTerm, Context, MachineState}, +}; -use crate::chain_query::ChainQuery; +use crate::{chain_query::ChainQuery, uplc::LoadedProgram}; pub type Value = String; -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ExecutionTrace { - pub identifier: String, - pub filename: String, - pub frames: Vec, -} - #[derive(Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct Frame { @@ -45,44 +39,46 @@ pub struct ExBudget { pub mem_diff: i64, } -impl ExecutionTrace { - pub async fn from_file( - filename: &Path, - parameters: &[String], - query: ChainQuery, - ) -> Result> { - println!("from file"); - let raw_programs = crate::uplc::load_programs_from_file(filename, query).await?; - let mut execution_traces = vec![]; +pub async fn load_file( + filename: &Path, + parameters: &[String], + query: ChainQuery, +) -> Result> { + println!("from file"); + let raw_programs = crate::uplc::load_programs_from_file(filename, query).await?; + let mut programs = vec![]; - println!("{} program(s)", raw_programs.len()); - for raw_program in raw_programs { - let arguments = parameters - .iter() - .enumerate() - .map(|(index, param)| crate::uplc::parse_parameter(index, param.clone())) - .collect::>>()?; - let applied_program = crate::uplc::apply_parameters(raw_program, arguments)?; - let states = crate::uplc::execute_program(applied_program.program)?; - let frames = parse_frames(&states, applied_program.source_map); - execution_traces.push(Self { - identifier: Uuid::new_v4().to_string(), - filename: filename.display().to_string(), - frames, - }) - } - println!("Done"); - Ok(execution_traces) + println!("{} program(s)", raw_programs.len()); + for raw_program in raw_programs { + let arguments = parameters + .iter() + .enumerate() + .map(|(index, param)| crate::uplc::parse_parameter(index, param.clone())) + .collect::>>()?; + let applied_program = crate::uplc::apply_parameters(raw_program, arguments)?; + programs.push(applied_program); } + println!("Done"); + Ok(programs) +} + +pub struct RawFrame<'a> { + pub label: &'a str, + pub context: &'a Context, + pub env: &'a Rc>, + pub term: &'a IndexedTerm, + pub ret_value: Option<&'a uplc::machine::value::Value>, + pub location: Option<&'a String>, + pub budget: ExBudget, } const MAX_CPU: i64 = 10000000000; const MAX_MEM: i64 = 14000000; -fn parse_frames( - states: &[(MachineState, uplc::machine::cost_model::ExBudget)], - source_map: BTreeMap, -) -> Vec { +pub fn parse_raw_frames<'a>( + states: &'a [(MachineState, uplc::machine::cost_model::ExBudget)], + source_map: &'a BTreeMap, +) -> Vec> { let mut frames = vec![]; let mut prev_steps = 0; let mut prev_mem = 0; @@ -90,40 +86,41 @@ fn parse_frames( let (label, context, env, term, location, ret_value) = match state { MachineState::Compute(context, env, term) => ( "Compute", - parse_context(context), - parse_env(env), - term.to_string(), - term.index().and_then(|i| source_map.get(&i)).cloned(), + context, + env, + term, + term.index().and_then(|i| source_map.get(&i)), None, ), MachineState::Done(term) => { - let prev_frame: &Frame = frames.last().expect("Invalid program starts with return"); + let prev_frame: &RawFrame = + frames.last().expect("Invalid program starts with return"); ( "Done", - prev_frame.context.clone(), - prev_frame.env.clone(), - term.to_string(), - term.index().and_then(|i| source_map.get(&i)).cloned(), + prev_frame.context, + prev_frame.env, + term, + term.index().and_then(|i| source_map.get(&i)), None, ) } MachineState::Return(context, value) => { - let prev_frame: &Frame = frames.last().expect("Invalid program starts with return"); - let ret_value = parse_uplc_value(value.clone()); + let prev_frame: &RawFrame = + frames.last().expect("Invalid program starts with return"); ( "Return", - parse_context(context), - prev_frame.env.clone(), - prev_frame.term.clone(), - prev_frame.location.clone(), - Some(ret_value), + context, + prev_frame.env, + prev_frame.term, + prev_frame.location, + Some(value), ) } }; let steps = MAX_CPU - budget.cpu; let mem = MAX_MEM - budget.mem; - frames.push(Frame { - label: label.to_string(), + frames.push(RawFrame { + label, context, env, term, @@ -142,7 +139,7 @@ fn parse_frames( frames } -fn parse_context(context: &Context) -> Vec { +pub fn parse_context(context: &Context) -> Vec { let mut frames = vec![]; let mut current = Some(context); while let Some(curr) = current { @@ -165,7 +162,7 @@ fn parse_context_frame(context: &Context) -> (String, Option<&Context>) { } } -fn parse_env(env: &[uplc::machine::value::Value]) -> Vec { +pub fn parse_env(env: &[uplc::machine::value::Value]) -> Vec { env.iter() .rev() .enumerate() @@ -180,6 +177,6 @@ fn parse_env(env: &[uplc::machine::value::Value]) -> Vec { .collect() } -fn parse_uplc_value(value: uplc::machine::value::Value) -> Value { +pub fn parse_uplc_value(value: uplc::machine::value::Value) -> Value { uplc::machine::discharge::value_as_term(value).to_string() } diff --git a/gastronomy/src/lib.rs b/gastronomy/src/lib.rs index f267a07..34707b6 100644 --- a/gastronomy/src/lib.rs +++ b/gastronomy/src/lib.rs @@ -1,19 +1,6 @@ -use std::path::Path; - -use anyhow::Result; - -use chain_query::ChainQuery; -pub use execution_trace::{ExecutionTrace, Frame}; +pub use execution_trace::Frame; pub mod chain_query; pub mod config; -mod execution_trace; +pub mod execution_trace; pub mod uplc; - -pub async fn trace_executions( - filename: &Path, - parameters: &[String], - query: ChainQuery, -) -> Result> { - ExecutionTrace::from_file(filename, parameters, query).await -} diff --git a/gastronomy/src/uplc.rs b/gastronomy/src/uplc.rs index 7384ae2..810dfe6 100644 --- a/gastronomy/src/uplc.rs +++ b/gastronomy/src/uplc.rs @@ -4,8 +4,9 @@ use anyhow::{anyhow, Context, Result}; use minicbor::bytes::ByteVec; use pallas::ledger::primitives::conway::{Language, MintedTx}; use serde::Deserialize; +pub use uplc::ast::Program; use uplc::{ - ast::{FakeNamedDeBruijn, NamedDeBruijn, Program}, + ast::{FakeNamedDeBruijn, NamedDeBruijn}, machine::{ cost_model::{CostModel, ExBudget}, indexed_term::IndexedTerm, @@ -19,6 +20,7 @@ use uplc::{ use crate::chain_query::ChainQuery; pub struct LoadedProgram { + pub filename: String, pub program: Program, pub source_map: BTreeMap, } @@ -48,12 +50,14 @@ fn identify_file_type(file: &Path) -> Result { } pub async fn load_programs_from_file(file: &Path, query: ChainQuery) -> Result> { + let filename = file.display().to_string(); match identify_file_type(file)? { FileType::Uplc => { let code = fs::read_to_string(file)?; let program = parser::program(&code).unwrap().try_into()?; let source_map = BTreeMap::new(); Ok(vec![LoadedProgram { + filename, program, source_map, }]) @@ -63,6 +67,7 @@ pub async fn load_programs_from_file(file: &Path, query: ChainQuery) -> Result::from_flat(&bytes)?.into(); let source_map = BTreeMap::new(); Ok(vec![LoadedProgram { + filename, program, source_map, }]) @@ -74,6 +79,7 @@ pub async fn load_programs_from_file(file: &Path, query: ChainQuery) -> Result::from_flat(&cbor)?.into(); let source_map = export.source_map.unwrap_or_default(); Ok(vec![LoadedProgram { + filename, program, source_map, }]) @@ -82,17 +88,21 @@ pub async fn load_programs_from_file(file: &Path, query: ChainQuery) -> Result { let bytes = std::fs::read(file)?; let multi_era_tx = MintedTx::decode_fragment(&bytes).unwrap(); - load_programs_from_tx(multi_era_tx, query).await + load_programs_from_tx(filename, multi_era_tx, query).await } } } -async fn load_programs_from_tx(tx: MintedTx<'_>, query: ChainQuery) -> Result> { +async fn load_programs_from_tx( + filename: String, + tx: MintedTx<'_>, + query: ChainQuery, +) -> Result> { println!("loading programs from tx"); let mut inputs: Vec<_> = tx.transaction_body.inputs.iter().cloned().collect(); if let Some(ref_inputs) = &tx.transaction_body.reference_inputs { @@ -113,6 +123,7 @@ async fn load_programs_from_tx(tx: MintedTx<'_>, query: ChainQuery) -> Result Result { pub fn apply_parameters( LoadedProgram { + filename, program, source_map, }: LoadedProgram, @@ -154,6 +166,7 @@ pub fn apply_parameters( .map(|(index, location)| (index + source_map_offset, location)) .collect(); Ok(LoadedProgram { + filename, program, source_map, })