Slide 1

Slide 1 text

Room 時代にも役に立つかも知れない SQLite の Tips 【 Y U M E M I x S a n s a n T e c h M e e t u p 】 A n d r o i d 開 発 に お け る チ ャ レ ン ジ を 語 る Sansan株式会社 技術本部 Bill One Engineeringグループ エンジニア 辰濱 健一

Slide 2

Slide 2 text

2014年にSansanに入社し、徳島県にある「Sansan 神山ラボ」にて勤務。 Sansan、Eight のアプリ開発を担当し、インドにも赴いて国際化対応を担当 した。 グローバル戦略統括部を経て、現在はBill Oneの国際化対応を担当。エンジニ アコミュニティの運営も行っている。 辰濱 健一 Sansan株式会社 技術本部 Bill One Engineeringグループ エンジニア

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Sansan神山ラボ

Slide 5

Slide 5 text

Agenda - ANR について - SQLite の詳細について - クエリのパフォーマンス計測 - パフォーマンス改善 - Room での tips 本セッションでは SQLite が原因で ANR が出ていた問題を解決した話をします。 (Room が出ていなかったときの話です) 近年は Room が補完してくれている部分もありますが、Room のコアとなる SQLite の事も知っておくと役に立つかも知れません。

Slide 6

Slide 6 text

ANR について

Slide 7

Slide 7 text

• ANR とは、Application Not Responding の略 • UI スレッドが長時間(5秒以上)ブロックされると発生 • ANR が発生すると、ユーザーはアプリを強制終了できる ANR について

Slide 8

Slide 8 text

ANR について ANR を発生させないためには • 通信処理やファイルの読み書きはメインスレッド で行わない • 重たい計算処理もメインスレッドで行わない

Slide 9

Slide 9 text

ANR の検出 • Play Console の Android Vitals • Firebase Crashlytics (new! 2021/秋〜) ANR について

Slide 10

Slide 10 text

ANR のスタックトレースからは、どの UI スレッドが待たされたかが分かるが、 どの処理によって待たされているのかは分からないことがある → CPU プロファイラを使ってどの処理が遅いか特定 【参考 / CPU Profiler を使用して CPU アクティビティを検査する】 https://developer.android.com/studio/profile/cpu-profiler SQLite まわりがあやしかったので、より具体的にどのクエリが遅いか特定 → SQLite をきちんと理解する必要があった ANR について

Slide 11

Slide 11 text

SQLite の詳細について

Slide 12

Slide 12 text

• オープンソースで軽量なリレーショナルデータベースライブラリ • マルチプラットフォーム • SQL が使える • データは一つのファイルに格納される • セキュリティ機能は搭載されていない 【参考 / What Is SQLite?】 https://sqlite.org/index.html • Android OS に標準で搭載されている • 現在では Room が SQLite の抽象化レイヤーとして提供されているので、 Room を利用することが強く推奨されている SQLite について

Slide 13

Slide 13 text

Android では Room の利用が強く推奨されている 出典:SQLite を使用してデータを保存する / Android デベロッパー https://developer.android.com/training/data-storage/sqlite

Slide 14

Slide 14 text

• マルチスレッド対応をしている 【参考 / Using SQLite In Multi-Threaded Applications】 https://www.sqlite.org/threadsafe.html • ロックはファイル単位 • テーブルや列単位のロックは存在しない • スレッドセーフだが、ロックの単位が大きいので待ち時間が多くなる → 1トランザクションごとにファイルのロックが発生している 【参考 / Appropriate Uses For SQLite】 https://www.sqlite.org/whentouse.html SQLite の詳細について

Slide 15

Slide 15 text

• ジャーナルモード ファイルへの更新方法を指定できる (DELETE | TRUNCATE | PERSIST | MEMORY | WAL | OFF) 【参考 / Pragma statements supported by SQLite】 https://www.sqlite.org/pragma.html#pragma_journal_mode • WAL モード:書き込みの反映は早い(が、読み込みは遅い) • SQLIte 3.7.0 (2010-07-21) or later でしか使えない • MEMORY モード:高速(だが揮発する) • 上限に注意。テストには向いてそう SQLite の詳細について

Slide 16

Slide 16 text

