Creating memory efficient reactive IObservable<IChangeset<T>>
based on Realm database query
#894
-
Realm IntroI am working on multiple applications that utilize the Realm database. It's an amazing object-relational database with reactive capabilities. Essentially, any query and object are live, and you can subscribe to object and query changes to update the UI reactively. Realm's .NET SDK is a practical tool that implements Direct binding to realm resultsI've created a sample project: RxRealm, demonstrating all of the use cases mentioned here. For instance, imagine you have an entity called IEnumerable<Product> products = realm.All<Product>();
// Assumes .NET MAUI collection view
collectionView.ItemsSource = products; Take a look Having just those 2 lines above will give you the following features:
The beauty of the Realm database is that you can have 1 million products in it and still use the above 2 lines of code without any pagination or worries about memory and performance. The actual data will only be fetched from the database when accessed. So, you can scroll through those 1 million products without worrying about lazy loading or performance issues. Everything will be very nice and smooth. Transforming model to a view modelHowever, your application will eventually become complicated. Instead of directly using the Approach 1: direct selectionThe naive way of doing this would be something like this: IEnumerable<ProductViewModel> products = realm.All<Product>().Select(p => new ProductViewModel(p)); The above approach will not work, as Realm's LINQ implementation doesn't support transformations through the Select operator and you will receive an exception if you try doing something like above Approach 2: converting to realm collection and using selectionThe next naive approach would be doing something like this: IEnumerable<ProductViewModel> products = realm.All<Product>().AsRealmCollection().Select(p => new ProductViewModel(p)); Even though I have a benchmark called
As you can see we allocate 1.24GB of memory for 1 million products with this approach. Approach 3: Dynamic dataThe next best thing is using realm.All<Product>().AsRealmCollection()
.AsObservableChangeSet()
.Filter(item => item.IsValid)
.Transform(p => new ProductViewModel(p)); Take a look at the benchmark called
As you can see, the memory consumption is 1.57GB, which is horrible. The only benefit of this solution compared to the previous one is that the result of this can be bound to an Approach 4: Dynamic data virtualizationWe need to instantiate Subject<VirtualRequest> pagination = new();
var products = _productsService.Realm.All<Product>().AsRealmCollection()
.AsObservableChangeSet()
.Virtualise(pagination)
.Transform(p => new ProductViewModel(p));
products.Subscribe(c => Console.WriteLine($"________________ Products count changed: {c.TotalChanges}"))
.DisposeWith(_disposables);
pagination.OnNext(new VirtualRequest(0, 50)); We will only create This approach is implemented in the sample project's Take a look at benchmark
As you can see, the time and memory consumption are much less than in the previous approaches. However, they're still substantial. You can also notice this performance hit if you launch the application and try to switch to the "Virtualized" tab - the application will require significant time to become responsive and show the page until the dynamic data completes loading the data. This happens because This approach also destroys the reactivity. When something changes inside the realm database and you use virtualization, the changes are not applied to the paginated list. Approach 5: Using SourceList and manually implementing paginationWith this approach, we implement the pagination manually. We can use This approach is implemented in If we take a look at benchmarks
Everything looks much better both in terms of processing time and memory consumption. This approach is better than any of the approaches mentioned above, which I am currently using in my app. The challenge of implementing a reactive and a memory-efficient solutionThe solution that I want to achieve is having an implementation of
Essentially, we should have a "window" that overlooks the source realm data. Data should be instantiated only within that window, changes should be reported only within that "window". I imagine that I will need to create something like I have seen people asking this same question in the ReactiveUI slack channel, but no one found a real solution. I decided to put an effort to create this article/discussion both to share my efforts solving this issue and also finally find the real, elegant, reactive and memory efficient solution that perfectly integrates into the |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 2 replies
-
Realm looks like a very intriguing project, I'm sorry I'd never heard of it before. Thanks for bring it up. By my reckoning, the version of public static IObservable<IChangeSet<TObject>> AsObservableChangeSet<TObject>(
this IEnumerable<TObject> source,
bool completable = false); So yeah, that's going to naively force the entire Realm collection to materialize, permanently in approach 3, and temporarily in approach 4. By my reckoning, what you want is this method, found on public static IObservable<IChangeSet<T>> ToObservableChangeSet<TCollection, T>(this TCollection source)
where TCollection : INotifyCollectionChanged, IEnumerable<T>
where T : notnull; This lines up VERY well with what Realm states their concept of a collection is... public interface IRealmCollection<out T>
: IReadOnlyList<T>,
IReadOnlyCollection<T>,
IEnumerable<T>,
IEnumerable,
INotifyCollectionChanged,
INotifyPropertyChanged
{ } I.E. it's an object that allows you to iterate over a set of items ( Really, it surprises me that Realm doesn't implement their own concept of a transform operator. Perhaps they plan to in the future, and if they ever did, I think I would recommend going with that. But until such time, DynamicData definitely can fill the gap, and that I do note that there doesn't appear to be a As far as adding support for |
Beta Was this translation helpful? Give feedback.
-
Hey @JakenVeina Thanks for getting back to me with this extensive answer. Realm is an amazing database. I am also amazed how few people know about it; it outperforms Sqlite, is much easier to use, and, most amazingly, has real-time sync capabilities through MongoDB AppServices service. It's definitely something you can try! Unfortunately, Realm doesn't seem to have any immediate plan to support the transformation (aka LINQ Select operator) any time soon as mentioned here. It also doesn't have a real pagination API that would allow doing skip/take that preserves the Regarding the usage of the [Benchmark]
public void LoadProductsWith_Virtualization_Transformation_ToObservableChangeSet()
{
Subject<VirtualRequest> pagination = new();
var products = _productsService.Realm.All<Product>().AsRealmCollection()
.ToObservableChangeSet<IRealmCollection<Product>, Product>()
.Virtualise(pagination)
.Transform(p => new ProductViewModel(p));
products.Subscribe(c => Console.WriteLine($"________________ Products count changed: {c.TotalChanges}"))
.DisposeWith(_disposables);
pagination.OnNext(new VirtualRequest(0, 50));
} When done on a large set of products, (1 million to be exact), this is what the benchmarks look like:
It results in 290MB of usage and tone of garbage collection when doing just 25 item virtualization. So, this method even though has some virtualization - it doesn't transform the objects outside of the virtualization, it does create an inner list wrapping the initial If we look inside the public static IObservable<IChangeSet<T>> ToObservableChangeSet<TCollection, T>(this TCollection source)
where TCollection : INotifyCollectionChanged, IEnumerable<T>
where T : notnull
{
............
var data = new ChangeAwareList<T>(source);
............ And here's the
Essentially, it does a Frankly, I will never have a query that returns 1 million records in my application. I have used my benchmarks to exaggerate reality and understand how DynamicData extensions behave. However, I do have queries that return thousands of records, and the performance hit becomes noticeable, especially on less powerful mobile devices. So, I need to find a better solution to the problem that will have virtualization and reactive change notifications built in. |
Beta Was this translation helpful? Give feedback.
-
If Realm itself doesn't support any pagination, I don't see how Dynamic Data could be made to interop with it, for large datasets. I also don't see how Realm is terribly useful, at all, then. If you can only bind an entire table at once, into a view, what's the point of using Realm? Just to capture incremental changes to the table after the binding is initialized? You stated "The actual data will only be fetched from the database when accessed." initially, but I don't see how this is possible if Realm doesn't support some kind of paging. That's really what paging is, it's saying "I don't need the entire dataset all at once, just a portion of it". Maybe there's an API in Realm at a lower-level than |
Beta Was this translation helpful? Give feedback.
If Realm itself doesn't support any pagination, I don't see how Dynamic Data could be made to interop with it, for large datasets. I also don't see how Realm is terribly useful, at all, then. If you can only bind an entire table at once, into a view, what's the point of using Realm? Just to capture incremental changes to the table after the binding is initialized? You stated "The actual data will only be fetched from the database when accessed." initially, but I don't see how this is possible if Realm doesn't support some kind of paging. That's really what paging is, it's saying "I don't need the entire dataset all at once, just a portion of it".
Maybe there's an API in Realm at a lower-l…