mirror of
https://github.com/nushell/nushell.git
synced 2025-08-12 21:47:49 +02:00
Debugger experiments (#11441)
<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> # Description <!-- Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes. Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience. --> This PR adds a new evaluator path with callbacks to a mutable trait object implementing a Debugger trait. The trait object can do anything, e.g., profiling, code coverage, step debugging. Currently, entering/leaving a block and a pipeline element is marked with callbacks, but more callbacks can be added as necessary. Not all callbacks need to be used by all debuggers; unused ones are simply empty calls. A simple profiler is implemented as a proof of concept. The debugging support is implementing by making `eval_xxx()` functions generic depending on whether we're debugging or not. This has zero computational overhead, but makes the binary slightly larger (see benchmarks below). `eval_xxx()` variants called from commands (like `eval_block_with_early_return()` in `each`) are chosen with a dynamic dispatch for two reasons: to not grow the binary size due to duplicating the code of many commands, and for the fact that it isn't possible because it would make Command trait objects object-unsafe. In the future, I hope it will be possible to allow plugin callbacks such that users would be able to implement their profiler plugins instead of having to recompile Nushell. [DAP](https://microsoft.github.io/debug-adapter-protocol/) would also be interesting to explore. Try `help debug profile`. ## Screenshots Basic output:  To profile with more granularity, increase the profiler depth (you'll see that repeated `is-windows` calls take a large chunk of total time, making it a good candidate for optimizing):  ## Benchmarks ### Binary size Binary size increase vs. main: **+40360 bytes**. _(Both built with `--release --features=extra,dataframe`.)_ ### Time ```nushell # bench_debug.nu use std bench let test = { 1..100 | each { ls | each {|row| $row.name | str length } } | flatten | math avg } print 'debug:' let res2 = bench { debug profile $test } --pretty print $res2 ``` ```nushell # bench_nodebug.nu use std bench let test = { 1..100 | each { ls | each {|row| $row.name | str length } } | flatten | math avg } print 'no debug:' let res1 = bench { do $test } --pretty print $res1 ``` `cargo run --release -- bench_debug.nu` is consistently 1--2 ms slower than `cargo run --release -- bench_nodebug.nu` due to the collection overhead + gathering the report. This is expected. When gathering more stuff, the overhead is obviously higher. `cargo run --release -- bench_nodebug.nu` vs. `nu bench_nodebug.nu` I didn't measure any difference. Both benchmarks report times between 97 and 103 ms randomly, without one being consistently higher than the other. This suggests that at least in this particular case, when not running any debugger, there is no runtime overhead. ## API changes This PR adds a generic parameter to all `eval_xxx` functions that forces you to specify whether you use the debugger. You can resolve it in two ways: * Use a provided helper that will figure it out for you. If you wanted to use `eval_block(&engine_state, ...)`, call `let eval_block = get_eval_block(&engine_state); eval_block(&engine_state, ...)` * If you know you're in an evaluation path that doesn't need debugger support, call `eval_block::<WithoutDebug>(&engine_state, ...)` (this is the case of hooks, for example). I tried to add more explanation in the docstring of `debugger_trait.rs`. ## TODO - [x] Better profiler output to reduce spam of iterative commands like `each` - [x] Resolve `TODO: DEBUG` comments - [x] Resolve unwraps - [x] Add doc comments - [x] Add usage and extra usage for `debug profile`, explaining all columns # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> Hopefully none. # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
use nu_engine::{eval_expression, CallExt};
|
||||
use nu_engine::{get_eval_expression, CallExt};
|
||||
use nu_protocol::ast::{Argument, Block, Call, Expr, Expression};
|
||||
|
||||
use nu_protocol::engine::{Closure, Command, EngineState, Stack};
|
||||
use nu_protocol::{
|
||||
record, Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature,
|
||||
@ -43,7 +44,7 @@ impl Command for Explain {
|
||||
let ctrlc = engine_state.ctrlc.clone();
|
||||
let mut stack = stack.captures_to_stack(capture_block.captures);
|
||||
|
||||
let elements = get_pipeline_elements(engine_state, &mut stack, block)?;
|
||||
let elements = get_pipeline_elements(engine_state, &mut stack, block, call.head)?;
|
||||
|
||||
Ok(elements.into_pipeline_data(ctrlc))
|
||||
}
|
||||
@ -62,9 +63,11 @@ pub fn get_pipeline_elements(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
block: &Block,
|
||||
span: Span,
|
||||
) -> Result<Vec<Value>, ShellError> {
|
||||
let mut element_values = vec![];
|
||||
let span = Span::test_data();
|
||||
|
||||
let eval_expression = get_eval_expression(engine_state);
|
||||
|
||||
for (pipeline_idx, pipeline) in block.pipelines.iter().enumerate() {
|
||||
let mut i = 0;
|
||||
@ -80,7 +83,7 @@ pub fn get_pipeline_elements(
|
||||
let command = engine_state.get_decl(call.decl_id);
|
||||
(
|
||||
command.name().to_string(),
|
||||
get_arguments(engine_state, stack, *call),
|
||||
get_arguments(engine_state, stack, *call, eval_expression),
|
||||
)
|
||||
} else {
|
||||
("no-op".to_string(), vec![])
|
||||
@ -106,7 +109,12 @@ pub fn get_pipeline_elements(
|
||||
Ok(element_values)
|
||||
}
|
||||
|
||||
fn get_arguments(engine_state: &EngineState, stack: &mut Stack, call: Call) -> Vec<Value> {
|
||||
fn get_arguments(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: Call,
|
||||
eval_expression_fn: fn(&EngineState, &mut Stack, &Expression) -> Result<Value, ShellError>,
|
||||
) -> Vec<Value> {
|
||||
let mut arg_value = vec![];
|
||||
let span = Span::test_data();
|
||||
for arg in &call.arguments {
|
||||
@ -145,8 +153,12 @@ fn get_arguments(engine_state: &EngineState, stack: &mut Stack, call: Call) -> V
|
||||
};
|
||||
|
||||
if let Some(expression) = opt_expr {
|
||||
let evaluated_expression =
|
||||
get_expression_as_value(engine_state, stack, expression);
|
||||
let evaluated_expression = get_expression_as_value(
|
||||
engine_state,
|
||||
stack,
|
||||
expression,
|
||||
eval_expression_fn,
|
||||
);
|
||||
let arg_type = "expr";
|
||||
let arg_value_name = debug_string_without_formatting(&evaluated_expression);
|
||||
let arg_value_type = &evaluated_expression.get_type().to_string();
|
||||
@ -166,7 +178,8 @@ fn get_arguments(engine_state: &EngineState, stack: &mut Stack, call: Call) -> V
|
||||
}
|
||||
Argument::Positional(inner_expr) => {
|
||||
let arg_type = "positional";
|
||||
let evaluated_expression = get_expression_as_value(engine_state, stack, inner_expr);
|
||||
let evaluated_expression =
|
||||
get_expression_as_value(engine_state, stack, inner_expr, eval_expression_fn);
|
||||
let arg_value_name = debug_string_without_formatting(&evaluated_expression);
|
||||
let arg_value_type = &evaluated_expression.get_type().to_string();
|
||||
let evaled_span = evaluated_expression.span();
|
||||
@ -184,7 +197,8 @@ fn get_arguments(engine_state: &EngineState, stack: &mut Stack, call: Call) -> V
|
||||
}
|
||||
Argument::Unknown(inner_expr) => {
|
||||
let arg_type = "unknown";
|
||||
let evaluated_expression = get_expression_as_value(engine_state, stack, inner_expr);
|
||||
let evaluated_expression =
|
||||
get_expression_as_value(engine_state, stack, inner_expr, eval_expression_fn);
|
||||
let arg_value_name = debug_string_without_formatting(&evaluated_expression);
|
||||
let arg_value_type = &evaluated_expression.get_type().to_string();
|
||||
let evaled_span = evaluated_expression.span();
|
||||
@ -202,7 +216,8 @@ fn get_arguments(engine_state: &EngineState, stack: &mut Stack, call: Call) -> V
|
||||
}
|
||||
Argument::Spread(inner_expr) => {
|
||||
let arg_type = "spread";
|
||||
let evaluated_expression = get_expression_as_value(engine_state, stack, inner_expr);
|
||||
let evaluated_expression =
|
||||
get_expression_as_value(engine_state, stack, inner_expr, eval_expression_fn);
|
||||
let arg_value_name = debug_string_without_formatting(&evaluated_expression);
|
||||
let arg_value_type = &evaluated_expression.get_type().to_string();
|
||||
let evaled_span = evaluated_expression.span();
|
||||
@ -228,8 +243,9 @@ fn get_expression_as_value(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
inner_expr: &Expression,
|
||||
eval_expression_fn: fn(&EngineState, &mut Stack, &Expression) -> Result<Value, ShellError>,
|
||||
) -> Value {
|
||||
match eval_expression(engine_state, stack, inner_expr) {
|
||||
match eval_expression_fn(engine_state, stack, inner_expr) {
|
||||
Ok(v) => v,
|
||||
Err(error) => Value::error(error, inner_expr.span),
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ mod info;
|
||||
mod inspect;
|
||||
mod inspect_table;
|
||||
mod metadata;
|
||||
mod profile;
|
||||
mod timeit;
|
||||
mod view;
|
||||
mod view_files;
|
||||
@ -18,6 +19,7 @@ pub use info::DebugInfo;
|
||||
pub use inspect::Inspect;
|
||||
pub use inspect_table::build_table;
|
||||
pub use metadata::Metadata;
|
||||
pub use profile::DebugProfile;
|
||||
pub use timeit::TimeIt;
|
||||
pub use view::View;
|
||||
pub use view_files::ViewFiles;
|
||||
|
169
crates/nu-command/src/debug/profile.rs
Normal file
169
crates/nu-command/src/debug/profile.rs
Normal file
@ -0,0 +1,169 @@
|
||||
use nu_engine::{eval_block_with_early_return, CallExt};
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::debugger::{Profiler, WithDebug};
|
||||
use nu_protocol::engine::{Closure, Command, EngineState, Stack};
|
||||
use nu_protocol::{
|
||||
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Type,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DebugProfile;
|
||||
|
||||
impl Command for DebugProfile {
|
||||
fn name(&self) -> &str {
|
||||
"debug profile"
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("debug profile")
|
||||
.required(
|
||||
"closure",
|
||||
SyntaxShape::Closure(None),
|
||||
"The closure to profile.",
|
||||
)
|
||||
.switch("spans", "Collect spans of profiled elements", Some('s'))
|
||||
.switch(
|
||||
"expand-source",
|
||||
"Collect full source fragments of profiled elements",
|
||||
Some('e'),
|
||||
)
|
||||
.switch(
|
||||
"values",
|
||||
"Collect pipeline element output values",
|
||||
Some('v'),
|
||||
)
|
||||
.switch("expr", "Collect expression types", Some('x'))
|
||||
.named(
|
||||
"max-depth",
|
||||
SyntaxShape::Int,
|
||||
"How many blocks/closures deep to step into (default 2)",
|
||||
Some('m'),
|
||||
)
|
||||
.input_output_types(vec![(Type::Any, Type::Table(vec![]))])
|
||||
.category(Category::Debug)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Profile pipeline elements in a closure."
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"The profiler profiles every evaluated pipeline element inside a closure, stepping into all
|
||||
commands calls and other blocks/closures.
|
||||
|
||||
The output can be heavily customized. By default, the following columns are included:
|
||||
- depth : Depth of the pipeline element. Each entered block adds one level of depth. How many
|
||||
blocks deep to step into is controlled with the --max-depth option.
|
||||
- id : ID of the pipeline element
|
||||
- parent_id : ID of the parent element
|
||||
- source : Source code of the pipeline element. If the element has multiple lines, only the
|
||||
first line is used and `...` is appended to the end. Full source code can be shown
|
||||
with the --expand-source flag.
|
||||
- duration_ms : How long it took to run the pipeline element in milliseconds.
|
||||
- (optional) span : Span of the element. Can be viewed via the `view span` command. Enabled with
|
||||
the --spans flag.
|
||||
- (optional) expr : The type of expression of the pipeline element. Enabled with the --expr flag.
|
||||
- (optional) output : The output value of the pipeline element. Enabled with the --values flag.
|
||||
|
||||
To illustrate the depth and IDs, consider `debug profile { if true { echo 'spam' } }`. There are
|
||||
three pipeline elements:
|
||||
|
||||
depth id parent_id
|
||||
0 0 0 debug profile { do { if true { 'spam' } } }
|
||||
1 1 0 if true { 'spam' }
|
||||
2 2 1 'spam'
|
||||
|
||||
Each block entered increments depth by 1 and each block left decrements it by one. This way you can
|
||||
control the profiling granularity. Passing --max-depth=1 to the above would stop at
|
||||
`if true { 'spam' }`. The id is used to identify each element. The parent_id tells you that 'spam'
|
||||
was spawned from `if true { 'spam' }` which was spawned from the root `debug profile { ... }`.
|
||||
|
||||
Note: In some cases, the ordering of piepeline elements might not be intuitive. For example,
|
||||
`[ a bb cc ] | each { $in | str length }` involves some implicit collects and lazy evaluation
|
||||
confusing the id/parent_id hierarchy. The --expr flag is helpful for investigating these issues."#
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
caller_stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let closure: Closure = call.req(engine_state, caller_stack, 0)?;
|
||||
let mut callee_stack = caller_stack.captures_to_stack(closure.captures);
|
||||
let block = engine_state.get_block(closure.block_id);
|
||||
|
||||
let default_max_depth = 2;
|
||||
let collect_spans = call.has_flag(engine_state, caller_stack, "spans")?;
|
||||
let collect_expanded_source =
|
||||
call.has_flag(engine_state, caller_stack, "expanded-source")?;
|
||||
let collect_values = call.has_flag(engine_state, caller_stack, "values")?;
|
||||
let collect_exprs = call.has_flag(engine_state, caller_stack, "expr")?;
|
||||
let max_depth = call
|
||||
.get_flag(engine_state, caller_stack, "max-depth")?
|
||||
.unwrap_or(default_max_depth);
|
||||
|
||||
let profiler = Profiler::new(
|
||||
max_depth,
|
||||
collect_spans,
|
||||
true,
|
||||
collect_expanded_source,
|
||||
collect_values,
|
||||
collect_exprs,
|
||||
call.span(),
|
||||
);
|
||||
|
||||
let lock_err = {
|
||||
|_| ShellError::GenericError {
|
||||
error: "Profiler Error".to_string(),
|
||||
msg: "could not lock debugger, poisoned mutex".to_string(),
|
||||
span: Some(call.head),
|
||||
help: None,
|
||||
inner: vec![],
|
||||
}
|
||||
};
|
||||
|
||||
engine_state
|
||||
.activate_debugger(Box::new(profiler))
|
||||
.map_err(lock_err)?;
|
||||
|
||||
let result = eval_block_with_early_return::<WithDebug>(
|
||||
engine_state,
|
||||
&mut callee_stack,
|
||||
block,
|
||||
input,
|
||||
call.redirect_stdout,
|
||||
call.redirect_stdout,
|
||||
);
|
||||
|
||||
// TODO: See eval_source()
|
||||
match result {
|
||||
Ok(pipeline_data) => {
|
||||
let _ = pipeline_data.into_value(call.span());
|
||||
// pipeline_data.print(engine_state, caller_stack, true, false)
|
||||
}
|
||||
Err(_e) => (), // TODO: Report error
|
||||
}
|
||||
|
||||
let debugger = engine_state.deactivate_debugger().map_err(lock_err)?;
|
||||
let res = debugger.report(engine_state, call.span());
|
||||
|
||||
res.map(|val| val.into_pipeline_data())
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Profile config evaluation",
|
||||
example: "debug profile { source $nu.config-path }",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Profile config evaluation with more granularity",
|
||||
example: "debug profile { source $nu.config-path } --max-depth 4",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
use nu_engine::{eval_block, eval_expression_with_input};
|
||||
use nu_engine::{get_eval_block, get_eval_expression_with_input};
|
||||
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
@ -52,6 +53,7 @@ impl Command for TimeIt {
|
||||
|
||||
if let Some(command_to_run) = command_to_run {
|
||||
if let Some(block_id) = command_to_run.as_block() {
|
||||
let eval_block = get_eval_block(engine_state);
|
||||
let block = engine_state.get_block(block_id);
|
||||
eval_block(
|
||||
engine_state,
|
||||
@ -62,6 +64,7 @@ impl Command for TimeIt {
|
||||
call.redirect_stderr,
|
||||
)?
|
||||
} else {
|
||||
let eval_expression_with_input = get_eval_expression_with_input(engine_state);
|
||||
eval_expression_with_input(
|
||||
engine_state,
|
||||
stack,
|
||||
|
Reference in New Issue
Block a user