• Android OS のバージョンと SQLite バージョン • SQLite は Android OS にバンドルされている • Realm のように、アプリ側でバージョンを指定できない • 前述の WAL モードも API 11 からしか使えない • Room はこのあたりのバージョン差異を吸収している SQLite の詳細について 出典:android.database.sqlite / Android デベロッパー https://developer.android.com/reference/android/database/sqlite/package-summary.html

Slide 17

Slide 17 text

• Android で SQLite を使うコードの例 SQLite の詳細について トランザクションの開始 トランザクションの終了

Slide 18

Slide 18 text

• トランザクションモード • Exclusive モード(default) • 他の全てのトランザクションをロックする • データの一貫性は向上するが、パフォーマンスが出ない • beginTransaction ではこのモードになる • Immediate モード • 他のトランザクション書き込みはロックするが、読み込みはできる • トランザクションの並列度が増し、パフォーマンスが向上する • Phantom Read(更新される前の値の読み込み)が発生するリスクを伴う • beginTransactionNonExclusive という API で呼び出し可能 SQLite の詳細について

Slide 19

Slide 19 text

• TransactionListener • トランザクションのコールバック を受け取ることができる • onBegin() • onCommit() • onRollback() • beginTransactionWithListener で Listener を渡して呼び出す • パフォーマンス測定に活用した SQLite の詳細について

Slide 20

Slide 20 text

クエリのパフォーマンス計測

Slide 21

Slide 21 text

前述の通り、SQLite ではファイル単 位のロックになってしまう。 実処理を短時間で行えるクエリであっ たとしても、直前にスロークエリが実 行されていたら、処理完了まではかな りの時間になってしまう。 → 次の2カ所の時間が欲しい • beginTransaction() → onBegin() • onBegin() -> endTransaction() クエリのパフォーマンス計測 これにより、「UX から推測される遅い処理」と「実処理の時間」を明確にする

Slide 22

Slide 22 text

そこで、SQLiteTransactionListener の出番 クエリのパフォーマンス計測 db.beginTransaction() (transaction lock...) listener.onBegin() (execute ...) db.endTransaction() listener.onCommit() or listener.onRollbaack()

Slide 23

Slide 23 text

クエリのパフォーマンス計測 独自のプロファイラを作った db.beginTransaction() (transaction lock...) listener.onBegin() (execute ...) db.endTransaction() listener.onCommit() or listener.onRollbaack() 1 2

Slide 24

Slide 24 text

クエリのパフォーマンス計測 独自のプロファイラを実装に仕込んでいく(地道に…)

Slide 25

Slide 25 text

クエリのパフォーマンス計測 アプリを動かしてみたログ Main thread で DB を触っている!! ANR のトリガーになるので要改修 • Logcat に出力しているので、DB アクセスがリアルタイムで分かる • どの操作をしたときに、どのクエリが実行されているか把握しやすい

Slide 26

Slide 26 text

クエリのパフォーマンス計測 アプリを動かしてみたログ db.beginTransaction() (transaction lock...) listener.onBegin() (execute ...) db.endTransaction() listener.onCommit() or listener.onRollbaack() 1 2 重たい処理!! 待ちが発生 → 実処理が短くても、長時間処理が返ってこないクエリが多々存在する

Slide 27

Slide 27 text

他にも、 • SQL の実行計画を分析(EXPLAIN)で実行計画を見る • .timer on … .timer off で実行時間を測る という手段も使いました。 DB Browser for SQLiteも便利 クエリのパフォーマンス計測 クエリ クエリ結果 クエリの実行情報

Slide 28

Slide 28 text

パフォーマンス改善

Slide 29

Slide 29 text

• メインスレッドでの呼び出し • Transaction 内で不必要な処理をしていた • 複数書き込みで都度 Transaction を張っていた • 不必要な再読込の抑制 • FirstScroll のための全件読み込み ボトルネックのパターン

Slide 30

Slide 30 text

• メインスレッド以外に切り替える方法 • RxJava であれば、observeOn • Coroutine であれば、withContext • Room だとデフォルトではメインスレッドで呼び出すとクラッシュするの ですぐ分かる(安心) メインスレッドでの呼び出しへの対応

Slide 31

Slide 31 text

Transaction 内で不必要な処理をしていた 加工処理などは Transaction 外に移動

Slide 32

Slide 32 text

