eaglesakuraの技術ブログ

技術的な話題とか、メモとか。

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()
}

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のローカルエミュレーターは同時にトランザクション接続しすぎるとフリーズする

問題点

  • 表題のまま
  • 100個くらいのUnitTestがgoroutineで2〜3のトランザクションを並列発行した
  • フリーズした

回避

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だけが割り振られるので、ローカルマシンからの開発がやりづらそう
    • そのうち解決しそうではある
    • Compute Engineのインスタンスを立てて、SSH経由で頑張ればつながるらしい
      • f1-micro料金が$5 / month

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 ヘッダをチェックし、香川付近の位置であれば弾くのも良い
  • コレであればコストは最低限になるのではないだろうか

ちゃんと対応するなら?

  • 香川県の人口は100万人弱
    • 日本人1億人として、日本を対象にしたサービスであればユーザーはざっくり1%
    • 1%のユーザーが、コストを上回るリターンを見込めるのであれば対応するほうが良い
  • クライアントごとに X-Allow-Kagawa-Game-Law みたいな設定を入れさせるのは非常に面倒だ
    • Chromeとか主要なエンジンが対応してくれないかな?

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. */ }))
}

コンパイラの気持ちをわかってあげよう

  • コンパイラの気持ちを考えれば、このバグは回避できたかもしれないんだ

10年代の思い出 / JavaのゲームをBREWへ移植した思い出ばなし

BREWとは

BREWとの出会い

  • 新卒で入社し、手取り17万円で働いていた会社で、 Javaに似たへんな言語 で作られたゲームを、BREWへ移植する仕事が降ってきた
    • ゲームの内容自体は面白かったが、ソレは別な話だ
  • 当時docomoユーザーだった俺は、BREWやKCP/KCP+なんて全く知らない
  • なので、手探りで開発をスタートすることとなった
    • 会社的には、前任者のコードがサンプルとして存在していた
  • 2010年前後の、お話である

BREWのざっくりとした開発環境

  • Windowsで開発環境が構築できる
  • Windowsでは x86環境で動作するシミュレータ が提供されている
    • これが後に悲劇を起こす
  • 会社の公式開発環境はVisual C++ 6.0だった
    • どういうわけか、会社には 無償で使えるインストーラ が社内サーバーにあった
    • 新卒で入り、毎月17万円(手取り)ものお賃金をいただき、キラキラしていた俺は、ソレに目をつむった
    • Visual Studio 2008(無償版)を途中から使い始めた
  • 実機動作時はARM環境向けにクロスコンパイルを行う必要がある
    • このコンパイラは有償である
    • MACアドレスによる認証が行われるが、なぜかこの職場のプログラマ用PCには同じMACアドレスを指すハードウェアが認識されていた
    • 新卒で入り、毎月17万円(手取り)ものお賃金をいただき、キラキラしていた俺は、ソレに目をつむった
  • 開発言語は公式にはC言語のサブセット、コンパイラC/C++対応されていた
    • これが後に悲劇を起こす
  • ハードウェアの仕様上、float演算が行えないことが再三警告されていた
    • これが後に悲劇を起こす
  • もとのゲームはJavaプリプロセッサを適用するような当時のガラケーらしいJavaのような言語で開発されていた
    • これが後に悲劇を起こす
  • 詳細な公式ドキュメントは手に入らない
    • これが後に悲劇を起こす
  • アプリの容量(プログラムと、ゲームリソースの容量合計)が1.5MB前後
    • これが後に悲劇を起こす
  • アプリをリリースするためには、KDDIのストア審査が必須である
    • これが後に悲劇を起こす

起こった悲劇たち

