Cybozu Inside Out | サイボウズエンジニアのブログ

 

「それで、そのコンテンツに効果はあったの?」という問いに答える 〜CausalImpactを使った動画の効果分析〜

こんにちは、開発本部データサイエンティストの山口です。

製品やサービスのユーザー向けにコンテンツを制作している方は、一度は以下のように考えたことがあるのではないでしょうか。

このコンテンツは本当に効果があったのだろうか?」と。

ここでは「コンテンツ」を「特定の認識・行動を、ユーザーに促す情報のまとまり(テキスト、音声、動画など)」を指すものとします。また、コンテンツの「効果」は「コンテンツの狙い通りにユーザーの行動が変化したこと」を意味するものとします。

多くの場合、コンテンツの視聴回数だけではユーザーの行動の変化までを知ることはできません。また、ユーザーの行動に変化があることがわかったとしても、それがコンテンツの効果なのか、別の要因によるものなのか、判別が難しい場合が多いでしょう。

この記事では、サイボウズが制作した製品の機能解説動画を例として、CausalImpactというパッケージを用いた効果分析の方法を紹介します。仕事上コンテンツ制作に関わりがあり、分析に興味のある方向けに、効果分析のおおまかな流れをお伝えできればと思います。

目次

サイボウズのケース: 製品の機能解説動画

サイボウズのテクニカルコミュニケーションチームでは、ユーザーの方に製品をより良く活用してもらうために、製品の機能解説動画を制作しています。

動画制作の詳細については、以下の2つの記事にまとめています。ご興味あれば、ぜひ御覧ください。

さて、上記の取り組みで動画を制作し、視聴回数の改善まで行うことができました。では、実際に本来の目的である「ユーザーの機能活用促進」は達成できているのでしょうか?それを確かめるために、今回はCausalImpactというRのパッケージを利用して分析を行いました。
(Rは統計解析向けのプログラミング言語です。詳細は記事末尾の参考文献に譲ります)

以降、分析方法の選択から実際にCausalImpactを使った分析までの流れを説明していきます。

  1. 適切な分析方法を選択する
  2. CausalImpactを利用する
  3. 分析結果を次のアクションにつなげる

なお、本分析はサイボウズの個人情報保護の規約に則り、個人を特定できないかたちで分析を行っています。

1. 適切な分析方法を選択する

この記事ではCausalImpactを利用した分析について紹介すると述べました。しかし、状況に応じて最適な分析方法は変わります。まずはどのようにして最適な分析方法を選ぶかについて、最も望ましい方法から順番に説明します。

1.1. 可能なら実験を行う

効果分析で最も有効な方法は、「実験」を行うことです。では、「実験」とは具体的にどういうものを指すのでしょうか。ここでは動画を例として、効果分析の基礎的な内容をかいつまんで説明します。

理想的な効果分析の方法は、以下のようなものです。

  • 全く同じ集団に対して
  • 完全に同じタイミングで
  • 介入を行った場合と介入を行わなかった場合の両方のデータを取って比較する

ここでは、「介入」を「対象者に影響を与える何らかのアクション」だと定義します。たとえば動画の場合、「対象者に動画を配信すること」が介入に相当します。同じ相手に同じ条件で、介入を行った結果と介入を行わなかった結果の両方があれば、2つの結果の「差」が介入の効果であると言えそうです。

では、理想的な効果分析の方法を利用することはできるのでしょうか。残念ながら、この方法は絶対に実現が不可能です。全く同じ相手に完全に同じタイミングで「動画を配信する」ことと「動画を配信しないこと」の両方を実行することが不可能だからです。

この理想的な効果分析の方法の代替として、「実験」を行うという選択肢があります。ここでは「実験」を「分析ができるように予め計画した介入を行い、必要なデータを収集すること」だと定義します。たとえば動画の場合、「予め決めたグループに動画を配信(介入)し、介入したグループと介入しなかったグループの両方のデータを収集すること」が実験に当たります。

実験であれば、一定の条件を満たせば実現することが可能です。また、適切に条件を満たした上での実験は、理想的な効果分析とかなり近い結果を出すことができます。

では、この適切な条件とは具体的にどういうものを指すのでしょうか。詳細な説明は省きますが、理想的な実験を行うための条件の1つに「分析者が対象者をランダムに選んで介入できる」というものがあります。

たとえば動画の場合、「分析者が動画を配信するグループと配信しないグループをランダムに決めて、確実にそのとおりに動画を配信できる」というのが実験の理想的な条件になります。WEBページ改善などで用いられる、いわゆる「A/Bテスト」は実験の方法のうちの1つです。

このような実験ができる場合、条件を揃えて実験をすることが最も有力な効果分析の選択肢になります。しかし、ユーザーに提供するコンテンツを、こちら側の都合で見せたり見せなかったりするのは気軽にできることではありません。ユーザーに不平等感を与えてしまったり、そもそも技術的に出し分けが難しい場合も考えられます。

そういった実験を行うのが難しいときは、比較対象となるグループを利用して、「実験をしたかのように」分析を行う方法を検討します。

1.2. 比較対象が用意できる場合は、比較対象を利用した分析を行う

「実験をしたかのように」分析を行うとはどういうことでしょうか。それは「対象者をランダムに選んだかのように」分析を行うということです。なぜ「ランダム」にこだわるかというと、もし対象者の選択がランダムではなかった場合、分析が誤った結論を導く可能性があるからです。

たとえば動画の場合で、「動画の配信(介入)を受けるかどうかを本人の希望によって決める」という状況を考えます。 動画の視聴を希望するユーザーはもともと製品活用への意欲が高く、動画の視聴を希望しないユーザーは相対的に製品活用への意欲が低いかもしれません。この状況で動画を配信(介入)しても、製品活用の度合いが上がったのが介入の効果なのか、もともとのモチベーションによる違いなのかを判別することができません。もし介入の対象者をランダムに選択できれば、もともとの属性(ここでは製品活用への意欲)に左右されない分析結果を得ることができます。これが実験の利点です。

では、実験ができない場合には効果分析ができないのかというと、そうではありません。介入を受けるかどうかに影響する「もともとの属性」が予めわかっていれば、それを調整することで正しい分析結果を得ることができます。この記事では詳細を省きますが、これには回帰分析や傾向スコア分析などの手法があります。

しかし、この方法にも限界があります。分析対象者が全員介入を受けていた場合は、介入の効果を比較する対象がいないので分析ができません。たとえば動画の場合、すべてのユーザーに同時に動画を配信した場合には、比較対象となるグループがいないので、この方法では効果分析ができません。

比較対象を用意できない状況で効果分析をするための選択肢の1つが、CausalImpactです。

1.3. 比較対象が用意できない場合は、CausalImpactの利用を検討する

CausalImpactはGoogleが公開しているRのパッケージです。もともとはマーケティングのキャンペーンの効果を推定するためにつくられました。CausalImpactは仮想的に比較対象をつくりだすことで、介入の効果を推定します。

たとえば動画の場合、動画配信を行う前のデータを用いて「もしも動画配信をしなかったら」という仮想の世界をつくり、それと現実世界とを比べます。動画配信が全く効果のないものであれば(悲しいですが)、動画配信をしていない仮想の世界と動画配信をした現実世界はだいたい同じになるはずです。もし動画配信をしなかった仮想の世界と現実世界が全然違うものであれば、動画配信に効果があったと考えることができます。

同じ原理はマーケティング以外の分野でも応用できると紹介されています。機能解説動画はユーザーに一斉に配信する形式で、比較対象のデータを用意できなかったため、CausalImpactを利用しました。

2. CausalImpactを利用する

CausalImpactを使った具体的な分析方法について説明します。なお、データの具体的な値などは伏せています。

2.1. データを準備する

はじめに分析に必要なデータを準備します。今回は製品の各機能がどの程度利用されているかを記録したログデータを使用します。

以下にログデータのサンプルを一行分だけ示します。データは架空のものです。

日付 ドメインID ログインユーザー数 分析対象の機能 利用回数
2021/11/4 ドメインA 300 スケジュールの編集 200

ここでの「ドメイン」はお客さまごとの製品環境を指します。

2.2. 分析用のコードを実行する

分析対象となるログデータに対してCausalImpactを実行します。今回は「製品へのログインユーザー数」を共変量として、「動画で解説した機能の利用回数」が有意に増加しているかを推定します。介入の期間はユーザーに動画の案内を出したタイミング(2021/11/14〜2021/11/15)としました。

以下にCausalImpactのプロット用のRのサンプルコードを示します。

# 全ドメインのログデータ(logdata)を日付(date)単位でグループ化して、
# 製品のログインユーザー数の合計(login_user)と、分析対象の機能の利用回数(target_func_use)の合計値を計算する
# 結果として残る列は[日付、全ドメインの機能の利用回数の合計値、全ドメインのログインユーザー数の合計値]の3列
logdata_grouped <- logdata %>% 
  group_by(date) %>% 
  summarise(login_user_sum = sum(login_user), target_func_sum = sum(target_func_use), .groups = "drop")

# 対象となる期間と介入の期間を設定する
# 今回は介入直後の変化に焦点を当てるため、介入後の期間(post.period)は短く設定した
time.points <- seq.Date(as.Date("2021-11-01"), by = 1, length.out = 30)
pre.period <- as.Date(c("2021-11-01", "2021-11-13"))
post.period <- as.Date(c("2021-11-14", "2021-11-15"))

# 製品のログインユーザー数を共変量(x1)、対象の機能の利用回数をyとする
x1 <- logdata_grouped$login_user_sum
y <- logdata_grouped$target_func_sum

data <- zoo(cbind(y, x1), time.points)

# CausalImpactを実行し、グラフを表示する
impact <- CausalImpact(data, pre.period, post.period, model.args = list(nseasons = 7))

impact.plot <- plot(impact,c('original')) +
  theme(axis.text.y = element_blank()) # y軸の値を非表示

plot(impact.plot)

コードの実行結果を以下に示します。詳しいグラフの見方は次の項で説明します。なお、y軸の値は伏せています。

画像: CausalImpactで出力したグラフ。11/15時点の実測値が予測幅を上回っていることが示されている。
CausalImpactで出力したグラフ

今回分析対象の「Garoon」は主にビジネスで利用される製品のため、土日に利用回数が落ちる傾向が表れています。

2.3 分析結果を解釈する

先ほどプロットしたグラフの3つの要素について説明します。

  • 黒の実線が実際に観測された値です
  • 黒の点線が「介入が行われなかった世界」ならこうなっていただろうという予測の値です
  • 青い帯はその予測の黒の点線が「偶然」ぶれたとしても、よっぽどのことがない限りこの範囲に収まるだろう、という予測の幅になります(いわゆる95%信頼区間です)

今回のグラフでは、グラフの中間あたり(11/15時点)で黒の実線(実際の値)が青い帯(予測された値の幅)の範囲を越えています。これは介入によって11/15に統計的に有意な変化があったことを意味しています。すなわち、動画の配信によって本来の目的である「ユーザーの機能活用促進」が(少なくとも短期的には)達成されていると言えそうです。

3. 分析結果を次のアクションにつなげる

ここまでの手順で、CausalImpactを利用して分析結果を得ることができました。この結果をもとに、今後どのようなアクションが必要になるかを考えます。ここでは動画配信の介入を例として、介入や分析を改善するアクションについて考えてみます。

3.1. 効果をより長い期間持続させる

今回の分析で、動画配信は機能活用の促進に効果があることがわかりました。引き続き動画を制作することは有益だと考えられます。一方で、今回確認された範囲では、効果の持続期間はごく短期的なものでした。本来は長期的に機能を利用し続けてもらえるほうが望ましいです。そのためには、効果がより長い期間持続する介入と、それを推定できる分析が必要になります。たとえば、機能利用の習慣化を促すような動画を配信し、その効果を数週間の累積で評価するとよいかもしれません。

3.2. 効果を個別ドメインごとに推定する

今回の分析では全ドメインの機能利用回数の合計値を使って効果分析を行いました。しかし、合計値で計算するとユーザー規模が大きいドメインの影響が大きくなり、相対的に小規模なドメインの変化を見落とす可能性があります。そのような見落としを避けるためには、個別のドメインごとに効果を評価する必要があります。たとえば、各ドメインに対して個別にCausalImpactを実行し、有意差が出たドメインの割合を検定するとよいかもしれません。

3.3. 効果を推定するための共変量を増やす

今回は共変量として「ログインユーザー数」という1つの変数を使いました。しかし、一般には適切な共変量が複数あるほうが推定の精度が高くなります。共変量として追加できる変数にはいくつか条件があるため、それらを満たす変数を選定・追加することが必要になります。たとえば、用途が近しい他製品のログデータを共変量として追加するとよいかもしれません。

改めて「それで、そのコンテンツに効果はあったの?」という問いに答える

この記事ではCausalImpactを使った動画の効果分析について紹介しました。今回の事例については「動画配信にはユーザーの機能活用を促進する効果があった」と答えることができそうです。そしてその効果の大きさや持続性についても一定の客観性をもって評価できました。

本来は実験などができるように、コンテンツの提供面などの設計を考えることが重要です。ですが、そういった選択肢がない中でコンテンツの効果分析をする必要に迫られる場合もあります。そんなときに、今回紹介したCausalImpactが役に立つ可能性は十分にあると考えます。

この記事が読者の皆さまの効果分析およびコンテンツ制作の参考になれば幸いです。

参考文献

効果分析の考え方について

効果分析について興味のある方は、入門書として個人的に以下の2冊がおすすめです。

CausalImpactについて

CausalImpactの公式のドキュメント等のリンクを記載します。それぞれ英語ですが、必要な部分だけピックアップして読んでも(視聴しても)参考になると思います。

理論的背景について

CausalImpactの使い方や理論的背景について、以下のサイトも参考にさせていただきました。

理論的背景について(ベイズ)

CausalImpactの理論的背景にある階層ベイズについて、Aicia Solidさんの動画を参考にさせていただきました。
(直接的にCausalImpactについて言及されているわけではないです)

Rについて

筆者は今回のCausalImpactを使った分析を機にRを学習したので、その際に参考にした書籍も紹介します。

 

アプリが依存している Swift Package ライブラリのライセンスを一覧表示するためにライブラリを作りました

はじめに

こんにちは、モバイルエンジニアの中村(@Kyomesuke)です。

私が担当している kintone のモバイルアプリ(iOS)では、現在脱レガシーを目指して幾つかの課題に取り組んでいます。 その一つとして、パッケージマネージャーを CocoaPods から Swift Package Manager に移行するリファクタリングに挑戦していたのですが、この移行に際して一つ課題がありました。

kintone モバイルやサイボウズ Office 新着通知アプリなど、サイボウズのモバイルアプリには依存している外部ライブラリのライセンスを一覧表示する画面があるのですが、これまでその機能をAcknowListというライブラリを使用して実現していました。 しかし AcknowList は CocoaPods にしか対応していないため、Swift Package Manager に対応するために代替を見つける必要がありました。 そこで、検索するとLicensePlistというライブラリがすぐに見つかったのですが、これでは代替できない理由もすぐに判明しました。 サイボウズ Office 新着通知というアプリは、Swift Package を用いたマルチモジュール構成になっているのですが、この構成では外部の Swift Package を解決する際にPackage.resolvedが出力されません。 LicensePlist はPackage.resolvedを基にライセンス情報を抽出する仕組みのため要件を満たしません。 したがって、Package.resolvedに依らないライセンス情報抽出の仕組みを作る必要があることがわかりました。

紆余曲折あって、Package.resolvedに依らず Swift Package ライブラリのライセンスを抽出して一覧表示する仕組みを作ることができたので、LicenseListというライブラリとして公開しました! 今回は LicenseList の仕組みと利用方法について解説していきたいと思います。

github.com

LicenseListのスクリーンショット
LicenseListのスクリーンショット

LicenseList の仕組み

ライセンス情報を抽出する流れ

まず、Package.resolved以外でライセンス情報を抽出できる方法を見つける必要がありましたが、代わりにいいものを見つけました。 Xcode 上でResolve Packagesしても、xcodebuildコマンドでビルドしてもDerivedDataの中にSourcePackagesというディレクトリが生成されます。 そして、その中にworkspace-state.jsonというファイルが出力されるのですが、このファイルからはPackage.resolvedとだいたい同様の情報が取得できます(具体的には、パッケージの名前やリポジトリの URL、バージョン情報が取得できます)。

ライブラリ名の一覧さえ取得できてしまえば、SourcePackages/checkouts/の中にはライブラリのソースがリポジトリから丸ごと取得できているため、その中の LICENSE ファイルを見つけ出して解析してあげることでライセンス情報は抽出できます。

そして、抽出したライセンス情報を plist 形式で保存し、アプリから読み込めるようにしておき、アプリの実行時に Swift の構造体に流し込み、見た目を整形して表示します。

ライセンス情報を抽出する流れの図
ライセンス情報を抽出する流れの図

LicenseList に含まれる各 product の役割

LicenseList の Package には spp (SourcePackagesParser)という executable と LicenseList という library の2つの product があります。

  • spp はアプリの Build Phases にて前述のworkspace-state.jsonを解析してSourcePackages/checkouts内の全てのライブラリのライセンスを抽出してlicense-list.plistというファイルに出力する役割を担います。
  • LicenseList (library)はアプリの実行時にlicense-list.plistを読み込み、ライセンス情報を構造体に流し込んで SwiftUI でレイアウトして表示する役割を担います。

LicenseList の利用方法

動作条件

  • iOS 13 以上
  • Xcode 13.0 以上
  • パッケージマネージャーとして Swift Package Manager だけを利用していること
  • SwiftUI でビュー構成されていても Storyboard や UIKit のコードベースでビュー構成されていても OK

事前準備

  1. Swift Package Manager で LicenseList を導入します。
    1. Xcode 上でFile > Add Packages...
    2. https://github.com/cybozu/LicenseList.gitを検索
    3. パッケージを追加(⚠️この時、spp はターゲットに紐付けせず、LicenseList だけターゲットに紐付けます)
  2. プロジェクトのSRCROOTlicense-list.plistを追加します。

    $ cd [source root path]
    $ echo '<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict></dict></plist>' > license-list.plist
    
  3. Add Files to ...license-list.plistをアプリのバンドルに紐づけます(⚠️このとき、Copy items if needed のチェックを外すこと!)。

  4. Build Phases に Run Script を追加して、spp を実行するスクリプトを登録します。

    SOURCE_PACKAGES_PATH=`echo ${BUILD_DIR%Build/*}SourcePackages`
    
    # Build SourcePackagesParser
    xcrun --sdk macosx swift build -c release \
      --package-path ${SOURCE_PACKAGES_PATH}/checkouts/LicenseList \
      --product spp
    
    # Run SourcePackagesParser
    ${SOURCE_PACKAGES_PATH}/checkouts/LicenseList/.build/release/spp ${SRCROOT} ${SOURCE_PACKAGES_PATH}
    

SwiftUI での使い方の例

import LicenseList

struct ContentView: View {
    let fileURL = Bundle.main.url(forResource: "license-list", withExtension: "plist")!

    var body: some View {
        NavigationView {
            NavigationLink("License") {
                LicenseListView(fileURL: fileURL)
                    .navigationTitle("LICENSE")
                    .navigationBarTitleDisplayMode(.inline)
            }
        }
    }
}

UIKit での使い方の例

import LicenseList

// UINavigationControllerが有効なViewControllerの中で
let fileURL = Bundle.main.url(forResource: "license-list", withExtension: "plist")!
let vc = LicenseListViewController(fileURL: fileURL)
vc.title = "LICENSE"
navigationController?.pushViewController(vc, animated: true)

LicenseList の開発中躓いたポイント

  • ライブラリによって LICENSE ファイルの拡張子が揃っていない
    • .md.txtや拡張子がついていなかったり様々なので、拡張子を無視してファイル名だけで判別しました。
  • ライセンスの記述に規格がないため、種類を判別する方法がライセンス本文の部分一致を見るしかない
  • Build Phases の Run Script で spp を実行するタイミング
    • 成果物であるlicense-list.plistをバンドルに含めるため、Copy Bundle Resources より前にスクリプトを実行する必要がありました。
  • iOS 13, 14, 15 で SwiftUI の挙動に差がある
    • iOS 13 にはListStyle.insetGroupedがなかったり、iOS 15 未満ではAttributedStringが利用できなかったりと、SwiftUI は OS のバージョンで使える API に差が激しいため、バージョンによる分岐を賢く書く必要がありました。こちらは別途 Zenn で記事にしています(SwiftUI: チェーンメソッドの途中で、iOS バージョンにより処理を分けたいときどうする?)。
    • ScrollViewの中に動的に更新される要素を入れている場合、iOS 14 以上では空要素から要素が後から入った際に幅が再計算されますが、iOS 13 では幅が0のままで内容物が表示されない不具合がありました。これは要素に対して.frame(maxWidth: .infinity)を入れることで解決できました。
  • UIKit のUINavigationControllerと SwiftUI のNavigationViewは相入れない
    • UINavigationController のpushViewController(_:animated:)を用いて SwiftUI の View に遷移する場合(UIHostingController(rootView:)を介する)、その遷移先の View 内でNavigationViewNavigationLinkを用いるとナビゲーションが破綻することがあります(遷移で進むのは良いが元の画面に戻れなくなる、iOS 13 だと画面の回転で遷移先が多重化するなど)。
    • SwiftUI の世界でも UINavigationController を扱って画面遷移をすることで、NavigationViewに起因する不具合を排除することができました(関連記事:SwiftUI の View から UIKit の navigationController にアクセスする方法)。

最後に

今回は、アプリが依存している Swift Package ライブラリのライセンス一覧を表示するライブラリを作った件について、その仕組みと使用方法を紹介しました。 ライブラリの開発を通して、Swift Package Manager の仕組みや仕様、SwiftUI と UIKit の連携方法について学ぶことができました。 Swift Package を用いたマルチモジュール構造を実用していて、ライブラリのライセンスを表示する要件があるという方は是非 #LicenseList を活用してみてください。 また、LicenseList にはテストターゲットでしか利用していないライブラリも一覧の対象になってしまうなど課題がありますので、OSS の強みを活かしてコミットしていただけると幸いです。

 

トラブルの芽を摘むための一歩進んだOSSのアップグレード戦略

はじめに

こんにちは、ストレージチームの大神です。ストレージチームでは様々なOSSを用いて新しいストレージインフラの開発・運用を行っています。OSSを使っていく上でいつどんな機能追加やバグ修正を取り込むかを決める「アップグレード戦略」を立てる事は重要です。サイボウズでは基本的にリリースごとにリリースノートの内容から変更点をチェックし、緊急のものがあれば即座にバージョンアップし、そうでなければ月一程度の頻度でバージョンアップしています。ここまでは比較的一般的なやり方ではないでしょうか。ストレージインフラに使用しているCephRookについては、さらに一歩踏み込んだアップグレード戦略を行っており、本記事ではCephのアップグレード戦略について紹介します。

アップグレード戦略の概要

OSSは基本的にGitHubやメーリングリスト等で開発や議論が進むため、これらを定期的にチェックする事により最新情報が得られます。Cephについては以下の様な手段で開発が進みます。

ストレージチームでは上記全ての情報を毎日チェックしています。流量は時期によりまちまちですが、全て合わせると日に100件程度のチェックを行っています。この結果、重大な不具合や機能追加を早期発見し、必要ならば公式リリースを待たずにサイボウズの独自パッチとして適用することもあります。

上述の更新を全て隅から隅まで確認するのは大変なので、以下の様な手段を用いてチェックを効率化しています。

  • サイボウズのストレージシステムの運用に関わるもので、かつデータ破壊やロスト等の重大な問題に関するもののみ確認を行う
  • 明らかに自分たちの対象外である事が分かるキーワードをリストアップしておき、そのキーワードについてのみの話であれば読み飛ばす
  • 本文を読む際も全てを隅から隅まで読むのではなく、全体をざっと読んで自分たちに関係ありそうか否かを判断する
  • 該当するものの中でも緊急であると思われるものを優先して読む
  • 余裕があれば緊急でないものも読んでおくと良い。他のチケットやIssue/PRを見たときに関連に気づけたりすることがある

実例

本節では、上記の取り組みで問題を早期に修正できた事例を紹介します。

バージョンアップ等でCephが管理するストレージデバイス(OSD)を停止する際、10~20秒程度I/Oが止まる現象を確認していました。このときはインフラ上のサービス提供に影響が出てしまうことに加えて、バージョンアップのたびにサービスを提供するチームに事前告知をしていました。

そのような折に、メーリングリストをチェックしていた所、OSDが自身の停止を、Cephクラスタ全体の整合性を保つためのモニターというサービスへ明示的に伝えるか否かを設定するパラメータが存在することがわかりました(デフォルトはoff)。このパラメータがonの場合はOSDが自ら停止した場合は即座にこの情報がモニターに伝わり、I/Oの停止時間が短くて済みます。これに対してサイボウズのCephクラスタのようにパラメータがoffになっていると、OSDが自ら停止した場合であっても異常終了した場合と同じくモニターはしばらくOSDの停止を認識できず、長時間I/Oが止まってしまうというわけです。

この事について議論しているPR上で「デフォルトでonにすべきでは?」と問い合わせた所、「確かにそうであり、PRを作ってくれないか?」といった回答を得られたため、PRの作成を行いました

この修正はのちに他のバグ修正PRに取り込まれるかたちでマージされました。サイボウズで運用中のCephバージョンへもバックポートされ、無事に適用されました。

おわりに

公式の情報を積極的に収集する事によるアップグレード戦略の概要と、それにより問題を早期に修正できた事例を紹介しました。このように公式の情報を収集することで、他にも、障害調査のノウハウが溜められたり、公式の開発状況が見えたりする効果もあります。サイボウズではこのような積極的な情報収集によるアップグレード戦略を今後も行っていきます。

最後になりますが、ストレージチームでは一緒に働いてくれるかたを募集中です。ぜひ一度下記の募集要項をごらんください。

cybozu.co.jp

 

SwiftPMによるマルチモジュール構成でSwiftGenをビルド時に実行する

はじめに

こんにちは、モバイルチームのオジマです。

私が担当しているサイボウズ Office 新着通知のiOSアプリでは、これまでXcodeGenとCocoaPodsを用いてマルチモジュールなアーキテクチャを構成していました。しかし、開発環境の構築においてRubyのバージョンなど気を使う点がいくつかあり、特にM1チップを搭載したMacで問題になることが多くありました。そのため、SwiftPMを用いたマルチモジュール構成への移行を行いました。

この記事では、SwiftPMによるマルチモジュール構成へ移行する際に発生したSwiftGenの利用シーンでの課題とそれに対する解決方法を紹介します。

課題

従来のXcodeGenとCocoaPodsを用いたマルチモジュール構成では、XcodeGenのpreGenCommandでSwiftGenによるコード生成を実行していました。マルチモジュール構成の基盤技術をXcodeGenからSwiftPMに移行したことによって、SwiftGenの実行タイミングを再考する必要がありました。当初はSwiftGenのコマンドを別途手動で実行していたのですが、煩雑な作業だったため自動化したいというモチベーションが生まれました。

自動化にあたり最初に検討したのは、XcodeのBuild PhasesでSwiftGenを実行する方法でした。しかしこの方法は、SwiftGenによるコード生成がSwiftPMで管理されているモジュールのビルドより後に行われてしまいます。つまりBuild PhasesでSwiftGenを実行すると以下の流れで処理が行われることになります。

  1. SwiftPMで管理されているモジュールのビルド
  2. XcodeのBuild Phasesに記載したSwiftGenコマンドの実行
  3. xcodeprojで管理されているソースのビルド

この順番ではSwiftGenによるコード生成がSwiftPMで管理されているモジュールのビルドに間に合わないため、期待通りの動作になりません。

解決方法

そこで今回は、SwiftPMのプラグイン機能を用いてこの課題を解決することにしました。プラグインはSwift 5.6から導入された機能です。この機能を利用することで、SwiftPMで管理されている各モジュールのビルド時にコード生成などの特定の処理を実行できるようになります。

残念ながらSwiftGenは、記事執筆時点でプラグイン機能を簡単に利用するためのPRが取り込まれていません。プラグインの対応は利用者側で整えることも可能ですが、今回は有志の方が作られたnicorichard/SwiftGenPluginを用いることにします。

今回は、以下のディレクトリ構成とPackage.swift、またswiftgen.ymlによってプラグイン機能を用いたビルド時のSwiftGenの実行を実現しました。

.
├── App
│   └── SampleApp.xcodeproj
├── Package.swift
├── Sources
│   ├── ModuleA
│   │   ├── Hoge.swift
│   │   ├── Resources
│   │   │   └── ModuleA.strings
│   │   └── swiftgen.yml
│   └── ModuleB
│       ├── Piyo.swift
│       ├── Resources
│       │   └── ModuleB.strings
│       └── swiftgen.yml
└── Tests
let package = Package(
    name: "SampleApp",
    platforms: ...,
    products: ...,
    dependencies: [
        .package(url: "https://github.com/nicorichard/SwiftGenPlugin", exact: "6.5.1")
    ],
    targets: [
        .target(
            name: "ModuleA",
            exclude: ["swiftgen.yml"],
            dependencies: [],
            plugins: [
                .plugin(name: "SwiftGenPlugin", package: "SwiftGenPlugin")
            ]
        ),
        .target(
            name: "ModuleB",
            exclude: ["swiftgen.yml"],
            dependencies: [],
            plugins: [
                .plugin(name: "SwiftGenPlugin", package: "SwiftGenPlugin")
            ]
        )
    ]
)
strings:
  - inputs: Resources/Hoge.strings
    outputs:
      templateName: structured-swift5
      output: ${DERIVED_SOURCES_DIR}/HogeStrings.swift

ここでは各モジュールごとにコード生成を行いたかったため、各モジュールのディレクトリにswiftgen.ymlを配置しています。また、Package.swiftではPackagedependencies引数にSwiftGenPluginを含めるとともに、ビルド時にSwiftGenによるコード生成を行いたいモジュールごとにTarget.PluginUsage.pluginでSwiftGenPluginを指定しています。各モジュールに配置したswiftgen.ymlはSwiftPMの管理対象から除外するため、Target.targetexclude引数でファイルを指定しています1

Xcode 13で起こる問題とその回避策

上記に挙げた解決方法は、Xcode 14.0 Beta 1では正常に動作しました。しかしXcode 13.4.1以下でかつSwiftPMで管理するモジュールが多い場合にReports Navigatorのレスポンスが悪くなる問題に遭遇しました。具体的には画像のようにパッケージの解決やビルドは終わっているにも関わらず、ビルドログが表示されずにインジケータが回り続けてしまいます。

ビルドが終わっているにも関わらずビルドログが表示されずにインジケータが回り続けてしまっている状態

時間を置くとビルドログは表示されインジケータは止まりますが、インジケータが表示されている間はXcodeのエディタ上でどこでエラーが発生したかが確認できません。ビルド後すぐにエラー発生箇所が特定できないこの問題は開発を行う上で大きな支障でした。

この問題の原因特定には至っていませんが、Package.Dependency.packageを利用せずに、Target.pluginTarget.binaryTargetを利用することでこの問題を回避することができました。これは実際にはnicorichard/SwiftGenPluginPackage.swift内部で行われていることを、ローカルのPackage.swiftに展開した状態です。この方法を用いるとビルドログはビルドに追従して表示され、ビルド終了後すぐにインジケータは停止します。

.
├── App
│   └── SampleApp.xcodeproj
├── Package.swift
├── Plugins
│   └── SwiftGenPlugin
│        └── main.swift
├── Sources
│   ├── ModuleA
│   │   ├── Hoge.swift
│   │   ├── Resources
│   │   │   └── ModuleA.strings
│   │   └── swiftgen.yml
│   └── ModuleB
│       ├── Piyo.swift
│       ├── Resources
│       │   └── ModuleB.strings
│       └── swiftgen.yml
└── Tests
let package = Package(
    name: "SampleApp",
    platforms: ...,
    products: ...,
    dependencies: [],
    targets: [
        .target(
            name: "ModuleA",
            exclude: ["swiftgen.yml"],
            dependencies: [],
            plugins: [
                .plugin(name: "SwiftGenPlugin")
            ]
        ),
        .target(
            name: "ModuleB",
            exclude: ["swiftgen.yml"],
            dependencies: [],
            plugins: [
                .plugin(name: "SwiftGenPlugin")
            ]
        ),
        .plugin(
            name: "SwiftGenPlugin",
            capability: .buildTool(),
            dependencies: ["swiftgen"]
        ),
        .binaryTarget(
            name: "swiftgen",
            url: "https://github.com/nicorichard/SwiftGen/releases/download/6.5.1/swiftgen.artifactbundle.zip",
            checksum: "a8e445b41ac0fd81459e07657ee19445ff6cbeef64eb0b3df51637b85f925da8"
        )
    ]
)

まとめ

今回は、SwiftPMを利用したマルチモジュール構成を導入した際に実際に直面した課題とその対応について紹介しました。課題解決を通して、Swift 5.6から新しく導入されたプラグイン機能を知ることができ、Swiftによる表現がさらに広がったことを実感しました。 SwiftGenを利用しているiOSプロダクトは数多くあると思うので、それらプロダクトの課題解決の一助になれば幸いです。


  1. swiftgen.ymlを管理対象から除外しない場合、パッケージの解決時に以下のような警告がでます。
    found 1 file(s) which are unhandled; explicitly declare them as resources or exclude from the target

 

社内のAndroidエンジニア達を集めてGoogle I/O報告LT会をやってみた

こんにちは、モバイルチームの向井田 (@mr_mkeeda) です。

Google I/O 2022は皆さん参加しましたか? Googleの人みんなPixel Watchぽい腕時計付けてましたね。 Googleがタブレット市場に戻ってきたのもアツい話でしたね。

Android関連のセッションもたくさんありました。 Androidアプリ開発者の人はセッションをチェックしましたか? いっぱいセッションがあるので、全部見るのは大変ですよね。

ということで、サイボウズではAndroidエンジニアを何人か集めて、Google I/OのAndroid系セッションの報告会をLT形式で開催してみました。 このブログではLT会をどのようにやったのかを紹介します。

LT会を実施した背景

サイボウズは2021年の秋頃からAndroidエンジニアの採用活動を改善し、Androidエンジニアが倍増しました (3人→6人に増えた)。 社内にAndroidエンジニアが増えたので、これを機会にサイボウズのAndroidコミュニティを盛り上げていきたい!という思いが私にはありました。

今まで社内で行われていたAndroidコミュニティらしい活動というと、こんな感じでした。

  • Google I/O, DroidKaigiなどのカンファレンスに参加する
    • 各々のメンバーが興味のあるセッションを見る
    • セッション内容のまとめをkintoneに投稿する人もいる
  • インターネットで見つけた技術ブログをkintone上で共有する
    • 週に1回、共有したものを口頭で話す

上記の活動は各メンバーのモチベーションに強く依存しており、積極的に興味を持ってAndroid技術情報を集めて共有するメンバーと、共有されるメンバーに二分された状態でした。 特に必須の業務だったわけではないので、ウォッチしているけど共有しないメンバーや、そもそもウォッチしていないメンバーもいたと思います。 この状態だと、サイボウズの中でAndroid技術を高め合うのは難しいです。 もっとメンバーそれぞれがモチベーションと主体性を持って技術を学び合う場を作りたいと思い、やり方を変えてみることにしました。

LT会の内容

開催したLT会の詳細は以下になります。

  • LTの発表者は社内のAndroidエンジニア全員 (6人)
  • 1人の発表時間は5分
  • 発表資料の形式は自由
  • Google I/OのAndroid系セッションの中から好きなセッションを1つ選んで発表する

LT会をやるにあたって大事にしたのはカジュアルさです。 Androidエンジニア全員にやってもらうには、発表のハードルをできるだけ下げる必要があると考えました。 そのため資料作りが大変にならない程度の発表時間で、発表資料の形式も完全に自由にしました。 また、全員が参加すれば不公平感ややらされている感も薄まりますし、人数が多いほうがワイワイ盛り上がって楽しく勉強できます。

発表当日はiOSエンジニアを含めた社内のモバイル開発に携わっている人に声をかけて、総勢16人ほどで実施しました。

実際に発表してもらった資料がこちらになります。

What’s new in Android development tools

Basic layouts in Compose

Common performance gotchas in Jetpack Compose

Input for all screens

Overview of the Privacy Sandbox on Android

Back to the basics of System Back

zenn.dev

LT会をやってみて

率直言って大成功だったと思います!

発表者はみんなそれぞれ詳しくセッションの内容を理解して説明してくれました。 発表時間が5分では足りないという声出るくらい、しっかり資料を作り込んできた人もいました。 質問時間やkintoneに用意したLT実況スレッド上での意見交換も起きており、みんなが知識を共有し合う場が生まれていました。

また、発表を聞いていた人からも多数の好評をいただきました。

内容濃くて勉強になりました。 これ見るだけでAndroid熱高まってめちゃいい取り組みだなと思いました

要点まとまってて非常に助かりました また聞きたいです👏👏👏👏

素晴らしい取り組みだったと思いますー!

今回のようなLT会は1回限りではなく、Android Dev SummitやDroidKaigiなどの他のカンファレンスでも同じようにやってみるつもりです。 勉強は押し付けられてやるものではないと思っているので、みんなが主体性を持って技術を高め合えるように勉強方法は工夫していきたいです。

それから、LT会が開催できたのはGoogleがGoogle I/OのセッションをYouTubeで無料公開してくれているからです。 我々Androidエンジニアがコミュニティを広げるきっかけを与えてもらえているので、とても感謝しています。

これからもサイボウズのAndroidコミュニティ活動で工夫した点は発信していきたいと思いますので、ご期待ください!