Closed ebelevics closed 1 year ago
@ebelevics Thank you for your detailed report. I can reproduce the issue. Stay tuned.
After fix I have:
under heavy scroll
Oh wow, that was fast. I suppose it actually was a Realm issue. When I'll be able to test fix?
You can fix it in your own example by using operator[]
instead if elementAt
like:
ListView.builder(
itemCount: cars.length,
itemBuilder: (context, i) {
final car = cars[i]; // <-- use operator[]
final textWidget =
Text('Car model "${car.model}" has owner ${car.owner!.name} ');
return textWidget;
},
)
if you make cars
a RealmResults
instead of just an Iterable
Problem is the default implementation of Iterable.elementAt
basically count from the start. The fix is to override with an efficient implementation.
BTW, it is much more efficient to add multiple cars per transaction. This will improve you insert performance significantly. As an example you can look at: https://github.com/realm/realm-dart/issues/1058#issuecomment-1422709666
Sorry .. I thought Authors could still post after locking π
@ebelevics Can you confirm that the suggested change in https://github.com/realm/realm-dart/issues/1261#issuecomment-1532927835 works for you?
The actual fix will be available as part of the next release.
Yes, on particular example it did fix the issue, and scroll was smoother. I also tried to map back to app model, but it again resulted to previous issue.
From issue #1133 I did understand you suggest using Realm object as local and aswell app suggestion, but my experience shows that you should separate those two layers - because before as beginner I have faced that you can't rely always that local models will work in Flutter in all cases. And changing from one to other db solution was always cumbersome process if needed. With separation I atleast preserve that app data model logic doesn't change. That is my logic behind this approach. Not to mention better testability, readability, usability and so on.
Maybe Realm objects are robust to replace app data models, I don't know. For example Dart 3 might change a lot of things. I'll try to refactor one project, and will tell about results. I will try lazy mapper also.
P.S. Also it was interesting that Isar or ObjectBox did well even with mapping to app layer, but those solutions have their own flaws and, particularly lack of encryption, and stronger support I suppose? Also using Realm in other languages made me stick and excited when I heard it was coming to Flutter.
Here is a simplified example of how to go about it, if you are not prepared to use realm objects as models. Notice how the list renders CarModel
objects that are created on the fly with the IterableMapper
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:realm/realm.dart';
part 'main.g.dart';
@RealmModel()
class _Car {
@PrimaryKey()
late String registration;
late String model;
@MapTo('color')
late int colorAsInt;
Color get color => Color(colorAsInt);
set color(Color value) => colorAsInt = value.value;
late _Person? owner;
}
@RealmModel()
class _Person {
@PrimaryKey()
late String name;
}
class CarModel {
final String registration;
final String model;
final Color color;
final PersonModel owner;
CarModel(this.registration, this.model, this.color, this.owner);
}
class PersonModel {
final String name;
PersonModel(this.name);
}
final config = Configuration.inMemory([Car.schema, Person.schema]);
final realm = Realm(config);
void main() {
// add a 100k cars
final owner = Person('Elon Musk');
int i = 0;
while (i < 1e5) {
realm.write(() {
// write in batches of 1000
do {
realm.add(
Car(
i.toRadixString(36).toUpperCase(),
'Tesla Model Y',
Colors.primaries[i % Colors.primaries.length].value,
owner: owner,
),
update: true); // update if already exists
++i;
} while (i % 1000 != 0);
});
}
runApp(const MyApp());
}
// A simple lazy mapper
class IterableMapper<To, From> with IterableMixin<To> implements Iterable<To> {
final Iterable<From> _iterable;
final To Function(From) _mapper;
IterableMapper(this._iterable, this._mapper);
@override
Iterator<To> get iterator => _iterable.map(_mapper).iterator;
@override
To elementAt(int index) => _mapper(_iterable.elementAt(index));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
final cars = IterableMapper(realm.all<Car>(), (car) {
return CarModel(
car.registration,
car.model,
car.color,
PersonModel(car.owner?.name ?? 'No owner'),
);
});
return MaterialApp(home: Scaffold(
body: ListView.builder(itemBuilder: (context, index) {
final car = cars.elementAt(index);
return ListTile(
title: Text(car.model),
subtitle: Text(car.registration),
leading: Container(
width: 20,
height: 20,
color: car.color,
),
trailing: Text(car.owner?.name ?? 'No owner'),
);
}),
));
}
}
Still just a few ms per frame during scrolling..
I tried to implement it but still not smooth
import 'dart:collection';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:realm/realm.dart';
part 'main.g.dart';
///------------- App Model
class CarApp {
String make;
String? model;
int? kilometers;
PersonApp owner;
IterableMapper<PersonApp, Person> drivers;
CarApp(this.make, this.model, this.kilometers, this.owner, this.drivers);
}
class PersonApp {
String name;
int age;
PersonApp(this.name, this.age);
}
///------------- Extensions
extension CarLocalExt on Car {
CarApp asApp() {
return CarApp(
make,
model,
kilometers,
owner!.asApp(),
IterableMapper(drivers, (d) => d.asApp()),
);
}
}
extension PersonLocalExt on Person {
PersonApp asApp() {
return PersonApp(name, age);
}
}
///------------- Local Model
@RealmModel()
class _Car {
late String make;
String? model;
int? kilometers = 500;
_Person? owner;
late List<_Person> drivers;
}
@RealmModel()
class _Person {
late String name;
int age = 1;
}
///------------- Mapper
///
// A simple lazy mapper
class IterableMapper<To, From> with IterableMixin<To> implements Iterable<To> {
final Iterable<From> _iterable;
final To Function(From) _mapper;
IterableMapper(this._iterable, this._mapper);
@override
Iterator<To> get iterator => _iterable.map(_mapper).iterator;
@override
To elementAt(int index) => _mapper(_iterable.elementAt(index));
}
///------------- MAIN
///
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late Realm realm;
_MyAppState() {
final config = Configuration.local([Car.schema, Person.schema]);
realm = Realm(config);
}
final timer = Stopwatch();
late final Duration initStateDuration;
// late RealmResults<Car> cars;
late IterableMapper<CarApp, Car> cars;
@override
void initState() {
timer.start();
final cars = realm.all<Car>();
// for (var i = 0; i <= 10000; i++) {
// realm.write(() {
// var drivers = List.generate(50, (index) => Person(index.toString(), age: 20));
// print('Adding a Car to Realm.');
//
// var car = realm.add(Car("Tesla", owner: Person("John"), drivers: drivers));
// print("Updating the car's model and kilometers");
// car.model = "Model 3";
// car.kilometers = 5000;
//
// print('Adding another Car $i to Realm.');
// realm.add(car);
// });
// }
this.cars = IterableMapper(realm.all<Car>(), (c) => c.asApp());
// this.cars = realm.all<Car>();
initStateDuration = timer.elapsed;
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: Column(
children: [
Container(
width: 100,
height: 50,
color: Color((Random().nextDouble() * 0xFFFFFF).toInt() << 0).withOpacity(1.0),
),
Text('Running initState() $initStateDuration on: ${Platform.operatingSystem}'),
Text('\nThere are ${cars.length} cars in the Realm.\n'),
Expanded(
child: ListView.builder(
itemCount: cars.length,
itemBuilder: (context, i) {
final car = cars.elementAt(i);
final textWidget =
Text('Car model "${car.model}" has owner ${car.owner.name} with ${car.drivers.length} drivers');
return textWidget;
},
),
),
ElevatedButton(
onPressed: () => setState(() {}),
child: Text("Press"),
),
],
),
),
),
);
}
}
Yes, you are a bit early. You will need the fix in #1262.
Until then you can fix it locally by using the following extension on Iterable<T>
extension<T> on Iterable<T> {
T fastElementAt(int index) {
final self = this;
if (self is RealmResults<T>) {
return self[index];
}
return elementAt(index);
}
}
and use it it IterableMapper<To, From>.elementAt
like this:
To elementAt(int index) => _mapper(_iterable.fastElementAt(index));
This not needed once #1262 lands in a realease.
With that I see:
on your sample on my Pixel 6 Pro API 33 emulator
Yes, just did corrections, now it is smoother.
So I have put "Press" button down for a reason, and have noticed that after setState(), it freezes for up 1 second (Which is alot). By using only RealmResults, I didn't get such behaviour. Do you have any suggestion what is the cause of it and is it possible to fix it?
@ebelevics What hardware are you on, when you do these tests?
Macbook Pro M1 on Android studio for building, physical testing phone is the same Google Pixel 6a
Just tried with same To elementAt(int index) => _mapper(_iterable.fastElementAt(index));
on older LG G7 phone, and got same freeze on 10 000 for 350ms on setState
. Scrolling of course after fastElementAt
is smooth
If you use the DevTool Memory Page .. do you see a lot GCs happening when pressing the button that triggers setState
?
Like:
Taken from an ancient LG G3
Doh π€¦ .. I believe this is due to my way too simple IterableWrapper<To, From>
try adding
@override
int get length => _iterable.length;
Actually this bound to haunt you in many different shades.. That wrapper really is way to simple. It really shouldn't use the IterableMixin
.
Thinking about it some more, this should cover many situations:
class MappedIterable<From, To> extends Iterable<To> {
final Iterable<From> _iterable;
final To Function(From) _mapper;
MappedIterable(this._iterable, this._mapper);
@override
Iterator<To> get iterator => _iterable.map(_mapper).iterator;
// Length related functions are independent of the mapping.
@override
int get length => _iterable.length;
@override
bool get isEmpty => _iterable.isEmpty;
@override
Iterable<To> skip(int count) =>
MappedIterable(_iterable.skip(count), _mapper);
// Index based lookup can be done before transforming.
@override
To elementAt(int index) => _mapper(_iterable.fastElementAt(index));
@override
To get first => _mapper(_iterable.first);
@override
To get last => _mapper(_iterable.last);
@override
To get single => _mapper(_iterable.single);
}
I have changed the name and order of type arguments to be more darty. I don't currently see a more efficient way to implement the other 22 methods on Iterable<T>
when working with mapped objects.
In release mode, even on my measly LG G3 I cannot make your sample skip a single frame any more.
But please note, that it would be even faster to work directly with the realm objects. This is because accessing properties on realm objects is also a lazy operations. When we map the object we cause every property to be visited, which may not actually be needed.
I faced now occasionally lag spikes during scroll with MappedIterable
, so... thank you for you effort, most likely I'll just try to migrate all app and local model code to Realm Objects alone. If I'll face some serious issues, I'll let you know.
P.S. This wasn't a long stretch π already issues on creating constructor named or whatsoever. Even tho I know Realm Objects have constructors, the lack of required keyword and that non nullable parameters are not named but positional arguments, I just don't get it from decision standpoint (there was a github issue regarding this topic (https://github.com/realm/realm-dart/issues/292)). Because if I'll change something in RealmObject model it will turn red all my code where constructor is present. Not only it is less error prone, it is more easier to know what argument this value present.
And why I have feeling there will be lot more issues like this. This is one of main reasons why I separate local models from app models, because app models are not affected by libraries, and are pure Dart objects, and you can do whatever you want, just need to parse values from server (json) or local (db) models.
So I just took another try on Iterable mapper, because it just didn't make sense that such simple procedure, caused so much performance trouble, and looked at map dart implementation. Thankfully the code was pretty simple and I just copied it straight to main.dart file. After looking to @nielsenko previous provided code and suggestions, I tried to modify MappedIterable
by myself step by step, and I have no idea, but the heavy scrolling got so much smoother and setState() also didn't freeze.
Here is performance of just using RealmResults: Heavy scroll (58 FPS average):
setState() (around 60ms) :
And here is with modified realmMap(): Heavy scroll (58 FPS average):
setState() (around 50ms) :
Which makes me very happy that I can still modify and use app model as I want separated from realm object model generated logic. I don't know will this code be relevant after commit update, but here it is:
typedef _Transformation<F, T> = T Function(F value);
class MappedIterable<F, T> extends Iterable<T> {
final RealmResults<F> _iterable;
final _Transformation<F, T> _mapper;
factory MappedIterable(RealmResults<F> iterable, T Function(F value) function) {
return MappedIterable<F, T>._(iterable, function);
}
MappedIterable._(this._iterable, this._mapper);
@override
Iterator<T> get iterator => MappedIterator<F, T>(_iterable.iterator, _mapper);
// Length related functions are independent of the mapping.
@override
int get length => _iterable.length;
@override
bool get isEmpty => _iterable.isEmpty;
// Index based lookup can be done before transforming.
@override
T get first => _mapper(_iterable.first);
@override
T get last => _mapper(_iterable.last);
@override
T get single => _mapper(_iterable.single);
@override
T elementAt(int index) => _mapper(_iterable[index]);
}
class MappedIterator<F, T> extends Iterator<T> {
T? _current;
final Iterator<F> _iterator;
final _Transformation<F, T> _mapper;
MappedIterator(this._iterator, this._mapper);
@override
bool moveNext() {
if (_iterator.moveNext()) {
_current = _mapper(_iterator.current);
return true;
}
_current = null;
return false;
}
@override
T get current => _current as T;
}
extension RealmResultsExt<E> on RealmResults<E> {
Iterable<T> realmMap<T>(T Function(E e) toElement) => MappedIterable<E, T>(this, toElement);
}
and then just use realmMap instead of map. If I would rename realmMap to map, it would reference to original Iterable with same issues as before.
Also interesting why RealmList<Person> drivers
doesn't have such issues as RealmResults<Car> cars
, cuz from code it uses same dart MappedIterable
.
P.S. Will try on my old LG G7 test results and will convert my project, and say did it had any positive effect. π The LG G7 performance was also better, no stutter on scrolling and setState
What happened?
I decided to write separate topic continuing (https://github.com/realm/realm-dart/issues/1133#issuecomment-1529449307) comment.
So I have been watching on why Realm was in my application stuttering. At start I was thinking it was because I converted all results from RealmResults to List, then I converted all my List to Iterable and even then I didn't get significant performance increase. Then I thought maybe it was because I converted local RealmModels to AppModels, but even after testing that was not the case. So I took most similar available NoSQL database solutions in Flutter Realm, Isar, ObjectBox and compared each other, I noticed interesting results.
Views in files are practically the same, only Realm has no drivers.lenght because I removed it for performance testing purposes.
Here are screens of app:
Realm with drivers: The insert part of Realm pretty with drivers compared to others DBs, but this is not the main concern of performance.
Realm without drivers: The insert part was logically faster, but as you can see the
all()
part on initState is pretty fast.Isar: On Isar I was able to insert 100000 with drivers much faster than in Realm. But as you can see first list drivers start with 1,2,3... and so on, because you have to insert in table record first then link it with parent. Which is a huge stepdown compared to Realm at given moment. Also{{ isar.cars.where().findAllSync()}} but thankfully you can use Future
.findFirst()
if fetching is longer than expected and you can show loading indicator for example. And 0.3s for 100000 records with drivers is reasonable performance.ObjectBox: It acted similar as Isar but now all drivers was loaded with
carBox.getAll()
. And also you can also yougetAllAsync()
, if you want to show loading indicator, so that UI doesn't stuck.So if I compare all 3 DB solution you would think that Realm performs the best, not quite as I had stuttering initially at loading and on scrolling (Reason why I made this post). The real surprise was in DevTools performance Tab.
Here is performance Devtools Tab while scrolling fast:
Realm with drivers: So for 1 flutter frame it took 250ms to build listview on scroll, and those long flutter frames where consistent on scroll. As you can see in CPU Flame Chart Realm does the heaviest work while building.
Realm without drivers: Well I thought maybe it's because of 50 drivers, so I got rid of them and... no. Still I had around 250ms. And again Realm did the heaviest work.
Isar: Looking at Isar it took only 10ms compared to Realm 250ms. And the smoothness of scrolling was so much more better, as you can see in performance tab. You can also see that Isar doesn't do heavy work while building widgets, and I could easily even convert to AppModel.
ObjectBox: ObjectBox build widgets in similar fashion as Isar, and frames were also consistently low ms.
Repro steps
So here are my observation. While I do like Realm from API standpoint, it suffers in performance compared to other NoSQLs greatly. And there is no way to do async get all, if it freezes UI. I want to point out I would not spend time on observation and writing post if I would not like Realm as DB solution. I have used Realm in Unity project before. I like Realm but this is no go for my next project where client states that performance is mandatory, and yet I have it in my pet project and it stutters, because of Realm (looking in DevTools). I don't know why, maybe because link target system for references works better, maybe I'm doing something wrong, but I would love to hear feedback.
Version
Flutter 3.7.11
What Atlas Services are you using?
Local Database only
What type of application is this?
Flutter Application
Client OS and version
Google Pixel 6a Android 13
Code snippets
Here is code that I used:
Realm:
Isar:
ObjectBox:
Stacktrace of the exception/crash you're getting
No response
Relevant log output
No response