eaglesakuraの技術ブログ

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

M1 Max Macbook Pro(14inch)の所感

購入したスペック

  • M1 Max
  • 10 CPU
  • 32 GPU
  • 64GB RAM
  • 512GB SSD

環境構築で躓いたところ

Android SDKAndroid Studioからインストールも認識もできない

  • なにかに引っかかってエラーらしい
  • IntellijAndroid Pluginに付属しているGUIでインストールするとAndroid Studioからも認識される
    • Apple Silicon環境ではemulatorコマンドが消されているので、Android Studioインストーラーがコケる
    • emulatorだけcanaryチャンネル(--channel=3)で手動でインストールすると以後正常動作する

javacがクラッシュする

  • JDK9から付属しなくなったいつものjavax
  • ごちゃごちゃやってるうちに治った

Android Emulatorが起動しない

  • Android StudioのAVD Managerで作成すると常に失敗する
  • コマンドラインで作成すると成功する
  • stableチャンネルに"emulator"がない(Apple Siliconだけ?)ようなので、canaryから取り寄せる
sdkmanager --channel=3 emulator
avdmanager create avd -n instrumentation -k "system-images;android-31;google_apis_playstore;arm64-v8a" -d "Nexus 9"

Flutterアプリビルド速度

  • VSCodeからdebugビルドが30秒〜15秒 -> 9秒〜10秒に短縮

Androidアプリビルド

  • Gradleコマンドでフルビルド75秒 -> 65秒に短縮

費用対効果

  • ビルド速度だけを見たら、40万円出すのは微妙と思うかも
  • 何やってもほぼファンはまわらず、静かに作業できる
    • 全部のUnit Testを並列実行させたらファンが回ったので、ファンは壊れてなさそう
  • 作業中も概ねキビキビ動いてくれる
    • 総合的な快適性はかなり上がった
    • iOSアプリ開発しないのであれば、40万円でデスクトップ構築したほうが安い
  • 逆に言えば、40万円払って出先でもデスクトップ(より多少劣るが)並のスペックを手に入れたと思えばまあ良いかも
  • アップグレード先としてはアリ、高性能なMacを必要とせず既にデスクトップで強力な母艦があるなら微妙
    • けど超静か
    • 静かで強力な開発環境がほしいなら(ARMでよければ)アリ

Android StudioとVSCodeの開発効率のメモ

コード生成

  • equals系のオーバーライドで差が出る
    • Android Studioは割と素直にできるし、data classは自動的に行われる
    • VSCode + Dartは標準機能に無いのでExtensionを追加
      • ただし、Classの記述状況によってはExtensionが生成してくれないので自分でどうにかすることがまれによくある
      • VSCode+Dartは基本機能が(ASに比べて)プアなので、Extensionで機能を拡張していく必要がある
        • Projectで必要なExtensionは標準設定としてシェアできるので、誰かがちゃんと整備すれば実用上あまり問題にならない
  • VSCode + Dart Extensionは型の単純なtypedefを認識してくれないので、一部コード補完が効かなくなる

リファクタリング

プロジェクト環境整備

  • VSCode圧勝。JSONで記述できるので、必要なExtensionやTaskを登録しておきやすい
    • Intellijのプロジェクトシェアは結構面倒

IDEの快適さ

  • 基本的にメモリを積んでCPUで叩けば何ら問題ない
  • VSCodeは参照の検索が遅い
  • VSCodeIDEのレイアウトが限られているのが難点
    • Terminalを上に持っていきたいが、できない
    • Intellijは自由にレイアウトできる
  • VSCodeはRemote Developmentが強い
    • なれたWindows母艦からリモートに接続して開発できる
    • WindowsiOSアプリを開発できる気になれる

Flutterアプリ開発環境の長所短所

M1X出ないのかよ!!

Apple発表会で出なかったので、手持ちの環境の再整備を行うことにした

各環境のレビュー

Intel Mac mini 2018 + Windows

  • i7 6C12T / RAM 64GB
  • 従来の開発環境

iOSアプリビルドにはmacが必須なので用意した環境。 WindowsからVSCode Remoteで接続して開発。

  • 利点
    • AndroidiOSもWebも開発できる
    • VMを立ち上げればWindows AppもLinux Appもビルドできるパーフェクト環境
  • 欠点
    • 根本的にi7が遅いのでビルドも遅い。
    • 騒音がひどい。

Intel Mac Book Pro 2020

  • i7 4C8T / RAM 32GB
  • 持ち運び用

リモートワークで自宅内ノマドする機会が増えたので用意した環境。

  • 利点
    • AndroidiOSもWebも開発できる
    • VMを立ち上げればWindows AppもLinux Appもビルドできるパーフェクト環境
  • 欠点
    • 根本的にi7が遅いのでビルドも遅い。
      • Mac miniと比べてもパワー不足。Ryzenとは比べられるレベルじゃない。
    • 騒音がひどい。

Windows

  • Ryzen 3950X 16C32T / RAM 64GB

  • 利点

    • ビルドが早い
    • Macと比べて動作が静か
  • 欠点
    • iOSビルドができない
    • Flutter以外の周辺ツールと日本語環境のかみ合わせが悪い
      • よく文字化けする
    • flutter pub getでmklinkできずにコケる
      • 管理者権限でpub getすれば通るけど気持ち悪い

Ryzen 3950X + Windows / WSL2

  • 利点
    • ビルドが早い
    • Macと比べて動作が静か
  • 欠点
    • iOSビルドができない
    • adb接続にトリッキーなことをしなければならない
    • Android Studioとか開けないのでAndroid Kotlin側のコードをいじるときに不便

Ryzen 3950X + Ubuntu

  • 利点
    • ビルドが早い
    • Macと比べて動作が静か
  • 欠点
    • iOSビルドができない
    • Windows系のツール(Photoshopとか)が使えない
    • ZoomやDiscordが安定して接続できない

最終的に

Android12対応でやったこと

もうすぐAndroid 12がリリースされるので、最低限対応した内容をメモする。

Android Studio AF対応

  • 新AndroidStudioにビルドツールを切り替えた

UnitTestをRobolectricからInstrumentation Testをメインに変更

  • Android Studio AFから、Robolectricが実行しづらくなった(できなくはないが、使いにくい)
  • なので、Instrumentation Testのみを行う方向に切り替えた
  • それに合わせてCIも変更した

Lint対応

  • Android 12でLint系が更新されたので、チクチクと変更

Activity.exported 属性の対応

  • AndroidManifest.xmlのActivityタグで、exported属性設定が必須になった
  • 内部・外部を問わず、aar内部で利用されているActivityにも適用されるので、ライブラリが古いとビルドが通らない場合がある
  • ビルドツールチェインがタコなので、問題が起きてももとのライブラリ名が不明

抽出

  • API 30の状態でapkをビルド -> 分解してAndroidManifest内のActivityを総チェック
  • exported属性がついていないActivityを洗い出して修正
  • androidx.test が内部で使用しているActivityも、古いバージョンだとexported属性が付いてないのでバージョンを更新

PendingIntentのフラグが厳密化されたので対応

  • androidx.work ライブラリはalpha版に切り替えないといけないので、stableが早く出てほしい

SONYのHDDレコーダーのリモート予約が正常動作しなくなったのでエンジニア的カンで直した

利用機器

  • SONY製 BDR-BDZ-FW2000(容量2TB, ホームサーバー機能対応のモデル。以下レコーダー)
  • マンション標準のネットワーク(グローバルIPは割り当てられない, 1家庭500~800MBps程度の速度が最大)
  • Google Nest Wi-Fi(ただしレコーダーはハブを介して有線接続)

事象

  • レコーダーのリモート予約機能が「レコーダーがネットワークに繋がっていない」という旨のエラーを表示してしまった
  • SONYのQ&Aはすべて試したが改善しない
  • 利用価値の半分がリモート予約(外出先で子供が「コレ予約してよ!」って言うことが多い)

観察

  • レコーダーのネットワークチェックは正常
  • そもそもホームサーバー機能の一部であるリモート視聴は行えるので、ネットワークは接続できている
  • 無線・有線を切り替えたが共に問題ない
  • iOS / Android両方で同じ事象

考察

  • アプリの挙動を見ると、リモート予約(POST/PUT)系だけではなく、「予約済み番組一覧」のGETが行えない
  • アプリのレビューやTwitterの検索をしてみるが、一斉不具合は起きていない
  • レコーダー <--> SONYサーバー <--> アプリの経路のうち、レコーダー <--> SONYサーバーの部分に(俺の環境のいずれか)問題がある恐れが濃厚である

更なる考察

  • アプリ側 / 経路内のプログラムの問題として、Invalidな予約データを受け取る -> 例外吐いて停止している恐れがある
  • レコーダーは年単位で予約している番組(ガイアの夜明けとか)が存在している
  • 古い番組のDate系に問題があるのではないか?

行動

  • 一旦すべての予約データを(後で治せるようにメモして)削除する
  • レコーダーを再起動する
  • これで無事に復旧した
    • 正確なところはわからないが、サーバーやアプリ側のValidationに問題があったのではないだろうか?

GCPを使って子供関係の通知をLINEに統合する

なにをやったのか

  • 子供が保育園・小学校・学習塾に通っている
  • それぞれ、お知らせがWebに掲載される(日記とかも含む)
  • 個別連絡はメールで届く(学年単位の緊急連絡とか)
  • いちいち学校のウェブサイトで確認したりメールを検索するのが面倒なので、LINEに通知を統合した

LINEの通知はIFTTTを使う

  • もうデファクトみたいな感じのIFTTT
  • IFTTTのWeb Hook -> LINE通知を組み合わせることで簡単に行えるので省略

Web Hookへの流し込み戦略

スクレイピング

  • 保育園や小学校、当然ながらRSSのような便利ソリューションは存在しない
  • なので、Firebase Functionsでポーリングすることにした
  • Firebaseの認証処理がスキップできる(デフォルトでAdmin権限が割り当て)られるので、各種処理が楽になる
  • 基本的に httpトリガー を使って、任意のタイミングで呼び出せるようにする
    • 関数の言語はなんでもいいけど、自分はGolang 1.13を使用した。
    • スクレイピング処理には github.com/PuerkitoBio/goquery を使用。
    • Webサイトの構造は当然サイトごとに異なるので頑張って個別対応。
  • Firebase Functions自体にもスケジューリング機能はついているけど、 Cloud Scheduler でKickするほうが再設定やデバッグが楽なので、スケジューリングは設定していない
  • 学校のCMSに負荷をかけるのは本意ではない(し、そこまでWebページの更新頻度は高くない)ので、cronは1時間に1回行ってる.

キャッシュ処理

  • IFTTTのWebHookにはキャッシュがないので、単純にCron実行すると同じ通知が何度も流れてしまう
  • そのため、 Firestore に通知済みのキャッシュを放り込んで、キャッシュが存在したらHookをスキップするようにする

通知とキャッシュのサンプルコード

  • 通知のタイトル・本文・リンクURLの組み合わせをMD5化して、それをキャッシュキーにする
  • キャッシュヒットしたら、それは通知済みなのでスキップ
var firebaseApp *firebase.App
var firestoreClient *firestore.Client
var webhook iftttWebhook.IFTTT

func init() {
    if app, err := firebase.NewApp(context.Background(), nil); err != nil {
        panic(err)
    } else if fs, err := app.Firestore(context.Background()); err != nil {
        panic(err)
    } else {
        firebaseApp = app
        firestoreClient = fs
    }

    webhook = iftttWebhook.New("割り当てられたAPIキー")
}

func md5sum(s string) string {
    sum := md5.Sum([]byte(s))
    return hex.EncodeToString(sum[:])
}

func notifyWebPageLink(ctx context.Context, title string, body string, link string) {
    uid := md5sum(title + "/" + body + "/" + link)

    ref := firestoreClient.Collection("ifttt").Doc("web-notify").Collection("cache").Doc(uid)
    if _, err := ref.Get(ctx); status.Code(err) != codes.NotFound {
        log.Println(fmt.Sprintf("    * Exist Document: %v", ref.Path))
        return
    }
    log.Println(fmt.Sprintf("    * Put Document: %v", ref.Path))
    log.Println(fmt.Sprintf("    * Title: %v", title))
    log.Println(fmt.Sprintf("    * Body: %v", body))
    if _, err := ref.Create(ctx, map[string]interface{}{
        "created_at": time.Now(),
        "title":      title,
        "body":       body,
        "link":       link,
    }); err != nil {
        log.Println(fmt.Sprintf("Firestore write failed: %v", err))
    }

    webhook.Emit("通知ID", title, body, link)
}
// これをhttpトリガーでPublish
func CronHoikuenWeb(w http.ResponseWriter, r *http.Request) {
        うまいことスクレイピング()...
    w.WriteHeader(http.StatusNoContent)
}

