Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Flutterビルドキャッシュの内部構造とテスト高速化への応用
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
Yoko
November 17, 2025
300
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Flutterビルドキャッシュの内部構造とテスト高速化への応用
FlutterKaigi 2025での登壇資料:
https://2025.flutterkaigi.jp/
Yoko
November 17, 2025
More Decks by Yoko
See All by Yoko
Flutter 棒グラフの実装から学ぶ Container と Canvas の使い分け
kazukiyokoi
0
200
Featured
See All Featured
Navigating the Design Leadership Dip - Product Design Week Design Leaders+ Conference 2024
apolaine
1
340
How to audit for AI Accessibility on your Front & Back End
davetheseo
0
420
Thoughts on Productivity
jonyablonski
76
5.2k
The Straight Up "How To Draw Better" Workshop
denniskardys
239
140k
Connecting the Dots Between Site Speed, User Experience & Your Business [WebExpo 2025]
tammyeverts
11
940
Making Projects Easy
brettharned
120
6.7k
Measuring Dark Social's Impact On Conversion and Attribution
stephenakadiri
2
220
Navigating the moral maze — ethical principles for Al-driven product design
skipperchong
2
390
B2B Lead Gen: Tactics, Traps & Triumph
marketingsoph
0
140
GitHub's CSS Performance
jonrohan
1033
470k
Leveraging Curiosity to Care for An Aging Population
cassininazir
1
270
Designing Powerful Visuals for Engaging Learning
tmiket
1
410
Transcript
Flutterビルドキャッシュの内部構造と テスト高速化への応用 トヨタ自動車株式会社 ソフトウェア PF開発部 Kazuki Yokoi FlutterKaigi 2025
自己紹介 経歴 1. LINEヤフー株式会社 2. 株式会社サイバーエージェント 3. トヨタ自動車株式会社 (NOW!) 趣味
温泉、雑談、親切 職務 ソフトウェア改善チームのリーダー SRE的なお仕事 (指標設計、メトリクス計測、パフォーマンスチューニング、現場改善 etc) 横井 一樹 Kazuki Yokoi
業務内容 Flutterによる車載組込みソフトウェア開発 グローバルにユーザを抱える超大規模なプロダクト 興味がある方は自分へメッセージください https://global.toyota/jp/newsroom/toyota/42758102.html
本日のセッション内容は、 所属企業のプロダクト・現場に関する内容ではありません
はじめに Flutterに限らず、アプリケーションの開発や配布の過程ではビルドが発生する ビルドは時間がかかる工程であり、キャッシュを活用して効率的に処理できる 通常、ビルドキャッシュは開発者が意識せずとも自動で動作するが、 その仕組みを理解することで開発効率をさらに高められる 本セッションでは、 Flutterにおけるビルドキャッシュの内部構造に触れたうえで、 テスト高速化へ応用するアプローチを解説する
ゴール • .dillやkernelなどの関連用語・関係性を把握する • ビルドキャッシュが有効となる条件をはじめ、内部の仕組みを理解する • それらを踏まえて、テスト高速化へ応用する手法を知る
タイムテーブル 本セッションの全体時間は13:30 - 14:00 • ①13:30 - 13:40 ◦ 前振りなど
◦ ビルドフローの全体像把握〜関連用語・関係性について • ②13:40 - 13:50 ◦ サンプルAppのflutter runをもとに、ビルド成果物やキャッシュ 内部構造の仕組みについて • ③13:50 - 14:00 ◦ サンプルAppのflutter testをもとに、テスト高速化の手法 とそ の成果について
ビルドフローの全体像を把握する いきなりビルドキャッシュの仕組みに触れると、視点が狭まりやすく、 全体像を見失う恐れがある まずはビルドフロー全体を俯瞰し、 その中でビルドキャッシュがどのプロセスに位置づけられるのかを把握する Flutter開発のすべての起点である `flutter run` からたどる👀 ※debugビルドに限定する
`flutter run` (debugビルド) するとどうなるか ①`flutter run` bin/flutter → flutter_tools ②RunCommandで設定解決
デバイス検出 , ターゲット, モード ③コンパイル制御 ResidentCompilerが frontend_serverを管理 ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 ⑦端末へインストール /起動 以後の差分はDevFS + VM Serviceで転送 ⑥Assets/生成物バンドル Assets, fonts, l10n ・プログラムをロード ・プラグインを登録 ・main()実行 など ⑧Appプロセス開始 Engine + Dart VM起動 ⑨VM Serviceに接続 Log, Hot Reload制御 ⑩VM Service準備完了 Flutter SDK (flutter_tools) VM Service URI通知 アタッチ ProcessManager.start Kernel(.dill) 出力 データ転送&実行 Dart SDK (frontend_server / Kernelコンパイラ / CFE) 実機/エミュレータ (Flutter Engine / Dart VM)
`flutter run` (debugビルド) するとどうなるか ①`flutter run` bin/flutter → flutter_tools ②RunCommandで設定解決
デバイス検出 , ターゲット, モード ③コンパイル制御 ResidentCompilerが frontend_serverを管理 ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 ⑦端末へインストール /起動 以後の差分はDevFS + VM Serviceで転送 ⑥Assets/生成物バンドル Assets, fonts, l10n ・プログラムをロード ・プラグインを登録 ・main()実行 など ⑧Appプロセス開始 Engine + Dart VM起動 ⑨VM Serviceに接続 Log, Hot Reload制御 ⑩VM Service準備完了 Flutter SDK (flutter_tools) VM Service URI通知 アタッチ ProcessManager.start Kernel(.dill) 出力 データ転送&実行 実機/エミュレータ (Flutter Engine / Dart VM) Dart SDK (frontend_server / Kernelコンパイラ / CFE) ビルドキャッシュの仕組みは Dart SDKに深く関係する 本セッションではここから深掘る
補足 ここで示した全体像は、あくまでdebugビルドの場合に限る 時間の都合上、releaseビルドも考慮して構成するのは困難のため、 本セッションでは debugビルドに限定して話を進める また、Flutterはクロスプラットフォームのフレームワークであり、 iOS, Android, Web, Linuxなどの各Platformごとの分岐も存在する
そのため、ここでのフローは抽象的な全体像として捉えていただきたい
`flutter run`における、 Dart SDK内部のビルドキャッシュについて深掘る
まずは用語から
【再喝】`flutter run` (debugビルド) するとどうなるか ①`flutter run` bin/flutter → flutter_tools ②RunCommandで設定解決
デバイス検出 , ターゲット, モード ③コンパイル制御 ResidentCompilerが frontend_serverを管理 ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 ⑦端末へインストール /起動 以後の差分はDevFS + VM Serviceで転送 ⑥Assets/生成物バンドル Assets, fonts, l10n ・プログラムをロード ・プラグインを登録 ・main()実行 など ⑧Appプロセス開始 Engine + Dart VM起動 ⑨VM Serviceに接続 Log, Hot Reload制御 ⑩VM Service準備完了 Flutter SDK (flutter_tools) VM Service URI通知 アタッチ ProcessManager.start Kernel(.dill) 出力 データ転送&実行 実機/エミュレータ (Flutter Engine / Dart VM) Dart SDK (frontend_server / Kernelコンパイラ / CFE)
Dart SDK内部の主要モジュール ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 frontend_serverって何?
Kernelコンパイラって何? Dart SDK (frontend_server / Kernelコンパイラ / CFE) CFEって何? .dillって何? 🤔
Dart SDK内部の主要モジュール : 用語説明 frontend_server Kernelコンパイラ(CFE)を常駐プロセスとしてラップしたもの https://chromium.googlesource.com/external/github.com/flutter/engine/%2B/refs/heads/flutter-3.7-candidate.25/flutter_frontend_server/README.md Kernelコンパイラ DartをKernelへ変換する Kernel
Dartの中間表現 https://github.com/dart-lang/sdk/tree/main/pkg/kernel .dill (dill = Dart Intermediate Language) Kernelのバイナリ表現を.dillという拡張子で保持する ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 Dart SDK (frontend_server / Kernelコンパイラ / CFE)
Dart SDK内部の主要モジュール : 用語説明 CFE (Common Front End) DartをKernelに変換するなど、FE処理全般を担う基盤ライブラリ群 https://github.com/dart-lang/sdk/tree/main/pkg/front_end
④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 Dart SDK (frontend_server / Kernelコンパイラ / CFE)
Dart SDK内部の主要モジュール : 関係性 frontend_server 常駐コンパイラサービス Kernelコンパイラ DartをKernelに変換するコンパイラ ex: IncrementalCompiler
CFE Dartフロントエンド基盤 Kernel Dartの中間表現 Dart上のClassのインスタンス群 インメモリで保持 .dill Kernelをバイナリ表現 オンディスクで保持 シリアライズ flutter_tools flutter_tools Dart SDK
Dart SDK内部の主要モジュール : 関係性 frontend_server 常駐コンパイラサービス Kernelコンパイラ DartをKernelに変換するコンパイラ ex: IncrementalCompiler
CFE Dartフロントエンド基盤 Kernel Dartの中間表現 Dart上のClassのインスタンス群 インメモリで保持 .dill Kernelをバイナリ表現 オンディスクで保持 シリアライズ flutter_tools flutter_tools Dart→Kernel(.dill) に変換する Dart SDK
まとめ 以下をざっくり把握 • 主要モジュールの用語説明 ◦ frontend_server, Kernelコンパイラ, .dill, CFE •
それらの関係性 • Dart SDKの役割の一つ ◦ Dart → Kernel(.dill) に変換する ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 Dart SDK (frontend_server / Kernelコンパイラ / CFE)
まとめ 以下をざっくり把握 • 主要モジュールの用語説明 ◦ frontend_server, Kernelコンパイラ, .dill, CFE •
それらの関係性 • Dart SDKの役割の一つ ◦ Dart → Kernel(.dill) に変換する →なぜDartをKernelに変換する? ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 Dart SDK (frontend_server / Kernelコンパイラ / CFE)
なぜKernelに変換する? ①クロスプラットフォーム対応のため 1つの共通的な中間表現( =Kernel)を出発点として、各 PF向けの変換・最適化が行われる また、KernelはDart上のインスタンスのため、ネイティブ /Webへ入力する際は.dillとして引数に渡す
なぜKernelに変換する? ①クロスプラットフォーム対応のため 1つの共通的な中間表現( =Kernel)を出発点として、各 PF向けの変換・最適化が行われる また、KernelはDart上のインスタンスのため、ネイティブ /Webへ入力する際は.dillとして引数に渡す • ネイティブ(iOS, Android,
Windows, Linux, etc) • Web(Chrome, Safari, Edge, etc)
なぜKernelに変換する? ①クロスプラットフォーム対応のため 1つの共通的な中間表現( =Kernel)を出発点として、各 PF向けの変換・最適化が行われる また、KernelはDart上のインスタンスのため、ネイティブ /Webへ入力する際は.dillとして引数に渡す • ネイティブ(iOS, Android,
Windows, Linux, etc) ◦ Debug: Kernel → kernel_blob.bin (= .dill) → VMがJIT実行 ◦ Release: Kernel → AOTスナップショット → ネイティブコードとして実行 ▪ 各OSのネイティブ成果物(Android: libapp.so, iOS: App.frameworkのように) • Web(Chrome, Safari, Edge, etc)
なぜKernelに変換する? ①クロスプラットフォーム対応のため 1つの共通的な中間表現( =Kernel)を出発点として、各 PF向けの変換・最適化が行われる また、KernelはDart上のインスタンスのため、ネイティブ /Webへ入力する際は.dillとして引数に渡す • ネイティブ(iOS, Android,
Windows, Linux, etc) ◦ Debug: Kernel → kernel_blob.bin (= .dill) → VMがJIT実行 ◦ Release: Kernel → AOTスナップショット → ネイティブコードとして実行 ▪ 各OSのネイティブ成果物(Android: libapp.so, iOS: App.frameworkのように) • Web(Chrome, Safari, Edge, etc) ◦ Debug: Kernel → dartdevc → JS ◦ Release: Kernel → dart2js → JS(最適化された配布用バンドル)
なぜKernelに変換する? ①クロスプラットフォーム対応のため 1つの共通的な中間表現( =Kernel)を出発点として、各 PF向けの変換・最適化が行われる また、KernelはDart上のインスタンスのため、ネイティブ /Webへ入力する際は.dillとして引数に渡す • ネイティブ(iOS, Android,
Windows, Linux, etc) ◦ Debug: Kernel → kernel_blob.bin (= .dill) → VMがJIT実行 ◦ Release: Kernel → AOTスナップショット → ネイティブコードとして実行 ▪ 各OSのネイティブ成果物(Android: libapp.so, iOS: App.frameworkのように) • Web(Chrome, Safari, Edge, etc) ◦ Debug: Kernel → dartdevc → JS ◦ Release: Kernel → dart2js → JS(最適化された配布用バンドル) Kernel Dartの中間表現 ネイティブ iOS, Android, Windows, Linux, etc Web Chrome, Safari, Edge, etc Dart Debug Release Debug Release クロスプラットフォーム出力を一本化
なぜKernelに変換する? ②開発速度向上のため Hot Reloadによって、Appを再起動せずに修正を即時反映できる これは実行中のDart VMに対して、 ”増分の.dill” をVM Service経由で適用することで実現している
なぜKernelに変換する? ②開発速度向上のため Hot Reloadによって、Appを再起動せずに修正を即時反映できる これは実行中のDart VMに対して、 ”増分の.dill” をVM Service経由で適用することで実現している 【補足】
”増分”という表現について Q: 「”増分の.dill” と言っているが、コード差分を示しているのであれば 減ることもあるので ”差分の.dill” と表現した方が良いのでは?」 A: 「ここでの”増分”とはコードが増えるという意味ではなく、前回結果を再利用し、必要な部分のみを段階 的に再計算する方式を指すコンパイラ分野の慣用語。コード量の増減は関係ない」 増分コンパイル / Incremental Compilation
なぜKernelに変換する? ①【配布】クロスプラットフォーム対応のため 1つの中間表現からネイティブ(JIT/AOT)とWeb(JS)へ分岐可能 ②【開発】開発速度向上のため 増分コンパイルにより.dillを活用し、Hot Reloadやウォームスタートを実現 ※あくまで主な目的。他にも tree shakingの容易性などある
ビルドキャッシュに触れるための関連用語の解説でした 本筋に進みます
ビルドキャッシュの実体 【おさらい】 .dillとは? Dartの中間表現であるKernelをバイナリにシリアライズしたファイル
ビルドキャッシュの実体 【おさらい】 .dillとは? Dartの中間表現であるKernelをバイナリにシリアライズしたファイル キャッシュとしても .dillは活用される flutter runでビルドする際、 ローカルに.dillというファイルが存在すれば、 デシリアライズしてKernelに復元される
ビルドキャッシュの実体 【おさらい】 .dillとは? Dartの中間表現であるKernelをバイナリにシリアライズしたファイル キャッシュとしても .dillは活用される flutter runでビルドする際、 ローカルに.dillというファイルが存在すれば、 デシリアライズしてKernelに復元される
= ビルドキャッシュの実体は .dill
なぜ.dillを復元すると、ビルドが高速になるのか frontend_server 常駐コンパイラサービス flutter run Dart SDK flutter_tools ResidentCompiler 【割愛】
様々な処理 Kernelコンパイラ DartをKernelに変換するコンパイラ ex: IncrementalCompiler CFE Dartフロントエンド基盤 Appのエントリポイントなどの 情報を引数として渡す DartをKernelに変換する
なぜ.dillを復元すると、ビルドが高速になるのか frontend_server 常駐コンパイラサービス flutter run Dart SDK flutter_tools ResidentCompiler 【割愛】
様々な処理 Kernelコンパイラ DartをKernelに変換するコンパイラ ex: IncrementalCompiler CFE Dartフロントエンド基盤 Appのエントリポイントなどの 情報を引数として渡す DartをKernelに変換する 🔥コード量が多ければ多いほど、コンパイル時間が長くなる 🔥
なぜ.dillを復元すると、ビルドが高速になるのか frontend_server 常駐コンパイラサービス flutter run Dart SDK flutter_tools ResidentCompiler 【割愛】
様々な処理 Kernelコンパイラ DartをKernelに変換するコンパイラ ex: IncrementalCompiler CFE Dartフロントエンド基盤 Appのエントリポイントなどの 情報を引数として渡す DartをKernelに変換する ⭐.dillがあれば復元可能→コンパイル対象が減る ⭐
補足 ビルド全体を見た時に、「Dart → Kernelへの変換」が常に最長ではない Gradle、Xcodeなどのネイティブ資源処理などビルドステップは多岐に渡る 今回は時間の都合上、コンパイル領域に絞っている 実務ではビルドステップごとに時間を計測し、 ボトルネックを特定しましょう
サンプルAppをflutter runして、 ビルド成果物を見てみる 👀
仕様 1つのコードベースで、クライアントの事業者ごとにデザインを切り替えて配布する App ※ソフトウェアOEMやホワイトラベルと呼ばれているビジネス形態 (なぜこの仕様にするかは、後のスライドで効果の説明がしやすくなるため) 実行コマンド flutter run \ --dart-define=CLIENT_ID=jupiter
\ --dart-define=FLAVOR=development \ --dart-define=API_KEY=abc 環境変数のCLIENT_IDごとにUIが切り替わる → ex) jupiter, mars, saturn, neptune … サンプルAppを`flutter run`
Appのrootに生成される主要な2つのdir ① .dart_tool/ 主な役割: 中間生成物置き場、依存関係の情報管理 ② build/ 主な役割: 各プラットフォームの完成物置き場 flutter
runで生成される成果物
① .dart_tool/ 主な役割: 中間生成物置き場、依存関係の情報管理 flutter_build/<hash>/app.dill
① .dart_tool/ 主な役割: 中間生成物置き場、依存関係の情報管理 flutter_build/<hash>/app.dill 完成されたKernel 各PFへ入力値として渡されたり、実行物としてコピーされる 例)iOS(シミュレータ)の場合 .dart_tool/flutter_build/<hash>/app.dill ↓
build/ios/Debug-iphonesimulator/App.framework/flutter_assets/kernel_blob.bin https://github.com/flutter/flutter/blob/master/packages/flutter_tools/lib/src/build_system/targets/ios.dart#L666
① .dart_tool/ 主な役割: 中間生成物置き場、依存関係の情報管理 flutter_build/<hash>/app.dill 完成されたKernel 各PFへ入力値として渡されたり、実行物としてコピーされる 例)iOS(シミュレータ)の場合 .dart_tool/flutter_build/<hash>/app.dill ↓
build/ios/Debug-iphonesimulator/App.framework/flutter_assets/kernel_blob.bin https://github.com/flutter/flutter/blob/master/packages/flutter_tools/lib/src/build_system/targets/ios.dart#L666 package_config.json dartのパッケージ解決情報 flutter pub getすると生成される ※flutter runの冒頭ステップでもpub getされる https://github.com/dart-lang/tools/tree/main/pkgs/package_config
② build/ 主な役割: 各プラットフォームの完成物置き場 <hash>.cache.dill.track.dill
② build/ 主な役割: 各プラットフォームの完成物置き場 <hash>.cache.dill.track.dill ビルドキャッシュの中核 ビルド時、Dart SDKのfrontend_server に--initialize-from-dillというオプションで渡される =増分コンパイルの起点
② build/ 主な役割: 各プラットフォームの完成物置き場 <hash>.cache.dill.track.dill ビルドキャッシュの中核 ビルド時、Dart SDKのfrontend_server に--initialize-from-dillというオプションで渡される =増分コンパイルの起点
ios/ 例ではiOSだが、プラットフォームごとに dir生成される kernel_blob.binの所在(Dart VMがJITで読み込むファイル) build/ios/Debug-iphonesimulator/App.framework/flutter_assets/kernel_blob.bin プラットフォームごとの dir内のflutter_assets/に存在する
【深掘り】 <hash>.cache.dill.track.dill ビルドキャッシュの中核 内部実装 このファイルはビルド情報に基づいて生成され、 ファイル名がそのままキャッシュキーとして扱われる つまり、 ビルド情報が異なればキャッシュが無効となり新規生成される ※注意: 他にも条件はある
【深掘り】 <hash>.cache.dill.track.dill ビルドキャッシュの中核 内部実装 このファイルはビルド情報に基づいて生成され、 ファイル名がそのままキャッシュキーとして扱われる つまり、 ビルド情報が異なればキャッシュが無効となり新規生成される ※注意: 他にも条件はある
では、どうやってビルド情報に基づいて生成されるのか?
<hash>.cache.dill.track.dillはどう生成されるか?
<hash>.cache.dill.track.dillはどう生成されるか? ①ファイルパスを生成するメソッド 例) 9aa8b8c74e6af46c4be658e24cd3b943 .cache.dill.track.dill
<hash>.cache.dill.track.dillはどう生成されるか? ①ファイルパスを生成するメソッド 例) 9aa8b8c74e6af46c4be658e24cd3b943 .cache.dill.track.dill ②outputを生成 dartDefinesと cacheFrontEndOptionsを 一つの文字列に結合(=ビルド情報)
<hash>.cache.dill.track.dillはどう生成されるか? ①ファイルパスを生成するメソッド 例) 9aa8b8c74e6af46c4be658e24cd3b943 .cache.dill.track.dill ②outputを生成 dartDefinesと cacheFrontEndOptionsを 一つの文字列に結合(=ビルド情報) dartDefines
= [ ‘CLIENT_ID=jupiter’, ‘FLAVOR=development’, ‘API_KEY=abc’] flutter run \ --dart-define=CLIENT_ID=jupiter \ --dart-define=FLAVOR=development \ --dart-define=API_KEY=abc
<hash>.cache.dill.track.dillはどう生成されるか? ①ファイルパスを生成するメソッド 例) 9aa8b8c74e6af46c4be658e24cd3b943 .cache.dill.track.dill ②outputを生成 dartDefinesと cacheFrontEndOptionsを 一つの文字列に結合(=ビルド情報) dartDefines
= [ ‘CLIENT_ID=jupiter’, ‘FLAVOR=development’, ‘API_KEY=abc’] flutter run \ --dart-define=CLIENT_ID=jupiter \ --dart-define=FLAVOR=development \ --dart-define=API_KEY=abc ③outputをmd5でハッシュ化 output = 一つの文字列にしたビルド情報 →9aa8b8c74e6af46c4be658e24cd3b943
<hash>.cache.dill.track.dillはどう生成されるか? ①ファイルパスを生成するメソッド 例) 9aa8b8c74e6af46c4be658e24cd3b943 .cache.dill.track.dill ②outputを生成 dartDefinesと cacheFrontEndOptionsを 一つの文字列に結合(=ビルド情報) dartDefines
= [ ‘CLIENT_ID=jupiter’, ‘FLAVOR=development’, ‘API_KEY=abc’] flutter run \ --dart-define=CLIENT_ID=jupiter \ --dart-define=FLAVOR=development \ --dart-define=API_KEY=abc ③outputをmd5でハッシュ化 output = 一つの文字列にしたビルド情報 →9aa8b8c74e6af46c4be658e24cd3b943. ④オプションと結合 ‘${buildPrefix}cache.dill’ =”③のハッシュ ”.cache.dill track-widget-creationが有効であれば、 末尾に ”.track.dill”を付与
<hash>.cache.dill.track.dillはどう生成されるか? まとめ --dart-defineの構成をそのままキーの材料にしているため、 keyの増減はもちろん、valueの中身が変わるとキャッシュが無効になる
<hash>.cache.dill.track.dillはどう生成されるか? まとめ --dart-defineの構成をそのままキーの材料にしているため、 keyの増減はもちろん、valueの中身が変わるとキャッシュが無効になる 例)keyの増減 例)valueの変化 flutter run \ --dart-define=CLIENT_ID=jupiter
\ --dart-define=FLAVOR=development \ --dart-define=API_KEY=abc flutter run \ --dart-define=CLIENT_ID=jupiter \ --dart-define=FLAVOR=development flutter run \ --dart-define=CLIENT_ID=jupiter 無効 flutter run \ --dart-define=CLIENT_ID=jupiter \ --dart-define=FLAVOR=development \ --dart-define=API_KEY=abc flutter run \ --dart-define=CLIENT_ID=neptune \ --dart-define=FLAVOR=development \ --dart-define=API_KEY=abc flutter run \ --dart-define=CLIENT_ID=neptune \ --dart-define=FLAVOR=production \ --dart-define=API_KEY=def 無効 無効 無効
<hash>.cache.dill.track.dillはどう生成されるか? まとめ --dart-defineの構成をそのままキーの材料にしているため、 keyの増減はもちろん、valueの中身が変わるとキャッシュが無効になる tips 「並び」も材料にしているため、key-valueに変化がなくても並びが変われば無効になる スクリプトなどでコマンドを動的生成する際に、 実装の不都合で並びが変わってしまえばキャッシュの恩恵が得られなくなるので注意 flutter run
\ --dart-define=CLIENT_ID=jupiter \ --dart-define=FLAVOR=development \ --dart-define=API_KEY=abc flutter run \ --dart-define=FLAVOR=development \ --dart-define=API_KEY=abc --dart-define=CLIENT_ID=jupiter \ flutter run \ --dart-define=API_KEY=abc --dart-define=CLIENT_ID=jupiter \ --dart-define=FLAVOR=development \ 無効 無効
補足 --dart-defineにはユーザ指定のものだけではなく、 SDK指定のものもある 実際に出力すると以下の通り 1. CLIENT_ID=jupiter 2. FLAVOR=development 3. API_KEY=abc
4. FLUTTER_VERSION=3.35.1 5. FLUTTER_CHANNEL=stable 6. FLUTTER_GIT_URL=https://github.com/flutter/flutter.git 7. FLUTTER_FRAMEWORK_REVISION=20f8274939 8. FLUTTER_ENGINE_REVISION=1e9a811bf8 9. FLUTTER_DART_VERSION=3.9.0 Flutter SDK情報も含んでいるためSDK更新後には無効となる <hash>.cache.dill.track.dillはどう生成されるか?
なぜ--dart-defineの変化でキャッシュが無効になるか 結論 --dart-defineはコンパイル時定数であり、変化するとコンパイル結果が変わり得るから コンパイル時に条件分岐の判定が可能 なので、--dart-defineが変化するとゼロからコンパイルが必要 ※dart特有の仕組みではなく、一般的にTree Shakingと呼ばれるもの どの言語でも特にReleaseビルドでは様々な最適化がされる コンパイル後、 実質こうなる
=コードサイズ最適化 true false
実際に<hash>.cache.dill.track.dill の中身を見てみよう 👀
【おさらい】 .dillとは? Dartの中間表現であるKernelをシリアライズしてバイナリ表現にしたファイル .dillの中身を見る
【おさらい】 .dillとは? Dartの中間表現であるKernelをシリアライズしてバイナリ表現にしたファイル 中身を強引に開いても、 人間は読むことが出来ないデータ形式 → (世界のどこかに読める人はいるかも) .dillの中身を見る
【おさらい】 .dillとは? Dartの中間表現であるKernelをシリアライズしてバイナリ表現にしたファイル 中身を強引に開いても、 人間は読むことが出来ないデータ形式 → (世界のどこかに読める人はいるかも) .dillの中身を見る 💐安心してください 💐
Dart SDKにdump_kernel.dartというスクリプトがある https://github.com/dart-lang/sdk/blob/main/pkg/vm/bin/dump_kernel.dart .dillを渡すと、人間が読むことが出来るレベルの .txtで出力される dart \ --packages=/Users/yokoi/dart/sdk/.dart_tool/package_config.json \ /Users/yokoi/dart/sdk/pkg/vm/bin/dump_kernel.dart
\ build/9aa8b8c74e6af46c4be658e24cd3b943.cache.dill.track.dill \ output.txt dartコマンドでdump_kernel.dartを実行し、 第一引数に対象の.dillを、第二引数にアウトプットのパスを指定 ※Flutter SDKのbinには含まれていないので、試すには dart-langの環境を別途準備する必要あり .dillの中身を見る
None
None
None
まとめ 見れるもの • 構造: Library → Class → Member(Field/Procedure) →
Statement/Expressionのツリー • 意味情報: 型・定数(#C…テーブル)・null安全・メソッドシグネチャなど • 参照: canonical nameやIDで他ノードを指す 見れないもの • 元コードのフォーマットやコメント行は保持されない .dillの中身を見る
補足 Q: .dillをdump_kernel.dartで.txtに変換した後に、再度その.txtを.dillに変換できるか? .dillの中身を見る
補足 Q: .dillをdump_kernel.dartで.txtに変換した後に、再度その.txtを.dillに変換できるか? A: できない(公式としてはサポート対象外) 理由 Kernelには合成ノードが加わり、元コードと 1:1の対応ができず完全な復元は不可能だから 合成ノードとは: コードには書いていないが、意味を保つためにコンパイラが後入れする
IR要素 (糖衣構文・カスケード演算子の展開、暗黙のコンストラクタ・フィールドやmixinの合成クラスの追加) .dillの中身を見る
ここまでのまとめ
【まとめ】「ビルドキャッシュの内部構造」編 1. flutter runを実行してから、端末で起動するまでの全体像 2. 実行過程における、Dart SDK内部の主要モジュールの役割・関係性 ◦ frontend_server、Kernelコンパイラ、CFE、.dill 3.
なぜDartをKernelに変換する必要があるのか ◦ 「①クロスプラットフォーム対応のため」と「②開発速度向上のため」 4. ビルドキャッシュの実体は.dillであること 5. なぜ.dillを復元すると、ビルドが高速になるのか 6. サンプルAppを例にした、ビルド成果物の主要ファイル ◦ /.dart_tool と /build 7. <hash>.cache.dill.track.dillはどう生成されるか ◦ ビルド情報に基づいている(dart-defineの構成など) ▪ なぜ基づく必要があるのか 8. .dillの中身を.txtに変換して実際に見ること
【まとめ】「ビルドキャッシュの内部構造」編 1. flutter runを実行してから、端末で起動するまでの全体像 2. 実行過程における、Dart SDK内部の主要モジュールの役割・関係性 ◦ frontend_server、Kernelコンパイラ、CFE、.dill 3.
なぜDartをKernelに変換する必要があるのか ◦ 「①多ターゲット対応のため」と「②開発速度向上のため」 4. ビルドキャッシュの肝となるファイルは .dillであること 5. なぜ.dillを復元すると、ビルドが高速になるのか 6. サンプルAppを例にした、ビルド成果物の主要ファイル ◦ /build、/.dart_tool 7. <hash>.cache.dill.track.dillはどう生成されるか ◦ ビルド情報に基づいている(dart-definesの構成など) ▪ なぜ基づく必要があるのか 8. .dillの中身を.txtに変換して実際に見ること 以上を踏まえて、 「テスト高速化への応用」編に進みます 🚀
テスト高速化への応用
まずは、`flutter test`の全体像から
`flutter test` するとどうなるか ①`flutter test` bin/flutter → flutter_tools ②RunCommandで設定解決 ターゲット,
オプション ③コンパイル制御 TestCompiler, ResidentCompiler がfrontend_serverを管理 ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 ⑥flutter_tester起動 ⑦.dillをロードして JIT実行 Engine + Dart VM起動 ⑧VM Service(任意) Flutter SDK (flutter_tools) ProcessManager.start Kernel(.dill) 出力 実行 Dart SDK (frontend_server / Kernelコンパイラ / CFE) flutter_tester (Flutter Engine / Dart VM)
`flutter test` するとどうなるか ①`flutter run` bin/flutter → flutter_tools ②【割愛】 ③コンパイル制御
TestCompiler, ResidentCompiler がfrontend_serverを管理 ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 ⑥flutter_tester起動 Assets, fonts, l10n ⑦.dillをロードして JIT実行 Engine + Dart VM起動 ⑧VM Service(任意) coverage etc Flutter SDK (flutter_tools) ProcessManager.start Kernel(.dill) 出力 実行 Dart SDK (frontend_server / Kernelコンパイラ / CFE) flutter_tester (Flutter Engine / Dart VM) `flutter run` と同様に `flutter test` も Dart → Kernelに変換するステップがある (細かな違いはあるが割愛)
`flutter test` するとどうなるか ①`flutter run` bin/flutter → flutter_tools ②【割愛】 ③コンパイル制御
TestCompiler, ResidentCompiler がfrontend_serverを管理 ④frontend_server起動(常駐) dartコンパイラ/ランタイム ⑤Kernel(.dill)生成 フル or 増分 キャッシュ利用 ⑥flutter_tester起動 Assets, fonts, l10n ⑦.dillをロードして JIT実行 Engine + Dart VM起動 ⑧VM Service(任意) coverage etc Flutter SDK (flutter_tools) ProcessManager.start Kernel(.dill) 出力 実行 Dart SDK (frontend_server / Kernelコンパイラ / CFE) flutter_tester (Flutter Engine / Dart VM) `flutter run` と同様に `flutter test` も Dart → Kernelに変換するステップがある (細かな違いはあるが割愛) つまり、 .dillのビルドキャッシュが存在し仕組みも同様にある
サンプルAppを`flutter test`して、 実行時間を見てみる 👀
仕様 unit testにて、 表示されているUIが各CLIENT_IDごとで期待通りかを検証するテストコード 実行コマンド flutter test \ --dart-define=CLIENT_ID=jupiter \
--dart-define=FLAVOR=development \ --dart-define=API_KEY=abc \ --update-goldens --coverage 環境変数のCLIENT_IDごとにUIが切り替わる → ex) jupiter, mars, saturn, neptune … サンプルAppを`flutter test`
--dart-define=CLIENT_IDのvalueを計10種変更し、 それぞれで`flutter test`を実行するスクリプトの実行結果 (構成に差分がある箇所をピンク文字色にしている) 各test実行はおよそ37秒、合計で6分13秒
実行後、ビルドキャッシュの .dillを確認する ファイルパスが異なるので注意 • `flutter run` ◦ build/<hash>.cache.dill.track.dill • `flutter
test` ◦ build/test_cache/build/<hash>.cache.dill.track.dill --dart-defineを変更して計10回実行したため、 <hash>.cache.dill.track.dillが10個存在している この状態であれば`flutter run`と同様に、 2回目以降の`flutter test`はビルドキャッシュが機能するため 実行が速くなる
各test実行はおよそ37秒、合計で6分13秒 各test実行はおよそ20秒、合計で3分25秒 ビルドキャッシュなし ビルドキャッシュあり
各test実行はおよそ37秒、合計で6分13秒 各test実行はおよそ20秒、合計で3分25秒 およそ3分短縮 ビルドキャッシュなし ビルドキャッシュあり
各test実行はおよそ37秒、合計で6分13秒 各test実行はおよそ20秒、合計で3分25秒 およそ3分短縮 ビルドキャッシュなし ビルドキャッシュあり コード量の多さに依存して時間が長くなる (自動生成コードが膨大、外部ライブラリに多く依存している etc) もちろん、 --dart-defineのvalue数の多さにも依存して時間が長くなる
もしコンパイルに 2,3分要する Appであれば、 10分単位で短縮される
各test実行はおよそ37秒、合計で6分13秒 各test実行はおよそ20秒、合計で3分25秒 およそ3分短縮 コード量の多さに依存して時間が長くなる (自動生成コードが膨大、外部ライブラリに多く依存している etc) もちろん、 --dart-defineのvalue数の多さにも依存して時間が長くなる もしコンパイルに 2,3分要する
Appであれば、 10分単位で短縮される 特にCI環境はビルドキャッシュなしで実行するケースが多いため、 恩恵が得られにくい ❌ ビルドキャッシュなし ビルドキャッシュあり
ビルドキャッシュの仕組みを テスト高速化へ応用 🚀
ビルドキャッシュの仕組みをテスト高速化へ応用 キャッシュが無効になる条件 「--dart-defineの構成が変化すれば」 →では、変化させずに`flutter test`を実行すればキャッシュが機能する 手段 `flutter test`時のみ、環境変数の指定方法を --dart-define(コンパイル時定数)ではなくランタイム環境変数を使う
--dart-defineではなくランタイム環境変数を使う シングルトンなClient設定値
--dart-defineではなくランタイム環境変数を使う シングルトンなClient設定値
--dart-defineではなくランタイム環境変数を使う シングルトンなClient設定値 `flutter test`時のみ、 CLIENT_IDをランタイム環境変数で使用 それ以外は、 CLIENT_IDを--dart-define(コンパイル時定数)で使用
--dart-defineではなくランタイム環境変数を使う シングルトンなClient設定値 `flutter test`時のみ、 CLIENT_IDをランタイム環境変数で使用 それ以外は、 CLIENT_IDを--dart-define(コンパイル時定数)で使用 CLIENT_IDごとに分岐 case ‘venus’:
case ‘mercury’: case ‘neptune’: case ‘jupiter’: …
--dart-defineではなくランタイム環境変数を使う シングルトンなClient設定値 `flutter test`時のみ、 CLIENT_IDをランタイム環境変数で使用 それ以外は、 CLIENT_IDを--dart-define(コンパイル時定数)で使用 CLIENT_IDごとに分岐 case ‘venus’:
case ‘mercury’: case ‘neptune’: case ‘jupiter’: … 【補足】ランタイム環境変数だとコード最適化が弱くなる releaseビルド(=AOT)におけるコンパイル時定数での分岐は、 該当値以外は成果物から削除される 一方でランタイム環境変数での分岐は、 該当値以外も成果物に含まれてしまう つまり、コード量削減がされないため `flutter test`のみに制限している
実行コマンドの変化 Before(--dart-define) flutter test --dart-define=CLIENT_ID=jupiter After(ランタイム環境変数) CLIENT_ID=jupiter flutter test
実行コマンドの変化 Before(--dart-define) flutter test --dart-define=CLIENT_ID=jupiter After(ランタイム環境変数) CLIENT_ID=jupiter flutter test 実行してみる
None
1つ目はビルドキャッシュ無し
1つ目はビルドキャッシュ無し 2つ目以降はビルドキャッシュあり
1つ目はビルドキャッシュ無し 2つ目以降はビルドキャッシュあり 合計で3分27秒
各test実行はおよそ37秒、合計で6分13秒 【Before】初回実行(ビルドキャッシュなし)
各test実行はおよそ37秒、合計で6分13秒 【Before】初回実行(ビルドキャッシュなし) 【After】初回実行(ビルドキャッシュは 2つ目以降あり) 1つ目のみおよそ37秒、合計で3分27秒 およそ3分短縮
各test実行はおよそ37秒、合計で6分13秒 【Before】初回実行(ビルドキャッシュなし) 【After】初回実行(ビルドキャッシュは 2つ目以降あり) 1つ目のみおよそ37秒、合計で3分27秒 およそ3分短縮 【成功】ビルドキャッシュの仕組みを テスト高速化へ応用 🚀
補足 あくまで効果が分かりやすく見えるようなApp構成にしています 実際のプロダクトではこういった構成は少ない認識ですが、 1ソリューションとしてご理解いただけたら幸いです また、ケースによってはテスト品質を損なうリスクもあるのでご注意ください
ニッチなテーマでしたが、何かの役に立てれば幸いです🙏 これからも良いプロダクト開発を通じて、Flutterコミュニティに貢献していきます 紹介できなかったネタ コード量の多いモジュールだけを事前コンパイルして .dill化し、 ビルド時に注入して差分コンパイルを実現 さらに、その.dillをチームでサーバ共有して活用 ※Dart SDKを直接改修する必要あり →興味がある方はセッション後に自分へ直接聞いてください
おわりに