diff --git a/Cargo.lock b/Cargo.lock index 29d0f173718..a8fe8228706 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -601,6 +601,7 @@ dependencies = [ "boa_engine", "console_error_panic_hook", "getrandom", + "js-sys", "wasm-bindgen", "wasm-bindgen-test", ] diff --git a/cli/src/debug/function.rs b/cli/src/debug/function.rs index 8337ba708b7..9d932bd04a0 100644 --- a/cli/src/debug/function.rs +++ b/cli/src/debug/function.rs @@ -2,7 +2,10 @@ use boa_engine::{ builtins::function::OrdinaryFunction, js_string, object::ObjectInitializer, - vm::flowgraph::{Direction, Graph}, + vm::{ + flowgraph::{Direction, Graph}, + trace::{TraceAction, Tracer}, + }, Context, JsArgs, JsNativeError, JsObject, JsResult, JsValue, NativeFunction, }; @@ -121,6 +124,39 @@ fn bytecode(_: &JsValue, args: &[JsValue], _: &mut Context) -> JsResult Ok(js_string!(code.to_string()).into()) } +// ==== Trace functionality ==== + +#[derive(Debug)] +struct FunctionTracer; + +impl Tracer for FunctionTracer { + fn should_trace(&self, frame: &boa_engine::vm::CallFrame) -> TraceAction { + if frame.code_block().traceable() { + if frame.code_block().was_traced() { + return TraceAction::Block; + } + return TraceAction::BlockWithBytecode; + } + TraceAction::None + } + + fn emit_bytecode_trace(&self, msg: &str) { + println!("{msg}"); + } + + fn emit_call_frame_entrance_trace(&self, msg: &str) { + println!("{msg}"); + } + + fn emit_call_frame_exit_trace(&self, msg: &str) { + println!("{msg}"); + } + + fn emit_instruction_trace(&self, msg: &str) { + println!("{msg}"); + } +} + fn set_trace_flag_in_function_object(object: &JsObject, value: bool) -> JsResult<()> { let Some(function) = object.downcast_ref::() else { return Err(JsNativeError::typ() @@ -145,6 +181,7 @@ fn trace(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult JsResult JsResult { +fn traceable(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { let value = args.get_or_undefined(0); let traceable = args.get_or_undefined(1).to_boolean(); @@ -162,6 +199,7 @@ fn traceable(_: &JsValue, args: &[JsValue], _: &mut Context) -> JsResult Result<(), io::Error> { add_runtime(&mut context); // Trace Output - context.set_trace(args.trace); + if args.trace { + context.init_trace(); + } if args.debug_object { init_boa_debug_object(&mut context); diff --git a/core/engine/src/context/mod.rs b/core/engine/src/context/mod.rs index 32df528b35d..486711e0f9d 100644 --- a/core/engine/src/context/mod.rs +++ b/core/engine/src/context/mod.rs @@ -12,6 +12,10 @@ pub use icu::IcuError; use intrinsics::Intrinsics; use crate::vm::RuntimeLimits; + +#[cfg(feature = "trace")] +use crate::vm::trace; + use crate::{ builtins, class::{Class, ClassBuilder}, @@ -445,11 +449,17 @@ impl Context { &self.vm.realm } - /// Set the value of trace on the context #[cfg(feature = "trace")] #[inline] - pub fn set_trace(&mut self, trace: bool) { - self.vm.trace = trace; + /// Initializes the default `Vm` trace from the context + pub fn init_trace(&mut self) { + self.vm.trace.activate_trace(); + } + + #[cfg(feature = "trace")] + /// Sets custom handling of trace messages. + pub fn set_tracer_implementation(&mut self, tracer: Box) { + self.vm.trace.set_tracer(tracer); } /// Get optimizer options. diff --git a/core/engine/src/vm/code_block.rs b/core/engine/src/vm/code_block.rs index 502f658c85f..9865589ef3b 100644 --- a/core/engine/src/vm/code_block.rs +++ b/core/engine/src/vm/code_block.rs @@ -69,7 +69,11 @@ bitflags! { /// Trace instruction execution to `stdout`. #[cfg(feature = "trace")] - const TRACEABLE = 0b1000_0000_0000_0000; + const TRACEABLE = 0b0100_0000_0000_0000; + + /// Stores whether the `CodeBlock` has been traced. + #[cfg(feature = "trace")] + const WAS_TRACED = 0b1000_0000_0000_0000; } } @@ -194,7 +198,7 @@ impl CodeBlock { /// Check if the function is traced. #[cfg(feature = "trace")] - pub(crate) fn traceable(&self) -> bool { + pub fn traceable(&self) -> bool { self.flags.get().contains(CodeBlockFlags::TRACEABLE) } /// Enable or disable instruction tracing to `stdout`. @@ -206,6 +210,20 @@ impl CodeBlock { self.flags.set(flags); } + /// Returns whether the frame has been previously traced. + #[cfg(feature = "trace")] + pub fn was_traced(&self) -> bool { + self.flags.get().contains(CodeBlockFlags::WAS_TRACED) + } + + /// Set the current frame as traced. + #[cfg(feature = "trace")] + pub fn set_frame_traced(&self, value: bool) { + let mut flags = self.flags.get(); + flags.set(CodeBlockFlags::WAS_TRACED, value); + self.flags.set(flags); + } + /// Check if the function is a class constructor. pub(crate) fn is_class_constructor(&self) -> bool { self.flags diff --git a/core/engine/src/vm/mod.rs b/core/engine/src/vm/mod.rs index 46fcdfc297d..0f10581e9f2 100644 --- a/core/engine/src/vm/mod.rs +++ b/core/engine/src/vm/mod.rs @@ -21,6 +21,9 @@ mod code_block; mod completion_record; mod inline_cache; mod opcode; +#[cfg(feature = "trace")] +pub mod trace; + mod runtime_limits; #[cfg(feature = "flowgraph")] @@ -28,10 +31,14 @@ pub mod flowgraph; pub(crate) use inline_cache::InlineCache; +#[cfg(feature = "trace")] +use trace::VmTrace; + // TODO: see if this can be exposed on all features. #[allow(unused_imports)] pub(crate) use opcode::{Instruction, InstructionIterator, Opcode, VaryingOperandKind}; pub use runtime_limits::RuntimeLimits; + pub use { call_frame::{CallFrame, GeneratorResumeKind}, code_block::CodeBlock, @@ -76,7 +83,7 @@ pub struct Vm { pub(crate) realm: Realm, #[cfg(feature = "trace")] - pub(crate) trace: bool, + pub(crate) trace: VmTrace, } /// Active runnable in the current vm context. @@ -108,7 +115,7 @@ impl Vm { native_active_function: None, realm, #[cfg(feature = "trace")] - trace: false, + trace: VmTrace::default(), } } @@ -181,6 +188,9 @@ impl Vm { } self.frames.push(frame); + + #[cfg(feature = "trace")] + self.trace.trace_call_frame(self); } pub(crate) fn push_frame_with_stack( @@ -193,6 +203,9 @@ impl Vm { self.push(function); self.push_frame(frame); + + #[cfg(feature = "trace")] + self.trace.trace_call_frame(self); } pub(crate) fn pop_frame(&mut self) -> Option { @@ -264,38 +277,6 @@ pub(crate) enum CompletionType { #[cfg(feature = "trace")] impl Context { - const COLUMN_WIDTH: usize = 26; - const TIME_COLUMN_WIDTH: usize = Self::COLUMN_WIDTH / 2; - const OPCODE_COLUMN_WIDTH: usize = Self::COLUMN_WIDTH; - const OPERAND_COLUMN_WIDTH: usize = Self::COLUMN_WIDTH; - const NUMBER_OF_COLUMNS: usize = 4; - - pub(crate) fn trace_call_frame(&self) { - let msg = if self.vm.frames.last().is_some() { - format!( - " Call Frame -- {} ", - self.vm.frame().code_block().name().to_std_string_escaped() - ) - } else { - " VM Start ".to_string() - }; - - println!("{}", self.vm.frame().code_block); - println!( - "{msg:-^width$}", - width = Self::COLUMN_WIDTH * Self::NUMBER_OF_COLUMNS - 10 - ); - println!( - "{:(&mut self, f: F) -> JsResult where F: FnOnce(Opcode, &mut Context) -> JsResult, @@ -368,13 +349,12 @@ impl Context { VaryingOperandKind::U32 => ".U32", }; - println!( - "{: { let mut fp = self.vm.frame().fp(); let mut env_fp = self.vm.frame().env_fp; + #[cfg(feature = "trace")] + self.vm.trace.trace_frame_end(&self.vm, "Throw"); if self.vm.frame().exit_early() { self.vm.environments.truncate(env_fp as usize); self.vm.stack.truncate(fp as usize); @@ -519,6 +506,9 @@ impl Context { } // Early return immediately. CompletionType::Yield => { + #[cfg(feature = "trace")] + self.vm.trace.trace_frame_end(&self.vm, "Yield"); + let result = self.vm.take_return_value(); if self.vm.frame().exit_early() { return ControlFlow::Break(CompletionRecord::Return(result)); @@ -538,11 +528,6 @@ impl Context { pub(crate) async fn run_async_with_budget(&mut self, budget: u32) -> CompletionRecord { let _timer = Profiler::global().start_event("run_async_with_budget", "vm"); - #[cfg(feature = "trace")] - if self.vm.trace { - self.trace_call_frame(); - } - let mut runtime_budget: u32 = budget; loop { @@ -563,11 +548,6 @@ impl Context { pub(crate) fn run(&mut self) -> CompletionRecord { let _timer = Profiler::global().start_event("run", "vm"); - #[cfg(feature = "trace")] - if self.vm.trace { - self.trace_call_frame(); - } - loop { match self.execute_one(Opcode::execute) { ControlFlow::Continue(()) => {} diff --git a/core/engine/src/vm/trace.rs b/core/engine/src/vm/trace.rs new file mode 100644 index 00000000000..441e60a4f17 --- /dev/null +++ b/core/engine/src/vm/trace.rs @@ -0,0 +1,236 @@ +//! Boa's `Trace` module for the `Vm`. + +use std::collections::VecDeque; +use std::fmt; + +use super::{CallFrame, Constant, Vm}; + +// TODO: Build out further, maybe provide more element visiblity and events/outputs +/// The `Tracer` trait is a customizable trait that can be provided to `Boa` +/// for customizing output. +pub trait Tracer: fmt::Debug { + /// Whether the current call frame should trace. + fn should_trace(&self, frame: &CallFrame) -> TraceAction { + if frame.code_block.name().to_std_string_escaped().as_str() == "
" { + if frame.code_block().was_traced() { + return TraceAction::Block; + } + frame.code_block().set_frame_traced(true); + return TraceAction::BlockWithFullBytecode; + } + TraceAction::Block + } + /// The output from tracing a `CodeBlock`'s bytecode. + fn emit_bytecode_trace(&self, msg: &str); + /// The output from entering a `CallFrame`. + fn emit_call_frame_entrance_trace(&self, msg: &str); + /// The trace output from an execution. + fn emit_instruction_trace(&self, msg: &str); + /// Trace output from exiting a `CallFrame`. + fn emit_call_frame_exit_trace(&self, msg: &str); +} + +/// `TraceAction` Determines the action that should occur +#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] +pub enum TraceAction { + /// No trace + None = 0, + /// Traces the frames code block + Block, + /// Partial codeblock with bytecode + BlockWithBytecode, + /// Full trace with the compiled bytecode + BlockWithFullBytecode, +} + +#[derive(Debug)] +pub(crate) struct ActiveTracer; + +impl Tracer for ActiveTracer { + fn emit_bytecode_trace(&self, msg: &str) { + println!("{msg}"); + } + + fn emit_call_frame_entrance_trace(&self, msg: &str) { + println!("{msg}"); + } + + fn emit_instruction_trace(&self, msg: &str) { + println!("{msg}"); + } + + fn emit_call_frame_exit_trace(&self, msg: &str) { + println!("{msg}"); + } +} + +/// `VmTrace` is a boa spcific structure for running Boa's Virtual Machine trace. +/// +/// It holds registered `Tracer` implementations and actions messages depending on +/// the `should_trace` method of the `Tracers`. +#[derive(Default)] +pub struct VmTrace { + tracers: Vec>, +} + +// ==== Public API ==== + +impl VmTrace { + /// Method for adding a compiled action on initialization. + pub fn set_tracer(&mut self, tracer: Box) { + self.tracers.push(tracer); + } + + /// Returns whether there is an active trace request. + #[must_use] + pub(crate) fn should_trace(&self, frame: &CallFrame) -> bool { + self.trace_action(frame) != TraceAction::None + } + + /// Returns the folded `TraceAction` of the current `Tracer`s + pub(crate) fn trace_action(&self, frame: &CallFrame) -> TraceAction { + self.tracers + .iter() + .fold(TraceAction::None, |a, b| a.max(b.should_trace(frame))) + } + + /// Adds Boa's default implementation of `Tracer` onto `VmTrace`'s current traces. + pub fn activate_trace(&mut self) { + self.tracers.push(Box::new(ActiveTracer)); + } +} + +// ==== Trace Event/Action Methods ==== + +impl VmTrace { + const COLUMN_WIDTH: usize = 26; + const TIME_COLUMN_WIDTH: usize = Self::COLUMN_WIDTH / 2; + const OPCODE_COLUMN_WIDTH: usize = Self::COLUMN_WIDTH; + const OPERAND_COLUMN_WIDTH: usize = Self::COLUMN_WIDTH; + const NUMBER_OF_COLUMNS: usize = 4; + + /// Trace the current `CallFrame` according to current state + pub(crate) fn trace_call_frame(&self, vm: &Vm) { + let action = self.trace_action(vm.frame()); + match action { + TraceAction::Block => { + self.call_frame_header(vm); + } + TraceAction::BlockWithFullBytecode => { + self.trace_compiled_bytecode(vm); + self.call_frame_header(vm); + } + TraceAction::BlockWithBytecode => { + self.trace_current_bytecode(vm); + self.call_frame_header(vm); + } + TraceAction::None => {} + } + } + + /// Emits the current `CallFrame`'s header. + pub(crate) fn call_frame_header(&self, vm: &Vm) { + let msg = format!( + " Call Frame -- {} ", + vm.frame().code_block().name().to_std_string_escaped() + ); + + let frame_header = format!( + "{msg:-^width$}", + width = Self::COLUMN_WIDTH * Self::NUMBER_OF_COLUMNS - 10 + ); + + for t in &self.tracers { + t.emit_call_frame_entrance_trace(&frame_header); + } + + if vm.frames.len() == 1 { + let column_headers = format!( + "{: ", + vm.frame().code_block().name.to_std_string_escaped() + ); + let frame_footer = format!( + "{msg:-^width$}", + width = Self::COLUMN_WIDTH * Self::NUMBER_OF_COLUMNS - 10 + ); + + for t in &self.tracers { + t.emit_call_frame_exit_trace(&frame_footer); + } + } + } + + pub(crate) fn trace_instruction( + &self, + duration: u128, + operand_kind: &str, + opcode: &str, + operands: &str, + stack: &str, + ) { + let instruction_trace = format!( + "{:) -> fmt::Result { + write!(f, "{:?}", self.tracers) + } +} diff --git a/ffi/wasm/Cargo.toml b/ffi/wasm/Cargo.toml index 2445c26a696..b1db23c1a2f 100644 --- a/ffi/wasm/Cargo.toml +++ b/ffi/wasm/Cargo.toml @@ -12,10 +12,11 @@ repository.workspace = true rust-version.workspace = true [dependencies] -boa_engine = { workspace = true, features = ["js"] } +boa_engine = { workspace = true, features = ["js", "trace"] } wasm-bindgen = { version = "0.2.91", default-features = false } getrandom = { version = "0.2.14", features = ["js"] } console_error_panic_hook = "0.1.7" +js-sys = "0.3.66" [dev-dependencies] wasm-bindgen-test = "0.3.42" diff --git a/ffi/wasm/src/lib.rs b/ffi/wasm/src/lib.rs index 16a6974f6ef..d7a1bcea3a5 100644 --- a/ffi/wasm/src/lib.rs +++ b/ffi/wasm/src/lib.rs @@ -2,8 +2,9 @@ #![cfg_attr(not(test), forbid(clippy::unwrap_used))] #![allow(unused_crate_dependencies)] -use boa_engine::{Context, Source}; +use boa_engine::{vm::trace::Tracer, Context, Source}; use getrandom as _; +use std::fmt; use wasm_bindgen::prelude::*; #[wasm_bindgen(start)] @@ -24,3 +25,179 @@ pub fn evaluate(src: &str) -> Result { .map_err(|e| JsValue::from(format!("Uncaught {e}"))) .map(|v| v.display().to_string()) } + +#[wasm_bindgen] +/// Evaluate some JavaScript with trace hooks. +/// +/// # Errors +/// +/// If the execution of the script throws, returns a `JsValue` with the error string. +pub fn evaluate_with_debug_hooks( + src: &str, + compiled_output_action: &js_sys::Function, + trace_output_action: &js_sys::Function, +) -> Result { + let compiled_clone = compiled_output_action.clone(); + let compiled_action = move |output: &str| { + let this = JsValue::null(); + let o = JsValue::from(output); + let _unused = compiled_clone.call1(&this, &o); + }; + + let trace_clone = trace_output_action.clone(); + let trace_action = move |output: &str| { + let this = JsValue::null(); + let o = JsValue::from(output); + let _unused = trace_clone.call1(&this, &o); + }; + + // setup executor + let mut context = Context::default(); + let mut tracer = WasmTracer::default(); + tracer.set_compiled_handler(Box::new(compiled_action)); + tracer.set_trace_handler(Box::new(trace_action)); + + context.set_tracer_implementation(Box::new(tracer)); + + context + .eval(Source::from_bytes(src)) + .map_err(|e| JsValue::from(format!("Uncaught {e}"))) + .map(|v| v.display().to_string()) +} + +#[derive(Debug)] +#[wasm_bindgen] +/// The WASM exposed `BoaJs` Object. +pub struct BoaJs { + compiled_action: Option, + trace_action: Option, +} + +#[wasm_bindgen] +impl BoaJs { + /// Create a new BoaJs Object. + #[must_use] + #[wasm_bindgen(constructor)] + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + compiled_action: None, + trace_action: None, + } + } + + /// Set a Js Closure action for handling Boa's ByteCompiler trace output. + pub fn set_compiled_output_action(&mut self, f: &js_sys::Function) { + let fun = f.clone(); + self.compiled_action = Some(fun); + } + + /// Set a Js Closure action for handling Boa's VM Trace output. + pub fn set_trace_output_action(&mut self, f: &js_sys::Function) { + let fun = f.clone(); + self.trace_action = Some(fun); + } + + /// Evaluate some Js Source Code with trace active. + /// + /// # Errors + /// + /// If the execution of the script throws, returns a `JsValue` with the error string. + pub fn evaluate_with_trace(&self, src: &str) -> Result { + // setup executor + let mut context = Context::default(); + + let mut tracer = WasmTracer::default(); + + if let Some(func) = &self.compiled_action { + let func_clone = func.clone(); + let action = move |output: &str| { + let this = JsValue::null(); + let o = JsValue::from(output); + let _unused = func_clone.call1(&this, &o); + }; + + tracer.set_compiled_handler(Box::new(action)); + }; + + if let Some(func) = &self.trace_action { + let func_clone = func.clone(); + let action = move |output: &str| { + let this = JsValue::null(); + let o = JsValue::from(output); + let _unused = func_clone.call1(&this, &o); + }; + + tracer.set_trace_handler(Box::new(action)); + }; + + context.set_tracer_implementation(Box::new(tracer)); + + context + .eval(Source::from_bytes(src)) + .map_err(|e| JsValue::from(format!("Uncaught {e}"))) + .map(|v| v.display().to_string()) + } + + /// Evaluate Js Source code without running trace. + /// + /// # Errors + /// + /// If the execution of the script throws, returns a `JsValue` with the error string. + pub fn evaluate(&self, src: &str) -> Result { + Context::default() + .eval(Source::from_bytes(src)) + .map_err(|e| JsValue::from(format!("Uncaught {e}"))) + .map(|v| v.display().to_string()) + } +} + +type ProvidedFunction = Box; + +#[derive(Default)] +pub(crate) struct WasmTracer { + compiled_handler: Option, + trace_handler: Option, +} + +impl fmt::Debug for WasmTracer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "WasmTracer") + } +} + +impl WasmTracer { + fn set_compiled_handler(&mut self, compiled_handler: Box) { + self.compiled_handler = Some(compiled_handler); + } + + fn set_trace_handler(&mut self, trace_handler: Box) { + self.trace_handler = Some(trace_handler); + } +} + +impl Tracer for WasmTracer { + fn emit_bytecode_trace(&self, msg: &str) { + if let Some(action) = &self.compiled_handler { + action(msg); + } + } + + fn emit_call_frame_entrance_trace(&self, msg: &str) { + if let Some(action) = &self.trace_handler { + action(msg); + } + } + + fn emit_instruction_trace(&self, msg: &str) { + if let Some(action) = &self.trace_handler { + action(msg); + } + } + + fn emit_call_frame_exit_trace(&self, msg: &str) { + if let Some(action) = &self.trace_handler { + action(msg); + } + } +}