Firebase Performance Monitoringを使用したApolloによるGraphQLリクエストのパフォーマンス測定
エクサウィザーズは、Firebase Performance Monitoring の制限を回避し Apollo iOS で GraphQL リクエストの個別パフォーマンスを測定する実装パターンを紹介している。
キーポイント
GraphQL と FPM の計測課題
GraphQL は通常単一エンドポイントへの POST となるため、Firebase Performance Monitoring (FPM) ではリクエストがすべて集約され、個別のパフォーマンス分析が困難になる問題がある。
カスタムネットワークトレースの活用
FPM が提供するカスタムネットワークリクエストトレース機能を用いて、開発者が手動でメトリクスを管理することで、特定の GraphQL 操作ごとの計測を実現する。
Apollo Interceptor を介した実装
Apollo の NetworkTransport にカスタムインターセプター(StartFPMMetricInterceptor など)を組み込み、リクエスト開始時に FPM メトリクスを開始・停止させる仕組みを構築している。
OperationName を活用した識別
Apollo で自動生成された Query/Mutation クラスの operationName を利用し、FPM 上で個別のリクエストとして明確に分類・可視化可能にするアプローチを示している。
Apollo Interceptor を用いたメトリクス計測の分離
リクエスト開始時に呼び出す `StartFPMMetricInterceptor` と、レスポンス終了時に呼び出す `StopFPMMetricInterceptor` を分けることで、GraphQL リクエストとレスポンスの詳細なパフォーマンスデータを取得可能。
Firebase 標準プロトコルによる実装
`NetworkPerformanceMonitorable` プロトコルを定義し、`FirebaseNetworkPerformanceMonitor` で実装することで、リクエストサイズやステータスコードなどのデータを Firebase の `HTTPMetric` に標準形式で渡している。
レスポンス情報の詳細な取得
停止処理において HTTP ステータスコードとレスポンスペイロードサイズを抽出し、これらをメトリクスに反映させることで、ネットワークパフォーマンスの多角的な分析を実現している。
影響分析・編集コメントを表示
影響分析
この記事は、GraphQL と Firebase を組み合わせたモバイルアプリ開発において、パフォーマンス監視の盲点を解消する具体的な実装パターンを提供しています。特に大規模な GraphQL API を扱うプロダクション環境では、単一エンドポイントによるデータ集約がボトルネックとなり得るため、本手法は運用品質向上に直結する実践的な知見です。
編集コメント
GraphQL の特性を逆手に取り、FPM の制限をインターセプターで回避する手法は、モバイルアプリのパフォーマンス最適化において非常に参考になります。特に Apollo の拡張性を理解している開発者にとって即戦力となる実装例です。
こんにちは。エクサウィザーズの介護記録AIアプリ「CareWiz ハナスト」(以下ハナスト)でiOSアプリ開発を担当している伊賀(@iganin_dev)です。
ハナストのテックリードの原のブログ記事にもありましたように、ハナストではAPI通信にGraphQLを利用しています。 本稿ではiOSアプリの通信ライブラリとしてApolloを用いた場合のGraphQLリクエストのパフォーマンスをFirebase Performance Monitoring(以下FPM)を使用して測定する方法に関して記載します。
本稿記載の内容は以下環境を前提に記載しています。
apollo-ios 0.51.0 (※ 1.0.0へのバージョンアップ検証中)
firebase-ios-sdk 9.6.0
本題に入る前に「CareWiz ハナスト」に関して簡単にご紹介します。 ハナストは簡単に言うと「音声入力で介護の記録をするアプリ」です。
以下のLPによくまとまっています。 利用シーンを紹介するデモビデオもありますので、是非ご覧ください。
hanasuto.carewiz.ai
Firebase Performance Monitoringとは
FPMはGoogleが提供しているFirebaseの機能群の一つです。 ネットワークリクエストをはじめ、さまざまな処理にかかった時間や処理の結果(成功・エラー)などを記録、集計しGUIを通してグラフィカルに確認することができます。
ライブラリを追加するのみでアプリの起動時間や画面の滞在時間などを測定してくれる非常に便利なツールです。 ネットワークリクエストも自動的に計測し、カスタムURLパターンを作成すれば特定のリクエストの計測もできます。
FPMでGraphQLリクエストのパフォーマンスを測定する場合の問題点
非常に便利なFPMですが、GraphQLのリクエストを測定しようとした場合に問題が発生します。GraphQLのリクエストは一般的には同一エンドポイントへのPOSTリクエストとなります。例えば、 https://sample.com/graphql
FPMではリクエストのパフォーマンスをURLのパスを元に分類します。従って、GraphQLリクエストのパフォーマンスを測定しようとした場合、そのままではすべての計測結果がsample.com/graphql という一つのエントリに集約されてしまいます。これでは、どのクエリやミューテーションがパフォーマンス上の課題を抱えているのかを判別することが困難です。
カスタムネットワークリクエストトレースについて
FPMでは自動収集するリクエストトレース以外に、開発者にて実装できるカスタムネットワークリクエストトレースを用意しています。HTTPMetricをurlとhttpMethodを引数で渡して初期化し、start() メソッドで計測を開始、stop() メソッドで計測を終了することで、任意のネットワークリクエストのパフォーマンスを計測できます。
guard let metric = HTTPMetric(url: url, httpMethod: .post) else { return } metric.start() metric.requestPayloadSize = requestPayloadSize Task { do { // ネットワークリクエスト実行 let (data, response) = try await URLSession.shared.data(from: url) metric.responsePayloadSize = responsePayloadSize metric.responseCode = response.httpResponse.statusCode metric.stop() } catch let error { // エラーハンドリング metric.stop() } }
先ほどの全てのGraphQLリクエストの計測結果がまとめて集約されてしまうという問題に対して、ハナストではカスタムネットワークリクエストトレースの仕組みを活用して対処しています。基本的な解決方法としてはリクエストごとにURLのパスを分けるというものです。
Apolloを用いて自動生成されたQueryやMutationのclassにおいて、そのOperationの名称をoperationName というプロパティで取得することができます。
SampleQuery.graphql.swift
query sample { user { id name } }
public final class SampleQuery: GraphQLQuery { ... public let operationName: String = "sample" ... }
このoperationName をURLのパスに含めることで、FPM上で各リクエストを個別に計測・集計できるようにします。
先ほどoperationName をURLのパスに含めることで解決できると述べましたが、Apolloを用いてGraphQLリクエストを送信する際、実際のリクエスト先URLを動的に変更するにはどうすればよいでしょうか。ApolloClientの設定を見てみましょう。
let cache = InMemoryNormalizedCache() let store = ApolloStore(cache: cache) let client = URLSessionClient() let transport = RequestChainNetworkTransport( interceptorProvider: SampleInterceptorProvider( store: store, client: client, apiConfig: apiConfig ), endpointURL: endpointURL ) ApolloClient(networkTransport: transport, store: store)
Apolloからリクエストを送ると、interceptorProvider で指定したインターセプタープロバイダの interceptors(for:) メソッドが呼ばれ、一連のインターセプターの配列が取得されます。
func interceptors(for _: some GraphQLOperation) -> [ApolloInterceptor]
ApolloInterceptor プロトコルに準拠したインターセプターは、リクエストの前後で任意の処理を実行することができます。このインターセプターの仕組みを利用して、リクエストの送信前後にFPMの計測開始・終了処理を挿入します。
func interceptors(for _: some GraphQLOperation) -> [ApolloInterceptor]
final class SampleInterceptorProvider { func interceptors(for _: some GraphQLOperation) -> [ApolloInterceptor] { var interceptors: [ApolloInterceptor] = [] // pre fetch interceptors - fetch前に行いたい処理のinterceptorをここに実装します interceptors.append(NetworkFetchInterceptor(client: client)) // post fetch interceptors - fetch後に行いたい処理のinterceptorをここに実装します } }
今回の実装では、FPMの計測開始を担当するStartFPMMetricInterceptor と、計測終了を担当する StopFPMMetricInterceptor という2つのカスタムインターセプターを作成します。これらを NetworkFetchInterceptor の前後に配置します。
StartFPMMetricInterceptor では、operationNameを取得し、ベースとなるエンドポイントURLにその名前をパスとして追加したURLを生成します。このURLとHTTPメソッド、リクエストペイロードサイズを用いてFPMの計測を開始します。
final class StartFPMMetricInterceptor: ApolloInterceptor { init(performanceMonitor: any NetworkPerformanceMonitorable) { self.performanceMonitor = performanceMonitor } private let performanceMonitor: any NetworkPerformanceMonitorable func interceptAsync<Operation>( chain: RequestChain, request: HTTPRequest<Operation>, response: HTTPResponse<Operation>?, completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void ) where Operation: GraphQLOperation { let operationName = request.operation.operationName let url = request.graphQLEndpoint.appendingPathComponent("/\(operationName)") let requestPayloadSize = try? request.toURLRequest().urlRequest?.httpBody?.count performanceMonitor.start(url: url, method: .post, requestPayloadSize: requestPayloadSize) chain.proceedAsync(request: request, response: response, completion: completion) } }
ApolloInterceptor プロトコルで定義されている interceptAsync<Operation> メソッド内で、chain.proceedAsync(...) を呼び出すことで、次のインターセプターに処理を引き継ぎます。
StopFPMMetricInterceptor では、ネットワークリクエスト後のレスポンスを受け取り、FPMの計測を終了します。
final class StopFPMMetricInterceptor: ApolloInterceptor { init(performanceMonitor: any NetworkPerformanceMonitorable) { self.performanceMonitor = performanceMonitor } private let performanceMonitor: NetworkPerformanceMonitorable func interceptAsync<Operation>( chain: RequestChain, request: HTTPRequest<Operation>, response: HTTPResponse<Operation>?, completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void ) where Operation: GraphQLOperation { let statusCode = response?.httpResponse.statusCode ?? 0 let responsePayloadSize = response?.rawData.count performanceMonitor.stop(statusCode: statusCode, responsePayloadSize: responsePayloadSize) chain.proceedAsync(request: request, response: response, completion: completion) } }
ネットワークのレスポンスからstatusCodeやレスポンスペイロードサイズを取得し、HTTPMetricに渡しています。 NetworkPerformanceMonitorable はFPMに依存しない計測インターフェースとして定義し、テスト容易性を高めています。
NetworkPerformanceMonitorable プロトコルと、そのFPMを用いた実装である FirebaseNetworkPerformanceMonitor の実装例は以下の通りです。
計測の開始時点で呼び出すstart(url:,method:) メソッドでは、HTTPMetricを初期化し、リクエストペイロードサイズを設定して計測を開始します。
stop(statusCode:, responsePayloadSize:) メソッドでは、レスポンスペイロードサイズとステータスコードを設定し、計測を停止します。
public protocol NetworkPerformanceMonitorable { func start(url: URL, method: PerformanceMonitorHttpMethod, requestPayloadSize: Int?) func stop(statusCode: Int, responsePayloadSize: Int?) func cancel() } public final class FirebaseNetworkPerformanceMonitor: NetworkPerformanceMonitorable { public init() {} private var metric: HTTPMetric? public func start(url: URL, method: PerformanceMonitorHttpMethod, requestPayloadSize: Int?) { let metric = HTTPMetric(url: url, httpMethod: method.convertToFirebaseHttpMethod()) metric?.requestPayloadSize = requestPayloadSize ?? 0 self.metric = metric metric?.start() } public func stop(statusCode: Int, responsePayloadSize: Int?) { guard let metric else { return } metric.responsePayloadSize = responsePayloadSize ?? 0 metric.responseCode = statusCode >= 0 ? statusCode : nil metric.stop() } public func cancel() { metric = nil } }
最後にInterceptorProvider を修正し、作成した2つのインターセプターを NetworkFetchInterceptor の前後に追加します。
final class SampleInterceptorProvider { func interceptors(for _: some GraphQLOperation) -> [ApolloInterceptor] { let performanceMonitor = FirebaseNetworkPerformanceMonitor() var interceptors: [ApolloInterceptor] = [] // pre fetch interceptors - fetch前に行いたい処理のinterceptorをここに実装します interceptors.append(StartFPMMetricInterceptor(performanceMonitor: networkPerformanceMonitor)) interceptors.append(NetworkFetchInterceptor(client: client)) // post fetch interceptors - fetch後に行いたい処理のinterceptorをここに実装します nterceptors.append(StopFPMMetricInterceptor(performanceMonitor: networkPerformanceMonitor)) } }
これでApolloを用いたGraphQLリクエストの都度StartFPMMetricInterceptor が operationName を含むURLでFPMの計測を開始し、リクエスト終了後に StopFPMMetricInterceptor が計測を終了します。その結果、FPMのコンソール上では各クエリやミューテーションごとにパフォーマンスデータが個別に集計・表示されるようになります。

今回の実装はGraphQLリクエストのCachePolicyとして.fetchIgnoringCacheCompletely を指定した場合を想定しています。キャッシュからの読み込み時など、ネットワークリクエストが発生しないケースについては、別途考慮が必要です。
今回はハナストでGraphQLリクエストのパフォーマンスをどのように測定しているのかをご紹介しました。
本稿ではご紹介できませんでしたが、エッジでの音声認識、ウェイクワード検知、フルSwiftUIでのアプリ開発、Swift Concurrencyの実践的導入などハナストiOSアプリの開発はチャレンジングで面白い課題に日々挑戦しています。
CareWiz事業部およびエクサウィザーズでは社会課題の解決に一緒に取り組む仲間を募集しています。 介護をより良くするプロダクトの開発、あるいはAIで社会課題を解決するエクサウィザーズに少しでも興味がありましたら、是非ご応募ください!
open.talentio.com
原文を表示
こんにちは。エクサウィザーズの介護記録AIアプリ「CareWiz ハナスト」(以下ハナスト)でiOSアプリ開発を担当している伊賀(@iganin_dev)です。
ハナストのテックリードの原のブログ記事にもありましたように、ハナストではAPI通信にGraphQLを利用しています。 本稿ではiOSアプリの通信ライブラリとしてApolloを用いた場合のGraphQLリクエストのパフォーマンスをFirebase Performance Monitoring(以下FPM)を使用して測定する方法に関して記載します。
本稿記載の内容は以下環境を前提に記載しています。
apollo-ios 0.51.0 (※ 1.0.0へのバージョンアップ検証中)
firebase-ios-sdk 9.6.0
本題に入る前に「CareWiz ハナスト」に関して簡単にご紹介します。 ハナストは簡単に言うと「音声入力で介護の記録をするアプリ」です。
以下のLPによくまとまっています。 利用シーンを紹介するデモビデオもありますので、是非ご覧ください。
hanasuto.carewiz.ai
Firebase Performance Monitoringとは
FPMはGoogleが提供しているFirebaseの機能群の一つです。 ネットワークリクエストをはじめ、さまざまな処理にかかった時間や処理の結果(成功・エラー)などを記録、集計しGUIを通してグラフィカルに確認することができます。
ライブラリを追加するのみでアプリの起動時間や画面の滞在時間などを測定してくれる非常に便利なツールです。 ネットワークリクエストも自動的に計測し、カスタムURLパターンを作成すれば特定のリクエストの計測もできます。
FPMでGraphQLリクエストのパフォーマンスを測定する場合の問題点
非常に便利なFPMですが、GraphQLのリクエストを測定しようとした場合に問題が発生します。GraphQLのリクエストは一般的には同一エンドポイントへのPOSTリクエストとなります。例えば、 https://sample.com/graphql
FPMではリクエストのパフォーマンスをURLのパスを元に分類します。従って、GraphQLリクエストのパフォーマンスを測定しようとした場合、そのままではすべての計測結果がsample.com/graphql
カスタムネットワークリクエストトレースについて
FPMでは自動収集するリクエストトレース以外に、開発者にて実装できるカスタムネットワークリクエストトレースを用意しています。HTTPMetricをurlとhttpMethodを引数で渡して初期化し、start()
guard let metric = HTTPMetric(url: url, httpMethod: .post) else { return } metric.start() metric.requestPayloadSize = requestPayloadSize Task { do { // ネットワークリクエスト実行 let (data, response) = try await URLSession.shared.data(from: url) metric.responsePayloadSize = responsePayloadSize metric.responseCode = response.httpResponse.statusCode metric.stop() } catch let error { // エラーハンドリング metric.stop() } }
先ほどの全てのGraphQLリクエストの計測結果がまとめて集約されてしまうという問題に対して、ハナストではカスタムネットワークリクエストトレースの仕組みを活用して対処しています。基本的な解決方法としてはリクエストごとにURLのパスを分けるというものです。
Apolloを用いて自動生成されたQueryやMutationのclassにおいて、そのOperationの名称をoperationName
SampleQuery.graphql.swift
query sample { user { id name } }
public final class SampleQuery: GraphQLQuery { ... public let operationName: String = "sample" ... }
このoperationName
先ほどoperationName
let cache = InMemoryNormalizedCache() let store = ApolloStore(cache: cache) let client = URLSessionClient() let transport = RequestChainNetworkTransport( interceptorProvider: SampleInterceptorProvider( store: store, client: client, apiConfig: apiConfig ), endpointURL: endpointURL ) ApolloClient(networkTransport: transport, store: store)
Apolloからリクエストを送ると、interceptorProvider
func interceptors(for _: some GraphQLOperation) -> [ApolloInterceptor]
ApolloInterceptor
func interceptors(for _: some GraphQLOperation) -> [ApolloInterceptor]
final class SampleInterceptorProvider { func interceptors(for _: some GraphQLOperation) -> [ApolloInterceptor] { var interceptors: [ApolloInterceptor] = [] // pre fetch interceptors - fetch前に行いたい処理のinterceptorをここに実装します interceptors.append(NetworkFetchInterceptor(client: client)) // post fetch interceptors - fetch後に行いたい処理のinterceptorをここに実装します } }
今回の実装では、FPMの計測開始を担当するStartFPMMetricInterceptor
StopFPMMetricInterceptor
NetworkFetchInterceptor
StartFPMMetricInterceptor
final class StartFPMMetricInterceptor: ApolloInterceptor { init(performanceMonitor: any NetworkPerformanceMonitorable) { self.performanceMonitor = performanceMonitor } private let performanceMonitor: any NetworkPerformanceMonitorable func interceptAsync<Operation>( chain: RequestChain, request: HTTPRequest<Operation>, response: HTTPResponse<Operation>?, completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void ) where Operation: GraphQLOperation { let operationName = request.operation.operationName let url = request.graphQLEndpoint.appendingPathComponent("/\(operationName)") let requestPayloadSize = try? request.toURLRequest().urlRequest?.httpBody?.count performanceMonitor.start(url: url, method: .post, requestPayloadSize: requestPayloadSize) chain.proceedAsync(request: request, response: response, completion: completion) } }
ApolloInterceptor
interceptAsync<Operation>
StopFPMMetricInterceptor
final class StopFPMMetricInterceptor: ApolloInterceptor { init(performanceMonitor: any NetworkPerformanceMonitorable) { self.performanceMonitor = performanceMonitor } private let performanceMonitor: NetworkPerformanceMonitorable func interceptAsync<Operation>( chain: RequestChain, request: HTTPRequest<Operation>, response: HTTPResponse<Operation>?, completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void ) where Operation: GraphQLOperation { let statusCode = response?.httpResponse.statusCode ?? 0 let responsePayloadSize = response?.rawData.count performanceMonitor.stop(statusCode: statusCode, responsePayloadSize: responsePayloadSize) chain.proceedAsync(request: request, response: response, completion: completion) } }
ネットワークのレスポンスからstatusCodeやレスポンスペイロードサイズを取得し、HTTPMetricに渡しています。 NetworkPerformanceMonitorable
NetworkPerformanceMonitorable
FirebaseNetworkPerformanceMonitor
計測の開始時点で呼び出すstart(url:,method:)
stop(statusCode:, responsePayloadSize:)
public protocol NetworkPerformanceMonitorable { func start(url: URL, method: PerformanceMonitorHttpMethod, requestPayloadSize: Int?) func stop(statusCode: Int, responsePayloadSize: Int?) func cancel() } public final class FirebaseNetworkPerformanceMonitor: NetworkPerformanceMonitorable { public init() {} private var metric: HTTPMetric? public func start(url: URL, method: PerformanceMonitorHttpMethod, requestPayloadSize: Int?) { let metric = HTTPMetric(url: url, httpMethod: method.convertToFirebaseHttpMethod()) metric?.requestPayloadSize = requestPayloadSize ?? 0 self.metric = metric metric?.start() } public func stop(statusCode: Int, responsePayloadSize: Int?) { guard let metric else { return } metric.responsePayloadSize = responsePayloadSize ?? 0 metric.responseCode = statusCode >= 0 ? statusCode : nil metric.stop() } public func cancel() { metric = nil } }
最後にInterceptorProvider
final class SampleInterceptorProvider { func interceptors(for _: some GraphQLOperation) -> [ApolloInterceptor] { let performanceMonitor = FirebaseNetworkPerformanceMonitor() var interceptors: [ApolloInterceptor] = [] // pre fetch interceptors - fetch前に行いたい処理のinterceptorをここに実装します interceptors.append(StartFPMMetricInterceptor(performanceMonitor: networkPerformanceMonitor)) interceptors.append(NetworkFetchInterceptor(client: client)) // post fetch interceptors - fetch後に行いたい処理のinterceptorをここに実装します nterceptors.append(StopFPMMetricInterceptor(performanceMonitor: networkPerformanceMonitor)) } }
これでApolloを用いたGraphQLリクエストの都度StartFPMMetricInterceptor
StopFPMMetricInterceptor

今回の実装はGraphQLリクエストのCachePolicyとして.fetchIgnoringCacheCompletely
今回はハナストでGraphQLリクエストのパフォーマンスをどのように測定しているのかをご紹介しました。
本稿ではご紹介できませんでしたが、エッジでの音声認識、ウェイクワード検知、フルSwiftUIでのアプリ開発、Swift Concurrencyの実践的導入などハナストiOSアプリの開発はチャレンジングで面白い課題に日々挑戦しています。
CareWiz事業部およびエクサウィザーズでは社会課題の解決に一緒に取り組む仲間を募集しています。 介護をより良くするプロダクトの開発、あるいはAIで社会課題を解決するエクサウィザーズに少しでも興味がありましたら、是非ご応募ください!
open.talentio.com
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み