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

@ -13,6 +13,7 @@ bench = false
[dependencies]
nu-engine = { path = "../nu-engine", version = "0.92.3" }
nu-protocol = { path = "../nu-protocol", version = "0.92.3" }
nu-utils = { path = "../nu-utils", version = "0.92.3" }
bincode = "1.3"
rmp-serde = "1.1"

View File

@ -1,10 +1,13 @@
use std::{cmp::Ordering, sync::Arc};
use std::cmp::Ordering;
use std::sync::Arc;
use crate::{
plugin::{PluginInterface, PluginSource},
util::with_custom_values_in,
};
use nu_protocol::{ast::Operator, CustomValue, IntoSpanned, ShellError, Span, Spanned, Value};
use nu_utils::SharedCow;
use serde::{Deserialize, Serialize};
#[cfg(test)]
@ -28,7 +31,7 @@ mod tests;
#[doc(hidden)]
pub struct PluginCustomValue {
#[serde(flatten)]
shared: SerdeArc<SharedContent>,
shared: SharedCow<SharedContent>,
/// Which plugin the custom value came from. This is not defined on the plugin side. The engine
/// side is responsible for maintaining it, and it is not sent over the serialization boundary.
@ -152,11 +155,11 @@ impl PluginCustomValue {
source: Option<Arc<PluginSource>>,
) -> PluginCustomValue {
PluginCustomValue {
shared: SerdeArc(Arc::new(SharedContent {
shared: SharedCow::new(SharedContent {
name,
data,
notify_on_drop,
})),
}),
source,
}
}
@ -379,7 +382,8 @@ impl Drop for PluginCustomValue {
fn drop(&mut self) {
// If the custom value specifies notify_on_drop and this is the last copy, we need to let
// the plugin know about it if we can.
if self.source.is_some() && self.notify_on_drop() && Arc::strong_count(&self.shared) == 1 {
if self.source.is_some() && self.notify_on_drop() && SharedCow::ref_count(&self.shared) == 1
{
self.get_plugin(None, "drop")
// While notifying drop, we don't need a copy of the source
.and_then(|plugin| {
@ -396,40 +400,3 @@ impl Drop for PluginCustomValue {
}
}
}
/// A serializable `Arc`, to avoid having to have the serde `rc` feature enabled.
#[derive(Clone, Debug)]
#[repr(transparent)]
struct SerdeArc<T>(Arc<T>);
impl<T> Serialize for SerdeArc<T>
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
impl<'de, T> Deserialize<'de> for SerdeArc<T>
where
T: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
T::deserialize(deserializer).map(Arc::new).map(SerdeArc)
}
}
impl<T> std::ops::Deref for SerdeArc<T> {
type Target = Arc<T>;
fn deref(&self) -> &Self::Target {
&self.0
}
}