Twitter での6年間 #2
(Twitter での6年間 1 からの続き)
SQLite の導入とモデルレイヤーの刷新がうまくいったあと、ぼくは次のプロジェクトを探していた。何をやれば最終的に一番ユーザーのためになるか。そのときに選んだのは、JSON パーザーの置き換えだった。当時の Twitter for iOS は、YAJL という C で書かれた JSON パーザーをプッシュ形式のインタフェースで使っていた。プッシュパーザーはドキュメントパーザーに比べてピークのメモリ使用量は多少低くなるものの、パフォーマンスが悪くなる傾向がある。プッシュパーザーを使う側のコードは見通しが悪くなりバグが入りやすく、チームにとって頭痛の種だった。それを iOS 標準の NSJSONSerialization に置き換えることにした。Twitter for iOS のコードベースに存在するほぼすべてのモデルクラスの JSON データからの変換部分を書き換えた。コードはぐっとシンプルになり、その過程でいくつか見つけたバグもなくなった。JSON データからモデルオブジェクトを作る部分のパフォーマンスは以前に比べて3倍以上速くなった。その結果、ホームタイムラインや通知タブで新しいデータを受信してから UI に反映するのにかかる時間が目で見てわかるほど短くなった。この変更をマージした日、どういうわけかそれに気づいたらしいマネージャーがぼくの席まで来てうれしそうに「よくやってくれた!」と言ってくれて、一緒にガッツポーズしたのを覚えている。
この時期、iOS チームのエンジニアの数が15人くらいになり、1つのチームとしてやっていくには無理が出てきたので、基盤を受け持つ iOS フレームワークチームと、機能を受け持つ iOS アプリケーションチームという2つのチームに分かれることになった。ぼくはマネージャーのすすめにしたがって、同じマネージャーが引き続き率いる iOS フレームワークチームに所属することになった。
この段階までで、ひとまずモデル部分はぼくが大丈夫だと思える水準になったと感じたので、次は UI 側の改善にとりかかることにした。まずはツイートのテキストを表示する部分に使っていた CoreText のコンポーネントを新しく作り直すことにした。既存のコンポーネントは、モデル部分とビュー部分がうまく分離されていなかったので、ビューをリフレッシュするたびに毎回テキストから文字ごとのレイアウトを計算しなおして画面に表示していた。テキストの描画は iOS の UI でもっともパフォーマンスのボトルネックになりやすい。これをビューとモデルに分離し、モデルに CoreText が提供するテキストレイアウトの中間表現を保持するようにした。こうすることで、ビューがほかのツイートを表示するために再利用されたあとも、モデルさえキャッシュしておけば同じツイートを再度表示する場合にはレイアウトの再計算がいらなくなり、タイムラインのスクロールパフォーマンスが飛躍的によくなった。
7月に2週間の夏休みをもらった。Twitter では制度上、有給休暇の上限はない。マネージャーが許可すればどれだけ休んでもいいことになっている。周りを見ていると、1年に4週間から6週間くらいの休みを取る人が多かったようだ。このときは、いわゆるグランドサークル (グランドキャニオン、アンテロープキャニオン、モニュメントバレー、ブライスキャニオン、ザイオンの国立公園) を自分で運転して回った。予定とかはとくになく、毎日行けるところまで進み、雄大な自然の景色を楽しみ、夕方になったらモーテルを探し、適当なところで食事をとる気ままな旅だ。ぼくは自然が好きで、だだっぴろいところで人に気を遣うわけでもなくゆっくりするのが好きなので、US の国立公園まわりはほんとうに楽しく感じた。
休暇から戻ってしばらくすると iOS 6 のリリースが近づいてきた。そのころの Twitter for iOS は、前の年に iOS 5 に追加された Twitter 連携機能にアカウント管理と認証を全面的に依存するようになっていた。しかし、どうやら iOS 6 で新しく Apple 側が追加したコードに深刻な問題があるようで、この連携部分が高い確率でクラッシュするようになった。このままの状態を放置しておくと、iOS 6 を利用するユーザーが増えるにしたがって、問題がどんどん大きくなっていく可能性があった。マネージャーは、ぼくがリードして、あともう1人のよくできるエンジニアと一緒にこの問題を解決するように言ってきた。しばらく調査した結果、クラッシュする問題を再現することができた。どうやら空きメモリが不十分な状態で Twitter for iOS のプロセスがバックグラウンド状態に入ると、iOS は accountsd という Twitter などのソーシャル連携機能を扱ってるデーモンプロセスを落としてしまうらしい。その状態で Twitter for iOS が API リクエストを認証しようとすると、存在しない accountsd に接続しにいこうとしてしまってクラッシュしてしまうということがわかった。例外ではなく内部でクラッシュするのでアプリ側での直接的な回避策はない。他にも、accountsd に対して複数のアプリが一度に接続しにいくとデッドロックを起こしてしまい、アプリが固まってしまう場合があることがわかった。Apple もとんでもないことをしてくれるものだ。とりあえず Apple に対して再現手順を含むバグレポートを送り、優先して直してもらえるように会社の上のほうから連絡をしてもらった。次に、Twitter for iOS がバックグラウンド状態のときには、accountsd にできるだけアクセスしないように変更を加えた。タイミングの問題などもあってクラッシュが完璧に防げるわけではないが、とりあえず短期的に出血を止めることはできた。その後、マネージャーともう1人のエンジニアと話し合った結果、Apple の accountsd のその時点での設計と実装を考えるとその後の先行きを信用することはできないという点で一致し、Twitter for iOS 側に認証処理を戻すことと、iOS 側のアカウント管理と Twitter for iOS 側のアカウント管理を別にした上でその間をできるだけ同期してユーザーの利便性を維持する、という方針が決まった。これを実装するためには、すでに iOS 側で管理されている認証キーを使って、アプリ側の認証キーをサーバーから1度だけ取得する必要があったのだが、サーバーサイドのエンジニアに確認した結果、すでにその API は存在しているらしい。ただし、短時間に大量のリクエストをさばくことは難しいので、最低1週間にわたって均等にリクエストがいくように設計してほしいということだった。そこでサーバー側のスイッチを変更するとそれがアプリ側に伝わって動作を変えられるフィーチャースイッチという既存の機構を使い、API サーバーの負荷を見ながらそのマイグレーション速度を変更できるようにしておいた。そして、認証キーを変換してる間にキューに入った API リクエストをいったん別のキューに移して留めておき、認証キーの変換が終わり次第留め置かれた API リクエストをもとのキューに戻して普通に処理を続行する仕組みを注意深く2週間の時間をかけて開発した。その結果、ユーザー側での認証キーの移行は予定通り問題なく進み、Apple の accountsd という問題児になやまされることももうなくなり、まれにアプリの起動時にデッドロックしてしまう問題も解決された。
(Twitter での6年間 3 に続く)