ぜのぜ

しりとりしようぜのぜのぜのぜ

Maestroよかった

disclaimer
色々言っているがちょっと触っただけだし、プロダクションで使っているわけでもないのでチラ裏の落書きを眺める気分で読んでね。

UIテストツールの Maestro を触ったらよかった話。

インストール

XcodeとCommand Line ToolsとMaestroのCLIを入れるだけ。Homebrewで入る。

brew tap mobile-dev-inc/tap
brew install maestro

https://maestro.mobile.dev/getting-started/installing-maestro

テスト

アプリをシミュレータにインストールしてテストファイルを書いたらあとは実行するだけ。

# flow.yaml
appId: org.wikimedia.wikipedia
---
- launchApp
- tapOn: "Search Wikipedia"
- inputText: "Hello World!"
- assertVisible: '"Hello, World!" program'
maestro test flow.yaml

左にターミナル、右にシミュレータ。シミュレータではWikipediaのアプリが起動して検索欄にHello World!と入力される。その後"Hello, World!" programを含む検索結果が表示される。
テストの様子

よかったところ

全体的に心地よかった。

YAMLでテストしたいことだけを書けば動くのがよいし、Maestro Studio(GUIでテスト対象を選んでYAMLをエクスポートできる)*1とかContinuous Mode(YAMLを編集したらテストを再実行してくれるモード)*2とかの周辺環境が整備されているのもよい。AppiumみたいにメインのCLIを入れてdriverを入れて、必要ならinspectorを入れて…みたいな手間もない。

何秒待ってアサートみたいなメンタルモデルじゃないのもよいと思うけどプロダクションで使うとそうでもないのかも*3

Flow(一連のテストのことをMaestroではこう読んでいる)の使い回し*4とかJavaScriptを走らせるステップ*5とかもいいんじゃないかなぁ。selectorも色々あってよさそう*6

あとドキュメントもシュッとしてる。

おわり

最初の体験がよかったからか、べた褒めでつまらなくなってきたので終わり。

実は、はてなエンジニア Advent Calendar 2024の9日目の記事でした。
明日は id:Windymelt の記事ですお楽しみに〜。

社内ISUCONに参加した

ISUCON14に合わせて社内ISUCONが開催された。僕はISUCON14に参加するのでその練習として参加したが、お祭り的に(かどうかはわからないが)これだけに参加した同僚もいた。結構盛り上がって楽しかった。

問題はISUCON12の予選だった。CloudFormationのテンプレートが用意されていたのでシュッと環境ができあがってすごかった。ISUCONのどんなことにも言えるが、ISUCONでは普段のスマホアプリの開発ではまったく馴染みがないことをするのですべてが面白い。

お題のアプリケーションはISUPORTSと呼ばれるISUCONの結果を管理できるサービス。複数のテナントに提供されて、テナントには複数の大会と複数のプレイヤーが属す、大会の結果はCSVで入稿される、SaaS自体を管理する人もいて、この人はテナントの追加や料金の確認などを行うという感じ*1

インフラ周りをやってくれたメンバーがデプロイとかベンチマークスクリプトの準備とかをしてくれている間にコードを眺めてTODOリストを書いた。 リストを書くのにSlackのプライベートチャンネルのCanvasを使ったが便利だった。チェックボックスが簡単に生やせる。 ちなみに言語はGoにした。

最初に目についたのは大会のランキング用のN+1で、スコアごとにユーザーを引いていた。これはJOINして一度に取るようにした。ちょうど前日にやっていたのでシュッとできてよかった。

--- a/src/webapp/go/isuports.go
+++ b/src/webapp/go/isuports.go
@@ -411,14 +411,15 @@ func retrieveCompetition(ctx context.Context, tenantDB dbOrTx, id string) (*Comp
 }
 
 type PlayerScoreRow struct {
-  TenantID      int64  `db:"tenant_id"`
-  ID            string `db:"id"`
-  PlayerID      string `db:"player_id"`
-  CompetitionID string `db:"competition_id"`
-  Score         int64  `db:"score"`
-  RowNum        int64  `db:"row_num"`
-  CreatedAt     int64  `db:"created_at"`
-  UpdatedAt     int64  `db:"updated_at"`
+   TenantID          int64  `db:"tenant_id"`
+   ID                string `db:"id"`
+   PlayerID          string `db:"player_id"`
+   CompetitionID     string `db:"competition_id"`
+   Score             int64  `db:"score"`
+   RowNum            int64  `db:"row_num"`
+   CreatedAt         int64  `db:"created_at"`
+   UpdatedAt         int64  `db:"updated_at"`
+   PlayerDisplayName string `db:"player_display_name"`
 }
 
 // 排他ロックのためのファイル名を生成する
@@ -1366,10 +1367,16 @@ func competitionRankingHandler(c echo.Context) error {
    }
    defer fl.Close()
    pss := []PlayerScoreRow{}
