Copy-on-write for record values (#12305)

# Description
This adds a `SharedCow` type as a transparent copy-on-write pointer that
clones to unique on mutate.

As an initial test, the `Record` within `Value::Record` is shared.

There are some pretty big wins for performance. I'll post benchmark
results in a comment. The biggest winner is nested access, as that would
have cloned the records for each cell path follow before and it doesn't
have to anymore.

The reusability of the `SharedCow` type is nice and I think it could be
used to clean up the previous work I did with `Arc` in `EngineState`.
It's meant to be a mostly transparent clone-on-write that just clones on
`.to_mut()` or `.into_owned()` if there are actually multiple
references, but avoids cloning if the reference is unique.

# User-Facing Changes
- `Value::Record` field is a different type (plugin authors)

# Tests + Formatting
- 🟢 `toolkit fmt`
- 🟢 `toolkit clippy`
- 🟢 `toolkit test`
- 🟢 `toolkit test stdlib`

# After Submitting
- [ ] use for `EngineState`
- [ ] use for `Value::List`
This commit is contained in:
Devyn Cairns
2024-04-13 18:42:03 -07:00
committed by GitHub
parent b508d1028c
commit 2ae9ad8676
52 changed files with 328 additions and 222 deletions

View File

@ -201,13 +201,13 @@ impl Value {
// the `2`.
if let Value::Record { val, .. } = self {
val.retain_mut(|key, value| {
val.to_mut().retain_mut(|key, value| {
let span = value.span();
match key {
// Grouped options
"ls" => {
if let Value::Record { val, .. } = value {
val.retain_mut(|key2, value| {
val.to_mut().retain_mut(|key2, value| {
let span = value.span();
match key2 {
"use_ls_colors" => {
@ -236,7 +236,7 @@ impl Value {
}
"rm" => {
if let Value::Record { val, .. } = value {
val.retain_mut(|key2, value| {
val.to_mut().retain_mut(|key2, value| {
let span = value.span();
match key2 {
"always_trash" => {
@ -263,7 +263,7 @@ impl Value {
"history" => {
let history = &mut config.history;
if let Value::Record { val, .. } = value {
val.retain_mut(|key2, value| {
val.to_mut().retain_mut(|key2, value| {
let span = value.span();
match key2 {
"isolation" => {
@ -305,7 +305,7 @@ impl Value {
}
"completions" => {
if let Value::Record { val, .. } = value {
val.retain_mut(|key2, value| {
val.to_mut().retain_mut(|key2, value| {
let span = value.span();
match key2 {
"quick" => {
@ -326,7 +326,7 @@ impl Value {
}
"external" => {
if let Value::Record { val, .. } = value {
val.retain_mut(|key3, value|
val.to_mut().retain_mut(|key3, value|
{
let span = value.span();
match key3 {
@ -393,7 +393,7 @@ impl Value {
}
"cursor_shape" => {
if let Value::Record { val, .. } = value {
val.retain_mut(|key2, value| {
val.to_mut().retain_mut(|key2, value| {
let span = value.span();
let config_point = match key2 {
"vi_insert" => &mut config.cursor_shape_vi_insert,
@ -426,7 +426,7 @@ impl Value {
}
"table" => {
if let Value::Record { val, .. } = value {
val.retain_mut(|key2, value| {
val.to_mut().retain_mut(|key2, value| {
let span = value.span();
match key2 {
"mode" => {
@ -451,7 +451,7 @@ impl Value {
}
Value::Record { val, .. } => {
let mut invalid = false;
val.retain(|key3, value| {
val.to_mut().retain(|key3, value| {
match key3 {
"left" => {
match value.as_int() {
@ -546,7 +546,7 @@ impl Value {
}
"filesize" => {
if let Value::Record { val, .. } = value {
val.retain_mut(|key2, value| {
val.to_mut().retain_mut(|key2, value| {
let span = value.span();
match key2 {
"metric" => {
@ -719,7 +719,7 @@ impl Value {
},
"datetime_format" => {
if let Value::Record { val, .. } = value {
val.retain_mut(|key2, value|
val.to_mut().retain_mut(|key2, value|
{
let span = value.span();
match key2 {

View File

@ -39,7 +39,7 @@ impl PluginGcConfigs {
self.plugins = HashMap::new();
}
val.retain_mut(|key, value| {
val.to_mut().retain_mut(|key, value| {
let span = value.span();
match key {
"default" => {
@ -88,7 +88,7 @@ fn process_plugins(
// Remove any plugin configs that aren't in the value
plugins.retain(|key, _| val.contains(key));
val.retain_mut(|key, value| {
val.to_mut().retain_mut(|key, value| {
if matches!(value, Value::Record { .. }) {
plugins.entry(key.to_owned()).or_default().process(
&join_path(path, &[key]),
@ -150,7 +150,7 @@ impl PluginGcConfig {
self.stop_after = PluginGcConfig::default().stop_after;
}
val.retain_mut(|key, value| {
val.to_mut().retain_mut(|key, value| {
let span = value.span();
match key {
"enabled" => process_bool_config(value, errors, &mut self.enabled),

View File

@ -75,7 +75,7 @@ pub trait Eval {
RecordItem::Spread(_, inner) => {
match Self::eval::<D>(state, mut_state, inner)? {
Value::Record { val: inner_val, .. } => {
for (col_name, val) in *inner_val {
for (col_name, val) in inner_val.into_owned() {
if let Some(orig_span) = col_names.get(&col_name) {
return Err(ShellError::ColumnDefinedTwice {
col_name,

View File

@ -538,7 +538,7 @@ impl FromValue for Vec<Value> {
impl FromValue for Record {
fn from_value(v: Value) -> Result<Self, ShellError> {
match v {
Value::Record { val, .. } => Ok(*val),
Value::Record { val, .. } => Ok(val.into_owned()),
v => Err(ShellError::CantConvert {
to_type: "Record".into(),
from_type: v.get_type().to_string(),

View File

@ -29,7 +29,7 @@ use fancy_regex::Regex;
use nu_utils::{
contains_emoji,
locale::{get_system_locale_string, LOCALE_OVERRIDE_ENV_VAR},
IgnoreCaseExt,
IgnoreCaseExt, SharedCow,
};
use serde::{Deserialize, Serialize};
use std::{
@ -110,7 +110,7 @@ pub enum Value {
internal_span: Span,
},
Record {
val: Box<Record>,
val: SharedCow<Record>,
// note: spans are being refactored out of Value
// please use .span() instead of matching this span value
#[serde(rename = "span")]
@ -538,7 +538,7 @@ impl Value {
/// Unwraps the inner [`Record`] value or returns an error if this `Value` is not a record
pub fn into_record(self) -> Result<Record, ShellError> {
if let Value::Record { val, .. } = self {
Ok(*val)
Ok(val.into_owned())
} else {
self.cant_convert_to("record")
}
@ -1159,7 +1159,7 @@ impl Value {
match current {
Value::Record { mut val, .. } => {
// Make reverse iterate to avoid duplicate column leads to first value, actually last value is expected.
if let Some(found) = val.iter_mut().rev().find(|x| {
if let Some(found) = val.to_mut().iter_mut().rev().find(|x| {
if insensitive {
x.0.eq_ignore_case(column_name)
} else {
@ -1220,13 +1220,15 @@ impl Value {
let val_span = val.span();
match val {
Value::Record { mut val, .. } => {
if let Some(found) = val.iter_mut().rev().find(|x| {
if insensitive {
x.0.eq_ignore_case(column_name)
} else {
x.0 == column_name
}
}) {
if let Some(found) =
val.to_mut().iter_mut().rev().find(|x| {
if insensitive {
x.0.eq_ignore_case(column_name)
} else {
x.0 == column_name
}
})
{
Ok(std::mem::take(found.1))
} else if *optional {
Ok(Value::nothing(*origin_span))
@ -1332,7 +1334,7 @@ impl Value {
for val in vals.iter_mut() {
match val {
Value::Record { val: record, .. } => {
if let Some(val) = record.get_mut(col_name) {
if let Some(val) = record.to_mut().get_mut(col_name) {
val.upsert_data_at_cell_path(path, new_val.clone())?;
} else {
let new_col = if path.is_empty() {
@ -1344,7 +1346,7 @@ impl Value {
.upsert_data_at_cell_path(path, new_val.clone())?;
new_col
};
record.push(col_name, new_col);
record.to_mut().push(col_name, new_col);
}
}
Value::Error { error, .. } => return Err(*error.clone()),
@ -1359,7 +1361,7 @@ impl Value {
}
}
Value::Record { val: record, .. } => {
if let Some(val) = record.get_mut(col_name) {
if let Some(val) = record.to_mut().get_mut(col_name) {
val.upsert_data_at_cell_path(path, new_val)?;
} else {
let new_col = if path.is_empty() {
@ -1369,7 +1371,7 @@ impl Value {
new_col.upsert_data_at_cell_path(path, new_val)?;
new_col
};
record.push(col_name, new_col);
record.to_mut().push(col_name, new_col);
}
}
Value::LazyRecord { val, .. } => {
@ -1456,7 +1458,7 @@ impl Value {
let v_span = val.span();
match val {
Value::Record { val: record, .. } => {
if let Some(val) = record.get_mut(col_name) {
if let Some(val) = record.to_mut().get_mut(col_name) {
val.update_data_at_cell_path(path, new_val.clone())?;
} else {
return Err(ShellError::CantFindColumn {
@ -1478,7 +1480,7 @@ impl Value {
}
}
Value::Record { val: record, .. } => {
if let Some(val) = record.get_mut(col_name) {
if let Some(val) = record.to_mut().get_mut(col_name) {
val.update_data_at_cell_path(path, new_val)?;
} else {
return Err(ShellError::CantFindColumn {
@ -1548,7 +1550,7 @@ impl Value {
let v_span = val.span();
match val {
Value::Record { val: record, .. } => {
if record.remove(col_name).is_none() && !optional {
if record.to_mut().remove(col_name).is_none() && !optional {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: *span,
@ -1568,7 +1570,7 @@ impl Value {
Ok(())
}
Value::Record { val: record, .. } => {
if record.remove(col_name).is_none() && !optional {
if record.to_mut().remove(col_name).is_none() && !optional {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: *span,
@ -1628,7 +1630,7 @@ impl Value {
let v_span = val.span();
match val {
Value::Record { val: record, .. } => {
if let Some(val) = record.get_mut(col_name) {
if let Some(val) = record.to_mut().get_mut(col_name) {
val.remove_data_at_cell_path(path)?;
} else if !optional {
return Err(ShellError::CantFindColumn {
@ -1650,7 +1652,7 @@ impl Value {
Ok(())
}
Value::Record { val: record, .. } => {
if let Some(val) = record.get_mut(col_name) {
if let Some(val) = record.to_mut().get_mut(col_name) {
val.remove_data_at_cell_path(path)?;
} else if !optional {
return Err(ShellError::CantFindColumn {
@ -1720,7 +1722,7 @@ impl Value {
let v_span = val.span();
match val {
Value::Record { val: record, .. } => {
if let Some(val) = record.get_mut(col_name) {
if let Some(val) = record.to_mut().get_mut(col_name) {
if path.is_empty() {
return Err(ShellError::ColumnAlreadyExists {
col_name: col_name.clone(),
@ -1747,7 +1749,7 @@ impl Value {
)?;
new_col
};
record.push(col_name, new_col);
record.to_mut().push(col_name, new_col);
}
}
Value::Error { error, .. } => return Err(*error.clone()),
@ -1763,7 +1765,7 @@ impl Value {
}
}
Value::Record { val: record, .. } => {
if let Some(val) = record.get_mut(col_name) {
if let Some(val) = record.to_mut().get_mut(col_name) {
if path.is_empty() {
return Err(ShellError::ColumnAlreadyExists {
col_name: col_name.clone(),
@ -1781,7 +1783,7 @@ impl Value {
new_col.insert_data_at_cell_path(path, new_val, head_span)?;
new_col
};
record.push(col_name, new_col);
record.to_mut().push(col_name, new_col);
}
}
Value::LazyRecord { val, .. } => {
@ -1854,6 +1856,7 @@ impl Value {
// Check for contained values
match self {
Value::Record { ref mut val, .. } => val
.to_mut()
.iter_mut()
.try_for_each(|(_, rec_value)| rec_value.recurse_mut(f)),
Value::List { ref mut vals, .. } => vals
@ -1989,7 +1992,7 @@ impl Value {
pub fn record(val: Record, span: Span) -> Value {
Value::Record {
val: Box::new(val),
val: SharedCow::new(val),
internal_span: span,
}
}
@ -2432,8 +2435,8 @@ impl PartialOrd for Value {
// reorder cols and vals to make more logically compare.
// more general, if two record have same col and values,
// the order of cols shouldn't affect the equal property.
let mut lhs = lhs.clone();
let mut rhs = rhs.clone();
let mut lhs = lhs.clone().into_owned();
let mut rhs = rhs.clone().into_owned();
lhs.sort_cols();
rhs.sort_cols();

View File

@ -151,7 +151,7 @@ impl Record {
///
/// fn remove_foo_recursively(val: &mut Value) {
/// if let Value::Record {val, ..} = val {
/// val.retain_mut(keep_non_foo);
/// val.to_mut().retain_mut(keep_non_foo);
/// }
/// }
///