mirror of https://github.com/boa-dev/boa.git
Browse Source
Currently some debugging stuff in JavaScript land is difficult to impossible, like triggering a GC collect, this is not impossible to do in JavaScript the way I triggered it was by creating a huge amount of object `for (let i = 0; i < 100000; ++i) { ({}) }` but this is cumbersome and not guaranteed to trigger a gc. This PR implements `--debug-object` flag that injects the `$boa` debug object in the context, the object is separated into modules currently `gc`, `function`, `object`. We can now do `$boa.gc.collect()`, which force triggers a GC collect. Or sometimes I wanted a trace (the current solution is great, you can trace stuff like `>>> 1 + 1` but that is also it's limitation), it traces everything, I sometimes have a scenario and just want to trace a single function in that scenario, that's why I added the `$boa.function.trace(func, this, ...args)` It only traces the function. ```js >> $boa.function.trace((a, b) => a + b, undefined, 1, 2) -------------------------Compiled Output: ''-------------------------- Location Count Opcode Operands 000000 0000 DefInitArg 0000: 'a' 000005 0001 DefInitArg 0001: 'b' 000010 0002 RestParameterPop 000011 0003 GetName 0000: 'a' 000016 0004 GetName 0001: 'b' 000021 0005 Add 000022 0006 Return 000023 0007 PushUndefined 000024 0008 Return ... (cut for brevity) ... ``` It also implements `$boa.function.flowgraph(func, options)`: ```js $boa.function.flowgraph(func, 'graphviz') $boa.function.flowgraph(func, { format: 'mermaid', direction: 'TopBottom' }) ``` Printing the object pointer: ```js $boa.object.id({}) // '0x566464F33' ``` It currently implements some functionality which we can grow it with our debugging needs since we are not restricted by a spec we can add whatever we want :) I was originally going to implement this in #2723 (but the PR is too big), for shapes having functions like: ```js $boa.shape.type({}) // Shared shape $boa.shape.id({}) // 0x8578FG355 (objects, shape pointer) $boa.shape.flowgraph({}) // printing the shape transition chain, like $boa.function.flowgraph ``` Shapes chains are very hard to debug once they are big... so having this type of debugging capability would make it much easier.pull/2787/head
Haled Odat
2 years ago
14 changed files with 586 additions and 1 deletions
@ -0,0 +1,193 @@
|
||||
use boa_engine::{ |
||||
builtins::function::Function, |
||||
object::ObjectInitializer, |
||||
vm::flowgraph::{Direction, Graph}, |
||||
Context, JsArgs, JsNativeError, JsObject, JsResult, JsValue, NativeFunction, |
||||
}; |
||||
use boa_interner::ToInternedString; |
||||
|
||||
use crate::FlowgraphFormat; |
||||
|
||||
fn flowgraph_parse_format_option(value: &JsValue) -> JsResult<FlowgraphFormat> { |
||||
if value.is_undefined() { |
||||
return Ok(FlowgraphFormat::Mermaid); |
||||
} |
||||
|
||||
if let Some(string) = value.as_string() { |
||||
return match string.to_std_string_escaped().to_lowercase().as_str() { |
||||
"mermaid" => Ok(FlowgraphFormat::Mermaid), |
||||
"graphviz" => Ok(FlowgraphFormat::Graphviz), |
||||
format => Err(JsNativeError::typ() |
||||
.with_message(format!("Unknown format type '{format}'")) |
||||
.into()), |
||||
}; |
||||
} |
||||
|
||||
Err(JsNativeError::typ() |
||||
.with_message("format type must be a string") |
||||
.into()) |
||||
} |
||||
|
||||
fn flowgraph_parse_direction_option(value: &JsValue) -> JsResult<Direction> { |
||||
if value.is_undefined() { |
||||
return Ok(Direction::LeftToRight); |
||||
} |
||||
|
||||
if let Some(string) = value.as_string() { |
||||
return match string.to_std_string_escaped().to_lowercase().as_str() { |
||||
"leftright" | "lr" => Ok(Direction::LeftToRight), |
||||
"rightleft" | "rl" => Ok(Direction::RightToLeft), |
||||
"topbottom" | "tb" => Ok(Direction::TopToBottom), |
||||
"bottomtop" | "bt " => Ok(Direction::BottomToTop), |
||||
direction => Err(JsNativeError::typ() |
||||
.with_message(format!("Unknown direction type '{direction}'")) |
||||
.into()), |
||||
}; |
||||
} |
||||
|
||||
Err(JsNativeError::typ() |
||||
.with_message("direction type must be a string") |
||||
.into()) |
||||
} |
||||
|
||||
/// Get functions instruction flowgraph
|
||||
fn flowgraph(_this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let Some(value) = args.get(0) else { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("expected function argument") |
||||
.into()); |
||||
}; |
||||
|
||||
let Some(object) = value.as_object() else { |
||||
return Err(JsNativeError::typ() |
||||
.with_message(format!("expected object, got {}", value.type_of())) |
||||
.into()); |
||||
}; |
||||
|
||||
let mut format = FlowgraphFormat::Mermaid; |
||||
let mut direction = Direction::LeftToRight; |
||||
if let Some(arguments) = args.get(1) { |
||||
if let Some(arguments) = arguments.as_object() { |
||||
format = flowgraph_parse_format_option(&arguments.get("format", context)?)?; |
||||
direction = flowgraph_parse_direction_option(&arguments.get("direction", context)?)?; |
||||
} else if value.is_string() { |
||||
format = flowgraph_parse_format_option(value)?; |
||||
} else { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("options argument must be a string or object") |
||||
.into()); |
||||
} |
||||
} |
||||
|
||||
let object = object.borrow(); |
||||
|
||||
let Some(function) = object.as_function() else { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("expected function object") |
||||
.into()); |
||||
}; |
||||
|
||||
let code = match function { |
||||
Function::Ordinary { code, .. } |
||||
| Function::Async { code, .. } |
||||
| Function::Generator { code, .. } |
||||
| Function::AsyncGenerator { code, .. } => code, |
||||
Function::Native { .. } => { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("native functions do not have bytecode") |
||||
.into()) |
||||
} |
||||
}; |
||||
|
||||
let mut graph = Graph::new(direction); |
||||
code.to_graph(context.interner(), graph.subgraph(String::default())); |
||||
let result = match format { |
||||
FlowgraphFormat::Graphviz => graph.to_graphviz_format(), |
||||
FlowgraphFormat::Mermaid => graph.to_mermaid_format(), |
||||
}; |
||||
|
||||
Ok(JsValue::new(result)) |
||||
} |
||||
|
||||
fn bytecode(_: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let Some(value) = args.get(0) else { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("expected function argument") |
||||
.into()); |
||||
}; |
||||
|
||||
let Some(object) = value.as_object() else { |
||||
return Err(JsNativeError::typ() |
||||
.with_message(format!("expected object, got {}", value.type_of())) |
||||
.into()); |
||||
}; |
||||
let object = object.borrow(); |
||||
let Some(function) = object.as_function() else { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("expected function object") |
||||
.into()); |
||||
}; |
||||
let code = match function { |
||||
Function::Ordinary { code, .. } |
||||
| Function::Async { code, .. } |
||||
| Function::Generator { code, .. } |
||||
| Function::AsyncGenerator { code, .. } => code, |
||||
Function::Native { .. } => { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("native functions do not have bytecode") |
||||
.into()) |
||||
} |
||||
}; |
||||
|
||||
Ok(code.to_interned_string(context.interner()).into()) |
||||
} |
||||
|
||||
fn set_trace_flag_in_function_object(object: &JsObject, value: bool) -> JsResult<()> { |
||||
let object = object.borrow(); |
||||
let Some(function) = object.as_function() else { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("expected function object") |
||||
.into()); |
||||
}; |
||||
let code = match function { |
||||
Function::Ordinary { code, .. } |
||||
| Function::Async { code, .. } |
||||
| Function::Generator { code, .. } |
||||
| Function::AsyncGenerator { code, .. } => code, |
||||
Function::Native { .. } => { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("native functions do not have bytecode") |
||||
.into()) |
||||
} |
||||
}; |
||||
code.set_trace(value); |
||||
Ok(()) |
||||
} |
||||
|
||||
/// Trace function.
|
||||
fn trace(_: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let value = args.get_or_undefined(0); |
||||
let this = args.get_or_undefined(1); |
||||
|
||||
let Some(callable) = value.as_callable() else { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("expected callable object") |
||||
.into()); |
||||
}; |
||||
|
||||
let arguments = args.get(2..).unwrap_or(&[]); |
||||
|
||||
set_trace_flag_in_function_object(callable, true)?; |
||||
let result = callable.call(this, arguments, context); |
||||
set_trace_flag_in_function_object(callable, false)?; |
||||
|
||||
result |
||||
} |
||||
|
||||
pub(super) fn create_object(context: &mut Context<'_>) -> JsObject { |
||||
ObjectInitializer::new(context) |
||||
.function(NativeFunction::from_fn_ptr(flowgraph), "flowgraph", 1) |
||||
.function(NativeFunction::from_fn_ptr(bytecode), "bytecode", 1) |
||||
.function(NativeFunction::from_fn_ptr(trace), "trace", 1) |
||||
.build() |
||||
} |
@ -0,0 +1,13 @@
|
||||
use boa_engine::{object::ObjectInitializer, Context, JsObject, JsResult, JsValue, NativeFunction}; |
||||
|
||||
/// Trigger garbage collection.
|
||||
fn collect(_: &JsValue, _: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
boa_gc::force_collect(); |
||||
Ok(JsValue::undefined()) |
||||
} |
||||
|
||||
pub(super) fn create_object(context: &mut Context<'_>) -> JsObject { |
||||
ObjectInitializer::new(context) |
||||
.function(NativeFunction::from_fn_ptr(collect), "collect", 0) |
||||
.build() |
||||
} |
@ -0,0 +1,48 @@
|
||||
// Allow lint so it, doesn't warn about `JsResult<>` unneeded return on functions.
|
||||
#![allow(clippy::unnecessary_wraps)] |
||||
|
||||
use boa_engine::{object::ObjectInitializer, property::Attribute, Context, JsObject}; |
||||
|
||||
mod function; |
||||
mod gc; |
||||
mod object; |
||||
mod optimizer; |
||||
|
||||
fn create_boa_object(context: &mut Context<'_>) -> JsObject { |
||||
let function_module = function::create_object(context); |
||||
let object_module = object::create_object(context); |
||||
let optimizer_module = optimizer::create_object(context); |
||||
let gc_module = gc::create_object(context); |
||||
|
||||
ObjectInitializer::new(context) |
||||
.property( |
||||
"function", |
||||
function_module, |
||||
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.property( |
||||
"object", |
||||
object_module, |
||||
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.property( |
||||
"optimizer", |
||||
optimizer_module, |
||||
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.property( |
||||
"gc", |
||||
gc_module, |
||||
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
) |
||||
.build() |
||||
} |
||||
|
||||
pub(crate) fn init_boa_debug_object(context: &mut Context<'_>) { |
||||
let boa_object = create_boa_object(context); |
||||
context.register_global_property( |
||||
"$boa", |
||||
boa_object, |
||||
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, |
||||
); |
||||
} |
@ -0,0 +1,27 @@
|
||||
use boa_engine::{ |
||||
object::ObjectInitializer, Context, JsNativeError, JsObject, JsResult, JsValue, NativeFunction, |
||||
}; |
||||
|
||||
/// Returns objects pointer in memory.
|
||||
fn id(_: &JsValue, args: &[JsValue], _: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let Some(value) = args.get(0) else { |
||||
return Err(JsNativeError::typ() |
||||
.with_message("expected object argument") |
||||
.into()); |
||||
}; |
||||
|
||||
let Some(object) = value.as_object() else { |
||||
return Err(JsNativeError::typ() |
||||
.with_message(format!("expected object, got {}", value.type_of())) |
||||
.into()); |
||||
}; |
||||
|
||||
let ptr: *const _ = object.as_ref(); |
||||
Ok(format!("0x{:X}", ptr as usize).into()) |
||||
} |
||||
|
||||
pub(super) fn create_object(context: &mut Context<'_>) -> JsObject { |
||||
ObjectInitializer::new(context) |
||||
.function(NativeFunction::from_fn_ptr(id), "id", 1) |
||||
.build() |
||||
} |
@ -0,0 +1,82 @@
|
||||
use boa_engine::{ |
||||
object::{FunctionObjectBuilder, ObjectInitializer}, |
||||
optimizer::OptimizerOptions, |
||||
property::Attribute, |
||||
Context, JsArgs, JsObject, JsResult, JsValue, NativeFunction, |
||||
}; |
||||
|
||||
fn get_constant_folding( |
||||
_: &JsValue, |
||||
_: &[JsValue], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
Ok(context |
||||
.optimizer_options() |
||||
.contains(OptimizerOptions::CONSTANT_FOLDING) |
||||
.into()) |
||||
} |
||||
|
||||
fn set_constant_folding( |
||||
_: &JsValue, |
||||
args: &[JsValue], |
||||
context: &mut Context<'_>, |
||||
) -> JsResult<JsValue> { |
||||
let value = args.get_or_undefined(0).to_boolean(); |
||||
let mut options = context.optimizer_options(); |
||||
options.set(OptimizerOptions::CONSTANT_FOLDING, value); |
||||
context.set_optimizer_options(options); |
||||
Ok(JsValue::undefined()) |
||||
} |
||||
|
||||
fn get_statistics(_: &JsValue, _: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> { |
||||
Ok(context |
||||
.optimizer_options() |
||||
.contains(OptimizerOptions::STATISTICS) |
||||
.into()) |
||||
} |
||||
|
||||
fn set_statistics(_: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> { |
||||
let value = args.get_or_undefined(0).to_boolean(); |
||||
let mut options = context.optimizer_options(); |
||||
options.set(OptimizerOptions::STATISTICS, value); |
||||
context.set_optimizer_options(options); |
||||
Ok(JsValue::undefined()) |
||||
} |
||||
|
||||
pub(super) fn create_object(context: &mut Context<'_>) -> JsObject { |
||||
let get_constant_folding = |
||||
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(get_constant_folding)) |
||||
.name("get constantFolding") |
||||
.length(0) |
||||
.build(); |
||||
let set_constant_folding = |
||||
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(set_constant_folding)) |
||||
.name("set constantFolding") |
||||
.length(1) |
||||
.build(); |
||||
|
||||
let get_statistics = |
||||
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(get_statistics)) |
||||
.name("get statistics") |
||||
.length(0) |
||||
.build(); |
||||
let set_statistics = |
||||
FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(set_statistics)) |
||||
.name("set statistics") |
||||
.length(1) |
||||
.build(); |
||||
ObjectInitializer::new(context) |
||||
.accessor( |
||||
"constantFolding", |
||||
Some(get_constant_folding), |
||||
Some(set_constant_folding), |
||||
Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, |
||||
) |
||||
.accessor( |
||||
"statistics", |
||||
Some(get_statistics), |
||||
Some(set_statistics), |
||||
Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, |
||||
) |
||||
.build() |
||||
} |
@ -0,0 +1,150 @@
|
||||
# Boa Debug Object |
||||
|
||||
The `$boa` object contains useful utilities that can be used to debug JavaScript in JavaScript. |
||||
|
||||
It's injected into the context as global variable with the `--debug-object` command-line flag, |
||||
the object is separated into modules. |
||||
|
||||
## Module `$boa.gc` |
||||
|
||||
This module contains functions that are related the garbage collector. It currently has the `.collect()` method. |
||||
|
||||
```JavaScript |
||||
$boa.gc.collect() |
||||
``` |
||||
|
||||
This force triggers the GC to scan the heap and collect garbage. |
||||
|
||||
## Module `$boa.function` |
||||
|
||||
In this module are untility functions related to execution and debugging function. |
||||
|
||||
### Function `$boa.function.bytecode(func)` |
||||
|
||||
This function returns the compiled bytecode of a function as a string, |
||||
|
||||
```JavaScript |
||||
>> function add(x, y) { |
||||
return x + y |
||||
} |
||||
>> $boa.function.bytecode(add) |
||||
" |
||||
------------------------Compiled Output: 'add'------------------------ |
||||
Location Count Opcode Operands |
||||
|
||||
000000 0000 DefInitArg 0000: 'a' |
||||
000005 0001 DefInitArg 0001: 'b' |
||||
000010 0002 RestParameterPop |
||||
000011 0003 GetName 0000: 'a' |
||||
000016 0004 GetName 0001: 'b' |
||||
000021 0005 Add |
||||
000022 0006 Return |
||||
000023 0007 PushUndefined |
||||
000024 0008 Return |
||||
|
||||
Literals: |
||||
<empty> |
||||
|
||||
Bindings: |
||||
0000: a |
||||
0001: b |
||||
|
||||
Functions: |
||||
<empty> |
||||
" |
||||
>> |
||||
``` |
||||
|
||||
### Function `$boa.function.trace(func, this, ...args)` |
||||
|
||||
It only traces the specified function. If the specified function calls other functions, |
||||
their instructions aren't traced. |
||||
|
||||
```JavaScript |
||||
>> const add = (a, b) => a + b |
||||
>> $boa.function.trace(add, undefined, 1, 2) |
||||
5μs DefInitArg 0000: 'a' 2 |
||||
4μs DefInitArg 0001: 'b' <empty> |
||||
0μs RestParameterPop <empty> |
||||
3μs GetName 0000: 'a' 1 |
||||
1μs GetName 0001: 'b' 2 |
||||
2μs Add 3 |
||||
1μs Return 3 |
||||
3 |
||||
>> |
||||
``` |
||||
|
||||
The `this` value can be changed as well as the arguments that are passed to the function. |
||||
|
||||
## Function `$boa.function.flowgraph(func, options)` |
||||
|
||||
It can be used to get the instruction flowgraph, like the command-line flag. |
||||
This works on the function level, allows getting the flow graph without |
||||
quiting the boa shell and adding the specified flags. |
||||
|
||||
Besides the function it also takes an argument that, can be a string or an object. |
||||
If it is a string it represets the flowgraph format, otherwire if it's an object: |
||||
|
||||
```JavaScript |
||||
// These are the defaults, if not specified. |
||||
{ |
||||
format: 'mermaid' |
||||
direction: 'LeftRight' // or 'LR' shorthand. |
||||
} |
||||
``` |
||||
|
||||
Example: |
||||
|
||||
```JavaScript |
||||
$boa.function.flowgraph(func, 'graphviz') |
||||
$boa.function.flowgraph(func, { format: 'mermaid', direction: 'TopBottom' }) |
||||
``` |
||||
|
||||
## Module `$boa.object` |
||||
|
||||
Contains utility functions for getting internal information about an object. |
||||
|
||||
## Function `$boa.object.id(object)` |
||||
|
||||
This function returns memory address of the given object, as a string. |
||||
|
||||
Example: |
||||
|
||||
```JavaScript |
||||
let o = { x: 10, y: 20 } |
||||
$boa.object.id(o) // '0x7F5B3251B718' |
||||
|
||||
// Geting the address of the $boa object in memory |
||||
$boa.object.id($boa) // '0x7F5B3251B5D8' |
||||
``` |
||||
|
||||
## Module `$boa.optimizer` |
||||
|
||||
This modules contains getters and setters for enabling and disabling optimizations. |
||||
|
||||
### Getter & Setter `$boa.optimizer.constantFolding` |
||||
|
||||
This is and accessor property on the module, its getter returns `true` if enabled or `false` otherwise. |
||||
Its setter can be used to enable/disable the constant folding optimization. |
||||
|
||||
```JavaScript |
||||
$boa.optimizer.constantFolding = true |
||||
$boa.optimizer.constantFolding // true |
||||
``` |
||||
|
||||
### Getter & Setter `$boa.optimizer.statistics` |
||||
|
||||
This is and accessor property on the module, its getter returns `true` if enabled or `false` otherwise. |
||||
Its setter can be used to enable/disable optimization statistics, which are printed to `stdout`. |
||||
|
||||
```JavaScript |
||||
>> $boa.optimizer.constantFolding = true |
||||
>> $boa.optimizer.statistics = true |
||||
>> 1 + 1 |
||||
Optimizer { |
||||
constant folding: 1 run(s), 2 pass(es) (1 mutating, 1 checking) |
||||
} |
||||
|
||||
2 |
||||
>> |
||||
``` |
Loading…
Reference in new issue