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

Performance comparison and issues #1261

Closed
ebelevics opened this issue May 3, 2023 · 22 comments · Fixed by #1262
Closed

Performance comparison and issues #1261

ebelevics opened this issue May 3, 2023 · 22 comments · Fixed by #1262

Comments

@ebelevics
Copy link

ebelevics commented May 3, 2023

What happened?

I decided to write separate topic continuing (#1133 (comment)) 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 you getAllAsync(), 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:

import 'dart:io';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:realm/realm.dart';

part 'main.g.dart';

///-------------

// class CarApp {
//   String make;
//   String? model;
//   int? kilometers;
//   PersonApp owner;
//   Iterable<PersonApp> drivers;
//
//   CarApp(this.make, this.model, this.kilometers, this.owner, this.drivers);
// }
//
// class PersonApp {
//   String name;
//   int age;
//
//   PersonApp(this.name, this.age);
// }
//
// ///-------------
//
// CarApp toCarApp(Car l) {
//   var car = CarApp(l.make, l.model, l.kilometers, toPersonApp(l.owner!), l.drivers.map((d) => toPersonApp(d)).toList());
//   return car;
// }
//
// PersonApp toPersonApp(Person l) {
//   var person = PersonApp(l.name, l.age);
//   return person;
// }
//
// ///-------------

@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;
}

///-------------

void main() {
  print("Current PID $pid");
  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 Iterable<Car> cars;

  @override
  void initState() {
    timer.start();
    cars = realm.all<Car>();
    initStateDuration = timer.elapsed;

    // for (var i = 0; i <= 15000; 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")));
    //     print("Updating the car's model and kilometers");
    //     car.model = "Model 3";
    //     car.kilometers = 5000;
    //
    //     print('Adding another Car to Realm.');
    //     realm.add(car);
    //   });
    // }

    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} ');

                    return textWidget;
                  },
                ),
              ),
              ElevatedButton(
                onPressed: () => setState(() {}),
                child: Text("Press"),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Isar:

import 'dart:io';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';

part 'main_isar.g.dart';

///-------------

class CarApp {
  String make;
  String? model;
  int? kilometers;
  PersonApp owner;
  List<PersonApp> drivers;

  CarApp(this.make, this.model, this.kilometers, this.owner, this.drivers);
}

class PersonApp {
  String name;
  int age;

  PersonApp(this.name, this.age);
}

///-------------

CarApp toCarApp(Car l) {
  var car =
      CarApp(l.make, l.model, l.kilometers, toPersonApp(l.owner.value!), l.drivers.map((d) => toPersonApp(d)).toList());
  return car;
}

PersonApp toPersonApp(Person l) {
  var person = PersonApp(l.name, l.age);
  return person;
}

///-------------

@collection
class Car {
  Id id = Isar.autoIncrement;

  late String make;
  String? model;
  int? kilometers = 500;

  final owner = IsarLink<Person>();
  final drivers = IsarLinks<Person>();

  Car(this.make, this.model, this.kilometers);
}

@collection
class Person {
  Id id = Isar.autoIncrement;

  late String name;
  int age = 1;

  Person(this.name, this.age);
}

@collection
class User {
  Id id = Isar.autoIncrement; // you can also use id = null to auto increment

  String? name;

  int? age;
}

///-------------

late Isar isar;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  isar = await IsarService.create();

  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final timer = Stopwatch();
  late final Duration initStateDuration;

  late Iterable<CarApp> cars;

  @override
  void initState() {
    timer.start();

    // isar.writeTxnSync(() {
    //   for (var i = 0; i <= 50000; i++) {
    //     var drivers = List.generate(50, (index) {
    //       var person = Person(index.toString(), 20);
    //       person.id = index;
    //       return person;
    //     });
    //     var car = Car("Tesla", "Model 3", 5000);
    //     car.owner.value = Person("John", 15);
    //     car.drivers.addAll(drivers);
    //
    //     isar.cars.putSync(car);
    //     print('Adding a Car to Isar.');
    //   }
    // });

    cars = isar.cars.where().findAllSync().map((c) => toCarApp(c));
    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 Isar.\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"),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class IsarService {
  static Future<Isar> create() async {
    final dir = await getApplicationDocumentsDirectory();
    final isar = await Isar.open(
      [CarSchema, PersonSchema],
      directory: dir.path,
    );

    return isar;
  }
}

ObjectBox:

import 'dart:io';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

import 'objectbox.g.dart';

///-------------

class CarApp {
  String make;
  String? model;
  int? kilometers;
  PersonApp owner;
  Iterable<PersonApp> drivers;

  CarApp(this.make, this.model, this.kilometers, this.owner, this.drivers);
}

class PersonApp {
  String name;
  int age;

  PersonApp(this.name, this.age);
}

///-------------

CarApp toCarApp(Car l) {
  var car = CarApp(
      l.make, l.model, l.kilometers, toPersonApp(l.owner.target!), l.drivers.map((d) => toPersonApp(d)).toList());
  return car;
}

PersonApp toPersonApp(Person l) {
  var person = PersonApp(l.name, l.age);
  return person;
}

///-------------

@Entity()
class Car {
  @Id()
  int id = 0;

  Color? color;
  late String make;
  String? model;
  int? kilometers = 500;

  final owner = ToOne<Person>();
  final drivers = ToMany<Person>();

  Car(this.make, this.model, this.kilometers);
}

@Entity()
class Person {
  @Id()
  int id = 0;

  late String name;
  int age = 1;

  Person(this.name, this.age);
}

///-------------

late ObjectBox objectBox;

Future<void> main() async {
  // This is required so ObjectBox can get the application directory
  // to store the database in.
  WidgetsFlutterBinding.ensureInitialized();

  objectBox = await ObjectBox.create();

  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late final Box<Car> carBox;
  late final Box<Person> personBox;

  final timer = Stopwatch();
  late final Duration initStateDuration;

  late Iterable<CarApp> cars;

  @override
  void initState() {
    carBox = objectBox.store.box<Car>();
    personBox = objectBox.store.box<Person>();
    timer.start();

    // for (var i = 0; i <= 50000; i++) {
    //   var drivers = List.generate(50, (index) {
    //     var person = Person(index.toString(), 20);
    //     return person;
    //   });
    //   var car = Car("Tesla", "Model 3", 5000);
    //   car.owner.target = Person("John", 15);
    //
    //   car.drivers.addAll(drivers);
    //
    //   carBox.put(car);
    //   print('Adding a Car to ObjectBox.');
    // }

    cars = carBox.getAll().map((c) => toCarApp(c));
    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 ObjectBox.\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"),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class ObjectBox {
  /// The Store of this app.
  late final Store store;
  late final Admin admin;

  ObjectBox._create(this.store) {
    if (Admin.isAvailable()) {
      admin = Admin(store);
    }
    // Add any additional setup code, e.g. build queries.
  }

  /// Create an instance of ObjectBox to use throughout the app.
  static Future<ObjectBox> create() async {
    final docsDir = await getApplicationDocumentsDirectory();
    final path = p.join(docsDir.path, "obx-example");
    final store = await openStore(directory: path);

    return ObjectBox._create(store);
  }
}

Stacktrace of the exception/crash you're getting

No response

Relevant log output

No response

@nielsenko
Copy link
Contributor

@ebelevics Thank you for your detailed report. I can reproduce the issue. Stay tuned.

@nielsenko
Copy link
Contributor

nielsenko commented May 3, 2023

After fix I have:
image
under heavy scroll

@ebelevics
Copy link
Author

Oh wow, that was fast. I suppose it actually was a Realm issue. When I'll be able to test fix?

@nielsenko
Copy link
Contributor

nielsenko commented May 3, 2023

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

@nielsenko
Copy link
Contributor

nielsenko commented May 3, 2023

Problem is the default implementation of Iterable.elementAt basically count from the start. The fix is to override with an efficient implementation.

@nielsenko
Copy link
Contributor

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: #1058 (comment)

@realm realm locked and limited conversation to collaborators May 3, 2023
@realm realm unlocked this conversation May 3, 2023
@nielsenko
Copy link
Contributor

Sorry .. I thought Authors could still post after locking 😊

@nielsenko nielsenko reopened this May 3, 2023
@nielsenko
Copy link
Contributor

@ebelevics Can you confirm that the suggested change in #1261 (comment) works for you?

The actual fix will be available as part of the next release.

@ebelevics
Copy link
Author

ebelevics commented May 3, 2023

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.

@nielsenko
Copy link
Contributor

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'),
        );
      }),
    ));
  }
}