アプリ容量天元突破

  • この問題にぶち当たるのは2度めである
    • 1度目は入社直後dojaでやらかしている
  • もともとの容量が大きいゲームだったため、プログラムもしくはグラフィックリソースを大幅に簡略化しなければならない
  • が、そんな開発リソースはない
  • そんなときに見つけたのが当時の2chの戦士たちである
    • すでにネットの海の藻屑になってしまったようだが、数年前まではBREW開発者Wikiがあった
  • BREWはメモリ保護がない (ので、下手なメモリにアクセスするとOSごとクラッシュもまれによくある) ことを生かして、
    1. プログラム全体をzip圧縮
    2. OSからは極小なブートローダーを起動
    3. ブートローダーがプログラムを解凍
    4. CPUの実行ポインタを解凍したプログラムの 特定アドレスに設定して強制実行
  • というワンダフルな手法が紹介されていたので、それを参考にさせてもらった

C言語のサブセットで開発するゲーム

  • 基本的にはC/C++言語であるが、現代に比べてハードウェア・ソフトウェア共に低性能だったため、言語のフル機能が使えるわけではなかった
  • 特に、 グローバル変数が使えない という点については頭を悩まさせてくれた
  • なにせ、もとのゲームプログラムがstatic変数もりもりマッチョマン(constではなく、書き換えもしてる)なので、この制限をどうにか突破しなければならなかった
  • 前述のように、このBREWが動く環境にはメモリ保護がない
  • 解凍したプログラムも、実行ポインタを書き換えれば普通に動く
  • なので、 自己書き換え もいけるんじゃね? というひらめきを実行した
// 実行バイナリに4byteぶんの領域を確保
// この先頭ポインタを int* にキャストして、無理やり自分自身を書き換える
// 普通にint変数を用意するとコンパイラが最適化しやがるので、文字列を使う
const char STATIC_MEMORY = "012"
  • この手段はARM(実機)ではうまくいくが、Windows シミュレータでは動作しない
    • シミュレータはWindows用のDLLファイルを作って読み込むので、メモリ保護がビンビンに働いてアクセスエラーになる
    • なので、 GLOBAL_VAR() SET_GLOGAL_VAR() GET_GLOBAL_VAR() のようなマクロを使ってアクセスすることで切り抜けた

Javaの配列.length問題

  • もとがJava言語ベースの実行環境であるため、配列は .length で長さを取得できる
  • ジグ配列もサポートされているし、バリバリ使われている
  • 移植先はC/C++やぞ、配列にそんなモダンな機能はない

方法1, コンパイラの手を借りる

  • ソースコードにベタ打ちされた配列は sizeof() でbyte数を取得できる
  • sizeof(array) / sizeof(array[0]) とすると、 配列全体のbyte数 / 1要素のbyte数 = 配列の要素数 を求めることができる
  • なので、定数的に定義されているジグ配列はチクチクとこの方法でlenを取り出すようにマクロへ置き換えた

方法2, new演算子オーバーロード

  • 動的に確保している配列のlengthは、sizeofじゃとれない
    • 例えばint配列はnewで確保すると int* なので、sizeofしても容量はポイント1個分なのだ
  • なので、メモリアロケータを加工して次のようなデータを返すようにした
    • [隠しヘッダ ここにbyte数とか書いとく][実データ]
    • new関数のなかで、ヘッダ + 実データ容量を確保して、実データの先頭アドレスを返す
    • lengthにアクセスしたいときは、隠しヘッダの中身を見て計算する
    • ついでに、Javaの初期化を再現するためにゼロ値埋めも行っている

これらを簡易的に行うためにtemplateを使う

  • templateで↑を自動的に扱えるように、ラッパーを作成した
    • .length() のようなメソッドを提供したり
  • おかげで、lengthに関わる移植が終わる頃にはだいぶコードがスッキリとしていた
  • template便利だな

  • そして次の悲劇が起きる

templateサポートの悲劇 / シミュレータはエミュレータじゃない

  • BREW用のARMコンパイラはtemplateもサポートしていた
  • なので、例えば vector3<int> みたいなtemplateを作り、それを別なtemplateの引数(templateの入れ子)をしていた
  • ある日、大量のコンパイルエラーが発生
  • シミュレータ(Windows環境)ではMicrosoft様の優秀なC++コンパイラによってサポートされていたtemplate入れ子が、ARMコンパイラでは非サポートだった
    • ドキュメントに書いとけボケぇ!
    • すまん、言える立場じゃなかったな。。。
  • template入れ子を排除すべく、ひたすらマクロに入れ替える日々が始まった
  • シミュレータはシミュレータであって、ARM実機を再現するわけじゃないと、このとき強く意識に根付いた
    • なので、iOS シミュレータ に対しては言いたいことあるが、まあコイツほど クソ 独特じゃないのでとても快適である

