nushell/crates/nu-cmd-lang/src/example_support.rs
Devyn Cairns cf321ab510
Make EngineState clone cheaper with Arc on all of the heavy objects (#12229)
# Description
This makes many of the larger objects in `EngineState` into `Arc`, and
uses `Arc::make_mut` to do clone-on-write if the reference is not
unique. This is generally very cheap, giving us the best of both worlds
- allowing us to mutate without cloning if we have an exclusive
reference, and cloning if we don't.

This started as more of a curiosity for me after remembering that
`Arc::make_mut` exists and can make using `Arc` for mostly immutable
data that sometimes needs to be changed very convenient, and also after
hearing someone complain about memory usage on Discord - this is a
somewhat significant win for that.

The exact objects that were wrapped in `Arc`:

- `files`, `file_contents` - the strings and byte buffers
- `decls` - the whole `Vec`, but mostly to avoid lots of individual
`malloc()` calls on Clone rather than for memory usage
- `blocks` - the blocks themselves, rather than the outer Vec
- `modules` - the modules themselves, rather than the outer Vec
- `env_vars`, `previous_env_vars` - the entire maps
- `config`

The changes required were relatively minimal, but this is a breaking API
change. In particular, blocks are added as Arcs, to allow the parser
cache functionality to work.

With my normal nu config, running on Linux, this saves me about 15 MiB
of process memory usage when running interactively (65 MiB → 50 MiB).

This also makes quick command executions cheaper, particularly since
every REPL loop now involves a clone of the engine state so that we can
recover from a panic. It also reduces memory usage where engine state
needs to be cloned and sent to another thread or kept within an
iterator.

# User-Facing Changes
Shouldn't be any, since it's all internal stuff, but it does change some
public interfaces so it's a breaking change
2024-03-19 19:07:00 +01:00

287 lines
10 KiB
Rust

use itertools::Itertools;
use nu_protocol::debugger::WithoutDebug;
use nu_protocol::{
ast::{Block, RangeInclusion},
engine::{EngineState, Stack, StateDelta, StateWorkingSet},
Example, PipelineData, Signature, Span, Type, Value,
};
use std::collections::HashSet;
use std::sync::Arc;
pub fn check_example_input_and_output_types_match_command_signature(
example: &Example,
cwd: &std::path::Path,
engine_state: &mut Box<EngineState>,
signature_input_output_types: &[(Type, Type)],
signature_operates_on_cell_paths: bool,
) -> HashSet<(Type, Type)> {
let mut witnessed_type_transformations = HashSet::<(Type, Type)>::new();
// Skip tests that don't have results to compare to
if let Some(example_output) = example.result.as_ref() {
if let Some(example_input_type) =
eval_pipeline_without_terminal_expression(example.example, cwd, engine_state)
{
let example_input_type = example_input_type.get_type();
let example_output_type = example_output.get_type();
let example_matches_signature =
signature_input_output_types
.iter()
.any(|(sig_in_type, sig_out_type)| {
example_input_type.is_subtype(sig_in_type)
&& example_output_type.is_subtype(sig_out_type)
&& {
witnessed_type_transformations
.insert((sig_in_type.clone(), sig_out_type.clone()));
true
}
});
// The example type checks as a cell path operation if both:
// 1. The command is declared to operate on cell paths.
// 2. The example_input_type is list or record or table, and the example
// output shape is the same as the input shape.
let example_matches_signature_via_cell_path_operation = signature_operates_on_cell_paths
&& example_input_type.accepts_cell_paths()
// TODO: This is too permissive; it should make use of the signature.input_output_types at least.
&& example_output_type.to_shape() == example_input_type.to_shape();
if !(example_matches_signature || example_matches_signature_via_cell_path_operation) {
panic!(
"The example `{}` demonstrates a transformation of type {:?} -> {:?}. \
However, this does not match the declared signature: {:?}.{} \
For this command `operates_on_cell_paths()` is {}.",
example.example,
example_input_type,
example_output_type,
signature_input_output_types,
if signature_input_output_types.is_empty() {
" (Did you forget to declare the input and output types for the command?)"
} else {
""
},
signature_operates_on_cell_paths
);
};
};
}
witnessed_type_transformations
}
fn eval_pipeline_without_terminal_expression(
src: &str,
cwd: &std::path::Path,
engine_state: &mut Box<EngineState>,
) -> Option<Value> {
let (mut block, delta) = parse(src, engine_state);
if block.pipelines.len() == 1 {
let n_expressions = block.pipelines[0].elements.len();
Arc::make_mut(&mut block).pipelines[0]
.elements
.truncate(&n_expressions - 1);
if !block.pipelines[0].elements.is_empty() {
let empty_input = PipelineData::empty();
Some(eval_block(block, empty_input, cwd, engine_state, delta))
} else {
Some(Value::nothing(Span::test_data()))
}
} else {
// E.g. multiple semicolon-separated statements
None
}
}
pub fn parse(contents: &str, engine_state: &EngineState) -> (Arc<Block>, StateDelta) {
let mut working_set = StateWorkingSet::new(engine_state);
let output = nu_parser::parse(&mut working_set, None, contents.as_bytes(), false);
if let Some(err) = working_set.parse_errors.first() {
panic!("test parse error in `{contents}`: {err:?}")
}
(output, working_set.render())
}
pub fn eval_block(
block: Arc<Block>,
input: PipelineData,
cwd: &std::path::Path,
engine_state: &mut Box<EngineState>,
delta: StateDelta,
) -> Value {
engine_state
.merge_delta(delta)
.expect("Error merging delta");
let mut stack = Stack::new().capture();
stack.add_env_var("PWD".to_string(), Value::test_string(cwd.to_string_lossy()));
match nu_engine::eval_block::<WithoutDebug>(engine_state, &mut stack, &block, input) {
Err(err) => panic!("test eval error in `{}`: {:?}", "TODO", err),
Ok(result) => result.into_value(Span::test_data()),
}
}
pub fn check_example_evaluates_to_expected_output(
example: &Example,
cwd: &std::path::Path,
engine_state: &mut Box<EngineState>,
) {
let mut stack = Stack::new().capture();
// Set up PWD
stack.add_env_var("PWD".to_string(), Value::test_string(cwd.to_string_lossy()));
engine_state
.merge_env(&mut stack, cwd)
.expect("Error merging environment");
let empty_input = PipelineData::empty();
let result = eval(example.example, empty_input, cwd, engine_state);
// Note. Value implements PartialEq for Bool, Int, Float, String and Block
// If the command you are testing requires to compare another case, then
// you need to define its equality in the Value struct
if let Some(expected) = example.result.as_ref() {
assert_eq!(
DebuggableValue(&result),
DebuggableValue(expected),
"The example result differs from the expected value",
)
}
}
pub fn check_all_signature_input_output_types_entries_have_examples(
signature: Signature,
witnessed_type_transformations: HashSet<(Type, Type)>,
) {
let declared_type_transformations = HashSet::from_iter(signature.input_output_types);
assert!(
witnessed_type_transformations.is_subset(&declared_type_transformations),
"This should not be possible (bug in test): the type transformations \
collected in the course of matching examples to the signature type map \
contain type transformations not present in the signature type map."
);
if !signature.allow_variants_without_examples {
assert_eq!(
witnessed_type_transformations,
declared_type_transformations,
"There are entries in the signature type map which do not correspond to any example: \
{:?}",
declared_type_transformations
.difference(&witnessed_type_transformations)
.map(|(s1, s2)| format!("{s1} -> {s2}"))
.join(", ")
);
}
}
fn eval(
contents: &str,
input: PipelineData,
cwd: &std::path::Path,
engine_state: &mut Box<EngineState>,
) -> Value {
let (block, delta) = parse(contents, engine_state);
eval_block(block, input, cwd, engine_state, delta)
}
pub struct DebuggableValue<'a>(pub &'a Value);
impl PartialEq for DebuggableValue<'_> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<'a> std::fmt::Debug for DebuggableValue<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0 {
Value::Bool { val, .. } => {
write!(f, "{:?}", val)
}
Value::Int { val, .. } => {
write!(f, "{:?}", val)
}
Value::Float { val, .. } => {
write!(f, "{:?}f", val)
}
Value::Filesize { val, .. } => {
write!(f, "Filesize({:?})", val)
}
Value::Duration { val, .. } => {
let duration = std::time::Duration::from_nanos(*val as u64);
write!(f, "Duration({:?})", duration)
}
Value::Date { val, .. } => {
write!(f, "Date({:?})", val)
}
Value::Range { val, .. } => match val.inclusion {
RangeInclusion::Inclusive => write!(
f,
"Range({:?}..{:?}, step: {:?})",
val.from, val.to, val.incr
),
RangeInclusion::RightExclusive => write!(
f,
"Range({:?}..<{:?}, step: {:?})",
val.from, val.to, val.incr
),
},
Value::String { val, .. } | Value::Glob { val, .. } => {
write!(f, "{:?}", val)
}
Value::Record { val, .. } => {
write!(f, "{{")?;
let mut first = true;
for (col, value) in val.into_iter() {
if !first {
write!(f, ", ")?;
}
first = false;
write!(f, "{:?}: {:?}", col, DebuggableValue(value))?;
}
write!(f, "}}")
}
Value::List { vals, .. } => {
write!(f, "[")?;
for (i, value) in vals.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{:?}", DebuggableValue(value))?;
}
write!(f, "]")
}
Value::Block { val, .. } => {
write!(f, "Block({:?})", val)
}
Value::Closure { val, .. } => {
write!(f, "Closure({:?})", val)
}
Value::Nothing { .. } => {
write!(f, "Nothing")
}
Value::Error { error, .. } => {
write!(f, "Error({:?})", error)
}
Value::Binary { val, .. } => {
write!(f, "Binary({:?})", val)
}
Value::CellPath { val, .. } => {
write!(f, "CellPath({:?})", val.to_string())
}
Value::CustomValue { val, .. } => {
write!(f, "CustomValue({:?})", val)
}
Value::LazyRecord { val, .. } => {
let rec = val.collect().map_err(|_| std::fmt::Error)?;
write!(f, "LazyRecord({:?})", DebuggableValue(&rec))
}
}
}
}