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

New Virtualization operator #888

Merged
merged 5 commits into from
Jun 3, 2024
Merged

Conversation

RolandPheasant
Copy link
Collaborator

Introduce SortAndVirtualize which replaces the old Vitualise mechanism. This is part of the drive to eliminate the Sort operator altogether.

Old

var virtualRequests =new BehaviorSubject<IVirtualRequest>(new VirtualRequest(0, 25))

myCache 
  .Sort(myComparer)
  .Virtualise(virtualRequests)

New

var virtualRequests =new BehaviorSubject<IVirtualRequest>(new VirtualRequest(0, 25))

myCache 
  .SortAndVirtualize(myComparer)

Additionally I also included a variant of SortAndBind for binding to the virtual output. This works on IList<T> and ReadOnlyObservableCollection<T>

var virtualRequests =new BehaviorSubject<IVirtualRequest>(new VirtualRequest(0, 25))
var myList = new List<MyType>();

myCache 
  .SortAndVirtualize(myComparer)
  .SortAndBind(myList)  \\ comparer is extracted from the output of virtualize

Once this is in, it's almost a direct lift of this code for Page. Then we can mark Sort as obsolete, and at some point delete the old sort + remove the index and previous index from the change object.

*
/*
For refresh, we need to check whether there was a previous add or update in the batch. If not use refresh,
otherwise use the previous update.
*/

source
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For virtualize I had to ensure unique changes, which previously did not work for Refresh


namespace DynamicData;

/// <summary>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have included Virtualize, Page and Top operators here. Maybe overkill to have one file per operator. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

I agree. We don't need one file per operator. We just need each file to not become unmanageably large. Putting several closely related operators together is fine with me.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Me, I would probably stick with one operator per-file, just for clarity.


namespace DynamicData;

/// <summary>
Copy link
Member

Choose a reason for hiding this comment

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

I agree. We don't need one file per operator. We just need each file to not become unmanageably large. Putting several closely related operators together is fine with me.

/// <param name="virtualRequests">The virtualizing requests.</param>
/// <returns>An observable which will emit virtual change sets.</returns>
/// <exception cref="ArgumentNullException">source.</exception>
public static IObservable<IChangeSet<TObject, TKey, VirtualContext<TObject>>> SortAndVirtualize<TObject, TKey>(this IObservable<IChangeSet<TObject, TKey>> source,
Copy link
Member

Choose a reason for hiding this comment

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

This thought applies to SortAndBind as well, but I know that's already published...

Do we really need to add a completely new operator here? Couldn't it just be an overload of Virtualize that has an extra IComparer<TObject> (or IObservable<IComparer<TObject>>) parameter? It might make it easier to discover.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I am torn on this one. Adding the SortAnd... clearly marks it apart from the old way. However, you're absolutely correct that it may not be required at all. That also holds true for the SortAndBind operator. Do we just rename these back to the original versions without the SortAnd prefix? I think we probably need a face to face on this question

Copy link
Collaborator

Choose a reason for hiding this comment

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

Using just .Virtualize() and .Bind() only works if there's no ambiguity at the call site.

using var subscription = someStream
    .SortAndBind(out var target)
    .Subscribe();

Right now, this is a valid invocation of the operator, if the items in someStream are IComparable<TObject>. If we were to swap .SortAndBind() to just .Bind() we lose a fair bit of clarity, I think, that there's a sort happening. And what if the user wants to opt-out of that implicit sort? Is that even possible?

I'm not sure what the equivalent scenario would be for virtualization, but that's the perspective we want to look at, to consider .SortAndVirtualize() versus just .Virtualize().

Comment on lines 29 to 34
var last = c.Last();

// If the last is a refresh we need to check whether, there was a previous add or update.
// If so , use the previous.
if (last.Reason != ChangeReason.Refresh)
return last;
Copy link
Member

Choose a reason for hiding this comment

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

With the logic you have below, I don't think you need this part. It would make sense as an optimization of a very common case, but I don't think that it would be faster than just letting the other logic run.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We need a benchmark here. Maybe a future improvement

Comment on lines 37 to 51
if (all.Length == 1)
return last;

/* Extreme edge case where compound has mixture of changes ending in refresh */

// find the previous non-refresh and return if found
for (var i = all.Length - 1; i >= 0; i--)
{
var candidate = all[i];
if (candidate.Reason != ChangeReason.Refresh)
return candidate;
}

// the entire batch are all refresh events
return all[0];
Copy link
Member

Choose a reason for hiding this comment

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

Consider:

                        var all = c.ToArray();
                        if (all.Length > 1)
                        {
                            /* Extreme edge case where compound has mixture of changes ending in refresh */
                            // find the previous non-refresh and return if found
                            for (var i = all.Length - 1; i >= 0; i--)
                            {
                                var candidate = all[i];
                                if (candidate.Reason != ChangeReason.Refresh)
                                    return candidate;
                            }
                        }
                        
                        // the entire batch are all refresh events
                        return all[0];

Point being... If you're writing manual loops to avoid LINQ performance penalties, then I would avoid using Last as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Actually it's less that I am using linq to avoid performance pitfalls, it more that I need to track backwards from the end of the collection. I also suspect you're right about avoiding using last but my rationale is that for 99% of all cases there will only ever be a single item in the grouping ... so I reasoned that avoiding a to list / array was possibly the best, and that .last() would only iterate once is almost all cases.

It's a tough balance to make, and it would be interesting to get some perf stats on it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

...ignore me, I already do a to array. I should revisit that code!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've changed the code as per your suggestion.

/// <typeparam name="TObject">The type of the object.</typeparam>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <typeparam name="TContext">The additional context.</typeparam>
public sealed class ChangeSet<TObject, TKey, TContext> : ChangeSet<TObject, TKey>, IChangeSet<TObject, TKey, TContext>
Copy link
Member

Choose a reason for hiding this comment

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

I'm going to have to keep in mind this is a thing now. I can't quite place it, but I feel like I needed it before...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, I needed some extra params to be passed down the chain, and historically I have inherited and created a new changeset. However I think this is an improvement.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm going to have to keep in mind this is a thing now. I can't quite place it, but I feel like I needed it before...

I'll wager it was for one of your various stream merging scenarios.

Comment on lines 7 to 9
<ItemGroup>
<Compile Remove="Cache\SortAndBindVirtualize.Fixture.cs" />
</ItemGroup>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Leftover development noise?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, removed.

/// <typeparam name="TObject">The type of the object.</typeparam>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <typeparam name="TContext">The additional context.</typeparam>
public sealed class ChangeSet<TObject, TKey, TContext> : ChangeSet<TObject, TKey>, IChangeSet<TObject, TKey, TContext>
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm going to have to keep in mind this is a thing now. I can't quite place it, but I feel like I needed it before...

I'll wager it was for one of your various stream merging scenarios.


namespace DynamicData;

/// <summary>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Me, I would probably stick with one operator per-file, just for clarity.

/// <param name="virtualRequests">The virtualizing requests.</param>
/// <returns>An observable which will emit virtual change sets.</returns>
/// <exception cref="ArgumentNullException">source.</exception>
public static IObservable<IChangeSet<TObject, TKey, VirtualContext<TObject>>> SortAndVirtualize<TObject, TKey>(this IObservable<IChangeSet<TObject, TKey>> source,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Using just .Virtualize() and .Bind() only works if there's no ambiguity at the call site.

using var subscription = someStream
    .SortAndBind(out var target)
    .Subscribe();

Right now, this is a valid invocation of the operator, if the items in someStream are IComparable<TObject>. If we were to swap .SortAndBind() to just .Bind() we lose a fair bit of clarity, I think, that there's a sort happening. And what if the user wants to opt-out of that implicit sort? Is that even possible?

I'm not sure what the equivalent scenario would be for virtualization, but that's the perspective we want to look at, to consider .SortAndVirtualize() versus just .Virtualize().

Comment on lines -3595 to -3612
/// <summary>
/// Returns the page as specified by the pageRequests observable.
/// </summary>
/// <typeparam name="TObject">The type of the object.</typeparam>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <param name="source">The source.</param>
/// <param name="pageRequests">The page requests.</param>
/// <returns>An observable which emits change sets.</returns>
public static IObservable<IPagedChangeSet<TObject, TKey>> Page<TObject, TKey>(this IObservable<ISortedChangeSet<TObject, TKey>> source, IObservable<IPageRequest> pageRequests)
where TObject : notnull
where TKey : notnull
{
source.ThrowArgumentNullExceptionIfNull(nameof(source));
pageRequests.ThrowArgumentNullExceptionIfNull(nameof(pageRequests));

return new Page<TObject, TKey>(source, pageRequests).Run();
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

If these operators truly are obsolete, I think we should just mark them as such, and remove them in the future, rather than removing them outright, right now.

@RolandPheasant RolandPheasant merged commit 275cc2c into main Jun 3, 2024
1 check passed
@RolandPheasant RolandPheasant deleted the feature/virtualise-sort-bind branch June 3, 2024 08:43
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