nushell/crates/nu-protocol/src/config/plugin_gc.rs
Devyn Cairns 2ae9ad8676
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`
2024-04-14 01:42:03 +00:00

253 lines
8.2 KiB
Rust

use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{record, ShellError, Span, Value};
use super::helper::{
process_bool_config, report_invalid_key, report_invalid_value, ReconstructVal,
};
/// Configures when plugins should be stopped if inactive
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct PluginGcConfigs {
/// The config to use for plugins not otherwise specified
pub default: PluginGcConfig,
/// Specific configs for plugins (by name)
pub plugins: HashMap<String, PluginGcConfig>,
}
impl PluginGcConfigs {
/// Get the plugin GC configuration for a specific plugin name. If not specified by name in the
/// config, this is `default`.
pub fn get(&self, plugin_name: &str) -> &PluginGcConfig {
self.plugins.get(plugin_name).unwrap_or(&self.default)
}
pub(super) fn process(
&mut self,
path: &[&str],
value: &mut Value,
errors: &mut Vec<ShellError>,
) {
if let Value::Record { val, .. } = value {
// Handle resets to default if keys are missing
if !val.contains("default") {
self.default = PluginGcConfig::default();
}
if !val.contains("plugins") {
self.plugins = HashMap::new();
}
val.to_mut().retain_mut(|key, value| {
let span = value.span();
match key {
"default" => {
self.default
.process(&join_path(path, &["default"]), value, errors)
}
"plugins" => process_plugins(
&join_path(path, &["plugins"]),
value,
errors,
&mut self.plugins,
),
_ => {
report_invalid_key(&join_path(path, &[key]), span, errors);
return false;
}
}
true
});
} else {
report_invalid_value("should be a record", value.span(), errors);
*value = self.reconstruct_value(value.span());
}
}
}
impl ReconstructVal for PluginGcConfigs {
fn reconstruct_value(&self, span: Span) -> Value {
Value::record(
record! {
"default" => self.default.reconstruct_value(span),
"plugins" => reconstruct_plugins(&self.plugins, span),
},
span,
)
}
}
fn process_plugins(
path: &[&str],
value: &mut Value,
errors: &mut Vec<ShellError>,
plugins: &mut HashMap<String, PluginGcConfig>,
) {
if let Value::Record { val, .. } = value {
// Remove any plugin configs that aren't in the value
plugins.retain(|key, _| val.contains(key));
val.to_mut().retain_mut(|key, value| {
if matches!(value, Value::Record { .. }) {
plugins.entry(key.to_owned()).or_default().process(
&join_path(path, &[key]),
value,
errors,
);
true
} else {
report_invalid_value("should be a record", value.span(), errors);
if let Some(conf) = plugins.get(key) {
// Reconstruct the value if it existed before
*value = conf.reconstruct_value(value.span());
true
} else {
// Remove it if it didn't
false
}
}
});
}
}
fn reconstruct_plugins(plugins: &HashMap<String, PluginGcConfig>, span: Span) -> Value {
Value::record(
plugins
.iter()
.map(|(key, val)| (key.to_owned(), val.reconstruct_value(span)))
.collect(),
span,
)
}
/// Configures when a plugin should be stopped if inactive
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PluginGcConfig {
/// True if the plugin should be stopped automatically
pub enabled: bool,
/// When to stop the plugin if not in use for this long (in nanoseconds)
pub stop_after: i64,
}
impl Default for PluginGcConfig {
fn default() -> Self {
PluginGcConfig {
enabled: true,
stop_after: 10_000_000_000, // 10sec
}
}
}
impl PluginGcConfig {
fn process(&mut self, path: &[&str], value: &mut Value, errors: &mut Vec<ShellError>) {
if let Value::Record { val, .. } = value {
// Handle resets to default if keys are missing
if !val.contains("enabled") {
self.enabled = PluginGcConfig::default().enabled;
}
if !val.contains("stop_after") {
self.stop_after = PluginGcConfig::default().stop_after;
}
val.to_mut().retain_mut(|key, value| {
let span = value.span();
match key {
"enabled" => process_bool_config(value, errors, &mut self.enabled),
"stop_after" => match value {
Value::Duration { val, .. } => {
if *val >= 0 {
self.stop_after = *val;
} else {
report_invalid_value("must not be negative", span, errors);
*val = self.stop_after;
}
}
_ => {
report_invalid_value("should be a duration", span, errors);
*value = Value::duration(self.stop_after, span);
}
},
_ => {
report_invalid_key(&join_path(path, &[key]), span, errors);
return false;
}
}
true
})
} else {
report_invalid_value("should be a record", value.span(), errors);
*value = self.reconstruct_value(value.span());
}
}
}
impl ReconstructVal for PluginGcConfig {
fn reconstruct_value(&self, span: Span) -> Value {
Value::record(
record! {
"enabled" => Value::bool(self.enabled, span),
"stop_after" => Value::duration(self.stop_after, span),
},
span,
)
}
}
fn join_path<'a>(a: &[&'a str], b: &[&'a str]) -> Vec<&'a str> {
a.iter().copied().chain(b.iter().copied()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn test_pair() -> (PluginGcConfigs, Value) {
(
PluginGcConfigs {
default: PluginGcConfig {
enabled: true,
stop_after: 30_000_000_000,
},
plugins: [(
"my_plugin".to_owned(),
PluginGcConfig {
enabled: false,
stop_after: 0,
},
)]
.into_iter()
.collect(),
},
Value::test_record(record! {
"default" => Value::test_record(record! {
"enabled" => Value::test_bool(true),
"stop_after" => Value::test_duration(30_000_000_000),
}),
"plugins" => Value::test_record(record! {
"my_plugin" => Value::test_record(record! {
"enabled" => Value::test_bool(false),
"stop_after" => Value::test_duration(0),
}),
}),
}),
)
}
#[test]
fn process() {
let (expected, mut input) = test_pair();
let mut errors = vec![];
let mut result = PluginGcConfigs::default();
result.process(&[], &mut input, &mut errors);
assert!(errors.is_empty(), "errors: {errors:#?}");
assert_eq!(expected, result);
}
#[test]
fn reconstruct() {
let (input, expected) = test_pair();
assert_eq!(expected, input.reconstruct_value(Span::test_data()));
}
}