+   query := `
+   SELECT player_score.id as "id", player_score.tenant_id as "tenant_id", player_id as "player_id", competition_id as "competition_id", score as "score", row_num as "row_num", player_score.created_at as "created_at", player_score.updated_at as "updated_at", player.display_name as "player_display_name"
+   FROM player_score JOIN player ON player_score.player_id = player.id
+   WHERE player_score.tenant_id = ? AND player_score.competition_id = ? 
+   ORDER BY row_num DESC
+   `
    if err := tenantDB.SelectContext(
        ctx,
        &pss,
-      "SELECT * FROM player_score WHERE tenant_id = ? AND competition_id = ? ORDER BY row_num DESC",
+       query,
        tenant.ID,
        competitionID,
    ); err != nil {
@@ -1384,14 +1391,14 @@ func competitionRankingHandler(c echo.Context) error {
            continue
        }
        scoredPlayerSet[ps.PlayerID] = struct{}{}
-      p, err := retrievePlayer(ctx, tenantDB, ps.PlayerID)
-      if err != nil {
-          return fmt.Errorf("error retrievePlayer: %w", err)
-      }
        ranks = append(ranks, CompetitionRank{
            Score:             ps.Score,
-          PlayerID:          p.ID,
-          PlayerDisplayName: p.DisplayName,
+           PlayerID:          ps.PlayerID,
+           PlayerDisplayName: ps.PlayerDisplayName,
            RowNum:            ps.RowNum,
        })
    }

あとはbulk insertをGitHub Copilotに書かせたりalpを導入したりした。

大会のランキングを計算して返すAPIが遅かったので触ったがうまくいかなかった。 スコアは大会を終了するまでなら何回でも入稿できるが、大会を終了したあとは入稿できなくなるという仕様だったのでまずは大会を終了するときにキャッシュを作った。 それではスコアが大して上がらなかったのでアクセスログを見ると、ランキングへのアクセスは大会の終了後には少なく入稿した直後に多かった。

それならばと入稿するときにキャッシュを作ったが、入稿するAPIがtimeoutして整合性チェックに通らなくなった。 次に、入稿が終わったらレスポンスだけ先に返してキャッシュはgoroutineで作るようにしたが、キャッシュを作り終わる前にランキングにアクセスされるのでキャッシュを使えなかった。 最後に、キャッシュができるまでランキングAPIのレスポンスを遅延させようとしたが実装できずタイムアップとなった。

調べてみると、スコアの入稿APIを早くしたうえでその処理の中でランキングを計算してDBに保存しておく*2、ランキングの計算処理を軽くする*3*4などをしている人がいた。一足飛びにやるには力が必要だがそんなものはないので、こういうことを段階的にやればよかった。一足飛びにできたら素早くスコアが上がっていいけど、できなかったら段階的にやったときに上がる分も上がらないのでリスクが大きい。

ふりかえると、本番と同じチームで練習ができてよかった。自主練の成果も多少は出たし気づきもあった。引き続き練習して本番前日は早く寝ようと思う。

最後に、企画・運営してくれた同僚と費用を持ってくれた我が社ありがとうございました。

Appleの言う(iOSの)boldはsemibold

UIKitとSwiftUIというヘッダーがあり、それぞれの列に3行ずつLorem ipsumと書いてある。1行目の文字が太く、2,3行目の文字が細い。

  • 1行目: 明示的にboldを指定した
  • 2行目: 暗黙的にboldを指定した(boldSystemFont(ofSize:)*1, bold()*2 )
  • 3行目: 明示的にsemiboldを指定した

環境

  • macOS: Sonoma 14.6.1 (23G93)
  • Xcode: 15.4(15F31d)
  • Swfit: 5.10

コード

import SwiftUI

struct ContentView: View {
    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 4) {
                Text("UIKit")
                VStack(alignment: .leading, spacing: 0) {
                    Label(font: .systemFont(ofSize: 20, weight: .bold))
                        .fixedSize()
                    Label(font: .boldSystemFont(ofSize: 20))
                        .fixedSize()
                    Label(font: .systemFont(ofSize: 20, weight: .semibold))
                        .fixedSize()
                }
            }
            VStack(alignment: .leading, spacing: 4) {
                Text("SwiftUI")
                VStack(alignment: .leading, spacing: 0) {
                    Text("Lorem ipsum")
                        .font(.system(size: 20, weight: .bold))
                    Text("Lorem ipsum")
                        .font(.system(size: 20))
                        .bold()
                    Text("Lorem ipsum")
                        .font(.system(size: 20, weight: .semibold))
                }
            }
        }
    }
}

struct Label: UIViewRepresentable {
    let font: UIFont

    func makeUIView(context: Context) -> UILabel {
        let label = UILabel()
        label.text = "Lorem ipsum"
        label.font = font

        return label
    }

    func updateUIView(_ uiView: UILabel, context: Context) {}
}

29日目, 1,050,114

最終回

わざと遅刻して残念今日で終了ですと言おうと思っていたが、昨日の時点のコードでベンチ回しても100万点出なかったので取り戻していたら普通に遅刻した。