float演算が使えない とは

  • たしかに、当時のARM CPUはfloat演算が行えない
  • 代わりに、同等のソフトウェア命令にコンパイラによって置き換えられる
  • そんなコンパイラの知識なんてなかったので、 float変数を作ったら実機でコンパイルエラーになる と思い込んでた
  • セコセコと固定小数演算したり
  • 気づいたのは、だいぶ後の祭りである

公式ドキュメントが手に入らない

  • 正確には、正規コンテンツプロバイダーが手に入るようなドキュメントが手に入らない
  • 零細ゲーム会社なんてそんなもんだ
    • 手取り17万円しか払えないんだぞ
  • なので、普通にネット上からDLできるざっくりとしたドキュメントを使用した
  • ほんとざっくりだったなお前
  • 例えば、ネットワーク通信時にAPIに渡す構造体のメモリをいつ開放していいのか、失敗時のハンドリングどうするのか、ほとんど書かれてなかった
    • 仕方ないので四六時中保持してた

そして悲劇が起こる

  • 課金APIの詳細な動作仕様が手に入らなかったことにより、課金で問題を起こす
  • 多大な残業と休日出勤と、パブリッシャーサイドの寛大な対応により、なんとか許しを得たはず
    • なお、残業代も割増賃金も辞退した
    • 自責なので仕方がないよね、と新卒のキラキラした目の僕は自分に言い聞かせた
    • んなわけない
    • 17万円しか払えない会社やぞ
    • 発生するわけないやろ

文字が描画できない

  • グラフィックス系のAPIさわってたら、「せやな」程度だけど
  • 当時のdojaのプログラムは、3Dの描画をした上に文字を描画できた
  • BREWは文字描画が専用APIで、3Dの描画と排他仕様だった記憶がある
    • 記憶が曖昧だ
    • つらい過去だったのかもしれない
  • 頑張ってそれっぽく見せるようにした思い出

半透明が描画できない

  • BREWの2D APIの仕様である
    • dojaはそこそこ高速に半透明が2D APIで描画できたのである
  • なんか、全部3Dに入れ替えたんだっけかな
    • 記憶が曖昧だ
    • つらい過去だったのかもしれない
  • 頑張ってそれっぽく見せるようにした思い出

KDDI検証

  • 俺はエクセルを書くためにゲーム会社に入社したわけではなかったんだ
  • ゲーム仕様をひたすらエクセル方眼紙に書き出し、検証資料を作り上げた
    • 記憶が曖昧だ
    • つらい過去だったのかもしれない

実機実行の壁

  • 当時のガラケーはどいつもコイツも実機でプログラムを実行するのが大きな壁だった
  • dojaはネットワーク越しでしかインストールできないため、ゲーム開発のパケ代が自腹である
  • BREWは安っぽいケーブルを接続して転送できるが、実機を転送モードにできるのは正規コンテンツプロバイダーだけである
    • なので、実機で画像ペラ1枚出ただけでも「うううおおおおおおおおおおお!!!!!」みたいな感動があった

そして、2010年代

  • そんな状態だったので、 誰でも無償で開発できて、誰でもマーケットへ公開できる Androidに心惹かれたのは当然であり、きっとBREWに触ってなかったら別なことを思っていたかもしれない
  • Androidに注力した2010年以降は、手取り17万円からは想像もできないほど金も時間も心も豊かになった
  • BREW、君は僕の2010年代を豊かにする大きなターニングポイントの役割を果たしてくれたのかもしれない

  • 今はただ、安らかに眠れ

  • それとこいつら、KCP+のカレンダーAPIの不具合じゃね?

補足

  • 2010年1月現在の手取り、正確には17.6万円弱だった
    f:id:eaglesakura:20200102011815p:plain
    当時の給与明細