TECH PLAY

dely株式会社

dely株式会社 の技術ブログ

236

はじめに こんにちは。dely開発部の伊ヶ崎( @_ikki02 )です。 本記事はdely Advent Calendar 2019の6日目の記事です。 qiita.com adventar.org 昨日は当社サーバサイドエンジニアの安尾が 「スピード優先の開発で溜まった技術的負債の返済計画(サーバーサイド編)」 という記事を書きました! 新機能の開発にとどまらず 技術的負債を返済していくのはとっても素敵なことですね! ぜひこちらも一読いただけると嬉しいです。 さて、本日は前職でデータサイエンティスト、 現職で機械学習エンジニアをしている経験から 私が感じている両職業のお話をしていきたいと思います。 まだまだ新しい職種なので、 実際はこんなことしてるんだ、と少しでもお役に立てれば幸いです。 (あらかじめお断りしておくと、 職業の優劣関係や、 どちらか片方の職業を賞賛する意図に基づくものではございません。 また、個人の体験談という位置付けとして見ていただきたく、 一般的にこうだ、と主張するものでもございませんので、 あらかじめご理解賜りますようお願い申し上げます。) 目次 はじめに 目次 データサイエンティストのお仕事 ①データアセスメントやPOC(※1)のレポート ②分析をするためのアドホックなプログラム ③ROI 従来業務の効率化 従来業務の高度化 僕の志向性(自動化) 機械学習エンジニアのお仕事 ETL処理*5 異常検知とマイクロサービス 機械学習エンジニアとデータサイエンティストの違い 最後に 次回予告 参考 delyについて データサイエンティストのお仕事 私がデータサイエンティストとしてお仕事していた際は、 主に自然言語のデータ解析と抽出を担当していました。 法人向けのビジネスで、金融機関や生命保険のクライアントが多かったです。 金融業界はセキュリティ要件が厳しく、データの社外持ち出しが難しいため、 週の2~3日くらい客先常駐することもありました。 具体的な業務のアウトプットとして3つ挙げるならば、 「①データアセスメントやPOC *1 のレポート」 「②分析をするためのアドホックなプログラム」 「③ROI *2 」 の3つに大別されると思います。 ①データアセスメントやPOC(※1)のレポート データサイエンティストの一般的な業務は統計的手法や機械学習を用いて、膨大なデータの中から一定の法則やルールを見出し、それを業務効率化/高度化の意思決定を支援することになるかと思います。 そのため、レポーティングを通して、今どのような課題があり、どういう状態だから、こうしたい、そのためにはこういう対策が必要というビジネス施策のロジックを整理して伝えることが重要になります。また、データサイエンティストが使う言葉には、統計や機械学習の専門用語が含まれるため、それを専門としない相手に分かり易く伝える、ある種の翻訳力も求められることが多いと思います。 ②分析をするためのアドホックなプログラム アドホックなプログラムは、上記レポートを作成する上で必要になるデータの痕跡を探るプログラムです。私は自然言語のデータを扱うことが多かったので、例えば、社外からのQAデータを分析する場合、QAは複数の質問に分けることができるかどうか、それらを個別に扱うか、統合して全体として扱うのがよいか、など、分析対象の絞込みやデータ加工の必要性が生じます。(時に上記の細かいデータの扱い方は業務ドメインに依存するため、クライアントにどのような意図でデータを処理したかレポートすることは依然重要です。)また、機械学習アルゴリズムによるスコアリングや分類結果(ただの数字であることが多い)を用いて、次の機械学習プロセスの入力とする、出力結果を利用しやすい形式に加工する、など出力結果の処理もプログラムで書くことになると思います。ちなみに、使用する言語は個々人や各会社に寄ると思いますが、Pythonで開発することが多いと思います。 ③ROI 多くのデータやクライアントとお仕事する中で感じたこととして、残念ながら、データサイエンティストの価値はまだまだ社会的に認知されているとは思えません。彼らがどのような価値を生むのか、そのイメージがつきにくい方も多いと思います。まだまだ新しい職種なので仕方がないと思う反面、だからこそ、個々人の体験を表現していくことで議論を成熟させていく必要があると思うので、 僭越ながら個人の考えを記載させて頂くと、 職種としてデータサイエンティストが生む価値、それは結局はROIで計られることになるのかなと思います。 特に、大きく2つの軸があると考えます。 従来業務の効率化(従来時間や手間がかかっていたことを省略化できる) 従来業務の高度化(従来難しかったことができるようになる) 従来業務の効率化 自然言語の文脈で言うと、何かしらの電子文書 *3 を毎日なり毎週なり読む業務があると思います。この際、まずは読まなければならない文書数に対して時給なりの単価を掛合わせ、総コストを定式化します (例:総コスト = 読まなければならない件数 ÷ 1時間に読める件数 × 時給)。ここで機械学習のソリューションを用いて、読むべき文書の優先順位付けや絞込みを実現し、上式の読まなければならない件数(または時間)を半分にできる場合、それがリターンに相当します。このリターンに対して必要なPOC経費や開発コストが見合う場合、意味のある取り組みとみなせると思います。 従来業務の高度化 上述の効率化については、従来業務が比較対象として存在するため、コスト計算は実はそんなに難しくありません。(論理的には。。。クライアントの業務を理解しそれを表現する際は、気を使うべきことも多く実際は結構難しいです。笑)もちろん金額に換算することが必ず正しいわけではなく、そのような場合は高度化の取組みが該当すると考えます。実際、従来業務を高度化し代替する場合 *4 は新しい変数が生じるため、定式化はより不確実なものになります。その結果、高度化に関する取組みは定性的な価値をより吟味することが多いように思います。例えば、アンケートの定性的な処理について、苦情を機械的に検知して迅速に対応したい、という要望が多くある一方で、感謝や賞賛の言葉をひろって担当者にフィードバックする取組事例は、優先順位がどうしても下がりがちなため、意外と少ない感覚があります。昨今の働き方改革も進む中で、このようにコストをかけて明るい職場を作っていきたいという取組みには一定の意味があると思います。 ※従来業務の効率化/高度化について下記記事を参考にさせていただいています。 qiita.com まとめてみると、定量的な効果と定性的な効果を加味して、費用対効果に見合うプロジェクトにしていくことがデータサイエンティストの役割なのかなとも思います。そのため、費用対効果が合わないと熟慮した際は、「やらない」ときっぱり断る姿勢も重要になると思います。お互い損ですからね。 (それでもやる、という鶴の一声プロジェクトはなるべく避けたいものです) 僕の志向性(自動化) データサイエンティストとしてのお仕事は非常に楽しかったです。 しかし、数年間お仕事していく中で、自動化に携わりたいという気持ちが強くなりました。上部ではアドホックなプログラムについて記述しましたが、ビジネスロジックや機械学習ワークフローをコード化(MLOps)して支援することに、より重きを置くようになっている自分がいました。そこで、よりエンジニアリングに特化した機械学習エンジニアとして現職のdelyにジョインしました。(なぜ自動化に拘ったか、生産性のお話はどこかでまた書きたいです。) 機械学習エンジニアのお仕事 (少しだけ宣伝させてください) delyはダウンロード数2000万超えのレシピ動画サービス「クラシル」と 同じく月間利用者数2000万超えの働く女性を応援するメディア「TRILL」の 2つのメインサービスを展開する事業会社です。 現在両サービスのデータ基盤の開発/運用と 機械学習を用いたシステムの開発に携わっています。 7月に入社して約半年なので、 この職業についてまだ十分に語れるわけではありませんが、 これまでの取組を通して振り返ってみたいと思います。 ETL処理 *5 入社初期に携わった仕事のひとつとして、ETLバッチ開発があります。クラシルでは日々多くの施策(イベント)を検討し開発しておりますが、新しいイベントの効果を測定するために、イベントログを都度追加するスタイルを取っています。新しく追加されたイベントログは、既存のインベントリに追加する必要があるのですが、こちらを日次で処理するよう、AWSのAthenaやGlueを用いて実装しています。 Glueを使えば自社データソースと連携してETLスクリプトをトリガーを指定して簡単に実行できるのですが、実際入社するまでサービスのことを知りませんでした。データサイエンティストの場合、最悪SageMakerやAthenaのようなデータサイエンス用のマネージドサービスだけで事足りることもあると思いますが、機械学習エンジニアの場合、システムが利用しているサービス全般との繋込み、すなわちアーキテクトスキルがより求められるのだと実感しました。 異常検知とマイクロサービス こちらは入社して一番力を入れて取り組んだプロジェクトになります。上述のETLバッチ処理が新施策に対する取組(の一部)だとすれば、こちらは既存機能のユーザー利用頻度に変化がないか測定評価する機能となります。 詳細はML@Loftで登壇した際の下記資料をご覧頂きたいのですが、 簡単に言うと、ETLと異常検知(SageMakerのビルトインアルゴリズムを利用)、slack通知までの各機能をコンテナでマイクロサービス化して開発しています。事前の検証を最低限にしつつスピード重視で開発したため、まだまだ発展途上ではありますが、異常検知の精度を高める工夫をしつつ(あまり異常は起きないにこしたことはないですが)、利便性を向上させ、活用範囲を広げていけるように開発できればと考えています。 delyにおけるevent designの取組について (注意:ONE PIECEの事前知識が必要です。) Event Design and Anomaly Detection | ML@Loft_20191023 from ikki02 www.slideshare.net 機械学習エンジニアとデータサイエンティストの違い データサイエンティストは 下記3象限のスキルセットを身に付けるべきだとよく言われると思います。 ↑データサイエンティスト協会が公開されたデータサイエンティストのスキルセット 異論はありませんが、ビジネスはチームで動くのもまた真だと思うので、現場感では上記の役割を分担しあっているのが実態に近い印象です。 つまり、工数配分を考えるなら、 データサイエンティスト: 「ビジネス力」×「データサイエンス力」 機械学習エンジニア:「データエンジニアリング力」×「データサイエンス力」 となると思います。 実際、機械学習エンジニアは、アーキテクトやシステム開発が主な役割となるため、分析は必要最低限しか実施しない点が、データサイエンティストとの大きな違いだと感じています。(もちろん複雑な因果推論など、時間をかけるべき時は社内のマーケティング部やユーザーの声を参考にしながら本筋から大きく外れないようにすることは大事です。)信頼性、拡張性、保守性を意識した開発が求められていることをよく感じます。 このように役割が異なるデータサイエンティストと機械学習エンジニアですが、どちらに優劣関係があるということではなく、どこに時間を割くか、その結果どこに時間をかけないかを補完し合う関係性だと考えています。 最後に もともとMLOpsや自動化に魅力を感じて 機械学習エンジニアにキャリアチェンジしましたが、 delyでは何兆円もの市場規模を誇る食の領域でシェアを取るべく邁進しています。 フーマーという中国のスーパーをご存知でしょうか? 機械学習に限らず、少しざっくりしたフードテックのお話になりますが、 中国のフーマーでは、 在庫をなるべくもたない倉庫でありスーパー 買い物をスマホで行い自宅まで30分で配送する 顔認証やアリペイを使ったセルフレジ 料理の配送をロボットが行う(スープは人が運ぶ) など、ユーザー体験を向上させつつコスト最適化を試みる新しい形態が提案されています。 www.youtube.com 少子高齢化と人口減少が深刻化するこれからの日本でも 生活にかかすことのできない食を、 より便利に、よりおいしく、より楽しめたらいいなと考えています。 delyでそんな挑戦を続けることができたらいいなと思います。 次回予告 さて次回は、当社PdMの奥原が「プロダクトマネージャー1年目の教科書」というタイトルで投稿します! すでにタイトルが面白そうです!お楽しみに! qiita.com adventar.org 参考 2019年版:データサイエンティスト・機械学習エンジニアのスキル要件、そして期待されるバックグラウンドについて https://tjo.hatenablog.com/entry/2019/02/19/190000 機械学習を使った事業を成功させるために必要な考え方や人材、フェーズとは? https://qiita.com/yoshizaki_kkgk/items/55b67daa25b058f39a5d データサイエンティスト協会「ミッション、スキルセット、定義、スキルレベル」 https://www.datascientist.or.jp/common/docs/skillcheck.pdf アリババの新型店舗『フーマー』とは? https://www.youtube.com/watch?v=sso6bITfuDU Stitch Fix Algorithms Tour https://algorithms-tour.stitchfix.com/ Netflix TechBlog https://medium.com/netflix-techblog delyについて delyの開発チームについて詳しく知りたい方はこちらもあわせてどうぞ! CXOとVPoEへのインタビュー記事はこちら! wevox.io *1 : POC: Proof of Concept。概念実証。AI関連技術の予測結果は不確実性が高いため、実務での利用に値する性能かどうか、アイデアをひとつずつ検証してその可能性を顕在化していくプロセスのこと。 *2 : ROI: Return on Investment。投資利益率のこと。要は投資したコストに見合うリターンがあるかどうか図る指標です。 *3 : 金融業界では決算関連資料であったり、銀行員/顧客間の金融商品案内記録、アンケート内容やメール本文、など。 *4 : 人が車を運転する、という活動を自動運転にする、など *5 : ETL:Extract, Transform, Loadの頭文字。データを抽出し、加工し、別システムへロードする、データが辿る一連の処理のこと。
アバター
こんにちは! dely株式会社サーバーサイドチームの安尾です。 本記事はdely Advent Calendar 2019の5日目の記事になります。 qiita.com adventar.org 昨日は辻さんが「Jupyterもいいけど、SageMath使って可能性もっと伸ばそう!」という記事を書きました。 tech.dely.jp 本日は 「スピード優先の開発で溜まった技術的負債の返済計画(サーバーサイド編) 」 というタイトルで、今delyのサーバーサイドチームの技術的負債についての考え方から、負債返済のために具体的に行なっていることをご紹介したいと思います。 技術的負債とは 抽象的な言葉なので、組織や人によって微妙に定義が異なるのではないかと思いますが、僕たちのチームでは 「未来の開発スピードを下げる原因となるプログラムやアーキテクチャのこと」 を総称して技術的負債と呼ぶようにしています。 具体例を挙げると、 複雑で修正が困難なプログラム サービスがスケールした際にボトルネックになるアーキテクチャ テストが書かれていないプログラム メンテナンスされていないgemの利用 実装者の退職や異動によって、開発目的や正確な仕様がわからないプログラム のようなものになります。 技術的負債に対する考え方 技術的負債というと、とてもネガティブなイメージをしてしまいがちですが、必ずしも悪いものだとは思っていません。 特にプロダクトのフェーズによって負債に対する考え方は大きく変わると考えています。 例えばまだユーザーが少ない新規サービスの場合、いかに高速にPDCAを回して資金がなくなるまでにサービスをマーケットフィットさせるかということが重要になります。 そういうフェーズのサービスにおいては、未来の開発スピードを考慮して、機能のリリースが遅れるくらいなら、あとで直さないといけないことが分かっていても早くリリースすることを優先することが合理的な判断だと言えることも多いでしょう。 クラシルにおいても、やはり立ち上げてからしばらくはスピード優先でガンガン開発してリリースするというスタイルを取っていました。 とは言え、その状態を続けていると負債の返済に追われて、追加機能や機能改善がだんだんできなくなってくるという状況が発生してしまいます。 クラシルでも大小含めてこれまでにも色々と問題が発生していましたし、今後何か問題が起こった際のインパクトや、これからだんだんエンジニアを増えていくであろうことを考えると、このまま前進し続けるより、一旦スピードを落としてでもある程度工数を使って負債を返しておくことで未来の開発スピードを上げられるという判断から負債の返済をチームの目標の一つとして取り組んでいくことになりました。 技術的負債返済計画 それでは、今サーバーサイドチームがどのような手順で負債返済を行なっているか紹介していきたいと思います。 1. 負債の洗い出し もともと負債が色々と溜まっているという認識はそれぞれ持っていたり各自で優先度が高いと思っている部分についてはドキュメントにまとめたりしていたのですが、全体像を誰も把握できていない状態で負債を返済しようと言っても何から手をつけて良いか、また本当にそれが優先すべき負債なのかというのが分からない状態でした。 そこで、まずは負債を一通り洗い出すことで全体像を把握しようというところから始めました。 やり方としては、まず僕が過去に課題として話題にあがったものや思いつく限りの負債をスプレッドシートにまとめて、それをベースとして各自が思いつく限り追加していくという形を取りました。 なお、一度まとめて終わりににするのではなく、日々の業務で新たに発見された課題があればここに追記しアップデートしていくことで、なるべく課題が埋もれていかないように気をつけています。 2. 優先順位付け 次にそれぞれの負債に対して重要度を5段階に分けて優先度付けをするということをしました。 重要度の基準については一旦下記でいくことに決めました。この基準についても会社の状況によって変わってくると思うので、今はこれにしていますが、都度変えていけば良いかなと思っています。 セキュリティインシデントの原因になる バグの原因になる アップグレードの妨げになる 開発スピードを落とす要因になる その他 洗い出した時点では大量にあって何から手をつけて良いか分からなかった負債一覧も重要度をつけてみると意外と本当に重要な負債は限られていることが分かってきました。 3. 担当決め 次に重要度が高いものを優先し、誰が何をやるかを決めました。 やり方は基本挙手制にしました。理由は単純で自分で取り組みたいものに取り組むのが一番モチベーションも上がるし、結果としてパフォーマンスも上がると思ったからです。 これをやった個人的な感想としては、課題によって求められるスキルが大きく変わるので、得意なものばかり選ぶのではなく各々が成長させたいスキルに関連するような負債を選択することで機能開発だけでは身につきづらいスキルをつけることができてとても良いなと感じています。 基本的に2、3の作業は四半期に一度目標設定の際に定期的にやっていく形にしようと思っています。 返済すること以上に未来の負債の予防がとても大切 たまった負債を返していくことはもちろん重要ですが、それと同じかそれ以上に、新規の開発がすぐに負債化しないように予防するということを僕たちはとても大切にしています。 新規開発で負債を予防するために色々と工夫しているのですが、今日はその一例を紹介していきたいと思います。 ルール決め、ドキュメント化を行う サーバーサイドのエンジニアが1人2人のときにはあまり気になっていなかったことですが、3人、4人と増えるに連れて、ルール化されていない部分のやり方が違う部分というのが結構あることがわかってきました。 例えば、 管理画面のテストを書くかどうか Controllerにもmodelにも書きづらいようなコードをどこに置くか 新機能の実装時にどの程度のドキュメントを書くか のようなことが人によって違うことが分かってきました。 短期的に見ると特に問題はないことかもしれませんが、僕たちはこういった細かい違いが属人化となり後々大きな問題となって返ってくると考えています。 なので、こういったルールがない部分を見つけるとこのままで良いのかを考え、後々問題になりそうだと判断したことについてはルールを決めてドキュメント化し、新しいメンバーが増えた際のオンボーディングで伝えるという運用をしています。 設計レビュー 僕たちのチームでは、新機能や機能改善を行う際には、 サーバーサイド設計仕様書 を書いて他のメンバーに共有して、実装に入る前にブラシュアップするということを徹底しています。 また、これはサーバーサイドのメンバーだけではなく、SREのメンバーも一緒にやることで、仕様を満たしているかということだけではなくその後中長期的に安定して運用できるかというような観点も含めてレビューをしています。 これをやることで1人で考えるよりもより良い設計になりますし、コードレビューやリリース前になって思わぬ問題に気づくというリスクを減少する効果もあります。 更にはドキュメントが残るので、実装時にいなかったメンバーでも、この機能はなぜ実装されたのか、どういう経緯でこういう設計になっているのかということを調べることが可能になります。 安易に管理画面に機能を追加しない、利用頻度が低い機能は削除する 割と最近決めたことではありますが、結構有効なのではないかと思っているのがこちらです。 エンドユーザーが使うアプリやWebに機能というのは、多くの方の目に触れて様々なフィードバックがあるので、徐々に洗練されていく傾向があるかと思いますが、社内メンバーが使う管理画面となるとそうではないので、エンジニアが社内からの要望に応えて気軽にどんどん機能を追加していった結果、誰も全体像が分からなくなってしまうということはよくあるのではないでしょうか? クラシルにおいても時間と共に管理画面が肥大化し、誰も全体像が分からないというような問題が発生していました。 その対策を議論し、極端な話そもそもコードがあるから負債化するのでコードがなければ負債化しないよねということになり、できる限りコードを増やさない方法を考えようという取り組みをしています。 具体的には、月一以上の頻度で使われる機能については管理画面に残すけれど、それより低い頻度でしか使わないものは削除して、必要に応じてエンジニアが手動なりスクリプトなりで対応するという形です。 そうして機能は必要最低限にすることによって、次のステップとしてドキュメントを整備したり、足りていないテストを書いたりするのも楽になるし、CIにかかる時間も短縮されるという感じで副次的なメリットも色々ある取り組みとなりました。 最後に 本記事では、クラシルのサーバーサイドが技術的負債についてどう考え、どのように返済を行なったり、未来の負債を予防するためにどのようなことをしているかを紹介しました。 技術的負債に苦しんでいる方にとって少しでも参考になると嬉しいです。 また、サーバーサイドチームでは、一緒に技術的負債を返しつつ、中長期的な視点で開発・運用を行なってくれる仲間を募集しています! これまで誰にも解決できなかった食や暮らしの課題を解決し、一緒に世界をより良くしていきましょう!! www.wantedly.com 次回は伊ヶ崎さんが「データサイエンティストと機械学習エンジニアをやって思ったこと」というタイトルで投稿します!
アバター
はじめに こんにちは。dely開発部の辻です。 本記事はdely Advent Calendar 2019の4日目の記事です。 qiita.com adventar.org 昨日は弊社CXO坪田が「突破するプロダクトマネジメント」という記事を書きました! プロダクトマネージメントっていつの時代も課題山積ですよね。弊社も多分に漏れずたくさんの課題を抱えているわけですが、それらをどのように 突破していくか 様々な観点からの具体的な取り組みが書かれていますので興味のある方は是非読んでみてください。南無。 blog.tsubotax.com さて本日は「Jupyterもいいけど、SageMath使って可能性もっと伸ばそう!」ということで、普段Jupyter Notebook使ってるという人向けに、どうせならSageMathを使ってやれること増やしませんか?という内容になっています。そこで、SageMathのインストールから基本的な使い方、趣味(?)や実務で普段どんなふうに活用しているかなどご紹介させてもらおうと思います。 目次 はじめに 目次 SageMathとは SageMathのインストール SageMakerでSageMathを使いたい! SageMathを使ってみよう 基本操作 楕円曲線で遊んでみる ちょっとだけMaximaの紹介 ルービックキューブ群 実務で使いどころ まとめ 参考 さいごに ちなみに SageMathとは SageMath (元々は単にSageという名前でした)は、主に数学に関するなんやかんやの処理が非常に便利に使えるというツールです。Pythonで書かれているためPythonでできることはもちろんできますし、Jupyter Notebook上でカーネルとして利用することもできます。同様の数式処理システムに Maxima というLISPで書かれたものがあるのですが、これはSageMathに同梱されていますので、個人的にはそちらもよく使います。 SageMath - Open-Source Mathematical Software System Maxima, a Computer Algebra System SageMathのインストール SageMath Download - osx/intel SageMathをMacにインストールするときには、上のリンクから「sage-8.9-OSX_10.14.6-x86_64.dmg」(2019.12.04時点)をクリックしてダウンロードして、お使いのシェルの設定ファイルにこんな(↓)エイリアスを書いてあげればOKです。最新バージョンは8.9です。 alias sage =' /Applications/SageMath/sage ' これで、ターミナルでSageMathが利用できるようになります。 または、ターミナルで以下のコマンドを実行するとJupyter notebookが開きます。 sage -n jupyter カーネルSage8.9でノートを開くと見慣れた形で表示されます。 あるいは、このように notebook() と入力すると、The Sage Notebook が開きます。 見た目がちょっと違いますが使い方はほとんど同じです。 ここで最初にお伝えしておきたいのが、SageMathは結構デカいということです。なので、ローカル環境にインストールするとまあまあ容量を食います。それが嫌という方はオンラインでの利用をおすすめします。ちょっと試してみたいという場合は、こちら(↓)の「Sage Cell Server」がおすすめです。 Sage Cell Server Sage Cell Serverを使うと小窓にコードを書いてEvaluateボタンを押すだけでSageMathのBuilt-in関数などがそのまま利用できてとても便利です。SageMathのサンプル集からコピペするだけで、こんな感じのシェルピンスキーのギャスケットが簡単に描けます。 あるいは、もっとちゃんと使いたいという方にはこちらの「CoCalc」というサービスがおすすめです。 cocalc.com こちらの「Sage worksheet」を選択してワークシートを作成すると、「.sagews」という拡張子のファイルが作成されます。見た目はちょっと違うんですが、操作方法などはJupyter Notebookとほとんど同じですので、使い勝手はいいと思います。少し使うだけなら無料で問題ないと思うのですが、使う頻度が多くなってメモリやCPU数を増やしたいという場合には有料となりますのでご注意ください。 SageMakerでSageMathを使いたい! SageMakerとSageMath、とても似た名前ですが全くの別物です。SageMakerはAWSが提供する機械学習サービスで、すべての開発者とデータサイエンティストに機械学習モデルの構築、トレーニング、デプロイ手段を提供するために生まれたサービスです。SageMakerではJupyter NotebookをVMインスタンスとして立ち上げることが可能なので、このノートブックインスタンスにSageMathを入れて普段から使っていきたいと思います。 さっそくノートブックインスタンスを立ち上げます。 ノートブックインスタンスのJupyter Notebookを開いてPython2をアクティベートします。 SageMathは基本Python2で動きます。Python3については以下のSageMath FAQで以下のような言及がありますので、実験的ではありますが利用は可能のようです。(個人的には実験していないのでぜひチャレンジした結果を教えていただきたいです。) As of August 2019, most of SageMath works fine with Python 3. However, we still consider Python 3 support to be experimental and no official Python 3 release has been made yet. You can build the source code of SageMath with Python 3 using the instructions at the bottom of https://wiki.sagemath.org/Python3-compatible%20code See trac ticket #15530 and trac ticket #26212 for tracking the current progress. ここで、 こちらのSageMathの公式にしたがってconda経由でインストールしていきます。 Install from conda-forge — Sage Installation Guide v9.0 conda install mamba -c conda-forge mamba create -n sage sage -c conda-forge そうすると、このようにターミナル上でsageコマンドが利用できるようになります。 しかし、そのままだと sage の文字が濃い青色でとても見づらいので、 $HOME/.sage/init.sage ファイルを作成して以下の1行を追加してあげると見やすくていい感じになります。 %colors Linux こんな感じです。 これらの設定を毎回インスタンス起動のたびにやるのはとても面倒ですので、SageMakerにはライフサイクル設定という仕組みがあります。そちらにここまでのコマンドを追記することで、インスタンス起動時にSageMathが使える状態にすることが可能です。詳しくは(↓)こちら。 https://docs.aws.amazon.com/ja_jp/sagemaker/latest/dg/notebook-lifecycle-config.html また、このようにSageMathの環境が利用できるカーネルに追加されます。 ちなみに、iPadを利用している方向けの話になってしまいますが、Junoというアプリを使って、SageMakerのノートブックインスタンスやCoCalcと連携させることが可能です。そうすると、自分の描いた3Dグラフを直接指で触って回転したりできるのでとても面白いです。 iPad上で描いたグラフを指で回わしているところ 有料ですが個人的にはとても重宝しています。 Juno Connect for Jupyter Rational Matter 仕事効率化 ¥1,220 apps.apple.com SageMathを使ってみよう それでは、ここから先はSageMathを実際に使っていきたいと思います。基本的にはPythonと同じように利用することができるので、SageMathを利用されたことがない人もすぐに慣れると思います。 基本操作 チュートリアル的なことをしてもあまり面白くないので、その辺りはこちらのSage観光ツアーをご参照ください。 Sage観光ツアー — Sage チュートリアル v9.0 せっかくなので、この中から少し例としてサンプルを紹介してみたいと思います。 まずは、こちらの単純な2次方程式の解を解析的に求めたい場合を考えます。 こちらの方程式の解をSageMathを使って求めるとこのよう(↓)になります。 変数xを定義して、解きたい方程式と変数をsolve関数の引数に入れるだけです。めちゃくちゃ簡単ですね。 一応念のため、こちらを関数としてグラフを描くとこのように(↓)なるので、まあ蛇足ですがこの解で合ってそうです。 それではもう一つ、機械学習などではたびたび登場する偏微分のかんたんな例を紹介してみようと思います。 こちらの関数をxとyそれぞれについて偏微分したい場合を考えます。 流れは先ほどの例と同様で、変数x,yを定義して、偏微分対象の関数を定義します。微分関数であるdiff関数の引数として偏微分対象の変数を入れるだけ、です。 こちらも非常にかんたんですね。 楕円曲線で遊んでみる さて、ここからはちょっと素人なりに頑張って楕円曲線で遊んでみたいと思います。ここまでの例だけですと、なんだScipyあればできるじゃんという話ですが、楕円曲線j-不変量の計算などを行う際にはSageMathの力を使うととても楽チンになります。ちなみに楕円曲線は暗号化アルゴリズムでおなじみ(量子超越性によって暗号が破られる日は来るのか?!)ですが、厳密には種数 1 の非特異な射影代数曲線、さらに一般的には、特定の基点 O を持つ種数 1 の代数曲線のことをいいます。では早速、以下の ヴァイエルシュトラス方程式 が与えられていると仮定したときに無限遠点を群演算における零元であるとして話を進めていきましょう。 まず、手始めに具体的な係数を当てはめて適当な楕円曲線を作ってみます。 この楕円曲線をSageMathではこのように(↓)表現します。(ここからはCoCalcを使っていきます。) EllipticCurveコンストラクタの引数は、EllipticCurve([a1, a2, a3, a4, a6 ])と以下の係数が対応しています。(対応する順序が 訳のわからないことになっている ので注意してください。) j-不変量 j = 1 を持つ楕円曲線を生成したい場合は、このように(↓)表現します。 このことから、j-不変量 1 の楕円曲線が以下のような式(↓)になることがわかります。楽しいですね。 それではここから、以下の代表的な楕円曲線についてSageMathを使っていろいろと遊んでいくことにしましょう。 この楕円曲線のグラフを描くと、このように点(0,0)を通りつながった円の形状と、右にハの字に広がった形状とがある、いわゆるよくみる楕円曲線の形になっています。 さて、それではSageMathを使ってこの楕円曲線の持つ加法群構造を調べていきたいと思います。 まずはこのように(↓)対象の楕円曲線を生成します。 一応念のため、ちゃんと種数1の非特異曲線になっているか確認してみます。 数式を確認したい場合はwebブラウザ上で数式を明瞭に表示するためMathJaxをimportしてインターフェースを利用します。 有理点(0,0)について、無限遠点が零元、同一曲線上の3点を加えると0となる加法群の構造を備えているか調べます。 確かに計算してみると、3点加えると0で、倍数も一見訳のわからない有理数ではありますが、ちゃんと同一曲線上で有理点になっていることが確認できます。 導手の数もこのように簡単に表せます。 最後に、この楕円曲線に随伴するL-級数、あるいはモジュラー形式の係数、あと階数をSageMathを計算してみます。あっという間です、膨大な手計算されていた頃の偉大なる数学者たちに対して申し訳ないほどの簡単さです。 こちらの例では30ですが、係数を10000以下すべてとした場合でも、計算するのにかかる時間はたったの約1秒ほどです。非常に高速に計算することができます。 ちょっとだけMaximaの紹介 冒頭で、ちょっとだけ触れましたがMaximaという数式処理システムがありまして、そちらはLISP言語の実装なのですがインターフェースがSageMathに同梱されているのでそのまま使うことができます。実はMaximaを単体でインストールして使う場合はgnuplotなどの設定が少々癖があって面倒なのですが、同梱されているのでサクッと使えて便利です。 たとえばこちらの1行で何が表示されるかといいますと。。 maxima.plot3d( "[cos(x)*(3 + y*cos(x/2)), sin(x)*(3 + y*cos(x/2)), y*sin(x/2)]" , "[x, -4, 4]" , "[y, -4, 4]" , '[plot_format, openmath]' ) このように「メビウスの帯」がXmaximaに表示されます。 Xmaximaはマウスでグルグル動かすことができるので先ほどのJunoがなくても手軽にグラフの形状を確認できて便利です。 ルービックキューブ群 SageMathに同梱されているインターフェースといえば、ちょっと変わり種のものとしてルービックキューブ群があります。 こういう感じでキューブの展開図を表示できます。 あるキューブの状態のときの解法を出力してくれて、その解が正しいかも確認できます。 実務で使いどころ さて、冒頭でご紹介した通り、SageMathは数学に関するなんやかんやの処理が非常に便利に使えるという点については、ここまで一通り使い方をみていただいて「ふむふむ、なるほど」とご理解いただけたものとして、では実際の業務にどのように応用していくのかという点がやっぱり気になると思います。 それについて結論からお伝えしますと、現時点では、 分析や最適化問題のシミュレーションがメイン です。とはいえ別にプロダクションコードには使えないというわけではないんですが、やっぱりSageMathがライブラリとしてサイズがデカすぎるのでポータビリティが非常に低いという点がネックになります。(デプロイも大変ですしね。) ただ、いずれもしかしたら、今後どこかのタイミングで楕円曲線やTDA(topological data analysis)など活用する機会がでてくるようなことがあれば、SageMathを利用したプロダクションコードが生まれる可能性もなくはないと個人的にはとても期待しています。ですので、まあ今はその準備段階と捉えて必要に応じて適度に利用しているといったところです。 それを踏まえまして、よく実務で登場する最適化問題の例として ナップザック問題 と、機械学習ではおなじみの最尤推定でよく使う EMアルゴリズム についてご紹介しましょう。 まず、ナップザック問題ですが、自分で実装するとなるとまあまあめんどくさいと感じている方は多いのではないでしょうか?(最終的には制約が多くなってしまうので、実装が複雑になるのは当然だとしても、少なくとも導入当初はサクッと評価したいですよね。)実はSageMathにはknapsack関数があらかじめ用意されています。 たとえばこんな問題があるとします。 問題 4つの商品(重さ、価値)で定義される項目が、 それぞれA(100g, 200円)、B(150g, 100円)、C(50g, 300円)、 D(70g, 250円)でした。 バッグの最大重量200gのとき、価格を最大にして詰められる組み合わせはどれか? これはつまり、福袋はある程度軽くて価値が高いものがいっぱい入ってると嬉しいといった趣旨の問題ですね。 SageMathを使えば、このように(↓)knapsack関数をimportしてそれぞれの条件の組みを引数に渡すだけで、はい終わりです。 一見、100gの商品Aを入れた方が高額になりそうな気もしますが、実はCとDを詰めたほうが全体では高額になるということがわかりました。 続きまして、EMアルゴリズムも一から実装するとなるとそれなりに骨が折れると思います。この場合もSageMathのBuilt-in関数を使って気楽に実装することができます。 今回はPRMLの例からよくある混合ガウス分布のEMアルゴリズムについてSageMathで実装するケースをご紹介させていただきます。EMアルゴリズム自体の説明は割愛させていただきますが、レコメンドでの利用だけでなく混合ガウス分布は割と実務のEDAでも頻繁に登場しますのでEMアルゴリズムを活用する場面はその点でも多いかと思います。 なお、こちらの例では平均初期値を(−1.5, 0.5) , (1.5, −0.5)とし、分散初期値を0.5として計算した場合となっています。 まず、全体として以下のような(↓)関数呼び出しを考えます。 pi_k = [ 0.5 , 0.5 ] mu_k = [vector([- 1.5 , 0.5 ]), vector([ 1.5 , - 0.5 ])] sigma_k = [matrix(RDF, D, D, beta*ident), matrix(RDF, D, D, beta*ident)] EM(pi_k, mu_k, sigma_k) そしてこちらが(↓)EM()の実装です。これは基本的にEMアルゴリズムをそのまま実装したものになります。 def EM (pi_k, mu_k, sigma_k): diff = 1 for l in range ( 21 ): pi_N = matrix(RDF, [[pi_k[k] * _gauss(X[n], mu_k[k], sigma_k[k]) for k in range (K)] for n in range (N)]) o_lnP = ln_p() gamma = matrix(RDF, N, K) for n in range (N): gamma.set_row(n, pi_N[n]/(pi_N[n].sum())) # Eステップ if l == 0 : action_f(mu_k, sigma_k, gamma) # Mステップ N_k = [gamma.column(k).sum() for k in range (K)] for k in range (K): mu_k[k] = 0 for n in range (N): mu_k[k] += gamma[n][k]*X[n] mu_k[k] /= N_k[k] sigma_k[k] = 0 for n in range (N): sigma_k[k] = sigma_k[k] + gamma[n][k]*covMatrix(X[n], mu_k[k]) sigma_k[k] /= N_k[k] pi_k[k] = N_k[k]/N lnP = ln_p() diff = abs (lnP - o_lnP) o_lnP = lnP # print if action_idx.has_key(l): action_f(mu_k, sigma_k, gamma) x, yは混合ガウス分布に従ったデータであるものとして、各種定数の初期化を行います。 # x, y は混合ガウス分布 # 定数の初期化 D = 2 K = 2 N = len (x) beta = 0.5 X = matrix(RDF, zip (x, y)) ident = identity_matrix( 2 ) # アクションのインデックス action_idx = { 0 : 0 , 1 : 1 , 5 : 5 , 20 : 20 } 最後に、EM()関数から呼び出される必要な関数を定義します。 def _gauss (v, mu, sigma): d = len (v); sigma_inv = sigma.inverse(); sigma_abs_sqrt = sigma.det().sqrt(); val = -(v - mu) * sigma_inv * (v - mu).column()/ 2 ; a = ( 2 *pi)**(-d/ 2 ) * sigma_abs_sqrt**- 0.5 return a * e**val[ 0 ]; def covMatrix (x, u): d = x - u return matrix(RDF, [[d[i]*d[j] for j in range (D)] for i in range (D)]) def ln_p (): sum = 0.0 for n in range (N): sum += ln(pi_N[n].sum()) return sum def action_f (mu_k, sigma_k, gamma): pt_plt = Graphics() for n in range (N): pt_plt += point(X[n], rgbcolor=(gamma[n][ 1 ], 0 , gamma[n][ 0 ])) r_cnt_plt = contour_plot( lambda x, y : _gauss(vector([x, y]), mu_k[ 1 ], sigma_k[ 1 ]), [x, - 2.5 , 2.5 ], [y, - 2.5 , 2.5 ], contours = 1 , cmap=[ 'red' ], fill= False ) b_cnt_plt = contour_plot( lambda x, y : _gauss(vector([x, y]), mu_k[ 0 ], sigma_k[ 0 ]), [x, - 2.5 , 2.5 ], [y, - 2.5 , 2.5 ], contours = 1 , cmap=[ 'blue' ], fill= False ) (r_cnt_plt + b_cnt_plt + pt_plt).show(aspect_ratio= 1 , figsize=( 3 ), xmin=- 2.5 , xmax= 2.5 , ymin=- 2.5 , ymax= 2.5 ) まとめ いかがでしたでしょうか? 本日は、Jupyter Notebookもいいんだけど、それプラスアルファのことができるSageMathをぜひ使ってみてはいかがでしょうというご紹介をさせていただきました。SageMathって何?というところから出発して、インストール方法やコマンドでの利用やオンラインでの利用についてご紹介した後、ほんの一部ですがSageMathの簡単な使い方などご紹介をさせていただきました。こんな便利なツールが無料であるなんて今の時代は本当に恵まれていますね、ぼくが学生時代にSageMath欲しかったなぁと心底思います。 参考 Sage チュートリアル http://doc.sagemath.org/pdf/ja/tutorial/tutorial-jp.pdf SageMath - Tour - Quickstart EMアルゴリズム - Wikipedia 楕円曲線論入門 作者: J. H. シルヴァーマン , J. テイト 出版社/メーカー: 丸善出版 発売日: 2012/08/25 メディア: 単行本 パターン認識と機械学習 上 作者: C.M. ビショップ 出版社/メーカー: 丸善出版 発売日: 2012/04/05 メディア: 単行本(ソフトカバー) パターン認識と機械学習 下 (ベイズ理論による統計的予測) 作者: C.M. ビショップ 出版社/メーカー: 丸善出版 発売日: 2012/02/29 メディア: 単行本 さいごに さて次回は、サーバサイドエンジニアの安尾が「スピード優先の開発で溜まった技術的負債の返済計画(サーバーサイド編)」というタイトルで投稿します! お楽しみに! qiita.com adventar.org ちなみに delyの開発チームについて詳しく知りたい方はこちらもあわせてどうぞ! CXOとVPoEへのインタビュー記事はこちら! wevox.io
アバター
はじめに はじめまして。 mochizukiです。 クラシルアプリのサーバーサイドをやってます。 昨日はAndroidエンジニアのumemoriさんが 「マルチモジュール時代のDagger2によるDI」 という記事を書いてくれました。 tech.dely.jp dely Advent Calendar 2019の2日目は Netflixがつくった Fast JSON API について書いてみようと思います。 qiita.com adventar.org Fast JSON API Netflix/fast_jsonapi A lightning fast JSON:API serializer for Ruby Objects. Performance Comparison We compare serialization times with Active Model Serializer as part of RSpec performance tests included on this library. We want to ensure that with every change on this library, serialization time is at least 25 times faster than Active Model Serializers on up to current benchmark of 1000 records. Please read the performance document for any questions related to methodology. Benchmark times for 250 records $ rspec Active Model Serializer serialized 250 records in 138.71 ms Fast JSON API serialized 250 records in 3.01 ms This gem is currently used in several APIs at Netflix and has reduced the response times by more than half on many of these APIs. 使ってみる まずは準備 ruby 2.6 . 3 rails 6.0 . 0 rails new fast_json_api --api + gem ' fast_jsonapi ' 追加して class Recipe < ApplicationRecord has_many :ingredients , dependent : :destroy end class Ingredient < ApplicationRecord belongs_to :recipe end 今回はクラシルっぽくこんな感じで。 中身は create_table :recipes do |t| t.string :title, null: false t.text :introduction t.timestamps end create_table :ingredients do |t| t.references :recipe, null: false t.string :name, null: false t.string :quantity_and_unit, null: false t.timestamps end こんな感じで。 データは最近クラシルに実装した ジャンル別ランキング機能より、殿堂入りの これにします! recipes = [ [ ' ネギダレが美味しい!鶏もも肉のソテー ' , ' ネギダレが美味しい、鶏もも肉のソテーはいかがでしょうか。 ' ] ] recipes.each_with_index do | array , index | title = array[ 0 ] introduction = array[ 1 ] Recipe .create({ title : title, introduction : introduction }) end ingredients = [ [ ' 鶏もも肉 ' , ' 300g ' ], [ ' 塩こしょう ' , ' 小さじ1/4 ' ], [ ' 片栗粉 ' , ' 大さじ2 ' ], [ ' 長ねぎ ' , ' 1本 ' ], [ ' ①しょうゆ ' , ' 大さじ1.5 ' ], [ ' ①酢 ' , ' 小さじ1 ' ], [ ' ①砂糖 ' , ' 大さじ1 ' ], [ ' ①ごま油 ' , ' 小さじ1 ' ], [ ' ①白すりごま ' , ' 大さじ1 ' ], [ ' ①すりおろし生姜 ' , ' 小さじ1/2 ' ], [ ' サラダ油 ' , ' 大さじ1 ' ], [ ' リーフレタス ' , ' 2枚 ' ] ] Recipe .all.each do | recipe | ingredients.each_with_index do | array , index | name = array[ 0 ] quantity_and_unit = array[ 1 ] recipe.ingredients.create({ name : name, quantity_and_unit : quantity_and_unit }) end end そして class Api :: V1 :: RecipesController < ApplicationController end こんな感じ。 やっと本題 基本的に ActiveModelSerializers と同様に使えます。 rails g で、serializerつくるとこんな感じです。 class RecipeSerializer include FastJsonapi :: ObjectSerializer attributes end なので class RecipeSerializer include FastJsonapi :: ObjectSerializer has_many :ingredients attributes :title , :introduction end class IngredientSerializer include FastJsonapi :: ObjectSerializer belongs_to :recipe attributes :name , :quantity_and_unit end こんな感じで定義します。 今回つくるAPI 殿堂入りレシピの ネギダレが美味しい!鶏もも肉のソテー の レシピ詳細を想定してつくってみます。 GET /api/v1/recipes/:id class Api :: V1 :: RecipesController < ApplicationController def show recipe = Recipe .find(params[ :id ]) json_string = RecipeSerializer .new(recipe).serialized_json render json : json_string end end { " data ": { " id ": " 1 ", " type ": " recipe ", " attributes ": { " title ": " ネギダレが美味しい!鶏もも肉のソテー ", " introduction ": " ネギダレが美味しい、鶏もも肉のソテーはいかがでしょうか。 " } , " relationships ": { " ingredients ": { " data ": [ { " id ": " 1 ", " type ": " ingredient " } , { " id ": " 2 ", " type ": " ingredient " } , (一部のみ表示) リレーションが紐付いてます。 Started GET "/api/v1/recipes/1" for ::1 at (0.2ms) SELECT sqlite_version(*) Processing by Api::V1::RecipesController#show as JSON Parameters: {"id"=>"1"} Recipe Load (0.3ms) SELECT "recipes".* FROM "recipes" WHERE "recipes"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] ↳ app/controllers/api/v1/recipes_controller.rb:3:in `show' (0.2ms) SELECT "ingredients"."id" FROM "ingredients" WHERE "ingredients"."recipe_id" = ? [["recipe_id", 1]] ↳ app/controllers/api/v1/recipes_controller.rb:4:in `show' Completed 200 OK in 34ms (Views: 0.6ms | ActiveRecord: 0.5ms | Allocations: 6624) (キャッシュなしの速度) 材料と分量も返す レシピ詳細では、材料と分量も返してあげないといけないです。 READMEに従って、 options = { include : %i[ingredients] } こういうのをつくって class Api :: V1 :: RecipesController < ApplicationController def show recipe = Recipe .find(params[ :id ]) options = { include : %i[ingredients] } json_string = RecipeSerializer .new(recipe, options).serialized_json render json : json_string end end 渡してあげます。 { " data ": { " id ": " 1 ", " type ": " recipe ", " attributes ": { " title ": " ネギダレが美味しい!鶏もも肉のソテー ", " introduction ": " ネギダレが美味しい、鶏もも肉のソテーはいかがでしょうか。 " } , " relationships ": { " ingredients ": { " data ": [ { " id ": " 1 ", " type ": " ingredient " } , { " id ": " 2 ", " type ": " ingredient " } , ] } } }, " included ": [ { " id ": " 1 ", " type ": " ingredient ", " attributes ": { " name ": " 鶏もも肉 ", " quantity_and_unit ": " 300g " } , " relationships ": { " recipe ": { " data ": { " id ": " 1 ", " type ": " recipe " } } } } , { " id ": " 2 ", " type ": " ingredient ", " attributes ": { " name ": " 塩こしょう ", " quantity_and_unit ": " 小さじ1/4 " } , " relationships ": { " recipe ": { " data ": { " id ": " 1 ", " type ": " recipe " } } } } , (一部のみ表示) 紐付いている材料と分量が返ってきています。 Started GET "/api/v1/recipes/1" for ::1 at (0.2ms) SELECT sqlite_version(*) Processing by Api::V1::RecipesController#show as JSON Parameters: {"id"=>"1"} Recipe Load (0.6ms) SELECT "recipes".* FROM "recipes" WHERE "recipes"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] ↳ app/controllers/api/v1/recipes_controller.rb:3:in `show' (0.2ms) SELECT "ingredients"."id" FROM "ingredients" WHERE "ingredients"."recipe_id" = ? [["recipe_id", 1]] ↳ app/controllers/api/v1/recipes_controller.rb:5:in `show' Ingredient Load (0.4ms) SELECT "ingredients".* FROM "ingredients" WHERE "ingredients"."recipe_id" = ? [["recipe_id", 1]] ↳ app/controllers/api/v1/recipes_controller.rb:5:in `show' Completed 200 OK in 51ms (Views: 0.4ms | ActiveRecord: 1.2ms | Allocations: 13646) (キャッシュなしの速度) キャッシュ READMEに従って cache_options enabled : true , cache_length : 1 .hours こんなのつけて class RecipeSerializer include FastJsonapi :: ObjectSerializer cache_options enabled : true , cache_length : 1 .hours has_many :ingredients attributes :title , :introduction end Completed 200 OK in 36ms (Views: 0.3ms | ActiveRecord: 0.8ms | Allocations: 13427) Completed 200 OK in 13ms (Views: 0.2ms | ActiveRecord: 1.4ms | Allocations: 4940) その他使い方 基本的に ActiveModelSerializers と同じです。 気になった方は、 README を参照してみてください。 最後に 明日はCXOの坪田さんの「突破するプロダクトマネジメント」です! 楽しみです!! delyではエンジニアを募集しています。 www.wantedly.com
アバター
こんにちは。dely株式会社のAndroidアプリチームのうめもりです。今年もdelyはAdvent Calendarをやることになりました。開発部の面々が色々な記事を今年も書いてくれますので、是非ほかの記事も見て行ってください。 qiita.com adventar.org この記事はdely Advent Calendarの1日目の記事です。早速やっていきましょう。 Androidのマルチモジュール構成のアプリケーション上でDagger2を用いて依存性解決を行うやり方を、簡単なマルチモジュール構成のアプリケーションを例に紹介します。 想定するモジュール構成 今回想定するアプリケーションのモジュール構成は次のようになっています。 App Module - メインのAndroid App Module、依存性の解決はすべてここで行う UI Module - Feature Moduleで定義したクラスを利用するActivityやFragmentをここに定義する Feature Module - UI Moduleから呼ばれるクラスを定義する 一般的なマルチモジュール構成に近いと思われるシンプルな構成にしてみました。実際アプリケーションが大きくなった場合は、UI ModuleやFeature Moduleを分割したりする必要が出てくると思いますが、その場合でも今回の方法と同じように対応することができるはずです。 記事内ではApp ModuleからFetaure Moduleへの依存は出てきませんが、このようなモジュール構成にするとApp ModuleからFeature Moduleへ依存する形でServiceを作成するなど、実際にはApp ModuleからFeature Moduleへの依存関係が発生すると思われます。 Dagger2のスコープ Dagger2のスコープは @Singleton @ViewScope の2つの定義にしました。 @Singleton は最初からあるスコープですが、 @ViewScopeについては新規に定義したScopeです。 Activity/FragmentごとにScopeを切るようにしてみました。 Scopeの定義は次のようになっています。 @dagger.Scope @Retention (AnnotationRetention.RUNTIME) annotation class ViewScope { } Feature Module側では何も考えずに依存性を定義して@Injectをつける Feature Module内では、通常のDagger2と同じようにクラス定義を行うだけです。 クラス定義の例 class RecipeFeatureImpl @javax.inject.Inject internal constructor ( recipeApiProvider: RecipeApiProvider ) : RecipeFeature { private val recipeApi = recipeApiProvider.recipeApi override fun createLatestRecipeFeedContainer(): FeedContainer<RecipeEntity> = FeedContainer( LatestRecipeFeedApi(recipeApi), 20 ) } Dagger2のModule定義が必要な場合は、Feature Module内に定義してもよいですし、App Module内に定義してそれを使ってしまうのもよいと思いますが、依存性解決の定義についてはApp Module内でほぼすべてを行うので、そちらに集約してしまうのがよいかもしれません。 UI Moduleではinject用のinterfaceを定義する UI ModuleではFeature Module側で定義したクラスに依存する形でActivityやFragmentを定義します。 あまり規模の大きくないアプリケーションでは実際にはこのModuleでDagger2のComponent定義を行ってしまってもいいとは思いますが、今回はComponent定義をUI定義から分離することを考えます。 必要なのは、ActivityやFragmentに対して依存性を注入するためのinterfaceです。なのでまずはそれを素直に定義しましょう。 injector定義のサンプル interface ViewInjectors { fun inject(mainActivity: MainActivity) fun inject(recipeDetailActivity: RecipeDetailActivity) fun inject(latestRecipeFeedFragment: RecipeFeedLatestFragment) } Dagger2のComponent定義はApp Module内で行うので、アノテーションなどをつける必要はありません。 さて、実際のActivityやFragment内では定義したinterfaceを使って依存性注入処理をする必要があるので、 どこからかinject用のinterfaceを取得して注入用のメソッドを呼ぶ必要があります。 今回は、カスタムApplicationクラスにinterfaceを実装することにして、Applicationクラスからinterfaceを取得するinterfaceを作成しましょう。実際のコードはこのようになります。 interface ViewInjectorsProvider { fun provide(activity: FragmentActivity): ViewInjectors fun provide(fragment: Fragment): ViewInjectors } そして、Activity/Fragment側では、定義したinterfaceにキャストしてinjectするコードを記述します。 class MainActivity : AppCompatActivity() { @javax.inject.Inject lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) (application as ViewInjectorsProvider).inject( this ) // 省略 } } Dagger2のComponentはApp Moduleに定義し、依存性解決を全て行う 最後に、App Module内でDagger2のComponent定義を行います。今回は、@Singletonと@ViewScopeの二つのScope定義があるので、Component定義も2つになります。 @javax.inject.Singleton @dagger.Component ( modules = // 依存Moduleを記述 ) interface SampleSingletonComponent { fun viewComponent( // SampleViewComponentの依存Moduleを記述 ): SampleViewComponent } Fragment/Activity側で実際にinjectするScopeのComponentは、UI Moduleで定義したinterfaceを継承します。 @ViewScope @dagger.Subcomponent ( modules = // 依存Moduleを記述 ) interface SampleViewComponent : ViewInjectors Component定義を行ったら、今度はApplicationに先ほどUI Moduleで定義したinterfaceを実装しましょう。一度ビルドを行えば、Daggerによって生成されたComponentを参照できるようになっているはずです。 class SampleApplication : Application(), ViewInjectorsProvider { private lateinit var appComponent: SampleAppComponent override fun onCreate() { super .onCreate() appComponent = DaggerSampleAppComponent.create() } override fun provide(activity: FragmentActivity) = appComponent.viewComponent( // インスタンス化に必要なModuleを渡す ) override fun provide(fragment: Fragment) = appComponent.viewComponent( // インスタンス化に必要なModuleを渡す ) } 終わりに 以上、マルチモジュール構成のアプリケーションでDagger2による依存性解決を行うやり方を紹介しました。 ポイントとしては、依存性解決にかかわるコードは最終的にモジュールを集約するモジュールにまとめて記述するようにするという点です。そこさえ守っていればそう難しくはありません。 さて、明日(12/2)はサーバーサイドエンジニアの望月さんによる「NetflixのFast JSON APIを使ってみた」という記事です。お楽しみに。 記事が公開されました tech.dely.jp Advent Calendarへのリンクはこちらです qiita.com adventar.org ちなみに delyではAndroidエンジニアを絶賛募集しております、もしご興味あればこちらのリンクからご気軽にエントリーください! www.wantedly.com delyの開発チームについて詳しく知りたい方はこちらもあわせてどうぞ! CXOとVPoEへのインタビュー記事はこちら! wevox.io 弊社Androidチームへのインタビュー記事です! note.com
アバター
こんにちは!クラシルiOSアプリを開発しているknchstです。 昨今のモバイルアプリケーション開発では様々な要件があり、それらを満たすよう実装するには数々の苦難がありました。その一つとしてUIの状態、所謂State管理が難しくなってきています。ネットワークに接続し、またUIをアニメーションさせたりと、データとUIを同期するのは困難を極めます。 Rxなどのリアクティブフレームワークの利用が当たり前になり、ReactNaviteやFlutterなどのフレームワークをプロダクトに採用する企業も増えてきて、モバイルアプリのトレンドの風も、まさにこの方向に向かって吹き始めていました。 そして今年のWWDCでAppleがSwiftUIを発表してついに、その風は大きくなり今後のモバイルアプリの方向性を決定付けたと言っても過言ではないでしょうか。 今回は、SwiftUIとFlutterでアプリを開発する際の実装の違いをサンプルアプリを通して比較していきたいと思います。 概要 SwiftUIとは SwiftUIは今年のWWDC2019でサプライズ発表された「宣言型UIフレームワーク」です。 特徴としては iOS, iPadOS, macOS, watchOS, tvOSなどのすべてのAppleプラットフォーム向けに開発することができる 宣言型シンタックス(Swift) Live Preview デザインツール Flutterとは FlutterはGoogleが2年ほど前にベータ版としてリリースされたフレームワークになります。クロスプラットフォームなアプリの開発フレームワークとして最近は様々な企業が導入しています。 特徴としては 宣言的なシンタックス(Dart) iOS, Android, Web に対応したクロスプラットフォーム ネイティブパフォーマンス 豊富な標準コンポーネント(Widget) Hot Reload Pluginによる高い拡張性 比較してみる SwiftUIとFlutterの比較はSwiftUIの公式チュートリアルでも、紹介されていた Landmarks というアプリで行います。Flutterで同じ Landmarks を再現したものがあるので、それを用いて比較していきます。 ソースコードはそれぞれ以下から取得できます。 SwiftUI   Flutter 開発環境 SwiftUI - Xcode SwiftUIの開発環境でも、Appleのプラットフォームのアプリ開発を長年支えてきたXcodeが担当します。SwiftUIの開発環境にはコードの変更がリアルタイムにプレビューされる機能(画像右)があり、iOSなどの様々な機能(ダークモード、アクセシビリティなど)もシミュレーションすることができます。 Flutter - Android Studio / Visual Studio Code Flutterの開発環境はAndroid開発でもおなじみのAndroid StudioまたはMicrosoftが開発したコードエディタのVisual Studio Codeで開発することができます。 XcodeにはSwiftUI向けにデザインツールが提供されており、GUIでコンポーネントを追加したりプロパティを変更することができ、Flutterの開発環境よりも強力です。 UI SwiftUI - View ViewはProtocolとして定義されており、Viewを定義するにはこれに準拠し、bodyプロパティを実装する必要があります。bodyプロパティでコンポーネントを返すことでそのViewが描画されます。 以下のサンプルコードでは、 Hello World と表示するTextコンポーネントを返しています。 import SwiftUI struct ContentView : View { var body : some View { Text( "Hello World" ) } } Flutter - Widget FlutterのUIコンポーネントはWidgetクラスを継承しています。 Flutter で用意されている Widget にはStateを持たないStatelessWidget と Stateを持つStatefulWidget があります。 以下のサンプルコードでは中央に Hello World と表示するTextコンポーネントを使ったことになります。 import 'package:flutter/material.dart' ; class ContentView extends StatelessWidget { @override Widget build ( BuildContext context) { return Center ( child : Text ( "Hello World" ), ); } } Layout SwiftUIとFlutterのレイアウト方法は大きく違いはありません。それぞれX, Y, Zの方向にレイアウトをすることができます。 Landmarksサンプルアプリでそれぞれの実装の違いを見ていきます。 LandmarksアプリのホームはリストのUIになっていて、各行は水平レイアウトで構築されています。 SwiftUIはHStackを利用し、その中にImage, Text, Spacerなどのコンポーネントを配置していきます。 FultterはRowを利用し、同じくコンポーネント配置していきます。 それぞれサンプルコードは以下になります。 SwiftUI HStack { landmark.image .resizable() .frame(width : 50 , height : 50 ) Text(verbatim : landmark.name ) Spacer() if landmark.isFavorite { Image(systemName : "star.fill" ) .imageScale(.medium) .foregroundColor(.yellow) } } Flutter Row ( children : < Widget > [ Image . asset ( 'assets/ ${landmark.imageName} .jpg' , width : 50.0 ), SizedBox ( width : 16 , ), Text ( landmark.name, style : TextStyle (fontSize : 16 ), ), Expanded ( child : Container (), ), landmark.isFavorite ? StarButton (isFavorite : landmark.isFavorite) : Container (), Icon ( Icons .arrow_forward_ios, size : 15.0 , color : const Color ( 0xFFD3D3D3 ), ), ] ) SwiftUIにはデフォルトでXcodeがマージンなどを設定してくれる分コードの記述が減りシンプルに見えますが、Flutterのレイアウトに関する命名の方がより直感的でわかりやすいように感じました。 List UIKitで利用していた、UITableViewはSwiftUIではListになります。 Flutterでは、複数の選択肢が提供されています。ListViewを使用して複数行のコンテンツを表示したり、SingleChildScrollViewを使用してスクロール可能なコンテンツを表示することができます。 LandmarksアプリではホームはリストのViewになります。 SwiftUIではForEachを使用してこのリストの要素を作成し、FlutterリストはSliverListを使用します。 両方のコードは次のとおりです。 SwiftUI List { ForEach(landmarks) { landmark in LandmarkRow(landmark : landmark ) } } Flutter SliverList ( delegate : SliverChildBuilderDelegate ( (context, index) { final landmark = landmarks[index]; return LandmarkCell ( landmark : landmark, ); }, childCount : landmarks.length, ), ) Navigation SwiftUIのNavigationはNavigationViewとNavigationLinkで実現します。 対してFlutterはRouteとNavigatorを使います。以下はそれぞれのサンプルコードです。 SwiftUI NavigationView { // NavigationViewでラップする List { ForEach(userData.landmarks) { landmark in NavigationLink( destination : LandmarkDetail (landmark : landmark ) // 遷移先を指定 ) { LandmarkRow(landmark : landmark ) } } } .navigationBarTitle(Text( "Landmarks" ))  // タイトルバーを設置 } Flutter SliverList ( delegate : SliverChildBuilderDelegate ( (context, index) { final landmark = landmarks[index]; return LandmarkCell ( landmark : landmark, onTap : () { // セルにタップイベント Navigator . push ( context, Route ( builder : (context) => LandmarkDetail ( landmark : landmark, ), ), ); }, ); }, childCount : landmarks.length ) ) Flutterはタップのイベントに対してNavigationを記述しないといけませんが、SwiftUIでは遷移先のLinkを設定します。 State Management Stateの管理は宣言型UIでもっとも大事な要素の一つです。 Landmarksアプリ内にお気に入り追加したlandmarkを絞り込むスイッチがあります。 SwiftUIとFlutterのStateの管理方法は別々のアプローチを取っています。 SwiftUIはStatefulなデータをUIにバインドさせます。 一方でFlutterはデータが更新された後、setStateメソッドで呼び出すことWidgetを更新しています。 SwiftUI struct LandmarkList : View { @State var showFavoritesOnly = true var body : some View { NavigationView { List { Toggle(isOn : $showFavoritesOnly ) { Text( "Favorites only" ) } ForEach(landmarkData) { landmark in if !self .showFavoritesOnly || landmark.isFavorite { NavigationLink(destination : LandmarkDetail (landmark : landmark )) { LandmarkRow(landmark : landmark ) } } } } .navigationBarTitle(Text( "Landmarks" )) } } } @Stateが付いているプロパティ showFavoritesOnly がStateを管理しています。 $マークをプロパティの前につけることによって UIにデータをバインドさせています。 showFavoritesOnly の変更に合わせてバインドされているUIが変わり、UIが変わると showFavoritesOnly の値も変わります。 Flutter CupertinoSwitch ( value : _showFavoritesOnly, onChanged : (state) { setState (() { _showFavoritesOnly = state; }); }, ) CupertinoSwitchはスイッチボタンであり、値が変更されるとonChangedメソッドが呼び出され、次にsetStateメソッドを呼び出して変数showFavoritesOnlyの新しい値を設定し、UIを更新します。 まとめ サンプルアプリを見ながらSwiftUIとFlutterの実装の比較をしてきました。もちろん現実のアプリは様々な要件があり、サンプルアプリよりも多くの比較対象があるかと思います。 今回紹介した宣言型UIに基づいた2つのフレームワークは多くの類似点がある印象でした。 とはいえ、それぞれが独自の思想と機能を持っているので要件に合わせて使い分ける必要もあるかと思います。サンプルアプリ程度ではわからない「つらみ」などもあると思うので、今後チャンスを伺ってプロダクトに導入していきたいです。 delyでは新しいメンバーを積極的に募集しています! もしご興味があればご応募・ご連絡ください! speakerdeck.com
アバター
こんにちは、iOSエンジニアのtakao( takaoh717 )です 今回はクラシルiOSアプリのフィードのパフォーマンス改善を行った話をご紹介します。 改善を行ったフィードはUICollectionViewで構成されており、レシピ、画像バナー、広告など複数の異なる型のデータを表示しているような画面です。 今回行った変更は以下の内容です。 差分更新ライブラリの導入とデータの管理、更新ロジックの変更 セルのサイズ計算を事前に行うよう修正 通信時やログ送信時の重い処理をバックグラウンドスレッドで実行 改善前の課題 改善を行う前は、アプリを動かしていると実際に分かるレベルでパフォーマンスに問題がありました。 スクロール自体の挙動が若干重くてスムーズじゃない(指の動きに対して若干ひっかかりがある) ページングの読み込みをしたときにスクロールが止まることがある 更新時に画面がチラつくことがある 差分更新ライブラリの導入とロジックの見直し まず最初に、差分更新ライブラリの導入を行いました。 これまでは、一部分のみ自前のロジックで差分更新を行って、基本的には reloadData() を多用しているような状態でした。 何度か差分更新を行うようにしたことはあるのですが、更新タイミングによるクラッシュなどが度々発生し、結局 reloadData() に戻すようなことをしていました。 そこで、今回のリファクタリングを気に reloadData() を使用しない状態にしておきたかったので、ライブラリを導入しました。 DifferenceKitの導入 差分更新用のライブラリは DifferenceKit を選択しました。 github.com 選定理由では以下の点が判断基準になりました。 パフォーマンスの高さ プロジェクトへの導入コスト、実装コストの低さ パフォーマンスについては実際に計測して比較はしていないのですが、公式のドキュメントに記載されている内容では、 RxDataSources や IGListKit よりも高速になっているようです。 実装面については、 class だけではなく struct にも対応している点や、 AnyDifferentiable を使って異なる型のデータを一つの配列で管理可能な点がポイントでした。 Examplesに動くアプリのコードが載っているのでそちらとドキュメントを参考にして実装しました。 実装方法 まず、フィードに表示するコンテンツを表す Model に Differentiable を準拠させます。 extension Model : Differentiable { public var differenceIdentifier : String { return id } public func isContentEqual (to source : Model ) -> Bool { return title == source.title } } 今回の実装ではフィードの1列を1Sectionで表現する構造にしてみました。 Section のデータ管理をしやすいように DataSource は以下のような実装にしました。 // Section毎のデータを保持する配列 var dataSources = [Section]() // Sectionに該当するデータ型を保持するためのenum enum FeedSectionModel : Differentiable { case hoge case fuga case piyo } // 1Sectionを一つのまとまりとして管理するために定義 typealias Section = ArraySection < FeedSectionModel, AnyDifferentiable > 更新時の実装も変更前と後の配列を用意して渡してあげるだけなので、とてもシンプルな実装になります。 また、 interrupt に特定の条件を渡しておけば、結果が true になった場合に差分更新をせずに reloadData() を行うようにできます。 クラシルはUI上の1つのセルのサイズが大きいため、例えば、リストの途中の位置などで一定数以上のセルの挿入があったりすると、スクロール位置などが大きくずれてしまったりするため、一定の個数以上の更新が必要な場合などは reloadData() を行うようにしました。 let new = dataSource new .append(ArraySection(model : FeedSectionModel.hoge , elements : newData )) // source: 更新前のデータ、target: 更新後のデータ let changeSet = StagedChangeset(source : dataSources , target : new ) collectionView.reload(using : changeset , interrupt : { $0 .changeCount > 3 }) { data in dataSource.data = data } パフォーマンスに大きく影響を与えている実装箇所の特定 ベースのリファクタリングが出来た後にどの実装が問題になっているのかを特定するため、InstrumentsのTimeProfilerを使って重い処理を特定する作業を行いました。 使い方はとてもシンプルですが、実際にアプリを動かしながら、カクつくタイミングにメインスレッドで実行している重い処理をひたすら確認していきました。 また、今回のようなMainThreadで実行されている重い処理を見たい場合の確認では以下のような設定を使用していました。 Instruments セルのサイズ計算を事前に行う スクロール自体の挙動が重くてスムーズな挙動にならない場合は func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) のサイズ計算が問題になっているパターンがあると思います。 実際にクラシルでもレシピや広告などのデータを取得した後に、 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) の中で表示するCellのサイズ計算を行っていたため、Instrumentsで見てみるとその中身の関数のときに負荷が跳ね上がっていました。 このような、データの内容によってセルのサイズが可変になる場合、データを取得したタイミングで先にサイズ計算を行い、 Model や Entity でサイズを保持するようにしました。 そうすることで、Layoutのサイズを返すときには事前に計算しておいたサイズを返すだけになり、メインスレッド上での処理が減るのでパフォーマンスが向上します。 Viewの描画に直接関わらない重い処理の修正 スクロール途中に急ブレーキがかかるようにスクロールが停止したり、次のページの読み込みが走ったタイミングでカクつきが発生したりしていた原因はViewの描画に直接関係のない重い処理をメインスレッドで行ってしまっていたものでした。 この場合も、どの処理がネックになっているかをまずInstrumentsで確認しました。 メインスレッドで実行していた重い処理 API通信後のJsonのパース ログの送信 サーバーから取得したデータの変換処理 これらの処理を DispatchQueue を使って、適切にバックグラウンドスレッドに引き渡すことによって、読み込み時などにメインスレッドの使用率が急上昇することがなくなり、UIのカクつきが解消されました。 まとめ UICollectionViewはUITableViewに次いで多くのiOSアプリで使われていると思います。 しかし、適切に実装していないとパフォーマンスの低下がユーザに見える形で分かりやすく出てしまうため、適度にメンテナンスすることが大事だと思います。 delyでは様々なポジションのエンジニアを積極採用中です!ご興味がある方はぜひご連絡ください。
アバター
はじめに こんにちは、delyでサーバーサイドエンジニアをやっている山野井です。 delyのサーバーサイドチームでは「サーバーサイド&SRE改善MTG」という取り組みを毎週行っています。 この取り組みは個々が日頃開発する上で感じている課題をdelyのサーバーサイドエンジニア、SRE、フロントエンドエンジニア間で共有・議論し、解決を目指すものです。 本記事ではその取り組みについてご紹介したいと思います。 何故この取り組みを行っているのか 以下の2つの課題を解決するためにこの取り組みを行っています。 1.個々で課題や問題だと感じていることを関係者で共有・議論する機会が少ない delyの開発チームは少数精鋭であり、日頃行う業務に追われているため、日常業務内で感じた課題は個々で解決するか、優先度があがらず手を付けられていない状態になってしまっていました。 個々で問題を抱えている状態は不安やストレスがたまりチームとして健全な状態ではないと思います。 2.分野横断の技術的なFBが少なく分野を跨いだスキルアップが難しい 自分の日常業務に関してのスキルアップは比較的容易ですが、普段作業しない分野においてはどうしてもスキルアップが難しくなってしまいます。 例えばサーバーサイドエンジニアでもフロントエンドのコードを書いたり、インフラの知識が求められます。 しかし、自分の専門外である技術のFBを得られる場がありませんでした。 MTGを行う目的 サーバーサイド&SRE MTGを行うにあたり、上記の2つの課題を解決するために大きく3つの目的を設定しました。 個々で課題や問題だと感じていることを明確にして参加者に共有する 課題や問題の明確化や解決策を参加者内で議論する 解決策をタスク化(緊急度、重要度)して解消につなげる 個人の課題にフォーカスしてみんなで議論することで、今まで一人で解決してきた状態から業務に対する心理的な不安・ストレスの解消を図ることができます。 また、普段違うチームで作業しているからこそ、参加者全員で議論やFBを行うことで、効率的な個人の成長につながると考えています。 ここで議論した結果に基づいて必要であればタスクを作成し、負債の解消につなげます。 どのように行っているか サーバーサイド&SRE MTGは週に1度の頻度で行っています。 参加者、発表者、議題決め担当者に分かれていて、議題決め担当者のみ参加者内で順番に持ち回り制です。 サーバーサイド&SRE MTGのおおまかな流れは以下のとおりです。 各メンバーが自由に議題を出す 議題決め担当者が議題を決める 決められた議題を出した人がMTGまでに資料を作る 資料を元に議論する 
ミーティングで議論した内容は議事録としてQiita:Teamに残し、いつでも見返すことができる状態にしています。 今まで扱ってきた議題の例として、アプリケーションの設計の見直しや、開発環境の整備等がありました。 実際に数ヶ月間この取り組みを行ってきてMTGの振り返り会を一度行いました。 振り返り会では、議題で上がった課題はその後改善ができているのか、また改善ができていないものに関しては何が原因で改善ができていなのか、今後どのようにして改善を進めていくかを議論しました。 この様にしてMTGの振り返り会を定期的に行いMTG自体のブラッシュアップも行っています。 どんな効果があったか サーバーサイド&SRE MTGをはじめたことで、チーム内にどのような変化が起きたかをヒアリングしてみたところ下記の様な意見がもらえました。 気になる事があった時に1人で抱えてモヤモヤせずに、共有して議論できるのが良い 全体に共有してフィードバックをもらえるので、自分1人で考えるよりも良い案が出る 比較的最近入った人間からすると何が問題になってるかがわからないので、それが可視化されるのは良いなと思う 過去に議論された結果がわかるのも良い 以前は課題感が漠然としていて、それに対して漠然とした不安を抱えていたような気がする。MTGを通して課題の重要度・緊急度・コストがある程度明確に認識できるようになってきたので、それに基づいて優先度を決めて意思決定が出来るようになってきた PRのレビューがしやすくなった 設計の議論がスムーズに行えるようになった(既存のサービスだけでなく、新しいサービスや基盤を作る際にも) 一人で問題を抱え込むストレスがなくなった 入社が遅い人でも迅速に現存する問題点をキャッチアップしてもらえた 引き継ぎ、部署異動しやすい 問題が忘れ去られることがない安心感がある(自分も忘れない) 自分が気づいていなかったことや感じていなかった問題を様々なバックグラウンドをもったエンジニアから教えてもらうことができる 個人的には、 自分が抱えていた問題を全員に共有することができたことでスッキリしたこと 過去にどのような議論が行われたのかを振り返る事ができることによって、似たような問題に遭遇しても参照することができるようになったこと が良かったと感じました。 まとめ 今回はサーバーサイドチームで行っている取り組みについてご紹介してきました。 参加者からの評判も良く、開発効率・生産性も向上していると感じています。 delyのサーバーサイドチームでは、今後ビジネスを拡大していくにあたって明らかに規模が追いついておらず、新しいメンバーの募集を積極的に行っているところです。 もしご興味があればご応募・ご連絡ください。 speakerdeck.com
アバター
こんにちは、6月にサーバーサイドチームからSREチームに異動した高山です。 私がSREチームにジョインすることとなり、SREのチームメンバーが増えることをきっかけに、現状のSREチームを見直すという取り組みを行っています。 弊社の開発組織にはもともとSREという職種が存在しています。しかし、実態としては、ソフトウェアも書けるインフラエンジニアがSREという名で活動しているというだけで、「一般的なインフラエンジニアの責務+困った時に頼られるなんでも屋」という曖昧なものでした。 その結果、 SREチームが実際には何をすべきチームなのか他のチームに正しい認識をしてもらえていない メンバーによって認識しているSREとしての責務境界がバラバラ 降ってくるタスクが多すぎて足元解決型のチーム 優先順位が定まらず本来SREチームがやるべきことに着手できていない(のでは) という状況になっていました。 これでは、SREチームの役割や責務範囲が属人的になり、本来のSREが発揮するべきバリューがチームとして効率よく発揮できないという問題が発生してしまいます。また、今後さまざまなバックグラウンドを持つメンバーが増えていくにあたり、さらに責務範囲の属人化が進むことが懸念されました。 そこで、SREチームとしてのミッションや役割、優先順位を明文化することで本来のSREチームとしてのバリューを発揮できるようにチームを見直す取り組みをはじめました。 目次 取り組みについて SRE本輪読会開催 SREチームミッションの決定 SREチームの役割のスコープ決定 これから取り組みたいこと 取り組みについて 取り組み1: SRE本輪読会開催 問題点としてあげた通り、弊社のSREチームは本来のSREチームに対する理解がチームメンバーによって様々でした。 (ここでいう本来のSREチームとは、Googleが提唱しているSREチームのこと) そもそもSREチームとは、「どのような問題を解決するために」「なにを目的として生まれたチームなのか」ということを全員が理解できていないと、足並みの揃わない、目的がブレたチームを発足することになります。ミッションや役割、優先順位を決めて執行したとしても、全員がその内容を理解できていないと本来時間を費やすべきでないことに時間を費やしたり、正確な意思決定ができなくなったり、納得感のないままミッションを遂行することになってしまうかもしれません。 そこで、SREチーム見直しの第一歩として本来のSREチームを知るべく、SRE本の輪読会を開催することにしました。 www.oreilly.co.jp 輪読会のやり方は以下のような方法で進めました。 ### 進め方 頻度: 週1 時間: 1時間 ルール: 1章ずつ持ち回り制で輪読会を開催 ### 事前準備 - 担当者 - 内容のサマリーと気づきをまとめ当日他のメンバーにドキュメントをシェア - 読んだだけで理解できなかったところは可能な限りググってまとめる - 全員 - 章の内容を読んでくる - 章の中で特に議論したい内容を担当者があげたドキュメントに追記 ### 当日 - 担当者がまとめてきた内容を各自読む(10分) - 共有内容と議論したい内容に基づいて議論する(50分) 輪読会をやってみてわかったことは、 SRE本から得られる学びがとても多い 輪読会の効果がすさまじい ということです。 まず、SRE本に書いてあること自体、とても勉強になります。今まで感覚的に「こうあるべき」と思っていたことが、実はそうでもなく、無駄なことに時間や神経をすり減らしていたと気付かされたり、逆に感覚的にこうだと思っていたことがきちんと明文化されて解決策が提示されていたりします。 また、SREという職種は会社ごとに役割にバリエーションがあったり、ふわっとしがちというイメージがありましたが、SRE本を読んでその責任範囲にかなり明確な定義があるということがわかりました。 さらに、輪読会のすばらしい効果を実感しました。 そもそも誰かに読まれることを前提にまとめを作成することで、一人で読むのと比べて理解度が上がりました。 また、メンバーの半分はSRE本をあらかじめ読んだことがあり、理解度の違うメンバー同士で議論することで、新しく読んだメンバーは効率よく理解が深まり、すでに読んでいるメンバーは認識が違っていた箇所や理解が曖昧だった箇所を潰すことでチームで認識を揃えることができました。 取り組み2: ミッションの決定 SRE本には、私たちのような少人数のチームでは真似できなかったり、サービスの特徴によって起こるGoogleならではの問題に対するアプローチや取り組みも含まれています。それゆえ、そのままGoogleのSREチームの概念を自分たちに投影して運用を試みても、無駄があったり、そもそも真似できない部分があります。 なので、自分たちが抱えている問題とGoogleが抱えている問題をきちんと切り分けて考え、自分たちに必要なルールを取捨選択したり、形を変えて自分たちのルールに落とし込んでいく必要があります。 そこで、弊社のSREチームでは共通の認識によって必要なルールを選定する意思決定ができるように、ミッションの策定を行いました。 まずは、こちらが決定したミッションです。 クラシルの信頼性や事業継続性を担保しつつ、ユーザーに価値を素早く提供できるように設計と運用を改善し続ける 信頼性とは:サービスに求められる機能を、定められた条件の下で、定められた期間にわたり、障害を起こすことなく実行する確率 ミッションの決定で行なったこと SRE本の理解 (SRE輪読会) 現状のタスクの洗い出し 他社のSREチームミッションの事例を集めて共通点や違いを分析 まずは、最低限SRE本の1章を理解してミッションを決定するのがいいと思います。 SRE本の中にも出てきますが、1章がSREチームの役割を理解するために一番大事な章となっています。 全てを読んで理解するのは時間がかかるので、まずはこの1章を最低限全員が理解した段階でミッションの決定を行いました。 次に、現状のタスクの洗い出しをして、SREチームがどのような責務を持っているかを明確にし、ミッションを決める際の参考にしました。 最後に、他社のSREチームのミッションの事例を集めて、SREチームが生まれた背景の分析や弊社のチームとの共通点の分析を行いました。 また、この時点でミッションはあとから柔軟に変化させていくという方針で決定しました。 ミッションは最初に最適なものをピンポイントに決めるということはとても難しいです。 あとから形を変えることができるので、悩みすぎずにこれと決めて事業ニーズの変化や次にお話しする役割のスコープ決定などに合わせて都度話し合っていくことが重要かと思います。 取り組み3: SREチームの役割のスコープの決定 取り組みの3つめとして、SREチームの役割のスコープを決定しました。 まず、こちらが弊社のSREチームが現状考えている役割のスコープです。 可用性の担保 パフォーマンス(レイテンシなど) リソースの効率的な活用 素早く安全な変更管理 モニタリング 緊急対応 キャパシティプランニング セキュリティ スコープの決定で行なったこと スコープを決定するにあたって、下記の手順を踏みました。 SRE本に挙げられているSREの責務のスコープと、現状の弊社SREチームのタスクのマッピング マッピングした上で、それぞれのスコープが弊社の今後のSREチームに必要か議論 スコープからはみ出した責務をSREチームが担うべきか議論 決定したスコープがミッションに沿ったものになっているかを見直す まずは、SRE本の第1章に登場する「SREの信条」として出てくるスコープと現状のSREチームのタスクをマッピングしました。 現状のタスクがSRE本で挙げられているSREの責任範囲にマッピングできないものは、弊社のSREチームが過度に持っている責務として分類しました。 そして、それぞれのスコープが弊社のSREチームに必要かどうかを議論しました。 ここでは、SRE本で挙げられていたスコープになにかしらのタスクが当てはまる結果となり、どのスコープも必要であるという結論になりました。 次に、GoogleのSREの責務からはみ出した責務をSREチームが担うべきか議論を行いました。 前述したように、Googleと弊社ではチーム構成も人数も違います。 例えば、SRE本に列挙されている責務のスコープにはセキュリティがありません。 GoogleにはSREチームとは別にセキュリティのスペシャリストチームがいる可能性が高いです。弊社は、セキュリティチームがないのでその責務をSREチームが兼任する必要がある、といったように、GoogleのSREチームの責務に加えて持たなければならない責務についても議論しました。 最後に、最終的に決まったスコープが、SREチームのミッションに沿ったものであるかどうかを確認しました。 必要なスコープがチームのミッションに即していない場合は、スコープを見直すか、チームミッションを見直す必要があります。 このスコープを決定することで、一気に弊社のSREチームの役割が明確になってきました。SRE本でわかりやすくスコープを切ってくれているため、スムーズに役割のスコープを決定することができました。 これからやっていきたいこと SREチームの役割のスコープを細かいアクションに落とし込む アクションごとの優先順位を決める 他チームへのSREチームのミッションや役割の共有 まとめ SRE本の輪読会や、ミッションの決定、役割のスコープの決定、MTGの設立などを通して、だんだんとdelyのSRE像がみえてきました。この取り組みにより、チームで足並みが揃い始めていると実感しています!また、SREチームの責務を判断する際の明確な基準が生まれて、意思決定がスムーズになりました。 しかし現時点では、SREチームとしてスタート地点に立ったにすぎません。delyのSREチームでは、そんな発展途上なSREチームを一緒に形にしてくれるSREを募集しています!
アバター
こんにちは。delyインターンのしょーといいます。 データサイエンスチームで1ヶ月間インターンさせていただきました。 本記事では、インターンで行なってきた事柄を紹介していきます。 目次 目次 1. コホート分析 分析手法 結果 2. アプリダウンロード数の推移 分析手法 結果 3. 動画視聴予測モデル作成 基礎となるデータフレームに至るまで 学習 精度向上に向けて このモデルを利用した機能提案 4. データサイエンスチームの取り組みに参加した 成果報告会を行なった 終わりに 1. コホート分析 レシピ詳細画面のUIが変わったことによるリテンションの変化を分析しました。 分析手法 コホート分析を用いました。なぜコホート分析を用いたかというと、リテンションの変化が一目で分かりやすいからです。 roboma.io 今回はGoogleアナリティクスではなく、Pythonを用いてコホート分析を行いました。 まず、以下のデータをSQLで取り出して分析に使用します。 date: 最初にレシピ詳細画面を開いた日 past_days: dateから何日後にアプリを起動したか user_count: dateの日にアプリを起動した人の中で、さらにpast_days後にアプリを起動した人数 total_count: dateの日にアプリを起動した人数 user_countをtotal_countで割ると、それぞれのリテンションを算出することができます。この値と、date、past_daysを用いてピボットテーブルを作成し、seabornで図を作成しました。 結果 完成した図がこちらです。 (イメージ図となっています、ご了承ください。) こちらを用いてUI変更前との比較を行なった結果、新しいUIの方がリテンションが改善されていることが分かりました。 2. アプリダウンロード数の推移 データレイクごとのアプリダウンロード数のデータを抽出し、それぞれのデータレイクにおいてデータが欠損してないかを調べました。 クラシルのデータレイクは以下を参照ください。 logmi.jp 今回はeternalpose、logpose、firebaseの3つのデータレイクを用いました。 分析手法 それぞれのデータレイクからアプリダウンロード数を、SQLを書いてデータを取り出し、seabornを用いて可視化しました。 日毎に5ヶ月の期間で推移を見ていきました。 結果 可視化したグラフは非公開とさせていただきます、申し訳ございません。 それぞれのデータレイクのアプリダウンロード数を確認したところ、firebaseにおいて、他の2つのデータレイクに比べて所々極端に少なくなっているところがあり、データの欠損が確認できました。このデータの欠損は今後のクラシルのデータ分析にも影響を与えると考えたので、開発チームに伝えてデータの修正をしていただきました。 また、logposeに格納されているアプリダウンロード数が、序盤から中盤まで他の2つのデータベースより少なかったことが分かりました。更に詳細に調査を行った結果、iOSのダウンロード数は正常でしたが、androidのダウンロード数が序盤から中盤まで少なかったため、androidのデータに何か異常が起きていたのだと考えられます。 3. 動画視聴予測モデル作成 ユーザが任意のレシピ動画を見るか見ないかを予測するモデルを作りました。   目標は実際のサービスに組み込めるようなモデルです。 以下用いるデータは、Athenaのeternalposeより取り出しています。 基礎となるデータフレームに至るまで まず初めのアプローチとして、あるユーザがお気に入りしている動画に含まれている食材のデータを用いることを検討しました。 user_id: あるユーザー video_cnt: ユーザがお気に入りしているビデオの個数 ingredient_count_list: ユーザがお気に入りした動画に含まれている食材とその個数のリスト しかしこのまま用いようとすると、ユーザのお気に入りしている食材の情報しか使用できません。また、個々の動画の情報も入れないと良いモデルは作れないだろうと考えました。 そこで、ユーザがお気に入りしている動画で使われている食材のカテゴリを使用することを考えます。しかし、このデータをAthenaから取り出すのにとても時間がかかってしまいました。さらにデータを取り出した後に、カテゴリでなく個々の食材をそのまま使用する方がいいのではないかと考え、結局苦労して取り出したデータは使用しないことにしました。(大きなタイムロスでした・・・) このようなアプローチを続けていく中で、まずは以下のデータセットを試してみることにしました。 user_id: あるユーザー video_id: ユーザーが見たビデオ watch: ユーザーがその動画を見たら1、見ていないなら0 fav: ユーザーがその動画をお気に入りしていたら1、していないなら0 また全食材の列を作り、動画で使われている食材の列に1、使われていない食材の列に0 このうち「fav」と動画の食材を説明変数に、「watch」を目的変数にロジスティック回帰で学習させたところ、99%の正解率が出ました。 しかし考察したところ、 ・ユーザー特性が何も入っていないこのモデルになんの意味があるのか? ・そもそもお気に入りしてるかを最初に知れることはありえないんじゃないのか? となり、大幅な改良が必要であることが分かりました。 学習 これまでのことを踏まえ、以下のデータを特徴量とし、ユーザが動画を見るか推測するモデルを作りました。以下、学習はロジスティック回帰を使用し、グリッドサーチでハイパーパラメータチューニングを行い、k分割交差検証で評価(k = 10)で評価を行います。 ・ユーザのアプリインストール日(days)(動画を見た日から何日前かが入っている) ・ビデオのカロリー(calorie) ・ビデオの再生時間(duration) ・ビデオの料理を作るのにかかる時間(cooking_time) ・ビデオに使われている食材1つ1つ(食材が使用されていればその食材の列に1、使用されていなければ0が入る) テストデータに対して90.4%もの正解率が出たものの・・ 上のような混同行列になり、またF1スコアは 0.059 となりました。これはつまり新規データに対して約6%程度でしか動画を見るか判断できないというものです。混同行列を見たときに、真陰性が7810と、他の要素より飛び抜けています。このことから、今回はとりあえず動画を見てない方に分類してしまえばおおよそ正解してしまうために、正解率は高く出たのだと推察されました。偽陰性が806であることからも上記のことが言えるはずです。 色々悩んだ挙句、どの特徴量を用いればより良いモデルを作れるか検討するために、ランダムフォレストを用いて、今回のモデルの特徴量ごとの重要度を算出してみることにしました。結果は以下のようになりました。値が大きい方がより重要な特徴量であり、範囲は 0 ~ 1 です。 days:0.42, dulation:0.04, calorie:0.03, cooking_time:0.02・・・ これより、daysだけとても重要であることが分かりました。今回ユーザの情報として用いたのはこのdaysだけであり、より良いモデルを作るために、ユーザの特徴量を増やしていけばいいのだと考えました。 精度向上に向けて 次の3つのことを行いました。 ①特徴量ごとの分布を正規分布に近づける 学習率は下がってしまうけれども、今回のような予測では、癌予測のように癌である人を確実に当てなければいけないものというよりは、様々なデータに対して予測できる汎化性能の方が重要と考えたためです。正規分布に近づけるアプローチとして、それぞれの特徴量の箱ひげ図と基礎統計量を観察し、外れ値を除去するように、平均値と中央値が近づくように、データを切り取ることをしました。 ②データ数を増やす これまで使用したデータはベルヌーイサンプルによってランダム抽出を行なっていました。データ数を増やす事によって、さらに汎化性能が上がることが予想できます。 ③特徴量を増やす 先程までの特徴量に加え、以下の特徴量を追加しました。 ・ユーザがお気に入りしている動画の個数 ・動画を見たときにユーザがログインしていたか(していれば1、していなければ0とした) ・ユーザがプレミアム会員か(プレミアム会員ならば1、通常会員ならば0とした) この結果、 テストデータに対する正解率: 94.2% F1スコア: 0.76 となり、これまでで最も良い結果が得られました。 このモデルを利用した機能提案 ユーザがアプリを開いたとき、そのユーザーに合ったおすすめ動画を4つ表示させる ユーザがアプリを開いたときに、API通信をしてdatabaseにアクセスします。そのdatabaseには各ユーザに対して提案する4つの動画が含まれており、このdatabaseを作るのに先程までのモデルを使用します。以下イメージ図です。 databaseの作り方 各ユーザに対して任意の数の動画をモデルに入れます。そこであるユーザーに対して、見ると判断されたビデオが4つ以上あったら、ランダムに4つの動画をそのユーザーに対する推薦動画としてdatabaseに入れます。もし見ると判断されたビデオが4つ未満のときには、4つに満たない分だけビデオの視聴ランキングの上位から選択して追加し、そのユーザーに対する推薦動画としてdatabaseに入れます。このdatabaseは様々な条件を考慮しつつ適当な頻度で更新をかけます。 databaseに登録されていないユーザーに対しては、ビデオの視聴ランキング上位4つを推薦することとします。 4. データサイエンスチームの取り組みに参加した 以上までが、私個人が取り組ませていただいた課題になります。 この他に、データサイエンスチームとしての取り組みに、私も一部参加させていただきました。内容はsakuraさんの記事にうまくまとめられています。 tech.dely.jp tech.dely.jp 成果報告会を行なった インターンの成果発表会を1時間ほど行わせていただきました。開発部の他チームからも聴きに来てくださったり、多くの質問をしてくださったりなど、とても貴重な時間でした。 最初どういうバックグラウンドの人が聴きに来てくださるか分からなかったので、どこまで説明するかとても悩みました。また、結構緊張しましたが、終始柔らかいムードで聴いてくださったためとても話しやすかったです。 発表は、使い慣れてるPowerpointがパソコンに入っていなかったため、Jupyter NotebookのRISEを用いて行いました。 qiita.com 終わりに いかがでしたでしょうか? この1ヶ月でとても多くの経験をさせていただき、物凄く成長することができました。これにはデータサイエンスチームの方々が、私のどんな質問にも丁寧に答えてくださったり、多くの貴重なお話をしてくださったことに尽きると思います。リソースが限られている中で、私に多くのリソースを割いてくださり本当にありがとうございました。 急成長を続けているベンチャー企業のスピード感を肌で感じつつ、皆がプロダクトのために熱心に取り組んでいる環境で急成長を遂げたい方、是非インターンに挑戦してみてください。
アバター
はじめに こんにちは。データサイエンスチームのsakura ( @818uuu ) です。 クラシルの検索改善を担当しています。 データサイエンスチームでは今月 Presto勉強会 を毎日行っていました。 本記事ではその取り組みをご紹介しようと思います。 Prestoとは Prestoとは、Amazon Athenaで使用されている分散SQLエンジンのことを指します。 ※本勉強会ではPrestoで動く「SQL記法」を勉強しています。 概要 Presto勉強会の概要です。 [内容] Prestoの公式ドキュメントを読み知見を深める [目的] Prestoの知られざる機能などを学び、開発効率化に活かす [時間] 毎日ランチに行く前の10〜20分 [参加者] データサイエンスチームメンバー(3人〜4人) こんなかんじでディスプレイに映しながら話し合いました Presto勉強会を一言でいうと 「毎日ランチ前にチームでPrestoの公式ドキュメントを読む取り組み」です。 実施内容 この ページ の単元を一つずつ読んでいきました 上記のドキュメントに従い1日大体1章ごと進めていきました。 わからないところは実際にAthenaでクエリを書いて試しながら進めていきました。 json_size関数を試している図 (公式ドキュメントだけでは理解するのが難しかったため) 取り組んでみた感想 複数人で公式ドキュメントを読む機会はなかなかないので貴重な経験となりました。 一番驚いたことは、ドキュメントの読み取り一つでも自分とメンバー間に違いがあったことです。 人によって読み取り方が様々で、 「この一行からそこまで読み取ることが出来るんだ」 「このメンバーだとそういった応用例まで考えているのか」 と技術ドキュメントの読み取り方の勉強としても参考になりました。 また、いくつか今後業務で活かせそうな関数も発見することができました。 今後活かせそうな関数などをメモしています もちろん、 bool_and() や to_iso8601(x) などこれいつ使うんだと思った関数もたくさんありました笑 一通り全ての関数に目を学んだことで「Prestoで出来ること/出来ないこと」を把握できたのがよかったと思います。 おわりに Presto勉強会で公式ドキュメントを一通り読んだことで、チーム全体でPrestoへの理解が深まりました。 この勉強会で得た知見を業務に活用していきたいと思います。 データサイエンスチームでは、Presto勉強会や サーベイチャレンジ など様々な取り組みを行っています。もしご興味があればご応募・ご連絡ください: ) www.wantedly.com
アバター
はじめに こんにちは。データサイエンスチームのsakura ( @818uuu ) です。クラシルの検索改善を担当しています。 データサイエンスチームでは今年の3月から サーベイチャレンジ という取り組みを行っています。 本記事ではその取り組みをご紹介しようと思います。 概要 サーベイチャレンジの概要です。 [内容] 論文を読み、データサイエンスチーム内で共有する [目的] 料理に関する様々な研究を知る・様々な分野の最先端技術を知る [作業時間] 基本的に業務時間内に実施 [共有時間] 週1回のチームMTG内で共有及び議論 [参加者] データサイエンスチームメンバー(2人〜5人) サーベイチャレンジを一言でいうと 「みんなで論文を読み、その知見を共有する取り組み」です。 進め方 サーベイチャレンジの進め方を紹介します。 各々が読む論文を mendeley に格納し、チーム内で共有 論文を読んだ感想をテンプレートに従って記載。(Githubのissueで管理) 論文を読んだ感想のテンプレート 3.週1回のチームMTGでそれぞれがその週読んだ論文について議論し合う 当初はGithubのみで管理していましたが、論文の管理が少し面倒でした。 そこで文献管理ツールの mendeley を導入し、簡単に一括管理ができるようにしました。便利です。 実施内容 どんな内容の論文を読んでいるか少しご紹介します。 実際に読んだ論文1 実際に読んだ論文2 今までに読んだ論文の一部 今までに30本以上の論文を議論しています。 取り組みに抱く感想 サーベイチャレンジを始めたことで論文を読む習慣が出来ました。 一人だとすぐ飽きてしまいそうですがチームで共有し議論することで一定のペースを保てて継続出来ているのが良いことだと思います。 また、実際にやってみてサービス開発に活かせるような知見をたくさん発見できたのが驚きでした。論文というと少し学術的な方面によっているのかな・・と思っていたのですがめっっちゃ役に立つ情報が眠っています。 最後に サーベイチャレンジをすることで継続的に最先端の技術を知ることが出来たり、様々な研究内容をチーム内で共有し議論することができています。 今後も続けていき新たな発見をしていければと考えています。
アバター
こんにちは!クラシルiOSアプリを開発しているknchstです。 6月のWWDC19はSwiftUIなどのサプライズもあり、とても盛り上がりましたね!様々なセッションがあったのですが、個人的にいいなと思ったのがXcode11のデバッグ機能についてです。 この記事では以下の項目について紹介します。 Device Conditions Environment Overrides Debugging SwiftUI View Hierarchies Device Conditions https://developer.apple.com/videos/play/wwdc2019/412/ Thermal state condition Xcode11から新たに端末の発熱をシミュレートする機能が実装されました。 これにより、端末を実際に発熱させることなく温度状態によるアプリの動作を確認することができるようになります。 Xcode11のメニューの Window → Devices and Simulators 内に DEVICE CONDITIONS という項目が追加されていて、ここで設定することができます。 設定できる項目 Fair(わずかに高い状態、バックグラウンドフェッチなどが延期される) Serious(高い状態、CPUやGPS、Bluetoothなどの使用量が削減される) Critical(かなり高い状態、あらゆるリソースが最小限になる) 上記の3つをシミュレートすることができます。 Network link condition 通信状態をシミュレートする機能はiOS端末単体ではありましたが、今後はXcodeから直接端末の通信状況をシミュレートすることもできるようになります。 こちらも Thermal State と同様でXcode11のメニューの Window → Devices and Simulators 内に DEVICE CONDITIONS という項目が追加されていて、以下画像赤枠内で設定することができます。 デフォルトで設定できるプロファイルも 100% packet loss Very poor network Edge Network - poor Edge Network - average Edge Network - good Edge Network - best 2G Network - poor 2G Network - better 3G Network - average 3G Network - good 3G Network - bet LTE Network WiFi Network WiFi Network (802.11ac) DSL Network High Latency DNS と増えて使いやすくなりました。 Environment Overrides https://developer.apple.com/videos/play/wwdc2019/412/ Appleのプラットフォームには表示に関する設定や様々なアクセシビリティがあり、ユーザーが様々な設定を行うことができます。例えば、 ライトモード & ダークモード ダイナミックタイプ アクセシビリティ などの項目があります。 これらの設定が変更された時に、アプリケーションのレイアウトが崩れないかを確認する必要があります。これらをデバッグする為に Environment Overrides という機能が新たに追加されました。 WWDCで行われたデモが以下になります。 Xcodeのデバッグ領域に Environment Overrides というボタンが追加されていて、ここから値を変更することができます。変更はリアルタイムに反映されます。また、シミュレーター・実機でも同様に動作します。 Debugging SwiftUI View Hierarchies SwiftUIが新たに加わり View Hierarchies のデバッガーも大きくアップデートされました。 Swift Reflection SwiftUIでは、ビューにあるプロパティが自動でインスペクトされデバッガーに表示されるようになります。実際にデバッガで確認してみると、インスペクタにProfileViewのプロパティの情報が表示されています。 CustomReflectable 新たに追加された CustomReflectable プロトコルに準拠することによって、独自のプロパティをインスペクタに表示することができます。 SwiftUIとその他のフレームワークのView Hierarchies SwiftUIのプロジェクトでは既存のUIKitで提供されている UIViewController などのクラスが利用できます。 上の画像のオレンジ色の枠は UIKit で実装されてもので、青色の枠は SwiftUI で実装されたビューになります。 まとめ SwiftUIの登場によってデバッグもリアルタイム性がでてきて、より効率的な開発ができるようになりました。またDevice ConditionsやEnvironment Overrideなどのハードの機能をシミュレートできる機能を活用することによって再現しにくいランタイムエラーをデバッグすることができるので、リリース後の予期せぬ不具合も未然に防ぐことができます。 この記事で全てを紹介できていないので、詳しく知りたい方は以下をご覧ください。 WWDC Session 412 Xcode Release Notes
アバター
はじめに こんにちは、delyでサーバサイドエンジニアをやっている山野井といいます。 kurashiruではサーバーサイドにRailsを使用しておりテストはRspecで書かれています。 CIはgithubリポジトリへのpushをフックしてAWS CodeBuild上でテストを走らせています。 またCI上のテストは parallel_tests gem を利用した並列化を行っていて、8プロセスで動いています。 弊社ではプロダクトの品質を保つ為、CIに通らないとデプロイできないルールを設けていまして、CIが完了するまでに時間がかかるとその分デプロイまでの時間もかかってしまうので1分でも早めたい気持ちがあります。 今回はアプリケーションコードには手を加えず、AWS CodeBuild上のCIの実行時間を少しづつ改善している話をしたいと思います。 実践 まずはCIの実行時間を改善する前にどこに時間がかかっているのかを把握する必要があります。 AWS CodeBuildには以下の11項目からなるphasesという概念があります。 SUBMITTED QUEUED PROVISIONING DOWNLOAD_SOURCE INSTALL PRE_BUILD BUILD POST_BUILD UPLOAD_ARTIFACTS FINALIZING COMPLETED CodeBuildが実行されると各phaseが順番に実行されるようになっています。 この中で INSTALL , PRE_BUILD , BUILD , POST_BUILD のphaseはプロジェクトのルートに置いた buildspec.yml に独自に処理を書くことができます。 以下がその例です。 version: 0.2 phases: install: commands: - service mysqld start pre_build: commands: - bundle install - bundle exec rake parallel:create - bundle exec rake parallel:migrate build: commands: - bundle exec parallel_rspec spec AWS CodeBuildでは以下の画像のようにAWSのコンソールから各phaseどれだけの時間がかかっているのかを見ることができるので、それを見ながら改善策を考えていきます。 弊社では PRE_BUILD フェーズで bundle install やdbの作成、migration処理を行っているのですが、ここに10分ほど時間がかかっていました。 よく見ると処理時間のほとんどがmigrationにかかっていることがわかりました。 bundle exec rake parallel:migrate コマンドはcpuのコア数分プロセスを立ち上げコマンドを実行するため、migration処理が8プロセスで実行されていました。 これを1つのDBのみmigrationを行い、migration後のダンプを取得し残りのデータベースに流し込む処理に変えることで10分ほど短縮することができました。 pre_build: commands: - bundle exec rake parallel:create - bundle exec rake parallel:migrate[1] - mysqldump -u root -prootpassword test > dump.sql - bundle exec parallel_test -e 'mysql -usomeuser -psomepassword test$TEST_ENV_NUMBER < dump.sql' Before After またテーブルに変更が無い限りは前回のDBの状態を用いても問題ないため、db/migrateのgitのhash値からキャッシュキーを生成し、ダンプそのものをキャッシュすることで 初回のmigration処理自体をスキップすることもできます。 pre_build: commands: - git log --pretty=format:'%H' -n 1 -- db/migrate > ~/mysql-dump-checksum - mkdir -p ./.mysql-dump - | if [ -e "./.mysql-dump/$(cat ~/mysql-dump-checksum).sql" ]; then bundle exec parallel_test -e 'mysql -usomeuser -psomepassword test$TEST_ENV_NUMBER < "./.mysql-dump/$(cat ~/mysql-dump-checksum).sql"' fi 次に BUILD フェーズですが BUILD フェーズでは主にRspecの実行を行っています。 今回はアプリケーションコードに手を加えない方法で高速化することを考えていたので特に変更は加えていません。 Rspec自体の速度を改善するには、一般的に遅いテストの洗い出しや、無駄なレコードの作成を行わないこと、無駄な通信を行わないことが挙げられます。 kurashiruでもここの最適化はさほど行っておらず、改善の余地があるので適宜改善していきたいと思っています。 最後に POST_BUILD フェーズですが、ここも5, 6分ほど時間がかかっていました。 AWSのコンソールからビルドログをよく見るとCodeBuildのキャッシュ機能によるs3へのアップロードに時間がかかっていました。 まだ設定の変更を完全に取り込むことはできていないのですが、CodeBuildのキャッシュタイプをs3から最近使用できるようになったローカルキャッシュに変更することで速度改善を試みています。 CodeBuildのローカルキャッシュは今年から利用できるようになった機能で、今まではキャッシュした結果をs3へアップロードしていたものをビルドホスト内に保持することができる機能です。 aws.amazon.com この機能を有効にすることでs3への通信コストがかからなくなるため、ここにかかる時間を0にすることが確認できています。 最後に 1つ1つが地味な改善ですが、デプロイまでの時間を短縮でき、従量課金にかかる費用も節約できるので今後も時間を見つけて改善していきたいと思います。 最後までお付き合いありがとうございました。
アバター
こんにちは。delyでAndroidエンジニアをしているkenzoです。 今回はAndroidの話ではなく、担当している別のプロジェクトの管理をいい感じにしようと思っていろいろやったけど、そんなにうまくいかなかった。という感じの内容です。 どんな話 「他部署の依頼でコンテンツを作成するプロジェクト」の管理を自動化した結果、うまくいったこと、失敗したことについてのお話です。 なんで自動化したの 今回のお話の背景 弊社では開発部が他の部署から依頼を受けてLP等のWebページを作成しています。 私は他の部署と開発部内の実装者とのやりとりの間に入って管理する取りまとめのようなことをしてきました。 やっていたことをざっくり言うと、依頼を受け、実装者に依頼して、配信の準備をする。という簡単なお仕事で、元々はスプレッドシートで管理をしていました。 発生した問題 新たに高い頻度でコンテンツを作る施策の開始や、開発部側で業務委託の方に実装をお任せするようになる等、次第にやりとりや確認・管理しなければいけないことが増えていきました。 そのため、業務の中でこれらのために使うリソースが増えてきたり、抜け漏れが発生しそうな気配を感じるようになったりと、既存のスプレッドシートを手作業で確認・更新するのにも限界を感じてきました。 どんな自動化したの GASを使って下記の処理が定期的に実行されるようにしました。 コンテンツの内容等が管理されているスプレッドシートから実装に関するものを抽出 githubのプロジェクトに1をtodoとしてissueを追加 関係者に共有しているカレンダーに1を追加 開発側や業務委託の方と共有しているスプレッドシートに1を追加 素材・成果物が揃ったり、配信が迫ったタイミングでslackに通知 1の変更に合わせて2, 3, 4の内容を更新 その他もろもろ、、、 これらの組み合わせで、今回のプロジェクトにおける業務フローのある程度の部分を自動化することができました。 うまくいったこと 作業削減 issueやカレンダーへの追加や更新を手動で行うという、そこそこ手間のかかる作業(定期的にスプレッドシートを確認して内容を様々な場所に追加・更新)をなくすことができました。 抜け漏れ防止 依頼が発生したり、配信日が近くなったら関係者のslackチャンネルにbotで通知を送りました。 抜け漏れを防ぐことができそうで、日々の心配事が減りました。 うまくいきそうだったこと やりとり減らせそう 必要なタイミングで必要な人に通知を送ることで、やりとりが減らせそうな気がしていました。 うまくいかなかったこと 想定以上の戻り作業の発生 今回行った業務フローの自動化ではそれぞれの工程が完遂されているかのチェックが抜けていたため、次の工程に進んでからの戻りが度々発生しました。 その結果、その都度手動でのやりとりを行うことになってしまいました。 業務委託先の方との信頼を築けなかった? あまりやりとりをしていないまま、業務委託先の方とのやりとりもbotでの運用に変更してしまったために信頼を築けなかったかもしれません。 そのためか、締切が近くなっても連絡がつかず、紹介してくれた別の社員に連絡をお願いすることになってしまい、コンテンツが配信ぎりぎりの完成となってしまったこともありました。 こうしたらよかったのかも 「想定以上の戻り作業の発生」に対して 戻りを完璧に防ぐのは不可能だし、いろいろな手立てを講じる工数もあまりありませんでしたが、頻繁に発生していたシンプルなミス(素材の不足や工程忘れ)についてくらいはチェックする仕組みを入れておく必要があったように思います。 また、手順をもっと明確にして初めに伝えておくことや、困った時に参照できるものを作っておくとかでも防げた可能性はあります。 他の業務でも同じことが言えそうですが、必要な内容を伝える・参照しやすくしておくことは大事ですね。 「業務委託先の方との信頼を築けなかった?」に対して もう少しやりとりをしたり、直接会う等して信頼関係が築けてからslack botの運用を始めるべきだったと感じました。 または、slack botでの通知はあくまで自分へのリマインドにとどめ、やりとりは自分で行う。といった方法を検討してもよかったのかもしれません。 信頼関係のないままslack botだけで完結させようとしてしまったため、なにかあった場合に気軽に言ってもらえるような心理的安全性を高められなかったのではと思います。 このような信頼関係は人と関わる業務全てにおいて大切なことだと思いますが、今回は効率を求めて自動化を進める上でおろそかにしてしまったのが反省点です。 まとめ 自動化によって便利になることはいっぱいあるので、これからもどんどんやっていこうと思います。 ただ、自動化するのが難しい部分も多々あると思います。もちろんそこは手作業等で補う必要があるのですが、ともすると、省いたかたちで自動化してしまうこともあるかもしれません。 そうすると、せっかく自動化したのに後々工数を取られたり、ミスに気付かないまま進んでしまうということも考えられます。 特に、今回私がミスった信頼関係における部分、人間的なコミュニケーションを省いてしまうと、理論的にはうまくいきそうに思えても、どこかで綻びが生まれて問題が起きることがありそうです。 業務を効率化する時でも、あえて人の手を介した方がうまくいくこともあると思うので、その辺りを頭に入れつつ今回の反省を活かし、これからも自動化を進めていきたいと思います。
アバター
こんにちは。開発部のsakura( @818uuu )です。 2019年4月23日に開催された Search Engineering Tech Talk 2019 Spring に登壇させていただきました。 会場は南青山にあるNAVITIMEさんで行われました。 NAVITIMEさんにははじめて行ったのですが、すごくきれいな会場で発表しやすい環境でもあったのでとても助かりました。 ありがとうございました! NAVITIMEの登壇会場、1階にありしかも人通りの多い通路に面していてなんとガラス張りである。  #searchtechjp — sakura (@818uuu) 2019年4月23日 あとお茶もらいました。ありがとうございます。 ナビタイム茶いただいた #searchtechjp pic.twitter.com/3esXjYZjBC — sakura (@818uuu) 2019年4月23日 登壇は久々だったのですごく緊張していました。 25分枠だったのでタイムスケジュールのことがものすごく不安でした。 登壇久しぶりで少し緊張してるでもここまできたらやるしかないう〜〜〜  #searchtechjp — sakura (@818uuu) 2019年4月23日 登壇は3名いて私は3番目の発表予定でした。 一人目はNAVITIMEさん、二人目はFessを作ってる方の発表でした。 どちらのサービスもばりばり使ったことあって好きな検索サービスだったので、そんな検索の中の人と一緒の舞台にたてるのが嬉しかったです。 お二人の発表資料はこちらになります。 「安心な移動」のためのPOI検索 from NavitimeJapan www.slideshare.net 社内ドキュメント検索システム構築のノウハウ from Shinsuke Sugaya www.slideshare.net お二人の発表が終わり緊張は頂点でした。↓登壇直前のツイート ひいいいいいいこわいいがんばろう、せっかくの機会もらえたんだしがんばろうでもひいこわいいい  #searchtechjp — sakura (@818uuu) 2019年4月23日 登壇はなんとか無事終えることができました。(よかった……) すごく話しやすい場を聞いてくださる方が作ってくださったので話しやすかった&楽しかったです。 本当にありがとうございました。 登壇資料はこちらになります。 登壇を終えてハッシュタグを拝見させていただいたのですが色々な感想をつぶやいてくださり本当にありがとうございます。 #searchtechjp - Twitter Search 懇親会でもたくさん検索のお話ができてよかったです: ) すごくすごく貴重な経験をさせていただきました。ありがとうございました!
アバター
こんにちは!dely でフロントエンドの開発をしています @all__user です。 今回は kurashiru のフロントエンド開発に導入されたビジュアルリグレッションテストについてご紹介したいと思います。 【反応を多くいただいた点について記事の最後に追記しました】 目次 目次 ビジュアルリグレッションテストとは 導入の背景 フロントエンドのテスト? SPA移行前後の比較 ツール reg-suit Loki Wraith BackstopJS テストのフロー GitHub + CodeBuild + BackstopJS ステージング環境 テストケースは Google スプレッドシートで管理 結果を S3 にアップロードして Slack に通知 まとめ 【追記】 運用が大変ではないか? 1pxの違いにそこまで工数かける? 広告が差し込まれたり変わっただけでテストが壊れるのでは? ビジュアルリグレッションテストとは ある変更を加える前後でスクリーンショットを作成し、それらを比較することで意図しない挙動が無いかを検証するテスト手法です。 最終的に描画されたピクセルに少しでも変化があれば検出することができるため、技術スタックを選ばず包括的にテストできます。 検出できるものは限られていますが、スタイルのチェックはもちろん、機能や動作を検証するためのテストとしても非常に優れています。 検出できるもの⭕ お気に入りボタンが表示されない 2ページ目以降がエラーページになる 画像が荒い 検出できないもの❌(検証に向かないもの) お気に入りボタンがクリックできない 2ページ目以降へ遷移できない 画像のalt属性が設定されていない 現在の kurashiru のように Rails のテンプレートと Vue を併用しているような状況でも、全体をカバーできます。技術スタックの移行フェーズでは特に効果を発揮すると思います。 テストはどこにどれだけのコストを割くかというバランスが非常に難しいと感じます。 まず最初に導入するテストとしてビジュアルリグレッションテストはとてもおすすめです。 導入の背景 kurashiru は Rails アプリケーションとして Sprockets + Slim + jQuery + CoffeeScript + SCSS で開発されてきました。 ログイン機能の開発をきっかけに部分的に SPA (Single Page Application) を導入し、現在は Webpacker + Vue + TypeScript + SCSS での開発へと移行中です。 tech.dely.jp そのようにして SPA 化を進めていく中で、機能開発が優先され、なかなかテストに手を付けられないという状況が続きました。 すでにある Capybara + RSpec によるテストケースだけでは、SPA 部分の開発が既存部分へ与える影響や共通モジュールを変更する際の影響の検知を担保できず、テストケースを増やして対応しようとすると、非常にコストがかかるだろうと考えられました。 なんとかしなければと思いつつ開発は続き、規模が少しづつ大きくなるにつれ、手動テストでの動作検証コストが無視できない大きさになってきました。 そして、今まさにテストが必要というところまで来ていると判断しビジュアルリグレッションテストを導入することにしました。 フロントエンドのテスト? フロントエンドのテストは難しいです。 一口にテストと言っても何を検査し担保したいのかによってテスト手法もまちまちです。 ロジック 操作 要素、テキスト スタイル ブラウザ間の差異 HTMLの構造や見た目に依存したテストは、正しくテストを書く難易度も高く、しかも変更により壊れてしまう可能性も高いため、得られるメリットがコストに見合わないと感じることもあります。 SPA移行前後の比較 今回一番達成したかった目的は、SPA移行前後のコードに対する検証です。 何かしらのテストの必要は感じていましたが、移行前後のコード全てに対してテストを追加するコストはかけられません。 ビジュアルリグレッションテストは 期待される動作の定義と検証 を簡単に行うことができます。 期待される動作の定義はスナップショットを撮るだけです。 現在の状態を期待される動作として定義し、移行後の状態と比較することで、移行前後のコードの挙動に変化がない(またはある)ことを確認できます。 ツール 調べてみたところ様々なツールがありました。 PhantomJS は開発が終了しているため、それをベースにしたツールは今回選択肢から外すことにしました。 また、マルチブラウザのテストを考慮すると Selenium ベースが望ましかったのですが、学習コストやフロントエンドエコシステムとの相性の観点から Node.js, Puppeteer を採用している BackstopJS を採用することにしました。 reg-suit スナップショットの比較とレポートの作成に特化したツールです。 スナップショットの生成をどのように行うかは自由なため、Headless Chrome を使ったり Selenium ベースのツールを使うなど柔軟に対応できそうです。 CIにも組み込みやすそうです。 github.com Loki Storybook に特化したツールです。 スナップショットの生成からレポートの作成、結果の承認まで全部入りのツールです。 ページ全体のスナップショットを利用するテストとは違い、コンポーネントの単体テストという位置づけです。 kurashiru でも最近 Storybook が導入されたのでいずれ試してみたいです。 github.com Wraith Ruby ベースのツールで、GitHubのスターも一番多いようです。 スナップショットの生成からレポートの作成、結果の承認まで全部入りのツールです。 メインは PhantomJS ベースのようですが Chrome にも対応しているようです。 BBC News が作っています。 github.com BackstopJS スナップショットの生成からレポートの作成、結果の承認まで全部入りのツールです。 今回はこのツールを採用しました。 Node.js 製でメインのブラウザに Puppeteer を採用しています。 Puppeteer の API をそのまま利用できるので、テストケースを async / await で書くことができます。 hideSelector や removeSelector で特定の要素を非表示にしたり取り除いたりすることができたりと、いい感じのパラメータが多く用意されています。 github.com テストのフロー GitHub + CodeBuild + BackstopJS CodeBuild の GitHub 連携の機能を利用し、特定のルールにマッチするブランチ名が push された時にテストが走るように設定しました。 CodeBuild ではテストに使用する Docker イメージを指定することができるのですが、 BackstopJS が提供している Docker イメージ があり、これを利用しています。 これで CI 環境 で Puppeteer を動かすための手間はかかりません。 一つだけ注意が必要なのは、そのままだと日本語フォントが入っていないため、フォントの入ったイメージを作るか、install フェイズなどでフォントを入れる必要があります。 version : 0.2 phases : install : commands : - apt-get update -y - apt-get install -y apt-transport-https - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list - apt-get install -y yarn - apt-get install -y fonts-ipafont-gothic fonts-ipafont-mincho # 日本語フォントをインストール - apt-get install -y python-dev - curl "https://bootstrap.pypa.io/get-pip.py" | python - pip install awscli pre_build : commands : - REGRESSION_TEST_BRANCH_NAME=$(git branch -a --contains $CODEBUILD_SOURCE_VERSION) - mkdir -p ./.yarn-cache - yarn install --cache-folder ./.yarn-cache build : commands : - yarn reg:testcase-gen # スプレッドシートから backstop.json を生成 - backstop reference # リファレンスのスナップショット - backstop test # テストのスナップショット post_build : commands : - aws s3 cp --recursive backstop_data/html_report s3://xxxxxxxxxxxxxxxx/$CODEBUILD_BUILD_ID/html_report/ - aws s3 cp --recursive backstop_data/bitmaps_reference s3://xxxxxxxxxxxxxxxx/$CODEBUILD_BUILD_ID/bitmaps_reference/ - aws s3 cp --recursive backstop_data/bitmaps_test s3://xxxxxxxxxxxxxxxx/$CODEBUILD_BUILD_ID/bitmaps_test/ - yarn reg:slack # Slackに通知 cache : paths : - '.yarn-cache' - 'node_modules/**/*' ステージング環境 Reference と Test には kurashiru の開発で利用しているステージング環境を使用しています。 開発者が任意のブランチをデプロイできるようになっています。 データベースの内容によってスクリーンショットに差が出ないように、ビジュアルリグレッションテスト用のインスタンスは同じデータベースを参照するようにしています。 テストケースは Google スプレッドシートで管理 BackstopJS のテストケースは backstop.json というファイルの scenarios プロパティで設定します。 { " scenarios ": [ { " label ": " recipes_show ", " onBeforeScript ": " src/on_before.js ", " url ": " https://example.com/recipes/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ", " referenceUrl ": " https://example.com/recipes/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ", " delay ": 3000 , " removeSelectors ": [ " .adsbygoogle ", " .google-ads " ] , " onReadyScript ": " src/on_ready.js ", " selectorExpansion ": true , " misMatchThreshold ": 0.1 , " requireSameDimensions ": true } ] } もちろんこのまま利用してもよいのですが、 backstop.json が巨大になるとメンテナンスが大変そうなので、Google スプレッドシートでテストケースを管理することにしました。 一行が一つのテストケースに対応しており、上記の JSON の各種パラメータを設定できるようにしています。 また、パタメータとは別に自由に設定できるタグ用のカラムを追加しました。 このタグを利用して、特定のテストケースのみを実行したり、どの画面か、管理サイトかなどで色分けできます。 テスト開始前にこのスプレッドシートのデータを取得し backstop.json を生成します。 各テストケースが見やすく編集もしやすくてとても便利なのですが、テストケースが Git の管理下ではなくなるというデメリットもあります。 結果を S3 にアップロードして Slack に通知 BackstopJS はテスト結果を HTML + アセット群というかたちで生成します。 このようにスクリーンショットの比較結果をとても見やすく表示してくれます。 この HTML + アセット群を静的サイト向けにセットアップした S3 にアップロードし、Slack に URL を通知します。 まとめ SPAへの移行では実際に多くの意図しない挙動を1px単位で見つけることができました。 このタイミングで導入できてとても良かったです。 ビジュアルリグレッションテストは他のテストを代替するようなものではないですが、技術スタックに依存しないことと、テストケースが壊れにくいというところが大きな利点かと思います。 ビジュアルリグレッションテストを導入して快適なフロントエンドライフを送りましょう! 【追記】 多くの方に読んでいただきとても嬉しいです🙌反応をいただいた点について追記しました! 運用が大変ではないか? 現状、リグレッションテストに関しては、ユニットテストやE2Eテストのように、コミット度にテストを回して、テストケースが全てパスしないとマージできない、という運用はしていません。 雰囲気としては、 「今回だいぶ変更箇所多くなったなー、一応リグレッションテスト見ておくか」 「うわ、差分めっちゃ出てる、こりゃよく分からんね...」 「そうすね、今回はざっと見て問題なさそうだったら、新しいほうをリファレンスにしちゃって下さい」 「了解です!大丈夫そうです!」 「マージ!」 こんな感じです。 テスト実行の有無は都度判断し、もやは有益でない差分については無視しても構わないくらいの運用です。 通常のテストとは違いこのくらいの運用でも十分効果があります。 1pxの違いにそこまで工数かける? 上記のような運用方法が前提であれば、そもそもこの点は解消するかもしれません。 一つの例として1pxの線が挙げられると思います。 デザイン上1pxの線が使われている部分はkurashiruにも多くあります。 1pxのズレで大きなデザイン崩れになることは無いかもしれませんが、1pxの線でも、その線が消えてしまうと大きく意味が変わってしまう、ということはよくあると思います。 その線が誤って消えてしまったりすると、 「すいません!ここの線が消えちゃってました!」 「あっ!すいませんすぐ修正しますー(たぶんあの変更だ...)」 のように、そのページを見て気づいた人からの報告ベースでの修正ということはよくあります。(それ自体は良いと思います🙌) 要は、開発者や関係者が手動で目grepで確かめないといけなかった部分を、ある程度置きかえられるので、ページ数にもよりますが、費用対効果としては悪くないと思います。 広告が差し込まれたり変わっただけでテストが壊れるのでは? これはその通りなのですが、BackstopJSの機能で対応可能です。 テストケースごとに特定のセレクタにマッチした要素を visibility: hidden にしたり、 display: none することができます。 下のリンク先にある hideSelectors 、 removeSelectors というオプションです。 github.com 他にも色々な機能があるので、ある程度のケースには対応できると思います。
アバター
こんにちは。delyでフロントエンドを担当している @all__user です。 今回はkurashiruでSSR(Server Side Rendering)を導入した事例についてご紹介したいと思います。 目次 目次 要約 経緯・背景 SSRの導入 SPAのSEO対策 SSRのコスト Rendertronの採用は見送り kurashiruのSSR構成 ルートメタフィールドを利用したデータ取得の仕組み 消極的SSRから積極的SSRへ まとめ 要約 SPAにしたい。SEOのことを考えるとSSRはしておきたい でも全然リソース足りない Railsを温存しつつスモールスタートでSSRできるようにした という内容です。 経緯・背景 kurashiruはもともとRails単体のアプリケーションでしたが、フロントエンドにVue.jsを採用し、現在では多くのページがSPAへと移行しました。 SPAではSEOの観点からSSRが必要であるとよく言われます。 kurashiruでも調査・議論を重ねた結果、SSRを導入することになりました。 SSRの導入 フロントエンドをSPA(Single Page Application)に置き換えていくタイミングでSSRを導入し、結果としては懸念していたSEOへの悪影響も無く 1 、無事SPAへと移行することができました。 tech.dely.jp SPAをやるにあたってとにかく一番頭をもたげたのはSSRどうするか問題でした。 SPAのSEO対策 正直に申しますと、個人的には「SSRはあったほうがいいけど、必須ではない」という主張の持ち主でした。 その内容は概ね以下のようなものです。 fetch as googleでレンダリングされていれば問題ない 初期レンダリングコストの削減が目的なら導入・運用コストに見合わない しかし、いくら調べてみても議論を重ねてみても、SEOに悪影響が無いとは言い切れませんでした。 kurashiruにとってSEOが悪化することはわずかな可能性でも避けたいことでしたが、それと同時に不要なものを導入して複雑化させるのも避けたいことでした。 そんな中、 Google I/O '18 の中でUser-Agentによってクローラーを識別し、クローラーに対してのみSSRした結果を返すダイナミックレンダリングという手法が紹介されていました。 www.youtube.com www.suzukikenichi.com このダイナミックレンダリングの説明の中で、GooglebotによるJavaScriptレンダリングは、現時点で完全ではないことが明示されています。 また、ダイナミックレンダリングがクローキングには当たらないことを示す内容でもありました。 developers.google.com GooglebotがSPAを完全にレンダリングできない可能性がある以上、SPAへの移行はSSRを前提に考える必要がありました。 SSRのコスト 一般的なSSRの構成は、ユーザーからのリクエストをSSRサーバーで受け、APIサーバーに必要なデータを問い合わせ、ページをレンダリングし、その結果を返すというものです。 この構成を採用する場合、以下のような懸念点がありました。 metaタグの仕組み(title、description、OGPなど)をRailsからSSRサーバーに移行する必要がある 全ユーザーを対象とする場合はスケールやキャッシュの仕組みなどをNode.jsサーバー用に構成し直す必要がある 非SPAのページと共存する場合、ALBなどの前段で振り分ける必要がある Node.jsサーバーの面倒は誰が見るの問題 kurashiruは単体のRailsアプリケーションとしてすでに十分大きかったため、SSRサーバーにフロントの機能を移すだけで大きなコストがかかります。 社内にNode.jsやSSRの経験が豊富にあれば、この構成を採用する可能性もありましたが、私自身にNode.jsの実戦経験は無かったですし、他のメンバーの技術スタックやリソースを考えても、この構成への移行コストは高く、その時点では採用するメリットはありませんでした。 仮にリソースを割いて移行できたとしても、「もしダメでもやめればいい」という判断がしにくくなる可能性がありました。 Rendertronの採用は見送り ダイナミックレンダリングの手法として紹介されているうちのひとつが、Rendertronを使った方法です。 github.com 話し合いの中で以下の懸念点が出てきました。 キャッシュ効率が高いことが前提となっている クローラーが同じURLを再訪したときしかキャッシュヒットしない 同じURLを再訪する頻度が少なそう だとすると、マシンパワーが必要なためレスポンスタイムが心配 お金がかかりそう あくまでクローラーに対してのレンダリング用で、ユーザーにも転用する未来が見えにくい これらの懸念からRendertronの採用を見送りました。 kurashiruのSSR構成 大枠はRendertronによるダイナミックレンダリングの構成と同じです。 この構成にすることで、既存のRails資産を流用しつつ、部分的にSSRを導入できます。 Webサーバーがリクエストを受けると、User-Agentからクローラーかどうかを判別し、クローラーの場合はSSRに必要な情報(リクエストパスなど)をJSONにまとめてSSRサーバー(Node.js)にPOSTします。 SSRサーバーがPOSTリクエストを受け取ると、ページのレンダリングに必要なデータをAPIから取得します。 SSRサーバーでデータの取得が完了すると、 vue-server-renderer を使用してクライアントサイドと同じコードを実行し、body要素直下にマウントされるルートコンポーネントをレンダリングします。 SSRサーバーでレンダリングが完了すると、レンダリング済みのHTML文字列をJSONにまとめ、Webサーバーに返します。 Webサーバーがレンダリング済みのHTMLを受け取ると、body直下にそのHTMLを埋め込み、完成したHTMLをブラウザに返します。 ルートメタフィールドを利用したデータ取得の仕組み クライアントサイドと同じように、レンダリングの過程でAPIサーバーから必要なデータをフェッチしますが、クライアントサイドと異なる点として、レンダリングを1ライフサイクル(ルートコンポーネントのbeforeRouteEnter, beforeCreate, created)内で同期的に行う必要があります。 そのため、レンダリングを始める前にすべてのデータのフェッチが完了している必要があります。 この問題を解決するために、例えばNuxt.jsでは asyncData というメソッドを使用することで、非同期のデータ取得をあらかじめ完了させておくことができます。 ja.nuxtjs.org kurashiruではNuxt.jsを導入していなかったため、同様の仕組みを用意する必要がありました。 コンポーネントのメソッドを利用する代わりに、 ルートメタフィールド を利用しています。 router.vuejs.org 簡略化したコードを以下に示します。 // ComponentA.vue export const generateSsrFetcher = ( { app } : { app: Vue } ) => { return { fetchEndpointA () { return app.$api.fetchEndPointA ( { id: app.$route.params. id } ); } , } ; } ; export default Vue.extend ( { /* ... */ } ); // routes.ts const routes = [ { path: '/component_a/:id' , name: 'component_a' , component: () => import( './path/to/ComponentA.vue' ), meta: { async getSsrMetadata () { const { generateSsrFetcher } = await import( './path/to/ComponentA.vue' ); return { generateSsrFetcher } ; } , } , } , ] ; // renderApp.ts async () => { // ... const app = createApp (); app.$router.push ( { path: '/component_a/123' } ); const metadata = await app.$route.meta.getSsrMetadata (); const ssrFetcher = metadata.generateSsrFetcher ( { app } ); const data = await ssrFetcher.fetchEndPointA (); // ... } Nuxt.jsのasyncDataでは this によるVueインスタンスの参照ができないという制約があります。 レンダリング前に実行されるメソッドということを考えると自然なことですが、 this.$route.params を利用できないなどの不便な点もあります。 これらを緩和するためにAPIからデータを取得する際、ルートインスタンスを参照できるようにしています。 2 消極的SSRから積極的SSRへ SSRの導入に関しては、SEO対策だけでなく、初期レンダリングコスト削減による体感速度の向上など、UX改善という側面もあります。 kurashiruではまだこのようなモチベーションでSSRに取り組めていないので、消極的SSRと呼んでいたりします。 Node.jsを運用する知見が少しづつ溜まってきたこともあり、全てのリクエストに対してSSRするなど、より積極的な導入も考えています。 このあたりは今後の課題です。 まとめ SPAへの移行を機にSSRを導入した事例についてご紹介しました。 SPA+SSRを同時に導入するということは、大きな変更を2つ同時に行わなければならず、単純明快なメリットが無いと特に採用ハードルが高くなると思います。 この記事で紹介したように、既存資産を流用しながら検証を行うことができれば、SPA+SSRは最良の選択になるかもしれません。 最後までお読みいただきありがとうございました。 SPA化に関しては下記の記事でも紹介していますので、ぜひご覧ください! tech.dely.jp 「SPAはSEOに良い影響があった」という意味ではないので注意が必要です。 ↩ Vue.js 2.6で入った serverPrefetch では、thisによるコンポーネント自身のインスタンス参照が可能なので、置き換えを検討しています。 ↩
アバター
こんにちは!クラシルのiOSアプリ開発を行っている takao です。 今回はiOSアプリのテストの実行に関する内容です。 現在のクラシルのテスト運用は、Bitrise上でfastlaneの run_tests というActionを使ってテストの定期実行を行っていますが、結果の確認がしやすい状態とは言えません。特にUITestに関しては画面を確認するために、必要な場合はローカルで実行したりしています。 そこで、何やらFirebaseが提供している Firebase Test lab(以下、Test lab) というサービスを使えば、テスト結果をスクリーンショットや動画で確認することが出来るということだったので、試してみました。 この記事ではTest labでiOSアプリのテストを行う方法とTest labを使うとどういう結果が得られるのかをご紹介します。 準備 Firebase プロジェクトを作成する Test labを使うためにFirebaseプロジェクトの追加を行っておきます。 サンプルアプリの作成 ボタンを押すとアラートが表示されるだけのサンプルアプリを作成しました。 UnitTestを書く 簡単なテストを書きます。 func testExample () { let model = TestLabSampleModel() model.updateTitle( new : "test" ) assert(model.titleLabel == "test" ) } UITestを書く こちらもボタンをタップしてアラートを閉じるだけの簡単なテストを書きます。 UI Recordingを使ってさっと書きました。 func testExample () { let app = XCUIApplication() app.buttons[ "Button" ].tap() app.alerts[ "(^o^)" ].buttons[ "close" ].tap() } アプリのXCTestをビルドする Test labを利用するためにはXCTestのビルドを行う必要があります。 ビルドされたXCTestを使って、UnitTestとUITestを行います。 XCTest Apple公式ドキュメント ① DerivedData格納場所を確認・変更(任意) 公式のドキュメントの手順では「プロジェクトの Derived Data の場所を構成する」が必要だと説明されていますが、こちらの設定は任意です。 Test labを頻繁に利用する場合に、ビルドされたファイルにアクセスしやすいようにするための設定なので、初回の場合やCIなどでテストを自動化する場合は不要です。 ② テストファイルをビルドする Xcode上でXCTestのビルドを行います。 ナビゲーションから【Product】→【Build For】→【Testing】の順に選択します。 ③ アップロード用にテストファイルを圧縮する Test labにアップロードするために、必要なファイルをまとめて圧縮します。 ファイルはDerivedDataの中にありますが、Xcodeから簡単にアクセスできます。 【File】→【Workspace Settings...】の順に進んで、DerivedDataの箇所にある矢印ボタンをクリックすると、Finderが開きます。 その中から以下のファイルを選択して圧縮します。 Debug-iphoneos PROJECT_NAME_iphoneosDEVELOPMENT_TARGET-arm64.xctestrun Firebaseコンソールでテストを実行する Test labのコンソール画面で圧縮したファイルをアップロードします。 アップロードが完了するとテストの設定を選択する画面になります。 端末の選択 端末のバリエーションが豊富で、タブレットを選択することも出来ます。 画面の向きやロケールも選択可能になっているため、横画面に対応しているアプリや多言語対応アプリなどは全部のパターンを手動でテストするのは大変なので嬉しいと思います。 利用可能な端末の種類とOSについても随時更新されていくようです。 Available devices in Test Lab テスト結果を確認する テスト結果はこのような感じで表示されます。   サンプルアプリだとスクリーンショットの情報が出なかったため、クラシルのアプリで試してみました。 テスト結果は実行した端末・OSごとに確認することが出来ます。 iPhoneSE iPhoneX また、UITestを動画タブで実行している際の動画を確認することが出来ますが、こちらはかなりざっくりしたコマ送り動画のような状態になっており、さらに、全てのテストの結果が一つの動画にまとめられているので、テストの数が多い場合は確認が難しそうです。。。 Firebaseコンソール以外での実行方法 Bitriseでテストを実行する 今回は確認していませんが、BitriseのWorkflowを使用すると、簡単にテストの実行が出来るようです。 実行結果もちゃんとBitrise上で確認出来るようになっているので、Bitriseを導入しているプロダクトの場合はこっちを使うのが良さそうです。 Device testing for iOS - Bitrise Docs Bitrise画面 gcloud コマンドラインでテストを実行する こちらも今回は試していませんが、CLI上でテストを実行することも出来るみたいです。 実行結果はFirebaseのコンソール上で確認出来ます。Firebaseのコンソール画面で確認したい場合はCLIを使って定期実行するのが良いかもしれません。 まとめ Firebaseのサービスはアプリに設定ファイルやSDKを追加しないといけないものが多いですが、Test labに関してはプロジェクトの追加を行うだけで他に特別なことをしなくて良いため、比較的導入がしやすいと思います。 また、端末の種類が充実していたり、画面の向きやロケールなどの設定を変更したりなども容易に出来るのが良かったです。 Bitriseを使えばCI環境でもすぐに導入できると思うので、テスト実行を行うための環境をまだ作っていないプロジェクトなどでは役立つと思います、ぜひお試しください。
アバター
こんにちは! dely, Inc.でプロダクトマネージャー兼開発部ジェネラルマネージャーをしている奥原 ( @okutaku0507 ) といいます。気がついたら、もう3月になってしまいました。皆さんは花粉と対峙されていますか。 この記事はレシピ動画サービスであるクラシルを開発しているdely開発部が2019年1Qに開発ブログに取り組み、その一環として2018年のAdvent Calendar ( dely Advent Calendar 2018 ) を実施した背景とその成果について書いて行きたいと思います。 目次 目次 1. 開発ブログに取り組んだ背景 2. KPIと実績 3. 開発ブログの成果 4. 終わりに   1. 開発ブログに取り組んだ背景 まず最初に、dely開発部はとても少数精鋭です。実際に人数を聞くといつも皆さんが驚かれるほどです (気になった方は こちら から僕に聞きにきてください) 。実際に会社に所属して開発ブログを運営されている方なら体感されていると思うのですが、開発ブログを運営していくことは驚くほど工数がかかります。自分たちの取り組んでいることを、社外にわかってもらえるように発信することは思っている以上に大変です。では、少数精鋭でリソースが限られている中、なぜ、僕たちがAdvent Calendarに取り組み、開発ブログを盛り上げていこうと思ったのか。その背景についてお伝えしたいと思います。 今ではどこの会社も同じ現状かと思いますが、delyにおいても「 クラシルの事業拡大と新規事業立ち上げのため、圧倒的に人材が足りない 」という課題を抱えています。そして、採用人事に携われている方々は痛感していると思いますが、 エンジニアとデザイナーの労働市場は本当に採用が困難になっています 。これらの課題を解決するために、僕らがどうすればいいのか考えました。 僕の考えでは、採用はマーケティングに似ていると思っています。 だれでも使えるマーケティングファネルの考え方 ( https://www.tam-tam.co.jp/tipsnote/others/post14695.html ) つまり、採用される方のUXを考えた時に、会社を認知する、興味を持つ、転職 (新卒で入社) の際に比較・検討する、入社するというようなファネルに分解できると思います。このファネルにおいて、delyの現状と照らし合わせた際にまず浮かび上がった課題が以下の通りです。 クラシルというプロダクトや社長の堀江さんは知っているけど、それを開発しているdelyの開発部のことは全く知らない。 クラシルというレシピ動画サービスは2017年に全国規模のTVCMを打ったのと、社長である堀江がTVに出たり、PRで露出する機会が多く、両者は広く知れ渡っていました。そのため、候補者や実際に入社された方にヒアリングすると上記の課題が出てきました。そもそも、クラシルを開発している僕らが認知すらされていない状態となっていました。 ファネルの上部である「 認知 」を獲得することを目的として、上記 課題の解決策として開発部ブログを盛り上げる という方法を考えました。 以上の拝見から、2019年1Qの開発部の目標として開発ブログの活性化を入れることになりました。   2. KPIと実績 そもそも、dely開発ブログである本ブログは2016年から存在はしていました。しかしながら、工数もかかるためほとんど運用されていなかったのが実情です。 実際に記事を書いていくにも、目標を立てておいた方がそれを目指せると考え、 2019年1Qが終わるまでに、通常の目標として42,000PV、ストレッチ目標として100,000PVをおきました 。通常の投稿で、ちょっとバズったとしてもPV数は数千程度だったので、部内では目標に対する厳しさすら立ち込めていました。まさかの結果となるとはつゆ知らず... 達成できるのかという一抹の不安がこみ上げる中始まった開発ブログですが、2019年1Q中のPVランキングを紹介します。    第一位は... tech.dely.jp 弊社SREの井上さんが書いた、AWSの料金が意図せず増えてしまうのを仕組みで解決するという記事でした。AWSで意図せず予算を溶かしたというこの上なく辛い経験をした人に刺さったのか、多くのSREの人に読んでいただけたようです。   第二位は... tech.dely.jp 僕が書いた、クラシルで実践しているリーンな プロダクト開発を事細かに紹介した記事です。僕のクリスマス全てを捧げたかいがありました。delyの開発部では、リーンなプロダクト開発を通して、リリースしてみないとわからないという不確実性を減らす仕組みを作り、ユーザーに価値あるプロダクトを提供することに力を注いでいます。   第三位は... tech.dely.jp 弊社SEOスペシャリストである、internet_ghostさん ( @ghost_inter_net ) が書いたSEOの記事です。アプリの会社と思われがちですが、最近ではwebチームの発足と共にweb版クラシルもガンガン開発されており、前年比2000%の成長を遂げています。webチームにも注目が集まっています。   さて、開発ブログがおいていた目標に対する実績ですが... 僕らの予想を大きく上回り、早々に ストレッチ目標であった10万PVを超えました 。開発部として、僕らがやっていたことが皆さんに注目していただけて本当に嬉しい限りです。ノウハウも多く書いたので、読んでいただけた皆さんの役に立てれば幸いです。   また、これは本質的ではないかもですが、 はてなブックマーク数ランキングでは10位を獲得 することができました。少数精鋭で行なった分、この順位は僕らの自信に繋がっています。 qiita.com 3. 開発ブログの成果 さて、目標であったPVを達成してもそれが何も繋がっていなかったら意味がありません。僕らが実感してる効果を列挙します。 オーガニックでの応募が増えた スカウトの返信率が向上した お会いする人にブログ読んだことがあると言われるようになった 面接にきていただける方が事前に読んでくれるようになった ブログの盛況で開発部全体が盛り上がった dely開発部の認知が拡大することで、wantedlyなどでのオーガニックでの応募が増えたように感じますし、面接に来ていただいた方に読んだことがあると言われる機会が増えました。また、副次的な効果としてスカウトの返信率が向上したように思います。考察として、認知を獲得することは採用全てのベースになると思いました。そして、開発ブログが盛り上がると開発部全体も盛り上がって良いなと感じました。   4. 終わりに 開発ブログの大成功をお祝いして、みんなが大好きなアレを開発部の皆で食べました。みんなの頑張りを労い、一体感が増してよかったです。これからも引き続き、開発ブログでの発信をしていきたいと思います。 お寿司目的で頑張ったのではないことだけは大事なので、明言しておきたいです 。 そして、皆さまが気になっている実際の採用ですが、開発ブログが全てではないですが、 優秀なメンバーの採用が徐々に決まってきています 。一緒になってクラシルをよりよくできるメンバーが増えて、とても嬉しいです。ですが、まだまだ積極的に採用を行なっています。 こちらが現在、募集しているポジションになります。プロダクト開発に携わるポジションは全て募集しているので、是非とも話だけでも聞きにきてください。 www.wantedly.com また、面接となるとハードルが高い方用にカジュアルに内部のことが聞ける「カジュアル1on1」という取り組みも行なっています。絶対に面接はしないので、是非とも中にいるメンバーに話を聞きにきてください。 bethesun.connpass.com さらに、行くのもためらわれた方用に気軽に僕とチャットできる選択もオープンにしています。起きている時間全て対応しますので、気軽にチャットしてきてください。 note.mu
アバター