// これをhttpトリガーでPublish
func CronShogakkouWeb(w http.ResponseWriter, r *http.Request) {
        うまいことスクレイピング()...
    w.WriteHeader(http.StatusNoContent)
}

GmailからIFTTTへの通知戦略

  • 塾も入退室時にメールで通知してくれる
  • 保育園や小学校は緊急連絡(遠足延期とか)がメールで来る
  • なので、メールもLINEにGOだ。

GASをcronで呼び出す

  • Firebase Functionsだと、自分のGmailへのアクセスが面倒(認証とか)
  • なので、Google Apps Scriptから通知を飛ばすことにした
  • GASはGmail APIを使うとOAuthが自動的に開始されるので、認証がらく。
    • 認証状態を保持したまま、Cronを走らせられる
    • 設定項目もユーザーフレンドリー(cron構文使わなくてよい)
  • メールはGoogleインフラなので、1分に1回ポーリングしても大丈夫そう
    • リアルタイム性を重視したいならpub/subに流して〜とかあるけど、面倒な割にそこまでする必要はない
  • gmail検索条件をそのまま利用できるので、 from:example@example.com newer_than:1h とかできるし、めちゃくちゃ複雑なクエリも作れる
    • けど俺はシンプルにfromと直近のn時間のみでクエリを投げた
    • LINE通知は嫁さんにも届くので、間違ってもFANZAの課金通知を飛ばすわけにはいかねぇんだよ!!
  • UrlFetchApp.fetch の投げ先は、Functionsで作ったhttpトリガー.
    • POSTリクエストでパラメータを受け取り、それをそのままさっきの notifyWebPageLink() に流すようにした
// Functions側はこう
func WebHookFromGmail(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    bodyBytes, err := ioutil.ReadAll(r.Body)
    if err != nil {
        panic(err)
    } else {
        log.Printf("read body: %v", string(bodyBytes))
    }

    var request map[string]string
    if err = json.Unmarshal(bodyBytes, &request); err != nil {
        panic(err)
    } else {
        log.Printf("parsed: %v", request)
    }

    title := request["title"]
    body := request["body"]
    link := request["link"]

    notifyWebPageLink(ctx, title, body, link)

    // ok
    w.WriteHeader(http.StatusNoContent)
}
// GAS側はこう。
function onCron() {
  notifyGmail("gmailの検索条件", "通知タイトル", "リンクURL")
}

function notifyGmail(query, from, link) {
  GmailApp.search(query).forEach(function(thread){
    thread.getMessages().forEach(function(message) {
      console.log("============================");
      console.log(message.getSubject());
      console.log(message.getBody());
      notifyExternalWebHook(from, message.getSubject() + "  ::  " + message.getBody(), link);
    });
  });
}


function notifyExternalWebHook(title, body, link) {
  var requestBody = JSON.stringify({
    title : title,
    body: body,
    link: link,
  });
  console.log(requestBody);
  var options =
  {
    method  : "POST",
    payload : requestBody,
  };
  UrlFetchApp.fetch("自分のエンドポイントURL", options);
}

最後に

  • セキュリティもくそもない書き捨てコードなので、何かあったら自己責任で頑張ろうね!
  • あと、IFTTTのWebHookにキャッシュつけてくんねかな!!

ビルドの並列化をしすぎてCPU占有率100%になってしまったAndroid Studioプロジェクトの対処

何が起きたか

  • モジュール分割や様々な工夫をしてビルド時の並列性を高めた
  • 高めすぎて、Ryzen 3950Xの 16コア 32スレッド の能力を超えた並列性を持ってしまった
  • その結果、Gradleのビルドを行うとすべてのCPUリソースを完全に食い尽くすようになった
    • ZoomとかDiscordとか、セッションが途切れるレベルで食い尽くす
    • メモリ空き容量は30GBくらいあるので大丈夫なはず

解決方法

  • /gradle.propertiesに次のオプションを加えて、プロセスの優先度を下げた
  • Gradle 5.0からのオプションらしい。デフォルトは normal が設定される。
org.gradle.priority=low

その他

  • まさか並列実行性でRyzenが遅れを取るとは思いもしなかった