😇

[flutter]hiveに大量のデータを保存すると重くなりすぎて厳しい

2022/05/12に公開

背景

課題

  • 古めのスマホで1000件ほどのJSONを読み込むと、アプリの動作がもっさりしてきた。
    • ペットのお世話を記録できるアプリを作っているのだが、記録を一日10件を3ヶ月くらい続けた場合1000件くらいになる。
    • 自分はPixel3XLで試した。
    • (※追記)debugモードで試してました。profileモードで試せばもっとサクサク動く可能性があります。
  • 特に記録の作成、更新がめっちゃ遅い
    • update_cache_handler で キャッシュを全ての記録を読み込み、(記録の作成なら)新しい記録を付け足し、キャッシュに書き込む処理をしている。ここがボトルネックになっている。
      • update_cache_handlerとはmutationのレスポンスが返ってきた時に呼ばれるコールバックのことで、関連するQueryのキャッシュを更新するために用いる。
pet_with_record.graphql
fragment PetWithRecord on Pet {
  ...Pet
  records { // この記録(Record)がめっちゃ多い場合を想定
    ...Record
  }
}
create_record_cache_handler.dart
UpdateCacheHandler<GCreateRecordData, GCreateRecordVars> createRecordCacheHandler =
    (CacheProxy proxy, OperationResponse<GCreateRecordData, GCreateRecordVars> response) {
  if (response.needUpdateCache) {
    final createdRecord = response.data!.createRecord;

    final pet = proxy.readFragment(GPetWithRecordReq((b) => b..idFields = createdRecord.petId.idFields)) ??
        GPetWithRecordData(); // キャッシュから読み込み
    final createdRecordBeforeIndex =
        pet.records.takeWhile((record) => record.recordAt.isBefore(createdRecord.recordAt)).length; 

    final newPet = pet.rebuild(
      (builder) => builder.records
          .insert(createdRecordBeforeIndex, GPetWithRecordData_records.fromJson(createdRecord.toJson())!),
    ); // 新しく作成した記録をinsert。recordAt昇順でソートされている前提

    proxy.writeFragment(GPetWithRecordReq((b) => b..idFields = createdRecord.petId.idFields), newPet); // キャッシュに書き込み
  }
};

原因

hiveは大量のデータの読み書きには向いていない

cf. https://github.com/hivedb/hive/issues/170

  1. RAM usage: Normal boxes keep all keys and values are in memory. Lazy boxes only keys. But 50,000 keys still add up.
  2. CPU: When a box is being opened, all of its entries have to be read and decoded. Your UI will freeze with a huge amount of entries.

This obviously depends on your device but I recommend keeping it below 1000 (5000 max) for best performance. In some cases 10,000 entries might work as well.

LazyBoxを使うと問題が軽減されるはずだが、flutterのGraphQLクライアントはLazyBoxをサポートしていない

Boxは全データをメモリに読み込むが、LazyBoxはkeyだけ読み込む。これによりアプリ起動時(= box open する時の)メモリやCPU負荷が軽減される可能性がある。
ただ、上記のようなupdate_cache_handlerの挙動には影響を与えないかもしれない(box open しているわけではないので)。

lazyBoxを使うとマシだよとの記述があるが、ferryやflutter-graphqlはlazyBoxをサポートしていない

具体的にいうと、lazyBoxだと以下のように型が合わない。
型は Box extends BoxBase、 LazyBox extends BoxBase という関係になっている。
https://github.com/gql-dart/ferry/blob/master/packages/ferry_hive_store/lib/src/hive_store.dart#L7-L10

https://github.com/hivedb/hive/blob/814594978426509290f67638b4a85039644f1570/hive/lib/src/box/lazy_box.dart#L5

解決策

SQLite用ORMのdriftを試すことにした。
https://pub.dev/packages/drift

GraphQL、手軽に正規化したキャッシュができて便利だなーと思ってたのに残念。

反省

  • 多くのデータをキャッシュするケースを技術選定時にとテストするべきだった。
  • 全データをメモリに読み込む、という時点でそこらへんの特性には気づくべきだった。
    • 技術選定時にリポジトリのIssueはざっとみた方が良いかもしれない。

Discussion