@nielsenko
Copy link
Contributor

Still just a few ms per frame during scrolling..
image

@ebelevics
Copy link
Author

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"),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

@nielsenko
Copy link
Contributor

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:
image

on your sample on my Pixel 6 Pro API 33 emulator

@ebelevics
Copy link
Author

Yes, just did corrections, now it is smoother.

@ebelevics
Copy link
Author

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?

Screenshot 2023-05-04 at 16 20 21

@nielsenko
Copy link
Contributor

@ebelevics What hardware are you on, when you do these tests?

@ebelevics
Copy link
Author

ebelevics commented May 4, 2023

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

@nielsenko
Copy link
Contributor

If you use the DevTool Memory Page .. do you see a lot GCs happening when pressing the button that triggers setState?

Like:
image

Taken from an ancient LG G3

@nielsenko
Copy link
Contributor

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.

@nielsenko
Copy link
Contributor

nielsenko commented May 4, 2023

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.

@ebelevics
Copy link
Author

ebelevics commented May 5, 2023

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 (#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.

@ebelevics
Copy link
Author

ebelevics commented May 7, 2023

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):
Screenshot 2023-05-08 at 00 56 38

setState() (around 60ms) :
Screenshot 2023-05-08 at 01 00 53

And here is with modified realmMap():
Heavy scroll (58 FPS average):
Screenshot 2023-05-08 at 00 58 35

setState() (around 50ms) :
Screenshot 2023-05-08 at 00 59 41

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

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 16, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants