つばくろぐ @takamii228

知は力なり

AtCoderの色が緑で停滞している

AtCoderを再開した話を以前書いた。

takamii.hatenablog.com

あれからもぼちぼち続けて晴れて緑になったのだけど、その後は800くらいで停滞している。

f:id:takamii228:20191002005624p:plain

水色になりたいと↑では語っていたのだけど、水色になるには今のABCでE問題まで常に解けるくらいにならないといけなさそうだ。

実力的にはEよりもまずABCDを確実に早く解けるようになるのが先だと思い、とりあえず過去問を埋めることにした。

f:id:takamii228:20191002005910p:plain

https://kenkoooo.com/atcoder/#/user/takamii228

8月中にA、B、Cは埋められそうな勢いだったけど仕事が忙しくなったので停滞気味。

A、B、Cを埋めるとsortとか簡単な全探索はパッと書けるようになったので、グラフ問題とかDPの式構築がサクッと解けるようになるのが次のステップかなと思った。

ゆるーく楽しみながら続けたいと思いましたまる。

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の実行時に気づけるようになりました。