branchを切ったらすぐにpushしてCIだけは通しておこう
前提
- masterやdevelpoブランチでビルドが通っている
- その状態で作業branchを作成する
- branchを作ったら、何も更新してなくてもすぐにpushしてCIは通しておく
理由
- CIが壊れるのは自分の更新だけが理由ではない
- CI設定の間違い
- サーバーが更新されて動作しなくなった
- ライブラリのバージョンがいつの間にか変わっていた
- dependenciesのバージョン名に
+
とかつけるのはやめよう
- dependenciesのバージョン名に
- キャッシュ有無で正常にCIが通らなくなった
- mavenリポジトリが死んでしまった
- bintray、おまえよく死ぬな
自分の作業によってCIが壊れたか
環境によって壊れたか
の違いをちゃんと切り分けてから作業しないと、無駄な確認作業が発生するぞ
Kotlinでinterface定義モジュールと実装モジュールを分離した上で実装を隠蔽する
やりたいこと
- interface定義と、その実装をなるべくキレイに切り分けたい
インターフェース定義用モジュール
// ":interface-module" に定義を書いていく... interface AuthService { fun login(id: String, password: String) }
実装用モジュール
// ":impl-module" に実装を書いていく... internal class AuthServiceImpl { override fun login(id: String, password: String) { // ログイン処理する.... } }
コレの問題点
- AuthServiceImplがinternalなので、他のモジュールから見えない
- けれどAuthServiceImplをモジュールの外に開放したくない(後で入替えたりとかやりやすいように)
解決
- interface側にcompanion objectを入れておく
interface定義を修正
// companionだけ書いておく interface AuthService { fun login(id: String, password: String) companion object }
実装用モジュールにExtensionも定義
// ":impl-module" に実装を書いていく... internal class AuthServiceImpl { override fun login(id: String, password: String) { // ログイン処理する.... } } // CompanionのExtensionとしてインスタンス生成メソッドを生やす // これはinternalにしない。 // 利用側からは `AuthService.newInstance()` と呼ぶことができる fun AuthService.Companion.newInstance() { return AuthServiceImpl() }
- これでinternal classを開放せずにインスタンスを作れる
Androidアプリビルドの並列最大化の限界値
ビルド速度に影響する要素
- CPU論理コア数
- シングルコアスペック
- メモリ
- プロジェクト自体の並列性
gradleのworker設定
- gradleには
--max-workers
設定がある - このオプションを闇雲に指定しても意味はない
- デフォルト値はCPU論理プロセッサー数
public DefaultParallelismConfiguration() { maxWorkerCount = Runtime.getRuntime().availableProcessors(); }
Workerが増える条件
- 並列可能なタスクが発生すると、Workerが増える
- Workerは予約されない。なので、
--max-workers
オプションをいくら設定しても、不必要なworkerは起動しない - mavenのライブラリ取得とか、並列実行しやすいタスクが発生すると一気に増えるのが観測できる
- ローカルキャッシュを全消しして、--max-workersをやたら増やすと壮観な図を観れる
// DefaultBuildOperationQueue @Override public void add(final T operation) { lock.lock(); try { if (queueState == QueueState.Done) { throw new IllegalStateException("BuildOperationQueue cannot be reused once it has completed."); } if (queueState == QueueState.Cancelled) { return; } workQueue.add(operation); pendingOperations++; workAvailable.signalAll(); if (workerCount == 0 || workerCount < workerLeases.getMaxWorkerCount() - 1) { // `getMaxWorkerCount() - 1` because main thread executes work as well. See https://github.com/gradle/gradle/issues/3273 // TODO This could be more efficient, so that we only start a worker when there are none idle _and_ there is a worker lease available executor.execute(new WorkerRunnable()); } } finally { lock.unlock(); } }
最終的に収束するWorker数
- どんなに頑張っても、module同士の依存によって増えるworker数には限度がある
- その状態ではCPUコア数よりもシングルスレッド性能のほうが重要となる
例
- これらのビルド速度は最終的にほぼ同一に落ち着いた
快適性
FirestoreとMemorystoreとDatastoreと
Google Cloud Platformのサーバーデータはどこに保存するか
- SQL系は月額固定課金なので、気軽には使えない(お金持ちを除く)
- なので従量課金制の保存先としてDatastoreを気に入っていた
DatastoreとMemcache
- DatastoreはFirestoreと両立できない
- アプリがFirestoreを使っており、同じGCPプロジェクトにGAEが同居するとDatastoreは虚空へと消える
- MemcacheがGoogle App Engineは標準APIで使えた
- GAEのRuntimeが刷新されたことでMemcacheアクセスが行えなくなった
Memorystore
- Memcacheの後継のように見えて、違う
- コイツは月額固定だ
- 最低環境で$50/monthが虚空へと消える
- Private IPだけが割り振られるので、ローカルマシンからの開発がやりづらそう
Firestore無料枠 VS Memorystore最低課金額
- Read/Write共に50,000の無料枠がある
- 250,000 Read/Write per Dayくらい使うと$50課金が発生する
- もう直接アクセスでいいんじゃね?
- 1msでも早くレスポンスを返したい場合を除く
Firestoreは安い
- 安い
- ガンガン使おう
Google App Engineで香川県ゲーム条例に対応する
雑感
X-AppEngine-City, X-AppEngine-CityLatLong
- Google App Engineはリクエスト元のIPからざっくりとした国・都市情報を取得できる
X-AppEngine-City
ヘッダをチェックし、香川県の都市だったら403 Forbidden
を返してしまうのが一番楽ではないだろうか- 万全を期すなら、
X-AppEngine-CityLatLong
ヘッダをチェックし、香川付近の位置であれば弾くのも良い - コレであればコストは最低限になるのではないだろうか
ちゃんと対応するなら?
LiveDataを複数回observeしても1個のObserverしか登録されない現象と解法
前提
- LiveDataのonActive/onInactiveで処理を行わせていた
- 強制的にLiveDataをactiveにするため、次のようなExtensionを作った
- LiveDataをactiveにしたいだけなので
- Observerはナニも処理していない
// 強制的にLiveDataをactiveにする fun <T> LiveData<T>.forceActiveAlive(owner: LifecycleOwner) { observeForever(Observer { /* drop value. */ }) }
問題点
- 2箇所以上から
LiveData.forceActiveAlive()
を呼び出す - 1箇所でもonDestroyが走ると、LiveDataがinactiveになる
理由
- Kotlinのコンパイラが、
Observer { /* drop value. */ }
をシングルトンとして最適化し、コンパイルしていた - そのため、2回め以降の
LiveData.forceActiveAlive()
で実行がキャンセルされていた
解法
- 毎度newが走るようにしてあげれば想定通りに動く
private class ObserverWrapper<T>(private val observer: Observer<T>) : Observer<T> { override fun onChanged(t: T?) { observer.onChanged(t) } } fun <T> LiveData<T>.forceActiveAlive(owner: LifecycleOwner) { observeForever(ObserverWrapper(Observer { /* drop value. */ })) }
コンパイラの気持ちをわかってあげよう
- コンパイラの気持ちを考えれば、このバグは回避できたかもしれないんだ