LazyRecord (#7619)

This is an attempt to implement a new `Value::LazyRecord` variant for
performance reasons.

`LazyRecord` is like a regular `Record`, but it's possible to access
individual columns without evaluating other columns. I've implemented
`LazyRecord` for the special `$nu` variable; accessing `$nu` is
relatively slow because of all the information in `scope`, and [`$nu`
accounts for about 2/3 of Nu's startup time on
Linux](https://github.com/nushell/nushell/issues/6677#issuecomment-1364618122).

### Benchmarks

I ran some benchmarks on my desktop (Linux, 12900K) and the results are
very pleasing.

Nu's time to start up and run a command (`cargo build --release;
hyperfine 'target/release/nu -c "echo \"Hello, world!\""' --shell=none
--warmup 10`) goes from **8.8ms to 3.2ms, about 2.8x faster**.

Tests are also much faster! Running `cargo nextest` (with our very slow
`proptest` tests disabled) goes from **7.2s to 4.4s (1.6x faster)**,
because most tests involve launching a new instance of Nu.

### Design (updated)

I've added a new `LazyRecord` trait and added a `Value` variant wrapping
those trait objects, much like `CustomValue`. `LazyRecord`
implementations must implement these 2 functions:

```rust
// All column names
fn column_names(&self) -> Vec<&'static str>;

// Get 1 specific column value
fn get_column_value(&self, column: &str) -> Result<Value, ShellError>;
 ```

### Serializability

`Value` variants must implement `Serializable` and `Deserializable`, which poses some problems because I want to use unserializable things like `EngineState` in `LazyRecord`s. To work around this, I basically lie to the type system:

1. Add `#[typetag::serde(tag = "type")]` to `LazyRecord` to make it serializable
2. Any unserializable fields in `LazyRecord` implementations get marked with `#[serde(skip)]`
3. At the point where a `LazyRecord` normally would get serialized and sent to a plugin, I instead collect it into a regular `Value::Record` (which can be serialized)
This commit is contained in:
Reilly Wood
2023-01-18 19:27:26 -08:00
committed by GitHub
parent be32aeee70
commit 3b5172a8fa
19 changed files with 443 additions and 147 deletions

View File

@ -262,6 +262,10 @@ fn nu_value_to_string(value: Value, separator: &str) -> String {
.map(|(x, y)| format!("{}: {}", x, nu_value_to_string(y.clone(), ", ")))
.collect::<Vec<_>>()
.join(separator),
Value::LazyRecord { val, .. } => match val.collect() {
Ok(val) => nu_value_to_string(val, separator),
Err(error) => format!("{:?}", error),
},
Value::Block { val, .. } => format!("<Block {}>", val),
Value::Closure { val, .. } => format!("<Closure {}>", val),
Value::Nothing { .. } => String::new(),

View File

@ -122,6 +122,15 @@ fn getcol(
.into_pipeline_data(ctrlc)
.set_metadata(metadata))
}
PipelineData::Value(Value::LazyRecord { val, .. }, ..) => Ok(val
.column_names()
.into_iter()
.map(move |x| Value::String {
val: x.into(),
span: head,
})
.into_pipeline_data(ctrlc)
.set_metadata(metadata)),
PipelineData::Value(Value::Record { cols, .. }, ..) => Ok(cols
.into_iter()
.map(move |x| Value::String { val: x, span: head })

View File

@ -380,6 +380,26 @@ fn find_with_rest_and_highlight(
.map_or(false, |aval| aval.is_true())
}
}),
Value::LazyRecord { val, .. } => match val.collect() {
Ok(val) => match val {
Value::Record { vals, .. } => vals.iter().any(|val| {
if let Ok(span) = val.span() {
let lower_val = Value::string(
val.into_string("", &filter_config).to_lowercase(),
Span::test_data(),
);
term.r#in(span, &lower_val, span)
.map_or(false, |aval| aval.is_true())
} else {
term.r#in(span, val, span)
.map_or(false, |aval| aval.is_true())
}
}),
_ => false,
},
Err(_) => false,
},
Value::Binary { .. } => false,
}) != invert
},
@ -440,6 +460,26 @@ fn find_with_rest_and_highlight(
.map_or(false, |value| value.is_true())
}
}),
Value::LazyRecord { val, .. } => match val.collect() {
Ok(val) => match val {
Value::Record { vals, .. } => vals.iter().any(|val| {
if let Ok(span) = val.span() {
let lower_val = Value::string(
val.into_string("", &filter_config).to_lowercase(),
Span::test_data(),
);
term.r#in(span, &lower_val, span)
.map_or(false, |value| value.is_true())
} else {
term.r#in(span, val, span)
.map_or(false, |value| value.is_true())
}
}),
_ => false,
},
Err(_) => false,
},
Value::Binary { .. } => false,
}) != invert
}),

View File

@ -136,6 +136,10 @@ pub fn value_to_json_value(v: &Value) -> Result<nu_json::Value, ShellError> {
}
nu_json::Value::Object(m)
}
Value::LazyRecord { val, .. } => {
let collected = val.collect()?;
value_to_json_value(&collected)?
}
Value::CustomValue { val, .. } => val.to_json(),
})
}

View File

@ -185,6 +185,10 @@ pub fn value_to_string(v: &Value, span: Span) -> Result<String, ShellError> {
}
Ok(format!("{{{}}}", collection.join(", ")))
}
Value::LazyRecord { val, .. } => {
let collected = val.collect()?;
value_to_string(&collected, span)
}
// All strings outside data structures are quoted because they are in 'command position'
// (could be mistaken for commands by the Nu parser)
Value::String { val, .. } => Ok(escape_quote_string(val)),

View File

@ -141,6 +141,10 @@ fn local_into_string(value: Value, separator: &str, config: &Config) -> String {
.map(|(x, y)| format!("{}: {}", x, local_into_string(y.clone(), ", ", config)))
.collect::<Vec<_>>()
.join(separator),
Value::LazyRecord { val, .. } => match val.collect() {
Ok(val) => local_into_string(val, separator, config),
Err(error) => format!("{:?}", error),
},
Value::Block { val, .. } => format!("<Block {}>", val),
Value::Closure { val, .. } => format!("<Closure {}>", val),
Value::Nothing { .. } => String::new(),

View File

@ -61,6 +61,10 @@ fn helper(engine_state: &EngineState, v: &Value) -> Result<toml::Value, ShellErr
}
toml::Value::Table(m)
}
Value::LazyRecord { val, .. } => {
let collected = val.collect()?;
helper(engine_state, &collected)?
}
Value::List { vals, .. } => toml::Value::Array(toml_list(engine_state, vals)?),
Value::Block { span, .. } => {
let code = engine_state.get_span_contents(span);

View File

@ -62,6 +62,10 @@ pub fn value_to_yaml_value(v: &Value) -> Result<serde_yaml::Value, ShellError> {
}
serde_yaml::Value::Mapping(m)
}
Value::LazyRecord { val, .. } => {
let collected = val.collect()?;
value_to_yaml_value(&collected)?
}
Value::List { vals, .. } => {
let mut out = vec![];

View File

@ -331,6 +331,18 @@ fn handle_table_command(
Ok(val.into_pipeline_data())
}
PipelineData::Value(Value::LazyRecord { val, .. }, ..) => {
let collected = val.collect()?.into_pipeline_data();
handle_table_command(
engine_state,
stack,
call,
collected,
row_offset,
table_view,
term_width,
)
}
PipelineData::Value(Value::Error { error }, ..) => {
// Propagate this error outward, so that it goes to stderr
// instead of stdout.