つばくろぐ @takamii228

知は力なり

flutter driverでassetsが原因で画像が表示されなくなった件 #flutter

f:id:takamii228:20190829204538j:plain

flutter driverでE2Eテストをゴリゴリ書こうとした時にハマった内容と暫定的な対処方法を共有します。

ある日突然flutter driverで画像が表示されなくなった

flutterではflutter driverというIntegration test用のFWが用意されています。

flutter.dev

公式を参考にそれっぽいシナリオを実行していたのですが、ある日突然今まで成功していたシナリオが失敗するようになりました。

あれれと思ってエミュレータを見てみると、画像が一切表示されていませんでした。

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を発見。

github.com

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

f:id:takamii228:20190829201850p:plain

対象方法

えーどうしよう...と思ってflutterのソースを眺めてたらそこの制御をしているコードを発見。

github.com

  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くらいええやん...。

github.com

最新の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テストが再び通るようになりました🎉🎉🎉

f:id:takamii228:20190829203547p:plain

flutter driverは公式の情報がまだまだ少なく、この後もいろいろ踏みそうですがなんとか食らいついて頑張っていこうと思います。

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のレビュー画面で実行することができます。

github.blog

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の設定を見てみたら、設定が見当たらない。

おかしいなと思ってよく見てみると・・・

f:id:takamii228:20190727155021p:plain

あー課金ユーザ向けだった。。。

仕方ないので似たようなことができるように自作しました。

まず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
set -o pipefail

TARGET_BRANCH="develop"
SOURCE_BRANCH=$CI_COMMIT_REF_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.

インプットとアウトプットの比率はバランスが大事ですね。

ある程度インプットの貯金がないとアウトプットも出ないですし、逆にインプットしすぎでもただの頭でっかちになってしまいます。

あとアウトプットに含まれるかもですが、「経験から学ぶ」という観点も大事ですよね。

個人的にはインプットの習慣は付きつつあるので、アウトプットの比率を高めないとなーと思うこの頃です。