flutter driverでassetsが原因で画像が表示されなくなった件 #flutter
flutter driverでE2Eテストをゴリゴリ書こうとした時にハマった内容と暫定的な対処方法を共有します。
※ 2020/10/21 追記あり
ある日突然flutter driverで画像が表示されなくなった
flutterではflutter driverというIntegration test用のFWが用意されています。
公式を参考にそれっぽいシナリオを実行していたのですが、ある日突然今まで成功していたシナリオが失敗するようになりました。
あれれと思ってエミュレータを見てみると、画像が一切表示されていませんでした。
flutter doctorはこんな感じ。
$ flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel unknown, v1.5.4-hotfix.2, on Mac OS X 10.14.6 18G87, locale ja-JP) [✓] Android toolchain - develop for Android devices (Android SDK version 29.0.0) [✓] iOS toolchain - develop for iOS devices (Xcode 10.2.1) [✓] Android Studio (version 3.4) [!] VS Code (version 1.37.1) ✗ Flutter extension not installed; install from https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter [✓] Connected device (2 available)
assetsの数が多すぎるのが原因っぽい
おかしいなと思ってコミットの差分を追ってみると、どうやら画像を大量に追加した前後で挙動がおかしくなっていた。
でも普通にflutter runすると正しく画像は表示されていた。おかしいなぁと思っていろいろググってみるとgithubのIssueを発見。
After days of investigation, I've found that it may have the same cause with #24703. When the AssetManifest.json file size larger than 10 * 1024, it will call compute, then Isolate.spawn will be called which won't work for flutter drive. So images can NOT be displayed. However when AssetManifest.json file size smaller than 10*1024, then It works well. That's why image assets can be displayed for some certain apps.
なんと、AssetManifest.jsonのファイルサイズが 10 * 1024を超えると新しくIsolate.spawnが呼ばれて、画像が読めなくなってflutter driverが動かなくなる模様。
AssetManifest.jsonはassetsのファイル名とパスをkey / valueでまとめたjsonファイルで、iosはApp.Frameworkの中に、androidはbuild配下にビルド時に自動生成されるファイルのようです。
assetsはpubspec.ymlでフォルダごと読み込むようにしていました。
... flutter: uses-material-design: true assets: - assets/ ...
ファイルのサイズを確認すると、確かに10 * 1024は超えていてflutterのObservatory URLを見に行ってみるとspawnのisolateが起動されていました。
$ ls -la AssetManifest.json -rw-r--r-- 1 takamii228 staff 10905 8 28 22:11 AssetManifest.json
対象方法
えーどうしよう...と思ってflutterのソースを眺めてたらそこの制御をしているコードを発見。
Future<String> loadString(String key, { bool cache = true }) async { final ByteData data = await load(key); if (data == null) throw FlutterError('Unable to load asset: $key'); if (data.lengthInBytes < 10 * 1024) { // 10KB takes about 3ms to parse on a Pixel 2 XL. // See: https://github.com/dart-lang/sdk/issues/31954 return utf8.decode(data.buffer.asUint8List()); } return compute(_utf8decode, data, debugLabel: 'UTF8 decode for "$key"'); }
Pixel 2XLだと、10 * 1024を超えるとparseに3msかかるらしい。3msくらいええやん...。
最新のmasterでもこのソースのままだったので、アドホックですがflutter driverの実行前後でこの上限を緩和するpatchを当てるようなシェルを組みました。
asset_bundle.patch
--- a/development/flutter/packages/flutter/lib/src/services/asset_bundle.dart +++ b/development/flutter/packages/flutter/lib/src/services/asset_bundle.dart @@ -67,7 +67,7 @@ abstract class AssetBundle { final ByteData data = await load(key); if (data == null) throw FlutterError('Unable to load asset: $key'); - if (data.lengthInBytes < 10 * 1024) { + if (data.lengthInBytes < 1000 * 1024) { // 10KB takes about 3ms to parse on a Pixel 2 XL. // See: https://github.com/dart-lang/sdk/issues/31954 return utf8.decode(data.buffer.asUint8List());
flutter-driver.sh
#!/bin/sh set -x # 実行前にパッチを当てる CURRENT_DIR=`pwd` cd $HOME patch -p1 -N < ${CURRENT_DIR}/asset_bundle.patch || true cd ${CURRENT_DIR} # flutter driverを実行 flutter driver --debug --flavor develop --target test_driver/app.dart -d xxxxxx # 実行後は元に戻す cd $HOME patch -p1 -N -R < ${CURRENT_DIR}/asset_bundle.patch || true cd ${CURRENT_DIR}
パッチを当ててから実行するようにしたら無事spawnのIsolateが消えてflutter driverでも画像が表示され、E2Eテストが再び通るようになりました🎉🎉🎉
flutter driverは公式の情報がまだまだ少なく、この後もいろいろ踏みそうですがなんとか食らいついて頑張っていこうと思います。
【追記 2020/10/21】パッチファイルの修正が必要
flutter 1.22.0よりasset_ bundle.dartに修正が入ったためパッチファイルも修正が必要になっています。画像が読み込まれなくなる事象はまだ発生するようなので引き続きご注意ください。
asset_bundle.patch
--- a/development/flutter/packages/flutter/lib/src/services/asset_bundle.dart +++ b/development/flutter/packages/flutter/lib/src/services/asset_bundle.dart @@ -71,7 +71,7 @@ abstract class AssetBundle { // that the null-handling logic is dead code). if (data == null) throw FlutterError('Unable to load asset: $key'); // ignore: dead_code - if (data.lengthInBytes < 10 * 1024) { + if (data.lengthInBytes < 1000 * 1024) { // 10KB takes about 3ms to parse on a Pixel 2 XL. // See: https://github.com/dart-lang/sdk/issues/31954 return utf8.decode(data.buffer.asUint8List());
GitLab CEでMerge Request時のCIを事前にマージしてから実行する
最近のお仕事ではCIにGitLab CIを使っています。
Merge Request時にGitLab CIを実行して、CI上で静的解析やビルド・テストを実行してコードをクリーンに保つようにしています。
しかしMerge Requestがしばらく放置されてしまい、当時はCIのPipelineが成功していたとしても、ターゲットブランチが先に進んでしまってからマージした場合、マージ後のCIが失敗してしまうケースがあります。
これを防ぐためにはコンフリクトを直すときのように常にローカルでfetch / rebaseしたり最新のターゲットブランチをマージして再Pushして解決することができますが、なかなか面倒です。
githubやgitbucketにはこれを防ぐ機能として、マージ先のブランチが先に進んでしまっていた場合にマージ先のoriginの情報をマージ対象のブランチにマージする Update branch 機能があり、PRのレビュー画面で実行することができます。
GitLabにも似たような機能がないかと探していたら、こんな機能を見つけました。
https://docs.gitlab.com/ce/ci/merge_request_pipelines/pipelines_for_merged_results/index.html
It’s possible for your source and target branches to diverge, which can result in the scenario that source branch’s pipeline was green, the target’s pipeline was green, but the combined output fails.
By having your merge request pipeline automatically create a new ref that contains the merge result of the source and target branch (then running a pipeline on that ref), we can better test that the combined result is also valid.
GitLab can run pipelines for merge requests on this merged result. That is, where the source and target branches are combined into a new ref and a pipeline for this ref validates the result prior to merging.
マージ対象のブランチにマージしてからPipelineを実行してくれるらしい。
これこれ、と思ってReposirtoyの設定を見てみたら、設定が見当たらない。
おかしいなと思ってよく見てみると・・・
あー課金ユーザ向けだった。。。
仕方ないので似たようなことができるように自作しました。
まずgitlab-ci.ymlの中でMerge Request実行時には最初にupdate-branch.shを実行するように設定します。
// Merge Request時はこっちを実行する mergeRequestBuild: stage: build tags: - ci-runner script: - ./update-branch.sh - ./lint.sh - ./build.sh only: - merge_requests // ブランチPush時はこっちを実行する branchBuild: stage: build tags: - ci-runner script: - ./lint.sh - ./build.sh
update-branch.shでは最新のターゲットブランチとマージ対象のブランチを作成して、マージ対象のブランチにターゲットブランチをマージします。
#!/bin/sh set -x set -e TARGET_BRANCH=$CI_MERGE_REQUEST_TARGET_BRANCH_NAME SOURCE_BRANCH=$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME if [ -n "$SOURCE_BRANCH" ]; then # CIの実行ごとにブランチを再利用しないようにコミットごとにテンポラリのブランチ名を作成する TMP_SOURCE_BRANCH=$SOURCE_BRANCH-`git rev-parse --short HEAD`-`date "+%Y%m%d%H%M%S"` TMP_TARGET_BRANCH=$TARGET_BRANCH-`git rev-parse --short HEAD`-`date "+%Y%m%d%H%M%S"` # Merge対象のブランチをテンポラリのブランチ名でチェックアウトする git checkout -f -b $TMP_SOURCE_BRANCH HEAD # Merge元のブランチをテンポラリのブランチ名でチェックアウトする git checkout -f -b $TMP_TARGET_BRANCH origin/$TARGET_BRANCH # Merge対象のブランチにMerge元のブランチをマージする git merge $TMP_SOURCE_BRANCH -m "merge $CI_COMMIT_REF_NAME into develop" fi
Merge Request対象のブランチ名はCI_COMMIT_REF_NAME
から取得することができました。
https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
gitalb ciでは毎回git checkout HEAD
でローカル環境のチェックアウトをしているため、CI上でブランチのマージを実行する場合は明示的にブランチを切ってcheckoutする必要がありました。
また今回はGitLab CI Runnerはshell
で実行しているため、ビルド実行後のブランチ情報は毎回残ってしまいます。そのため毎回の実行でブランチ名が一位になるようにコミットハッシュ+実行日時をいれるようにしました。
コンフリクトが発生する場合は失敗mergeReuqestBuildは失敗するのでConflictが発生していることがCIの実行時に気づけるようになりました。
自分の情報収集のやり方を整理してみた
後輩とかに「どうやって情報収集してるんですか?」と聞かれることが多々あったので、整理してみました。
GitPitch Presents: github/takami228/mypresentation/how-to-get-informtaion
Modern Slide Decks for Developers on Linux, OSX, Windows 10. Present offline. Share online. Export to PPTX and PDF.
インプットとアウトプットの比率はバランスが大事ですね。
ある程度インプットの貯金がないとアウトプットも出ないですし、逆にインプットしすぎでもただの頭でっかちになってしまいます。
あとアウトプットに含まれるかもですが、「経験から学ぶ」という観点も大事ですよね。
個人的にはインプットの習慣は付きつつあるので、アウトプットの比率を高めないとなーと思うこの頃です。