Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Oco (Owned Clones Once) smart pointer #1480

Merged
merged 29 commits into from
Aug 26, 2023
Merged

Conversation

DanikVitek
Copy link
Contributor

@DanikVitek DanikVitek commented Aug 1, 2023

This PR is dedicated to integrating the following type into the Leptos ecosystem:

/// An immutable smart pointer to a value.
pub enum Immutable<'a, T: ?Sized + 'a> {
    /// A static reference to a value.
    Borrowed(&'a T),
    /// A reference counted pointer to a value.
    Counted(Rc<T>),
}

This smart pointer is designed primarily to replace usages of Cow<'static, str> with Immutable<'static, str>. Leptos uses strings just to pass them later as a &str to the DOM, so all strings are ultimately immutable. In that case, it makes more sense to be able to refer to the same data when cloning strings, or when working with custom types, that manipulate strings.
This was the case for me: in my web app, I'm passing around large strings, that represent images of high resolution.
In total, this should decrease the average memory usage of an application at any given point in time, though some cloning might occur when converting &str and String into Rc<str> due to the way it's implemented. But the original String will be immediately dropped and all further cloning will be essentially no-cost.

@DanikVitek DanikVitek marked this pull request as ready for review August 1, 2023 17:11
@gbj
Copy link
Collaborator

gbj commented Aug 1, 2023

though some cloning might occur when converting &str and String into Rc due to the way it's implemented.

Can you say more about this one? I’m not familiar with the mechanics of converting String to Rc<str>, does it require copying the contents of the String? If so that might be quite costly. Or do you mean something else? I’ll run some benchmarks against this in any case.

@DanikVitek
Copy link
Contributor Author

DanikVitek commented Aug 1, 2023

image
image
image

As you can see, the <Rc<str> as From<&str>>::from references to <Rc<[T]> as From<&[T]>>::from, which calls RcFromSlice::<T>::from_slice, which creates a cloned iterator over the slice elements.
from_iter_exact then writes element-by-element into the new location.

@DanikVitek
Copy link
Contributor Author

To think about it, that's an obvious behavior for an Rc as under the hood it's a pointer to the array of [8 bytes strong count, 8 bytes weak count, ...bytes of the slice], so there is no straightforward cheap way to just convert an array slice of data into a reference-counted slice of data, as it must be prepended with 16 bytes just to work. This means that the data must be shifted by 16 bytes, which is simpler done by just cloning to another place, with 16 bytes already reserved.

I think it would be possible to manually create a special low-level data structure, similar to Rc, that works at the same spot the original data is allocated, if there is enough capacity so that complete cloning is not required.
For example, if a String is longer than 16 bytes, then the first 16 bytes get moved to the end of allocated space (assuming that the String has enough capacity for 16 more bytes), but the rest n-16 bytes stay in place and the NewRc remembers those first 16 bytes to be located at the end.
Though, this NewRc would work properly only for String and Vec<u8> or Vec<i8>, not for some complex item types bigger than a byte and not for slices, as they have no preallocated capacity.

So the regular Rc seems to be the most universal and simple to work with.

@DanikVitek DanikVitek changed the title Immutable smart pointer Owned Clones Once smart pointer Aug 3, 2023
@DanikVitek DanikVitek changed the title Owned Clones Once smart pointer Oco (Owned Clones Once) smart pointer Aug 3, 2023
@DanikVitek
Copy link
Contributor Author

In the last commits the original Immutable type was replaced with an Oco type:

/// "Owned Clones Once" - a smart pointer that can be either a reference,
/// an owned value, or a reference counted pointer. This is useful for
/// storing immutable values, such as strings, in a way that is cheap to
/// clone and pass around.
/// The Clone implementation is amortized O(1), and will only clone the
/// [`Oco::Owned`] variant once, while converting it into [`Oco::Counted`].
pub enum Oco<'a, T: ?Sized + ToOwned + 'a> {
    /// A static reference to a value.
    Borrowed(&'a T),
    /// A reference counted pointer to a value.
    Counted(Rc<T>),
    /// An owned value.
    Owned(<T as ToOwned>::Owned),
}

After analyzing the possible consequences of the introduction of the Immutable type, such as The Great Cloning phase, it was decided to replace it with an ultimately better type variant that would not perform implicit cloning of all Strings, but once the Oco::Owned variant is explicitly cloned, it would be converted into Oco::Counted. This will not introduce a one-time complete cloning of all Strings, and will never clone any data without a need.

@gbj gbj added this to the 0.5.0 milestone Aug 4, 2023
@rakshith-ravi
Copy link
Contributor

@DanikVitek I'm not sure I understand what this type does. Could you please help me with a layman's explanation of what this is used for?

@DanikVitek
Copy link
Contributor Author

DanikVitek commented Aug 8, 2023

@rakshith-ravi
This type is suitable for situations like storing strings or vectors that may be copied several times consequently. This would look like this:

let s1 = Oco::from("hello, world".to_string()); // Oco::<'_, str>::Owned(String)
let s2 = s1.clone(); // value of s1 is cloned into s2 and converted into Oco::<'_, str>::Counted(Rc<str>)
let s3 = s2.clone(); // the same value as in s2, with just the reference counter incremented

Also now, if you have a custom type that may be cloned a lot and contains Oco<'static, str> inside, you can implement IntoView for it just by cloning the inner Oco into the View::Text variant

@rakshith-ravi
Copy link
Contributor

Okay, so essentially this clones the first time, but not after that? Why would we need that? Also, wouldn't StoredValue help with giving us a copy for a value that needs to be moved around?

Sorry I'm poking a lot, but I'm not sure Oco would be the right name for the type. At a first glance, it seems very confusing as to what it does. I'm trying to understand this better, so that I can hopefully suggest a better name if you are up for it.

@DanikVitek
Copy link
Contributor Author

DanikVitek commented Aug 8, 2023

@rakshith-ravi
I'm not against the propositions. I picked this name as it provides some explanation of the type: "Owned variant is cloned only once" - "Owned clones once" - Oco. Maybe it is not completely accurate, but it was short and analogous to Cow.
I'm not sure about StoredValue as it is not a smart pointer and it may be not so efficient to use as an internal state of View and requires cloning of the value every time it is accessed. Originally, View was using Cow<'static, str> for this, so I've just decided to optimize it for my use case with Rc<str>

@gbj
Copy link
Collaborator

gbj commented Aug 8, 2023

This is intended primarily as an internal type, used in a few places in the reactive system and DOM renderer to make it much cheaper to clone things like text nodes. This type guarantees that the contents of the string are cloned a maximum of one time, even if you clone and reuse it in many places.

Obviously open to name suggestions but I wouldn't anticipate users of the library needing to use this themselves.

@rakshith-ravi
Copy link
Contributor

This is intended primarily as an internal type

Ahh, got it. If it's not used by the library users a lot, then I suppose it shouldn't be much of an issue.

SingleCloneRc? Nah, that's not it.
OnceClone (like once_cell)?
MaybeOwned? Ehh that doesn't make sense.

I'll let you know if I can come up with something

Copy link
Collaborator

@gbj gbj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry it's taken me so long to review this in more depth.

I think this is in good shape overall.

It looks like it needs a rebase as there are some merge conflicts.

@@ -10,7 +10,7 @@ That’s where the [`leptos_meta`](https://docs.rs/leptos_meta/latest/leptos_met

`leptos_meta` provides special components that let you inject data from inside components anywhere in your application into the `<head>`:

[`<Title/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Title.html) allows you to set the document’s title from any component. It also takes a `formatter` function that can be used to apply the same format to the title set by other pages. So, for example, if you put `<Title formatter=|text| format!("{text} — My Awesome Site")/>` in your `<App/>` component, and then `<Title text="Page 1"/>` and `<Title text="Page 2"/>` on your routes, you’ll get `Page 1 — My Awesome Site` and `Page 2 — My Awesome Site`.
[`<Title/>`](https://docs.rs/leptos_meta/latest/leptos_meta/fn.Title.html) allows you to set the document’s title from any component. It also takes a `formatter` function that can be used to apply the same format to the title set by other pages. So, for example, if you put `<Title formatter=|text| format!("{text} — My Awesome Site").into()/>` in your `<App/>` component, and then `<Title text="Page 1"/>` and `<Title text="Page 2"/>` on your routes, you’ll get `Page 1 — My Awesome Site` and `Page 2 — My Awesome Site`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not change parts of the API that then require the user to do additional work. For example, the text of the <Title/> is not something that's going to be cloned frequently—in fact, I don't think the <Title/> component ever clones it.

@@ -36,11 +36,11 @@ impl std::fmt::Debug for TitleContext {

/// A function that is applied to the text value before setting `document.title`.
#[repr(transparent)]
pub struct Formatter(Box<dyn Fn(String) -> String>);
pub struct Formatter(Box<dyn Fn(Oco<'static, str>) -> Oco<'static, str>>);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(See comment above — I'd rather keep this one Fn(String) -> String, as it ~never needs to be cloned and is additional work for the user)

@DanikVitek
Copy link
Contributor Author

Ok, I'll revert those changes and merge main branch into this one

@DanikVitek DanikVitek requested a review from gbj August 26, 2023 14:17
Copy link
Collaborator

@gbj gbj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-ran the js-framework-benchmark comparing this branch to current main and found no performance effect, which is perfect: there are no nodes being cloned in the implementation of that benchmark, so no improvement would be expected, and there was no performance regression.

There is a small increase in WASM binary size, about ~8kb in release mode at opt-level = "3". This is completely fine: ~1kb or less at opt-level = "z" and compressed.

It is, of course, a breaking change in a technical sense, but given that it did not require updating even a single example, I think the impact on users should be minimal.

There are a number of places where we do clone Views, including in the Suspense/Transition implementation, so I would expect this to benefit users across the board.

Thank you for all your work on this, and well done! Merging now.

@gbj gbj merged commit 793c191 into leptos-rs:main Aug 26, 2023
54 checks passed
@DanikVitek DanikVitek deleted the immut_smart_ptr branch August 27, 2023 21:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants