みなさん、こんにちは。
ピクトリンク事業部 商品技術開発部 の 足立 です。
今回は通例となってきた フリュー Advent Calendar 2024 の 3日目 の記事となります。
記事の内容としては、iOS アプリのビルドを Github Action で実行しているのですが、そのビルド時間を改善するためにやったことの紹介になります。
地道な対応でしたが効果としては絶大だったので、記事にさせていただきました。
Github Action とは
この記事を読まれている方はほとんどがエンジニアだと思うので、簡単に紹介させていただきます。
Github Action は Github が提供している CI/CD のプラットフォームです。
リポジトリに紐づいているので、PRを上げたタイミングでのテスト実行や、PRマージ後に最新のアプリを自動で作成するなど、さまざまなことができます 😍アリガトウッ!!
改善前の状況
私が担当している ピクトリンクカレンダー でも、 Github Action を利用しており、手動ビルドに限った設定を行なっています。
自動にしていないのは「ビルドに時間がかかる = Github Action の実行時間の長さによる料金が爆上がり」するため、一旦必要なタイミングでのビルドに制限しています。
ビルドするタイミングは「企画メンバーへの動作確認依頼」や「App Store へのアップロード」する時に使っています。
さて、では実際にどれくらいの時間がかかっていたというと・・・
30分!!さんじゅっぷん!!です😂
iOS アプリの場合、どうしても時間がかかってしまいますが、さすがにこのまま運用を続けるには弊害が出てきます。
- 確認用アプリを打ち合わせまでにつくりたい
- スプリントレビューの直前に完了したPBIもアプリに入れて配布したい
- Github Action の Workflow の改善後の動作確認をしたい
などなど、すぐにビルドを終わらせたい状況はたくさんあり、急いでいる時は待っているだけでもストレスを感じることは、皆さんも経験があるのではないでしょうか?
どのように改善したか
結論をひとことで言うと「ビルドに時間がかかっていたライブラリの利用をやめた」だけです。
原因
実装では NotificationCenter でのデータやりとりを type-safe に使えるようにするライブラリである typed-notifications を利用していました。
この typed-notifications
自体に問題があるわけではなく、依存関係にある swift-syntax が直接的な原因になっており、ビルド時のモジュールコピータイミングで、「10 〜 15分 かかる」状態になっていました 🫠
解決方法
解決方法は色々ありますが、今回は「対象のライブラリを使わない」方法を選択しました。
理由として以下が挙げられるため、修正による影響範囲をできるだけ少なくしつつ、 Notification Center
の拡張機能として実装することを決めました。
Notification Center
で代用が可能なことが明確- データのやり取りは
Dictionary
なのでCodable
が利用可能
実装方針として、以下の2つを使っています。
解決方法1: Codable
を使い type-safe
に値を受け渡す
NotificationCenter
では、データの受け渡しでは Dictionary
を使うため「通知を送る側/受け取る側」でパラメータを常に整合が取れている状態にしなければなりません。
パラメータ名やデータ型の間違いで、値の受け渡しができないだけでなく、クラッシュにつながる可能性も出てきます。
Notification Center
を使うにあたって、基本的な情報や変換処理のみを共通部品として提供し、移行作業自体にもあまり時間をかけないようにしました。
実装のサンプルは以下になります。
/// Notification Center で受け渡すデータ struct AdventCalendar: Codable { let year: Int let day: Int let blogTitle: String let blogURL: URL } /// Notiication Center に登録する情報の定義 struct NotificationDefinition<Payload: Decodable> { let name: Notification.Name let payloadKey: String init(name: String) { self.name = Notification.Name(rawValue: name) self.payloadKey = "\(name)_payload" } func payload(_ userInfo: [AnyHashable: Any]?) -> Payload? { guard let payloadJson = userInfo?[payloadKey] as? String else { return nil } let data = decode(payloadJson) return data } ...... } /// 便利拡張として、受け渡すデータの定義を一括管理 extension NotificationDefinition { static var adventCalendarDay3: NotificationDefinition<AdventCalendar> { .init(name: "AdventCalendarDay3") } }
ポイントは NotificationDefinition
の中に、通知の受け渡しで必要となる情報を一括管理できるようにしているところです。
さらに adventCalendarDay3
として共通の定義することで、保守性を意識してみました。
payload(_:)
についてはどこに定義するかは悩みましたが、 NotificationDefinition
にすることで「受け渡すデータのEncode/Decode の間違いをなくす」ことを目的としてこのようにしました。
解決方法2: Combine
で Wrap method を作成
次に、通知の発火・通知の購読する側です。
extension NotificationCenter { func post<T: Codable>( _ definition: NotificationDefinition<T>, payload: T? = nil, object: Any? = nil, userInfo: [AnyHashable: Any]? ) { var notifyInfo: [AnyHashable: Any] = [ definition.payloadKey: encode(payload) as Any ] if let userInfo { notifyInfo = notifyInfo.merging(userInfo) { x, _ in x } } let notification = Notification(name: definition.name, object: object, userInfo: notifyInfo) self.post(notification) } func publisher<T: Codable>( _ definition: NotificationDefinition<T>, object: AnyObject? = nil ) -> AnyPublisher<NotificationData<T>, Never> { return self.publisher(for: definition.name, object: object) .compactMap { notification in let payload = definition.payload(notification.userInfo) return NotificationData(payload: payload, center: self, object: object) } .eraseToAnyPublisher() } } /// Combine 利用時の返却データ struct NotificationData { let payload: T? let center: NotificationCenter let object: AnyObject? }
post(name:object:userInfo:) と publisher(for:object:) のラッパーメソッドを定義し、NotificationDefinition
を扱えるようにしました。
ポイントは publisher(_:object:)
の返却値を NotificationData
としているところです。
本来受け渡したいデータだけでなく、NotificationCenter
自体の情報も返却することで、購読する側の柔軟性をあげています。
また post(_:payload:object:userInfo:)
の方では、実際に受け渡したいデータ以外に userInfo
もあえてパラメータとして受け取るようにしています。
こちらも柔軟性を考慮したものですが、同じ Dictionary の Key
が定義されると「パラメータの方がかき消される」ことになるところは注意でしょうか。
※ より安全に使うならば userInfo
自体をなくすか、専用のキー情報を定義し、そちらに格納する方法もありそうです。
使い方
最後に使い方の紹介ですが、そこまで難しいものではないので、さらっと見ていただければと。
private var cancellable = Set<AnyCancellable>() // 通知の購読 NotificationCenter.default .publisher(.adventCalendarDay3) .map { $0.payload } .sink { [weak self] adventCalendar in print(""" \(adventCalendar.year)年のフリューアドベントカレンダー \(adventCalendar.day)日目の記事は「\(adventCalendar.blogTitle)」です。 URL: \(adventCalendar.blogURL.absoluteString) """) } .store(in: &cancellable) // MARK: - let day3 = AdventCalendar( year: 2024, day: 3, blogTitle: "ピクトリンクカレンダーで改善した話でなにか書きます", blogURL: URL(string: "https://www.furyu.jp/news/2024/11/pictlink_calendar/") ) // 通知の発火 NotificationCenter.default .post( .adventCalendarDay3, payload: day3, userInfo: nil)
実行結果
2024年のフリューアドベントカレンダー 3日目の記事は「ピクトリンクカレンダーで改善した話でなにか書きます」です。 URL: https://www.furyu.jp/news/2024/11/pictlink_calendar/
解決方法3: typed-notifications
をプロジェクトから除外
忘れてはいけないのが、typed-notifications
を利用している箇所全てを、上記 Notification Center
の拡張機能に置き換え、対象のライブラリはプロジェクトから除外します。
改善後の状況
上記改善を行い、ビルドした結果がこちらになります。
驚きの結果!!10分 です!!💗
この結果には正直自分も驚きました。最初は、モジュールコピーの時間がなくなるので、15分前後になると思っていました。
ただ、冷静になると当たり前で typed-notifications
や swift-syntax
の Checkout など、関連するすべての処理がなくなるのでこれくらい早くなって当然ですね😆
今後について
今回は typed-notifictoins
相当の機能が、自前の実装で置き換えられる規模感だったので問題なく対応できましたが、他のライブラリも同様の対応ができるとは限りません。
では、どうするかというと XCFrameworks の導入を積極的にやっていきたいと考えています。
導入するライブラリがすでに XCFrameworks
に対応したものであれば良いのですが、そうでないものもたたあります。
その場合にどうするのかを今後は調べつつ、さらに改善していきたいと思っています。
また、キャッシュの扱い方を変えるだけでもかわるといいますが、こちらは色々試していますが、どうもうまくいかないことも多く、もう少し色々と調べていきたいと思います。
おまけ: Github Action 実行ログチェッカー
今回、この原因に気づくきっかけは「ビルドが遅いので実行ログを眺めていたら、長い間処理が止まっている」状態に気づいたことでした。
今後の運用や他のプロジェクトの改善をする場合、全てのログを追いかけたり、処理時間の計算をすることは大変なので、今後の活用も踏まえて「Github Action 実行ログチェッカー」なるモノを作成しました。
ただの表計算を使っているので、大したモノではありませんが、こちらもついでに紹介させていただきます。
利用方法は簡単で「Github Action の実行ログを貼り付ける」だけで「閾値に沿った対応優先度のチェックマークがつく」だけです。
他のプロジェクトでも時間がかかっているものがあるので、チェックしてみようと思います。
さいごに
どのプロジェクトでも、ビルド改善は積極的に行われていると思います。
専任チームによる改善を進めるプロジェクトもあると思いますが、多くが私たちと同じ「施策をすすめつつ、開発チーム内で改善を行う」状況だと思います。
私、個人としてはこの状況自体は悪いとは思いませんが「改善することの優先度をさげ続ける」状態になってしまうのは避けるべきと考え、積極的に開発チーム以外のメンバーに「開発改善の重要性、必要性を認識してもらう」ことを行なっています。
これからもプロダクト品質はもちろんですが、開発の品質、プロジェクト進行の効率化などを進められるように、日々考え動けていけたらと思います。
何か新たな改善ができたら紹介させていただきますので、よろしくお願いします。
それでは、明日以降もアドベントカレンダーをお楽しみください!