複数書き込みで都度 Transaction を張っていた 一つのオブジェクトを保存する API を何度も呼ばずに List で受け取れる API も提供する 他にも、複数のテーブルをまとめて一つの Transaction で保存するのも有効

Slide 33

Slide 33 text

• Activity#onResume で DB にアクセスしていて、 Activity に返ってくるたびに DB にアクセス → ViewModel などの中間層でキャッシュして、キャッシュ値を読み込む (キャッシュの更新タイミングには気をつけないといけない) 不必要な再読込の抑制

Slide 34

Slide 34 text

• View に出していないデータの更新による再読み込み • バックグランド処理がトリガーになっていた • 対応 • テーブルを一覧と詳細で分割 • 一覧更新を DB からの変更通知ではなく、 アプリ内イベントでトリガーに更新するようにした 不必要な再読込の抑制 一覧に出している プロパティ 変更されても 一覧は更新不要

Slide 35

Slide 35 text

• FirstScroll のための全件読み込み • 全件読み込まないと、FirstScroll のための 情報を作れない • ページングと相性が悪く、全件読み込むしかない… • 対応 • 全プロパティではなく、FirstScroll 構築に必要な サブセット(id と 日付)だけ一旦読み込む • 他の情報は表示時に遅延読み込み 不必要な再読込の抑制

Slide 36

Slide 36 text

サブセットだけの読み込みの例 不必要な再読込の抑制

Slide 37

Slide 37 text

• id と FirstScroll に必要な情 報だけを全件取得する • 表示領域の id のみ、初回表 示時に遅延読み込み • スクロールのパフォーマン スへの影響は軽微&一覧が 数万件ある場合などは大き な速度改善になった • Realm のプロパティの遅延 評価みたいですね 全件読み込みの高速化&遅延読み込み

Slide 38

Slide 38 text

これらの改善を行って、 20倍の高速化になりました!

Slide 39

Slide 39 text

Room での tips

Slide 40

Slide 40 text

ビルド時に生成されるクラスで SQLite を叩いている 生成されたコードを見ると 何をやっているのかよくわかる

Slide 41

Slide 41 text

• ある程度カスタマイズができる(が、通常そんなに触らなくても良さそう) • setQueryCallback(…) • いつ、どのクエリが実行されたのかを把握するには便利 • CRUD のクエリだけなく、BEGIN TRANSACTION なども出る • setJournalMode(…) • デフォルトで最適なモードがあたっている(ので、気にすることはないはず) 【参考 / RoomDatabase.Builder】 https://developer.android.com/reference/androidx/room/RoomDatabase.Builder Roomdatabase.Builder

Slide 42

Slide 42 text

@Transaction をつけると1つのトランザクションにまとめられる (ファイルへの反映回数が1回にまとまるので高速化) 複数の処理を1つのトランザクションにまとめる

Slide 43

Slide 43 text

• Dao で SELECT する列を明示して、サブセットの型で返す テーブルのサブセットだけ取得したいとき 出典:YouTube / What’s new in Room (Android Dev Summit '19) https://youtu.be/_aJsh6P00c0?t=784

Slide 44

Slide 44 text

• Repository 生成時に SupportSQLiteOpenHelper も渡す • Repository 内で SupportSQLiteOpenHelper#getWritableDatabase などを使う Dao で SQLite を直で触りたい場合(あるのか?)

Slide 45

Slide 45 text

• Room は Flowable や LiveData で DB の変更通知を受け取れるが、 テーブルが更新されればとにかく通知される仕組みになっている • 関心のないオブジェクトや列の変更でも emit されてしまい、意図しない 再読み込み(さらなる DB のアクセス)を誘発してしまう • InvalidationTracker や InvalidationTracker.Oberver の実装を見ればよくわかる • 対策 • Dao の Flowable に distinctUntilChanged() をかまして使う • LiveData の場合は MediatorLiveData で distinct して流す 【参考 / Avoid false positive notifications for observable queries】 https://medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1#5e38 Room の変更通知について

Slide 46

Slide 46 text

まとめ

Slide 47

Slide 47 text

まとめ - Room は SQLite のラッパーライブラリ - SQLite の知見があると Room の挙動をよく理解できる - Room の変更通知を過信すると、意図しない emit が含まれることがある - これからの Room の進化に期待! - 暗号化の提供…まだかな?

Slide 48

Slide 48 text

No content