Settings... > General > Window > Native full screen windows のチェックを外す
お好みで Settings... > Appearance > General > Auto-hide menu bar in non-native fullscreen のチェックを外す
普通にフルスクリーンにする
これが こうなる
真のフルスクリーンの煩わしさが無くなって幸せ
Settings... > General > Window > Native full screen windows のチェックを外す
お好みで Settings... > Appearance > General > Auto-hide menu bar in non-native fullscreen のチェックを外す
普通にフルスクリーンにする
真のフルスクリーンの煩わしさが無くなって幸せ
車
以上!!!!!
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
全体的に心地よかった。
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 の記事ですお楽しみに〜。
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などをしている人がいた。一足飛びにやるには力が必要だがそんなものはないので、こういうことを段階的にやればよかった。一足飛びにできたら素早くスコアが上がっていいけど、できなかったら段階的にやったときに上がる分も上がらないのでリスクが大きい。
ふりかえると、本番と同じチームで練習ができてよかった。自主練の成果も多少は出たし気づきもあった。引き続き練習して本番前日は早く寝ようと思う。
最後に、企画・運営してくれた同僚と費用を持ってくれた我が社ありがとうございました。
⌘Qを誤爆すると悲しいから
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) {} }
最終回
わざと遅刻して残念今日で終了ですと言おうと思っていたが、昨日の時点のコードでベンチ回しても100万点出なかったので取り戻していたら普通に遅刻した。