dely engineering blog

レシピ動画サービス「kurashiru」を運営するdelyのテックブログ

AppleのUI実装をさぐる

こんにちは。delyデザインエンジニアのJohn(@johnny__kei)です。
本記事はdely Advent Calendar 2018の10日目の記事です。

Qiita: https://qiita.com/advent-calendar/2018/dely
Adventar: https://adventar.org/calendars/3535

前日は、プロダクトデザイナーのkassyが「ユーザーの声に振り回されないデザインの改善プロセス」という記事を書きました。いいプロダクトを作るには、ユーザーの声を鵜呑みにするのではなく、きちんと判断する必要がありますよね。

はじめに

みなさんは、iOSアプリ開発をするときに、XcodeのDebugging View Hierarchiesを使用していますか?

Debugging View Hierarchiesを使用すると、アプリが現在の状態で停止され、Viewの階層や、プロパティを確認できます。

https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/debugging_with_xcode/Art/dwx-sw-dvh-1_2x.png 出典: Apple Specialized Debugging Workflows

AutoLayoutが効いていないのを調査するときに、使用したりする方もいらっしゃると思います。

Debugging View HierarchiesはUIKitのUINavigationBarなどのクラスにも適用され、どのようなView階層か見ることができます。
アプリ独自のUIパーツを作成するときに、できるだけ、View構造や、メソッド、プロパティなどを、UIKitにそろえると、使いやすくなると思います。

そこで
前半は、UIKitのいくつかのクラスのView階層について書きます。
後半は、前半をふまえて、サンプルの実装について書きます。

UIKitのView階層

- UINavigationBar

UINavigationBarのsubviewには_UIBarBackground(非公開クラス)があります。
下の画像を見るとわかるように、UIVisualEffectViewやshadowImageが設定されるimageViewなどがあることがわかります。

f:id:JohnnyKei:20181205151301p:plain

UINavigationControllerのnavigationBarでは、StatusBarまでnavigationBarが伸びているように見えるのは、この_UIBarBackgroundがはみ出しているからです。 自分で、UINavigationBarをViewControllerのviewにaddSubviewする場合は、適応されないので、UINavigationControllerの方で、そういった実装がされると推測できます。

imageViewも、Viewから高さ0.5の分だけ下にはみ出ています。これを発見したとき、ビビりました。

f:id:JohnnyKei:20181205151202p:plain

- UIPageControl

横スクロールでページングがあるときに、よく使用されたりします。 UIPageControlはdotのサイズやdot間のマージンは変更できません。

f:id:JohnnyKei:20181205180702p:plain f:id:JohnnyKei:20181205180204p:plain

dot自体は、単なるUIViewであることがわかります。 また、dotは7ptで、dot間のマージンは9ptということがわかりました。
結構シンプルな作りになっています。

また、UIPageControlは、InterfaceBuilderとCodeでの初期状態に違いがあったのも、新しい発見でした。

--- currentPageIndicatorTintColor pageIndicatorTintColor
InterfaceBuilder UIColor.white UIColor.white.withAlphaComponent(0.2)
Code nil nil

サンプル実装

- BottomBar

SafeAreaの登場で、下部に、Viewを配置したいときに、どういう風に実装したらいいか、悩む場合がありますよね。
自分なりに、こうしたらいいんじゃないかという実装を書いていきます。

f:id:JohnnyKei:20181205175842p:plain

static let viewHeight: CGFloat = 49.0のように本来表示したい高さを定義します。 そして、実際にViewControllerのviewにのせるのはこんな感じに制約をつけます。

NSLayoutConstraint.activate([
    bottomBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    bottomBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    bottomBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -BottomBar.viewHeight),
    bottomBar.bottomAnchor.constraint(equalTo: view.bottomAnchor)
    ])

こうすることで、safeAreaのbottomがある場合は、contentの高さと、safeArea分伸びた状態で表示することができます。

このBottomBarの上に、Viewをのせる場合は、contentViewにのせます。 contentViewは高さは以下のように固定されているので、ボタンはcontentViewに対して、制約をつけることで、いい感じに表示することができるようになります。

contentView.heightAnchor.constraint(equalToConstant: BottomBar.viewHeight)

さらに、前述した、UINavigationBarのshadowImageを表示するimageViewのように、border部分は、はみ出して作ってあります。 これも、contentViewにのせるようにしていて、UINavigationBarのようにしてあります。

contentView.addSubview(topBorder)

topBorder.heightAnchor.constraint(equalToConstant: 0.5)
topBorder.bottomAnchor.constraint(equalTo: contentView.topAnchor)

f:id:JohnnyKei:20181205173613p:plain

- PageControl

前述したように、UIPageControlはdotのサイズやdot間のマージンは定義されていないので、変更することができません。 そこで、UIPageControlとほぼ同じ、プロパティやメソッドを持ち、dotのサイズやdot間のマージンを設定できる、PageControlを実装してみます。

f:id:JohnnyKei:20181205181725p:plain

UIControlは、UIViewのサブクラスなので、普通にViewをのせていくことで大丈夫です。 さらに、UIPageControlのような実装にするには、タップしてPageが変化したときに、UIControl.Event.valueChanged イベントを発火する必要があります。 UIPageControlの説明にも書いてあります。

その実装は以下のようになっています。

override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
        guard let touchPoint = touch?.location(in: self), bounds.contains(touchPoint) else {
            return
        }
       // touchPointが、currentPageのdotの左右どちらかでなどのロジックで判断し、変更があれば発火する
      sendActions(for: .valueChanged)
}

dotのサイズやマージンを定義することで、サイズの計算も簡単になります。

// UIPageControlに合わせる。
var dotSize: CGFloat = 7.0
var dotMargin: CGFloat = 9.0

// pageCountに応じた、size計算
func size(forNumberOfPages pageCount: Int) -> CGSize {
      let height = (15.0 * 2) + dotSize
      guard pageCount > 0 else {
          // UIPageControlがこんな感じの値
          return CGSize(width: dotSize, height: height)
      }
      let width = dotSize * CGFloat(pageCount) + dotMargin * CGFloat(pageCount - 1)
      return CGSize(width: width, height: height)
}

numberOfPages, currentPageなどのプロパティの実装に関しても、結構シンプルになっているので、ぜひサンプルの実装をみてもらえばと思います。
UIPageControlに実装されていることは、全て実装しています。(たぶん)
実装のサンプルは置いておきます。
GitHub Sample Code

まとめ

いかがでしたでしょうか?
Debugging View Hierarchiesを使用すると、AppleのUIKit内部の実装が見れておもしろいですよね。 UI部分の階層構造しかみれませんが、そこから滲み出る、ロジック部分も想像すると、なお面白いと思います。
また、View階層をデザイナーに見せることで、内部構造を理解してもらえるので、次から、そこを考えて、デザインを作ってくれるようになるかもしれませんね。

明日は、サーバーサイドエンジニアのjoeによる「好きな技術を使って作る!くだらないslackBot運用のすヽめ」です。お楽しみに!  

jQueryへの依存を外す方法

こんにちは!dely でフロントエンドエンジニアをしている @all__user です。

この記事は dely Advent Calendar 2018 の8日目の記事です。

昨日は、iOSエンジニアのほりぐち( @takaoh717 )が「iOS版クラシルの開発からリリースまでの流れ」というタイトルで投稿しました。

tech.dely.jp

iOS 版 kurashiru の開発体制の遍歴がよく分かるような内容となっていますので、ぜひチェックしてみてください!

はじめに

ここ一年間で Web 版 kurashiru のフロントエンドは Rails から Vue の SPA へと少しづつ置き換えられてきました。
今回はその中でも jQuery への依存を外す際に行ったことにフォーカスを当ててまとめてみたいと思います。

目次

リプレース or 少しづつ置き換え

現在 jQuery を使った Web サイトを運用していて SPA への移行を考えている方の中には、リプレースするか少しづつ置き換えていくかで悩んでいる方もいるかもしれません。

最終的に kurashiru では少しづつ置き換える方法を取りました。
リプレースという選択肢もありましたが、割けるリソースの数、 SEO への影響などもろもろを考慮すると、エイヤっ!で移行するにはリスクが大きすぎると判断したためです。

稼働中の Web サイトを SPA へ書き換えるという作業は、いってみれば走っている車のタイアを交換するようなものです。人が住んでいるマンションを、人が住んでいる状態のまま建て替えるようなものと言ってもいいかもしれません。

jQuery を残したまま Vue に置き換えていくこともできますが、jQuery を使用している部分が無くなるまで jQuery と Vue が併存することになります。バンドルサイズはできるだけ小さく抑えたかったので、jQuery への依存を先に外すことにしました。

polyfill を入れる

Array.fromArray.prototype.findIndex などのメソッドは古いブラウザでサポートしていない場合があります。
どのブラウザでもこれらのメソッドを安全に呼び出せるように polyfill を導入しました。

これまではマルチブラウザ対応を jQuery がやってくれていましたが、依存を外していくにあたり別の方法でサポートする必要があります。

kurashiru では polyfill.io という CDN を使用して、User-Agent ごとに最適な polyfill を読み込むようにしています。

github.com

Sprockets 👉 Webpacker

まず最初に Sprockets のエントリーポイントをすべて Webpacker に移しました。

github.com

Webpacker はデフォルトで CoffeeScript に対応しているので、対象となる CoffeeScript ファイルを import したファイルを用意して Webpacker のエントリーポイントに移すだけです。

gem 👉 npm

gem の jquery-rails を削除し npm の jquery に置き換えます。

www.npmjs.com

CoffeeScript 👉 TypeScript

decafjs を使って CoffeeScript を JavaScript へ自動変換します。

www.npmjs.com

と、これで動いてくれれば良いのですが、まあまあ動かないところがあります。
ソースコードを読みながら地道に修正しつつ TypeScript に書き換えていきました。
decafjs はあくまでも TypeScript 版の下書きを作ってくれるツールくらいの感覚で使用しました。

jQuery 👉 VanillaJS

VanillaJS とは特定のフレームワークを使わずに DOM の標準 API のみを使って書くことを、バニラアイスのようなプレーンな状態ということに例えてよく使われている表現です。

この工程では特に、ここまでの過程で TypeScript に書き換えてきたことが効いてきます。
TypeScript で型をしっかり縛ることで typo や型エラーなどのケアレスミスで消耗すること無く、安心してリファクタリングを進められます。

$() 👉 Array.from(document.querySelectorAll())

jQuery のメソッドは基本的にセレクタにマッチした要素全てに対して操作を行いますが、標準 API では配列のループを回して一つ一つの要素に対して操作を行う必要があります。
.querySelectorAll() の戻り値は Array ではなく NodeList なので、Array.from() を使って Array に変換しておきます。

.each() 👉 .forEach()

.each() のコールバックに渡る引数は index, element の順ですが、.forEach() の場合は逆の element, index の順となります。

.width() 👉 .getBoundingClientRect().width

.getBoundingClientRect() は実行コストが少し気になりますが、.width().height() を再現する際にとても便利です。

.slideUp() 👉 ?

.slideUp() のように単純に標準 API へ置き換えることができないメソッドは、個別に機能を作って対応しました。
すでに Vue のアニメーションの仕組みにのっとって定義されたモーション用のクラス(.fade-enter, .fade-enter-active, .fade-enter-to みたいなやつ)があったので、それを使い、いい感じのタイミングで要素の classList に対してクラスの付け外しを行うことで再現しました。

各工程で動作確認をする

これまでの各工程の一つ一つはそれほど難しくないかもしれませんが、全ての工程を一気にやってしまうと、ちょっとした不具合があった際に、どこまでロールバックすれば良いかが分からなくなってしまいます。
できるだけ各工程の間に動作検証をいれるようにして作業を進めました。

挙動を変えないようにする

上記のように書き換えを行っていると、どうしても途中で挙動を変えたくなる箇所が出てきます。
あーこうした方がいいのにな、よしついでに直しちゃおう、っていうことが必ず出てきます。
でも、これをやってしまうと、何か不具合が起きた際に、書き換えにミスがあったのか、挙動を変えたことが原因なのかが分からなくなってしまいます。
気になった部分はコメントなどに残しておき、動作を確認できてから手を付けたほうが、着実に作業を進められます。

動作検証はビジュアルリグレッションテストで

動作確認を各工程で行う、と一口に言っても何をどこまで行えば良いのか、というのは非常に難しい問題です。
kurashiru ではビジュアルリグレッションテストという方法を使って動作検証を行いました。

ビジュアルリグレッションテストとは、コードに変更を加える前後でスクリーンショットを取り、その画像を比較することで動作検証を行うというテスト手法です。
これを各工程ごとに実施することで、書き換え前後で挙動が変わっていないことを確認しながら作業を進められます。

BackstopJS

kurashiru では BackstopJS というツールを使用してビジュアルリグレッションテストを行っています。

github.com

CI/CD などに組み込みやすいように Docker イメージが提供されていたり、コンフィグ類も過不足なく柔軟に設定できるためとてもオススメです。

意図的に 1px padding を変更した例
意図的に 1px padding を変更した例

差分が強調表示されるので分かりやすい
差分が強調表示されるので分かりやすい

一見まったく同じ見た目でも、きちんと差分を見つけ出すことができます。

まとめ

jQuery の依存を外すまでの工程を順を追ってご紹介してきました。

  1. polyfill の導入
  2. Sprockets から Webpacker への移行
  3. gem から npm への移行
  4. CoffeeScript から TypeScript への移行
  5. jQuery から VanillaJS への移行

細かい変換を繰り返しながら、各工程で動作確認をしていくことが重要です。

と、ここまで書いてきましたが、実際にはかけられる工数との兼ね合い、移行後のコードがその後どう使われるのかによっても違うので、ある程度エイヤっ!で書き換えることも正直たくさんありました。
そのあたりはよしなにやっていきましょう。

さいごに

SPA 化にあたっては、紆余曲折、チーム内でもさまざまな議論が行われました。
他にも Webpacker つらい、、、などご紹介したい話はたくさんありますが、それはまた別の機会にご紹介できればと思います!

明日はプロダクトデザイナーの @kassy が「ユーザーの声に振り回されないデザインの改善プロセス」というタイトルで投稿します。
こちらもぜひご覧ください!

iOS版クラシルの開発からリリースまでの流れ

この記事はdely Advent Calendar 2018の7日目の投稿です。
昨日は、Androidエンジニアのkenzoが「Androidにいい感じの動きをさせていく話(ViewPagerとその他Viewとの連動)」というタイトルで投稿しました。 Android開発を行っている方はぜひチェックしてみてください。

Qiita: https://qiita.com/advent-calendar/2018/dely
Adventar: https://adventar.org/calendars/3535

1. はじめに

こんにちは、delyでkurashiruのiOSエンジニアをしているほりぐち( @takaoh717 )です。
今回は、delyが運営しているレシピ動画サービス「kurashiru(クラシル)」のiOSアプリがどのようなフローを経てストアに公開されているかをざっくりとご紹介したいと思います。
この内容が、同じようなチーム規模の会社の方やこれからリリースフローなどを確立していくフェーズの方々などの参考になれば幸いです。  

2. dely開発部のチーム体制

クラシルは現在アプリダウンロード数が両OSで1400万を超える規模のサービスですが、iOSチームは現在2人体制で開発を行っています。
この人数だと当然リソースが潤沢と言える状況ではないため、何をどのように行うかがとても重要です。 また、delyの開発チームにとって最も重要なことはユーザに本質的な価値を提供するサービスを作ることです。 そのためにはスピード感をもって開発を進めることや施策の最適な優先順位決め、プロダクトの品質を保つための仕組みづくりなどの点に特に考慮する必要があると思っています。  

3. スクラム開発

クラシルの開発チームは半年ほど前から1週間を1スプリントとしたスクラム開発を行っています。

f:id:takaoh717:20181205180337p:plain
施策立案からリリースまでの全体の流れ

スプリント計画会

まず、スプリントの始まりには計画会を開いています。
ここで、今デザインフェーズにあるものと実装フェーズにある施策をそれぞれ確認し、そのスプリントで必要な要件定義・デザイン・実装のタスクの洗い出しと担当の割り振り、工数見積もりを行います。  

デザインフェーズ

UIデザイン、プロトタイピング実装、インタビューなどのサイクルを回します。
このあたりの話についての詳細は PM奥原のこちらのスライドをご覧いただくと分かりやすいと思いますが、ここで重視しているのはきちんとユーザーに使われるものをリリースするための仕組みです。  

実装フェーズ 

デザインフェーズが完了すると、プロトタイピングを行ったエンジニアやデザイナーが開発仕様書を作成します。
弊社ではドキュメントの共有にQiita:Teamを活用しているため、仕様書もQiita:Teamを使って書きます。また、Sketchで作成されたUIもZeplinで共有されます。
実装フェーズの実装者は「開発仕様書」「Zeplin上のUIデザイン」「プロトタイプ」を確認しながら、アプリの実装を進めます。
デザインフェーズで実装されたプロトタイプは基本的にはリリースするアプリには含まない方針で作られているものなので、ここではプロトタイプのコードは使用せずに新しくコードを書いていきます。
(プロトタイピング段階でプロダクトコードレベルで作り込まれたUIコンポーネントなどはそのまま使用することがあります。)   

4. iOSチームの開発スタイル

1スプリントの流れ

f:id:takaoh717:20181205202251p:plain
iOSチームの1スプリントの動き

  • 水:スプリント計画会で今スプリントのタスクが決定
  • 木〜火:実装
  • 水〜金:PRレビュー → 修正 → デバッグ
  • 金:Appleの審査に提出
  • 月:リリース

iOSチームは1スプリント(1週間)をざっくりとこのような形で動いています。
スプリントの開始が水曜日になっているのは、iOSの審査のタイミングを考慮してこの形が理想だろうと考えた結果です。
また、ここまできっちりとスケジュールを決めている理由は、実装後のデバッグや修正の時間をきちんと確保し、少ないリソースでもプロダクトの品質を確保できるような仕組みづくりが必要だったからです。
スケジュールを固定にしていなかった頃は、少し無理な実装スケジュールでも直前に割り込みで対応したりなどして、リリース前の検証が疎かになったり審査提出予定日のギリギリまでエンジニアが頑張るなどという問題が起きていました。

Gitのブランチ運用

  • /master
    • AppStoreで配信しているアプリと同じ状態
  • /develop
    • 次のリリースで配信されるもの
  • /feature
    • 何らかの新しい機能を実装をするためのブランチ(developから派生)
  • /improvement
    • 新機能ではないがユーザが見て分かる改善を行うためのブランチ(developから派生)
  • /fix
    • バグ・不具合の修正を行うためのブランチ(developから派生)
  • /refactor
    • リファクタリングを行うためのブランチ(developから派生)
  • /other
    • CI/CDやライブラリなどのツールに関する修正やアップデートを行うためのブランチ(developから派生)
  • /release/v.◯.◯.◯
    • スプリントが終わった段階で次のアップデートでリリースされるバージョン
  • /hotfix
    • releaseブランチを作成してから不具合などの緊急度の高い修正必要になった場合に修正を行うためのブランチ(releaseから派生)

Gitのブランチは現在上記のような運用を行っています。基本的な形はGit flowに近いと思います。
開発作業用のブランチを5種類に分けているのは、ブランチの名前だけでどういう変更が含まれたものかはっきりさせるために分けています。
また、この名称と同じタグをプルリクに設定してそこからCHANGELOGに追記されるようにしているため、その部分とも連動しています。

流れ

  1. masterブランチ → developブランチ派生
  2. developブランチ → 実装内容に応じて、開発ブランチ(feature or improvement or fix or refactorブランチ)派生
  3. 開発ブランチで作業、PR作成、レビュー
  4. 開発ブランチ → developにマージ
  5. releaseの要件が定まったら(主にスプリントの最終日)、develop → releaseブランチ派生
  6. releaseブランチで緊急度の高いバグがあった場合に releaseブランチ → hotfixブランチ派生
  7. hotfixブランチで修正、PR作成、レビュー
  8. hotfix → release、hotfix → developにそれぞれマージ
  9. releaseブランチからアーカイブビルドを行い、AppStoreConnectにアップロード
  10. リリース後に release → master、release → develop にそれぞれマージ

この運用方式にしてから半年以上が経過していますが、今は特に問題なく運用ができています。
以前は様々なタイミングで緊急の修正を行った際などのフローが不明確だったことなどがあり、何度かコミュニケーションが発生したりコンフリクトの修正が必要になったりと無駄な時間が発生してしまうことがありました。

デバッグ

releaseブランチを作成したら、社内のメンバーにデバッグをしてもらうためにDeployGateでアプリの配布を行います。
DeployGateでの配布にはSlack、fastlane、Bitriseなどを活用して作業を自動化してあります。
ここでは次にリリースをするバージョンの変更箇所を中心に確認します。また、バグや不具合などの問題以外でも少し気になったことなどもよく報告されることがあります。

審査提出 → リリース

デバッグとhotfixの修正が全て完了したら、審査に提出します。
審査通過後は、プロモーションコードをSlackで配布し、社内のメンバーにも最終確認を行ってもらいます。
ここで問題が見つからなければそのままリリースを行います。

リリース後

リリースした後は、まずはログの確認やデバッグ時に見つからなかったクラッシュ、バグの確認を行います。
さらに、必要に応じてiOSエンジニアもSQLを書いてデータ分析を行い、分析結果をもとにさらなる改善をしていきます。

5. おわりに

以上がクラシルのiOS開発からリリースまでの大まかな流れです。
この記事で紹介した仕組みは全てこの1年以内に確立したものです。弊社の開発チームはまだまだ歴史も浅く、メンバーも少ない少数精鋭チームですが、プロダクトを第一に考えて柔軟に仕組み作りを行っています。
もし機会があれば、もう少し深掘りした説明もどこかでしようかと思います。

また、弊社では、プロダクト志向でクラシルの開発ができるiOSエンジニアを募集しています。
少しでも興味があったり、記事に対してご意見などある方がいらっしゃればぜひお声がけください!

www.wantedly.com

明日はフロントエンドエンジニアの @all_user が「jQueryへの依存を外す方法」というタイトルで投稿します。こちらもぜひご覧ください!

Androidにいい感じの動きをさせていく話(ViewPagerとその他Viewとの連動)

こんにちは。delyでAndroidのエンジニアをしているkenzoです。
この記事はdely Advent Calendar 2018の6日目の記事です。

Qiita: https://qiita.com/advent-calendar/2018/dely Adventar: https://adventar.org/calendars/3535

 

昨日は検索エンジニアのsakuraが「クラシルの検索をよくするために1年間取り組んだこと」を書きました。
普段検索に携わる方はもちろん、それ以外の方にとっても面白い内容となっていますので、ぜひご覧ください。

はじめに

弊社のプロダクト開発はデザインフェーズと実装フェーズの2つのフェーズに分かれています。(詳しくはこちら↓)

speakerdeck.com

後半の実装フェーズでは、前半のデザインフェーズで出来上がったデザインを元に画面・機能を作成していくことになります。
デザインフェーズでは、かなりいい感じに動くプロトタイプを作成したり、それを用いてユーザーテストを行ったりと、より良いものをユーザーに提供できるように弊社のデザイナーやデザインエンジニアが頑張ってくれています。

で、下のようなデザインが出来上がっているわけです。(これはデザインの指示ではなくて実際に実装した画面ですが)

f:id:kenzo_aiue:20181203183239g:plain

え、めちゃ動く。。。 (はじめてデザインを見たときのきもち)

初めて見ると一瞬たじろぐかもしれませんが、動きも含めてユーザーに提供する価値なので、難しそうだからといって実装しないわけにはいきません。

今回はこのようないい感じに動くデザインを実装に落とし込んでいく際にやったことの一例を紹介します。

ViewPagerとその他Viewとの連動

まずはこちらをご覧ください。

f:id:kenzo_aiue:20181204135415g:plain

このようにViewPagerを用いたアプリの場合、現在開いているページによってViewPager外に表示される内容が変わることがあると思います。(画面下部や右上の★)

もちろんこれでOKな場合も多いとは思いますが、もう少し細かい動きにこだわったUIを作成したい場合もあるかと思います。

たとえばこんな感じです。

f:id:kenzo_aiue:20181204141554g:plain

今回はこれの実装方法をご紹介します。

用いるのはOnPageChangeListeneronPageScrolledです。

val viewPager: ViewPager = findViewById<ViewPager>(R.id.view_pager)
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
    
    override fun onPageScrollStateChanged(state: Int) {
    }

    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
    }

    override fun onPageSelected(position: Int) {
    }
})

onPageScrolledViewPagerのスクロール中に呼び出され、その際に引数として下記の値が渡されます。 (positionOffsetPixelsは今回は使いません)

  • position: 表示されている最初のページのindex
  • positionOffset: positionで指し示されるページからのオフセットの割合を0以上1未満の値で
  • positionOffsetPixels: position`で指し示されるページからのオフセットのpixel値

つまり、どのタイミングでどんな値が返るかというと、、

position: 0 positionOffset: 0.0

f:id:kenzo_aiue:20181204145013p:plain

position: 0 positionOffset: 0.31944445

f:id:kenzo_aiue:20181204145041p:plain

position: 0 positionOffset: 0.6259259

f:id:kenzo_aiue:20181204145142p:plain

position: 1 positionOffset: 0.0

f:id:kenzo_aiue:20181204145216p:plain

という感じです。

以上のような値がスクロールしている間に何度も呼ばれるonPageScrolledで渡されます。

この値を使ってその他Viewのtranslationやalpha、scaleをセットします。
上のサンプルの「いいいいい」のViewの例だと、

  • positionが0の時はpositionOffsetの増加に従って表示位置が上がる
  • positionが1の時はpositionOffsetの増加に従って表示位置が下がる
  • positionが2(上記以外)の時は常に表示位置が下のまま

なので、下記のようにtransilationYをセットします。

override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
    view.translationY = when (position) {
        0 -> view.height - view.height * positionOffset
        1 -> view.height * positionOffset
        else -> view.height.toFloat()
    }
}
// viewは「いいいいい」のviewです

このような動きになります。

f:id:kenzo_aiue:20181204141929g:plain

もう1つ、上のサンプルの「★」のViewの例だと、

  • positionが0の時(下記以外)は不透明度が0のまま
  • positionが1の時はpositionOffsetの増加に従って不透明度が上がる(だんだん見えるようになる)
  • positionが2の時は不透明度が1のまま(ページが増えるようならpositionOffsetの増加に従って不透明度を下げる)

なので、下記のようにalphaをセットします。

override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
    star.alpha = when (position) {
        1 -> positionOffset
        2 -> 1f // 今回はpositionの最大が2なのでこれでよいですが、ページが増える場合は1f - positionOffsetとします
        else -> 0f
    }
}
// starは「★」のviewです

このような動きになります。

f:id:kenzo_aiue:20181204133352g:plain

これで、ViewPagerをスクロールさせた時に他のViewと連動させることができました。

おわりに

Androidでも他のフロントエンドでも、動きのある画面を作るのはとっつきにくかったり難しかったりと、実装に若干のハードルはあると思います。
ですが、やっぱり自身がユーザーとしてサービスに触れる際に、いい感じに動いてくれると気持ちよかったり、わかりやすかったりと、良い体験ができていると感じます。
使ってくれる人がより良い体験をできるように、ものづくりをする人間としてはそういうとこ頑張らないとなーと思うところです。

 

ちなみに上でデザインの例としてあげた「クラシルかんたん献立機能」が今回リリースされました!
使ってみてもらえるととても嬉しいです。
アプリはこちら: Android iOS

 

明日はiOSエンジニアの堀口の「iOS版クラシルの開発からリリースまでの流れ」です。お楽しみに!

クラシルの検索をよくするために1年間取り組んだこと

こんにちは: )

delyで検索エンジニアをしているsakura (@818uuu) です。この記事はdely Advent Calendar 2018の5日目の記事です。

4日目の記事は、SREの井上の「超手軽に構築する!サーバレスなWEBパフォーマンス定点観測基盤」でした。WEBパフォーマンスの定点観測・分析に興味がある方はぜひ読んでみてください。

tech.dely.jp

はじめに

私はdelyに入社した2018年1月から検索エンジニアをしています。

delyが運営している料理動画アプリ「クラシル」では訪れたユーザーのほとんどが検索をする、いわば検索がコアになっているサービスです。
検索エンジニアとしてその検索体験を少しでも向上させるべく日々試行錯誤しています。

弊社のエンジニアが検索体験について語っているインタビュー記事もありますので、ぜひご覧ください。

careerhack.en-japan.com

私が入社する前まで弊社には検索エンジニアという役職は存在せず、他の役職のエンジニアが検索部分を掛け持ちで担当していました。
従って、会社にとって検索エンジニアという役職を置くのは初めての試みでしたし、私自身も検索エンジニアという役職についたのは今回が初めてです。

この記事ではクラシルの検索を改善するために、検索エンジニアとして1年間クラシルの検索に向き合った記録をお届けします。
検索エンジニアを目指している方やサービスを運営していて「検索機能は提供しているけどなかなか手をつけれていない」と悩みを抱えている方にとってこの記事が少しでもご参考になると幸いです。

目次

検索離脱率の改善

結果:辞書シノニムの整備を行い、検索離脱率を5%低下させることに成功しました🎊

検索エンジニアになって特に気にかけたのがこの検索離脱率という指標でした。入社当初は「一般的な検索離脱率が何%くらいなのか」という知見すらなかったため、その数値が高いのか低いのかすら判断ができませんでした。

調査を進めていくうちに、検索離脱率が高い一番の理由は辞書シノニムであることに気づきました。
辞書シノニム管理の運用についての詳細はこちらに記載しています。

tech.dely.jp

検索キーワードごとに検索離脱率を出し、検索離脱率が高いキーワードから優先的に対処していきました。
辞書シノニム改善をはじめてすぐに結果が表れ、運用を地道に続けていると結果的に検索離脱率を5%以上低下させることができました。

検索結果0件のキーワードの削減

結果:検索結果0件のキーワードを1000個以上削減・対応し、ユーザーの検索体験を向上させることが出来ました📒

クラシルには23,000件以上のレシピ動画があり一般的な料理はほぼ網羅していますが、たまに検索結果が0件のキーワードが見つかります。 「今流行りのチーズドッグを作りたい!」と思って「チーズドッグ」と検索してくれたとしても検索結果が0件だと、検索体験は非常に悪いですよね。

検索結果が0件の要因は主に2つあります。
1つは先ほど上述した辞書シノニムが未整備の場合、もう1つはそのキーワードに合致するレシピがそもそも存在しない場合です。

キーワードに合致するレシピがそもそも存在しない場合は、調理部さんと相談してレシピ動画を作成していただきます。 検索結果が0件キーワードの一覧の生データをただ渡すだけだとレシピのかぶりが発生してしまったり、実は既にレシピ作成中だったりして手間が発生してしまうので、調理部さんにも定期的にアドバイスを伺い工夫しながらデータをお渡ししています。

辞書シノニムの改善と新たにレシピを作成してもらったことで1年間で少なくとも1000個以上のキーワードに対応することができました。

f:id:sakura818uuu:20181205111437p:plain:w200
『とろーり チーズドッグ』も調理部さんと相談して作成していただいたレシピのひとつです。今ではたべれぽが50件以上ある人気のレシピです。

人気のキーワードの機能追加

結果:人気のキーワードの機能追加により、入力の手間を省きタップするだけで人気のキーワードで検索できるようになりました👆
また、この機能は分析・改善もうまくいっています。


今年新しくリリースされた機能の1つに、"人気のキーワードの表示"があります。 クラシル内でよく検索されているキーワードや急上昇しているキーワードを表示する機能です。


ハロウィンなど特別なイベント時にはそのイベントに関連する人気のキーワードを表示するようにしました。 この施策はとても数値がよかったです🎈

f:id:sakura818uuu:20181204164426p:plain:w150
ハロウィンの時の人気のキーワード

人気の検索キーワードでどのキーワードがクリックされやすいかなどを分析し、それに基づいて運用方法を改善したり新たな施策を考えたりしています。そして、その分析結果を定期的に社内共有サービスにレポートしています。

人気の検索キーワード(新規機能)の分析を始めた際に学んだことを共有しておきます。
新規機能をリリースした後「全体の何%にこの機能は使われているのか」や「どれくらいの人がこの機能を使ってくれているのか」などを必ず調べると思います。
しかしリリースしてすぐに分析してしまうと、まだアプリのバージョンをアップデートしていないユーザーも多く正確な概算値を測ることができません。 例えば、人気のキーワード機能はリリースしてから約10日間経ってからイベント数が落ち着きました。

f:id:sakura818uuu:20181204154423p:plain:w300
人気のキーワードのイベント数推移 : 2週間ほど経ってから数値が落ち着きました

他の新規機能でも約2週間経ってから数値が落ち着いた事例があります。つまり、新規機能は2週間以上たってはじめて全体の何%に使われているのかがわかるということです。 (私はこのことを知らずにリリースして1週間で速報値をレポートしてしまいました😨)

クラシルでは開発のサイクルがとてもはやい反面、既にリリースされた機能に対してケアできていないことも何度かありました。
人気の検索キーワードはリリースされてからも継続的に分析することでうまく改善を回せている好例だと思います。

検索品質ガイドラインの策定

結果:検索品質ガイドラインを策定し、クラシルの検索の方向性をドキュメント化しました👣ただし運用に難ありです。

Googleには検索エンジンの品質を評価するために検索品質ガイドラインを設けています。Googleのを参考にクラシルでも検索品質ガイドラインを策定してみる挑戦もしました。

f:id:sakura818uuu:20181204161905p:plain
Google General Guidelines(Google検索品質評価ガイドライン)

tech.dely.jp

ただ、これは作ったばかりで改善の余地が多いのと運用がままなっていないという現状があります。(それだけ伸びしろがあるということですね…笑)

検索絞り込み機能の追加

結果:検索絞り込み機能がリリースされましたが、想定より使用してくれるユーザー数が少なかったです😔

f:id:sakura818uuu:20181203175701j:plain

今年から検索絞り込み機能も増えました。 含めたくないキーワードやジャンル、15分以内で作れる料理など特定の条件に応じて絞り込むことができる機能です。

検索絞り込み機能は、ユーザーからこういう機能がほしい!とアプリレビューやCSを通して要望のあった機能のひとつです。もちろん社内でもこんな機能があればいいね〜と案で上がっていました。
私は主に分析を担当したのですが、実際にリリースしてみると想定より利用者数がものすごく少なかったのです。 具体的にいうと検索ユーザーの数%(一桁)しか絞り込み機能を利用していなかったのです。
ジャンルの精度が悪いのかな?と思い精度をほぼ100%にしましたがそれでも使ってくれるユーザーの数は増えませんでした。

数値を見守り続けたところ、少ないのではなくこれが定常的な値ということがわかりました。 検索をした後になにかタップする障壁がいかに高いのかを思い知らされた反面、通常の検索でユーザーが求めているものを検索結果を叶えなければ…と強く感じるのでした。

検索情報のリサーチと共有

結果:情報収集を地道に続けた結果、クラシルの検索ログを深い視点で見ることができるようになりました!

検索エンジニアはユーザーがどんなキーワードで検索しているか毎日チェックします👀
クラシルの検索キーワードのログから以下のことを確認します。

  • ユーザーがどんな食材でレシピを探しているのか
  • どんなキーワードが急上昇しているか
  • いまトレンドの食材/メニューはなにか
  • 検索下位にはどんなキーワードが入っているか(辞書シノニムが未整備の可能性が高いため)

検索ログの意味合いをきちんと読み取るために、世間一般の食にまつわる情報をたくさん検索し情報収集しています。
ユーザーはクラシル以外にも無数の食との接触点を持っています。
「新大久保で流行っているメニューをクラシルでも作りたい」「テレビで〇〇がダイエットに効くって評判だけどクラシルでもその食材を使ったレシピあるかな?」などそこから食べたいものや作りたいレシピを決めることも多いでしょう。

食との接触点一覧

  • Twitter
  • instagram
  • Youtube
  • グルメサイト
  • ニュースサイト
  • ブログ
  • 流行りの飲食店
  • など

「この情報収集する作業本当に価値を生んでいるのかな?」と思うときもあったのですが、地道に続けているとクラシルの検索ログがどのキーワードでどのくらい影響を受けるのかが少しずつわかってきました👏その他にも副次的に良い効果がたくさん生まれました!

上記で調べた情報は社内に定期的に共有しています📘

f:id:sakura818uuu:20181205114432j:plainf:id:sakura818uuu:20181205114452j:plain
検索情報共有の一部

さいごに

クラシルの検索を改善するために、検索エンジニアとして1年間クラシルの検索に向き合った記録をお届けしました。 いかがでしたでしょうか。

この記事を一言で伝えると、 「自社の検索の性質をよく把握し、その性質に合わせて検索改善・分析をすることが重要」ということです。

検索エンジニア1年目を振り返ると、ものすごく手探りで自分の無力さを感じた1年間でした。
経験・技術力不足でなかなかうまくいかなかったり、周りに同じ職業の相談相手がいなくて悩むことがとても多かったり…… 。 でも、この1年間やってきたことは無駄ではなかったと思います。無駄ではなかったと思いたいし、思い切りたいです。

検索エンジニア2年目は1年目よりももっと検索をよくできると信じています。
そして、いつか胸を張って検索エンジニアですといえるようになりたいです。


明日は、弊社のAndroidエンジニアの川口より「Androidにいい感じの動きをさせていく話(ViewPagerとその他Viewとの連動) - dely engineering blog」の話が書かれます。是非、お楽しみにしていてください!

超手軽に構築する!サーバレスなWEBパフォーマンス定点観測基盤

はじめに

本記事はdely Advent Calendar 2018の4日目の記事です。

dely Advent Calendar 2018 - Adventar

dely Advent Calendar 2018 - Qiita

昨日は弊社の機械学習エンジニアの辻がNixOSについての記事を書きましたので興味のある方は是非読んでみてください。

tech.dely.jp

こんにちは!delyでSREをやっている井上です。 本記事では、WEBパフォーマンスの定点観測の仕組みを手軽に構築出来るようにしたのでその紹介をしたいと思います。

本記事では下記のサービスやツールを利用しています。

  • AWS
  • Terraform
  • sitespeed.io

前置きはいいから構築方法だけ知りたいという方は こちら!

f:id:gomesuit:20181202191536p:plain

目次

WEBパフォーマンスについて

2018年7月のGoogleのスピードアップデートでも分かる通り、WEBページの読み込み速度はPCかモバイルかに関わらずより一層重要視されてきています。 読み込み速度の遅いWEBページを改善しようとなったとき、まずはじめに現状を把握する必要がありそのためにはWEBのパフォーマンス計測を行う必要があります。

現在、WEBのパフォーマンスを計測する手段としては

などいくつかありますが、本記事では現在delyで利用しているsitespeed.ioというツールを紹介したいと思います。

sitespeed.ioについて

sitespeed.ioはオープンソースのWEBパフォーマンス計測ツールです。

www.sitespeed.io

主な機能

sitespeed.ioを使うことで下記のようなデータをWEBブラウザで閲覧することが可能になります。

各種スコア

f:id:gomesuit:20181126225433p:plain:w600

Waterfall

f:id:gomesuit:20181126225636p:plain:w600

Visual Metrics

f:id:gomesuit:20181126225720p:plain:w600

Browser Metrics

f:id:gomesuit:20181126225736p:plain:w600

レンダリング時の動画再生

f:id:gomesuit:20181126225824p:plain:w600

スコアに対するアドバイスの表示

f:id:gomesuit:20181126225851p:plain:w600

レスポンスの遅いリクエスト

f:id:gomesuit:20181126225927p:plain:w600

サイズの大きい画像やjavascript

f:id:gomesuit:20181126225915p:plain:w600

主な特徴

また前段で挙げたWEBパフォーマンス計測ツールと違い、下記のような特徴を持っています。

  • Dockerによる簡単な実行

公式のdocker imageがあるので、下記のコマンドによって簡単に実行することが可能です。

$ docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:7.7.2 https://www.sitespeed.io/
  • 結果のhtml出力

実行後、計測結果がhtmlで出力されているのでWEBブラウザで簡単に閲覧することが可能になっています。

※ YYYY-MM-DD-HH-mm-ss は実行時刻が入ります

$ open sitespeed-result/www.sitespeed.io/YYYY-MM-DD-HH-mm-ss/index.html

他にも下記のような機能を標準で備えているので、柔軟な運用が可能になっています。

  • htmlをS3にアップロードする機能
  • 結果をSlackに通知する機能
  • 結果のメトリクスをjson形式で出力する機能

定点観測について

WEBのパフォーマンスにおいてボトルネックを特定するだけであれば、その時点で数回の計測を実施すればよいですが、ボトルネックに対して対策を行った後、どの程度改善したのかについて知りたい場合は対策後に再度計測を実施する必要があります。

複数の対策を長期的に実施していく場合などは、対策を実施する度に計測を実施する必要があり非常に大変です。

また、上記とは反対に何かのタイミングで意図せずパフォーマンスが悪くなっていないかをチェックしたいときやパフォーマンスが悪くなった前後でまたそのタイミングで何が原因かなどは遡って確認したいこともあります。

そういった要件を達成するためには、一定間隔で繰り返しWEBパフォーマンス計測を実施する必要があります。

定点観測の方法

定期的にWEBのパフォーマンス計測を行うには下記のような方法が考えられます。

  • 定期的に手動で実施

一番単純なのは定期的に手動で行うことです。 ただ現実的には手作業で行うため忘れることがあったり、そもそも同じ時間に実行するのが難しかったりします。

  • サーバを構築してcronやCIツールで定期的に実行

次はサーバを立ててcronで実行したりJenkinsなどのCIツールで定期的に実行する方法です。 この方法は、他の処理が裏側で動いていてパフォーマンス計測に影響があることを考慮する必要があったり、 そもそもサーバの管理が必要になってしまうことにデメリットを感じてしまいます。

  • ECSのタスク実行機能やAWS Batchで定期的に実行

理論的には実現できそうですが、WEBパフォーマンスの定点観測を行うだけなので少し大げさな気がします。

上記に3つほど方法を挙げて見ましたが、どれもいまいちという感じです(主観)。 そこで提案したいのがAWSのCodeBuildによる定期実行です!

CodeBuildのメリット

CodeBuildは本来CIで利用するサービスですが、サーバレスな実行環境としても優れていると思います。 WEBパフォーマンス計測の実行環境として使うことにおいては下記のようなメリットがあります。

  • サーバレスなのでリソースを気にしなくてよい
  • 実行時間による従量課金
  • CloudWatch Event連携でcronフォーマットによる定期実行も可能

定点観測基盤の構築方法

ここからは具体的な構築方法を紹介します。

本記事で使用するコードは全てGithubにあげています。 github.com

AWSのリソースをTerraformを使ってコード化しているので、定点観測基盤を手軽に構築することが可能です。

構成図

f:id:gomesuit:20181202191536p:plain

  • CodeBuild

sitespeed.ioを実行する実行環境として利用します。

  • CloudWatch Event

CodeBuildを定期的に実行するスケジューラとして利用します。

  • S3

sitespeed.ioによる計測結果の格納と、HTMLのホスティングとして利用します。

  • Glue

sitespeed.ioによる計測結果のスキーマを定義するために利用します。

  • Athena

sitespeed.ioによる計測結果に対してSQLを実行するために利用します。

前提

基盤構築に伴って最低限下記が実行環境に設定されている必要があります。

  • awscliのインストールとcredentialの設定

Terraformの利用に伴ってAWSのアクセスキーとシークレットキーの設定が必要です。

  • Terraformのインストール

  • IAMに対するTerraformを実行するための十分な権限設定

  • AWS CodeBuildとGitHubの接続

AWSコンソール上のCodeBuildにおいてプロジェクト設定時のページで下記のように、「GitHubアカウントを切断」と表示されている必要があります。されていない場合は「GitHubに接続」ボタンからGitHub連携を済ませてください。

f:id:gomesuit:20181201200034p:plain

CodeBuildとGitHubが接続されていないとTerraformのapply時にエラーになりますのでご注意ください。

準備

1.レポジトリのフォーク

下記のレポジトリをご自身のアカウントにForkします。Cloneして新しくレポジトリを作成しても大丈夫です。

GitHub - gomesuit/webperf-by-codebuild

2.Terraformのtfstateファイル管理用のS3バケットの作成

空のS3バケットを1つ作成します。 既存のものでも大丈夫ですが、tfstateファイルのkeyをコードにべた書きしているので新規でS3バケットを作成することをおすすめします。

3.terraform.tfvarsの作成

sitespeed.ioの出力したhtmlをS3のホスティング機能を使って参照するため、自身のIPでアクセス制限をかけます。 またCodeBuildがソースを取得する先を、手順で作成したレポジトリのURLに変更します。

サンプルファイルがあるのでコピーしてから、

$ cd terraform
$ cp terraform.tfvars.sample terraform.tfvars

ファイル内のIPアドレスとレポジトリを変更します。

# S3のアクセス元IP
my_ip = "XXX.XXX.XXX.XXX/32"

# 作成した自身のGitHubレポジトリ
git_repository = "https://github.com/<user-name>/<repository-name>"

4.backend.tfvarsの作成

Terraformのtfstateファイル管理用のS3バケットを terraform init のタイミングで指定するためファイルを作成します。

サンプルファイルがあるのでコピーしてから、

# ./terraform ディレクトリで実施
$ cp backend.tfvars.sample backend.tfvars

ファイル内のバケット名を変更します。

# tfstateを格納するS3バケット
bucket = "自身で作成したS3バケットの名前"

5.Terraform作業ディレクトリの初期化

下記のコマンドでTerraformの作業ディレクトリを初期化します。 エラーがでなければOKです。

# ./terraform ディレクトリで実施
$ terraform init -backend-config backend.tfvars

6.対象サイトの指定

計測するページのURLをurls.csvに記載します。 一行に ドメイン名,対象URL,カテゴリ の順で記載します。 複数行記載すると1回のCodeBuildの実行で複数のURLが計測されます。 カテゴリ名は対象URLをカテゴライズするために付与します。任意の文字列を入力してください。

例えば https://www.kurashiru.com/ を計測対象URLとする場合、下記のようになります。 トップページなのでカテゴリはtopとしました。 計測対象のWEBサイトはご自身が管理されているものを記載してください。

www.kurashiru.com,https://www.kurashiru.com/,top

構築

Terraformを実行します。

# ./terraform ディレクトリで実施
$ terraform apply

しばらくすると下記のようにコンソールに出力されるので、確認の後に「yes」と入力するとリソースの生成が始まります。

Plan: 16 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

上記コマンドで生成したリソースを削除する場合は下記コマンドを実行します。

# ./terraform ディレクトリで実施
$ terraform destroy

sitespeed.ioによる計測結果の参照

最大1時間待つか設定済みのCodeBuildを手動で1度実行するとS3に計測結果が置かれます。

HTML

S3バケットにhtmlが出力されているのでブラウザで開くことで計測結果を閲覧することができます。

例えばURLは下記のようなものになります。

https://s3-ap-northeast-1.amazonaws.com/webperf-by-codebuild-d85de1e5e348/raw/www.sitespeed.io/desktop/top/2018/12/01/15/38/index.html

f:id:gomesuit:20181202005018p:plain

SQL

AWSコンソールのAthenaでSQLを実行すると結果が取得できるようになっています。

SQL例

SELECT * FROM "webperf_by_codebuild_d85de1e5e348"."json" limit 10;

f:id:gomesuit:20181202004551p:plain

VIEW作成

このままでも問題はないのですが、SQLを作成するときにSQLが複雑にならないようにVIEWを設定するのをおすすめします。 例えば下記のようなVIEWを生成することでS3のURLとtimestampのカラムを事前に定義しておくことが可能です。

SQLの下記の部分を自身でTerraformを実行した結果に置き換えてください。

  • 「<Terraformによって生成されたS3バケット>」
  • 「<Terraformによって生成されたデータベース>」
CREATE OR REPLACE VIEW "<Terraformによって生成されたデータベース>"."site" AS 
SELECT
  CAST("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("YEAR", '-'), "MONTH"), '-'), "DAY"), ' '), "hour"), ':'), "minute") AS timestamp) "time"
, "concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"('https://s3-ap-northeast-1.amazonaws.com/<terraformによって生成されたS3バケット>/raw/', "domain"), '/'), "device"), '/'), "category"), '/'), "year"), '/'), "month"), '/'), "day"), '/'), "hour"), '/'), "minute"), '/index.html') "link"
, *
FROM
  "<Terraformによって生成されたデータベース>"."json"

置き換え例

CREATE OR REPLACE VIEW "webperf_by_codebuild_d85de1e5e348"."site" AS 
SELECT
  CAST("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("YEAR", '-'), "MONTH"), '-'), "DAY"), ' '), "hour"), ':'), "minute") AS timestamp) "time"
, "concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"('https://s3-ap-northeast-1.amazonaws.com/webperf-by-codebuild-d85de1e5e348/raw/', "domain"), '/'), "device"), '/'), "category"), '/'), "year"), '/'), "month"), '/'), "day"), '/'), "hour"), '/'), "minute"), '/index.html') "link"
, *
FROM
  "webperf_by_codebuild_d85de1e5e348"."json"

結果例

SELECT * FROM "webperf_by_codebuild_d85de1e5e348"."site" limit 10;

f:id:gomesuit:20181202022902p:plain

URLとtimestampのカラムが追加されていることを確認できます。

Redashによる可視化例

弊社では例えば下記のSQLを実行することによって、

SELECT
  *
FROM
  "<Terraformによって生成されたデータベース>"."site"
WHERE
  device = 'desktop'
  AND time >= current_timestamp - interval '1' month
  AND category = 'top'
  AND domain = 'www.kurashiru.com'
ORDER BY
  time desc;

下記のような結果を得ています。

f:id:gomesuit:20181127175929p:plain

また上記の結果を下記のようにRedashを使ってグラフ化しています。

f:id:gomesuit:20181127163011p:plain

上記は弊社での例ですが、sitespeed.ioによって様々なメトリクスが取得されているので色々な指標の相対的な変動を計測することが可能になっています。

カスタマイズ

sitespeed.ioの試行回数の調整

WEBサイトのパフォーマンスはサードパーティーコンテンツによって影響を受けるのでリクエストするたびにパフォーマンスにずれが発生することが多いです。 そのため1回の計測で数回施行することが一般的です。 sitespeed.ioは設定ファイルのパラメータを変更することで試行回数を調節することができます。

設定ファイルはレポジトリのルートにあるconfig.jsonというファイルで、iterationsというパラメータを3から変更することで施行回数を調節することが可能です。

{
  "browsertime": {
    "iterations": 3,
    "speedIndex": true
  },
  "s3": {
    "region": "ap-northeast-1"
  },
  "crawler": {
    "depth": 1
  },
  "plugins": {
    "load": ["analysisstorer"]
  }
}

計測間隔の調整

計測のトリガーはCloudWatch Eventで行っています。そのためCloudWatch Eventの設定を変更することで計測間隔の調整をすることが可能です。

Terraformの該当ファイルは terraform/codebuild_trigger.tf です。schedule_expressionの cron(0 * * * ? *) を変更することで間隔を変更することが可能です。

反映するにはもう一度 terraform apply を実行します。

料金について

本記事の設定で実際にかかっている料金をご参考までにお伝えします。

現在4つのURLの計測を1時間毎に実行していますが、CodeBuildの該当料金は1ヶ月に約$40になります。 決して安くはないですが、サーバの管理がいらないため負荷や障害を気にせず運用することが可能です。

さいごに

WEBパフォーマンスの定点観測についてお話しました。

手間をかけずにとりあえずパフォーマンス計測を始めたいという要件にもってこいではないでしょうか。ぜひ皆様のWEBパフォーマンス分析の参考にしていただけたらと思います。

明日は検索エンジニアのsakura (@818uuu) が「クラシルの検索エンジニアが検索をよくするために取り組んだこと まとめ」というタイトルで投稿します!お楽しみに!

tech.dely.jp

ぼくが普段使っているOS - NixOSの話

 こんにちは。

開発部の辻です。普段はデータサイエンティスト・機械学習エンジニアをやっています。

 

本記事はdely Advent Calendar 2018の3日目の記事です。

adventar.org

 

昨日12月2日は、弊社delyの開発部プロダクトーナー兼GM奥原の記事でした。この1年でdelyは「kurashiru」の成長とともに体制も大きく変化しました。そんな中、若きプロダクトオーナー兼GMとなった彼の苦悩や意気込みなどが赤裸々に綴られています。どうぞご一読ください。

 

tech.dely.jp

 

さて、今回は特定のテーマに沿う必要はないとのことだったので、機械学習とは関係なく、ぼくが普段使っているクライアントOSの話をさせてもらおうと思います。ふつう、クライアントPCで使うOSといえば、MacOSやWindowsを使ってる方が多いかと思いますが、エンジニアの方ですと、なんらかのLinuxディストリビューションを使っている人もそれなりに多いのではないでしょうか?ぼく自身もとあるLinuxをクライアントOSとして3年ほど使っていまして、ちょっとクセの強いOSではあるのですが、使っていくうちにだんだんと安心感を感じてくる面白いOSなので、この機会にぜひご紹介させて頂きたいと思います。

 

ぼくとNixOSとの出会い

ぼくはNixOS(最新バージョンは18.09)というOSを使っています。多分ほとんどの方がご存じないと思います(笑)。ぼくはこのOSを2015年の中頃から使い始めて、最初の頃はMac上のVirtualBoxに割り当てて使っていたのですが、ある日ふと気がついたら、自宅で使っているほぼすべてのPCがNixOSになってしまいました。もちろんdelyでの開発に使っているクライアントマシンもこのNixOSです。

 

NixOS Linux

 

f:id:long10:20181126114437p:plain

  

NixOSについて

NixOSのサイトに行くと、この様な記載があります。

 

The Purely Functional Linux Distribution

NixOS is a Linux distribution with a unique approach to package and configuration management. Built on top of the Nix package manager, it is completely declarative, makes upgrading systems reliable, and has many other advantages

つまり、純粋関数型Linuxディストリビューションなのが売りと言っています。この説明にもあるように、NixOSはNixパッケージマネージャーの上に構築されていて、完全に宣言的に扱うことが可能となっています。というわけでまずはNixパッケージマネージャーについてご紹介したいと思います。

 

Nixパッケージマネージャー

Nix: The Purely Functional Package Manager

 

こちらのNixのサイトに行くと、次のように記載されています。

 

The Purely Functional Package Manager

Nix is a powerful package manager for Linux and other Unix systems that makes package management reliable and reproducible. It provides atomic upgrades and rollbacks, side-by-side installation of multiple versions of a package, multi-user package management and easy setup of build environments.

やはり、純粋関数型なんですね。要するに、NixOSというのは純粋関数型パッケージマネージャー上で動くOS、だから純粋関数型OSというわけなんです。このNixパッケージマネージャーついては、いわゆる通常のパッケージマネージャーの一つですので、aptやyum、homebrewなどと同じようにパッケージを管理するのに扱えます。以下のコマンドでmacOSにインストールすることも可能です。

 インストールコマンド

curl https://nixos.org/nix/install | sh

 

Nixパッケージマネージャーは、nixpkgs(the Nix Packages Collection)に登録されているライブラリの中からパッケージをインストールします。ソースはgithubでリポジトリ管理されていて、Hydraという継続ビルド管理システムで管理されています。

 

github.com

   

Nixの仕組み

 もう少しNixの仕組みについて見ていきたいと思います。 

こちらがNix研究の元となったEelco Dolstraさんの論文です。非常にエキサイティングな内容となっています。

https://grosskurth.ca/bib/2006/dolstra-thesis.pdf 

 

全体としてはこのような外観となっています。

 

[仕組み全体の外観] 

f:id:long10:20181127131118p:plain

 

[主な仕組み]

  • パッケージレシピ関数ファイルが一意に管理される(導出コンパイル用)
  • パッケージレシピ関数はNix言語(Nix Expression Language)で記述する
  • Nix言語はパッケージ管理用DDL
  • NixはNix言語のコンパイラの役割
  • パッケージレシピ関数ファイルはNixストアにインストールされる
  • 導出コンパイル = パッケージレシピ関数のビルド + Nixストアへインストール

 

[Nix言語]

構文例はこちら

Nix Expression Language - NixOS Wiki

Nixにおけるすべての設定関数はこのNix言語で記述します。nix repl を使って構文を試すことができます。

nix-repl> rec{x = 1; y = 3; z = x + y;}.z
=> 4

 

[Nixストア]

  • インストールパッケージは必ずNixストアにインストールされる

  • ストア内に同じパッケージの複数バージョンが共存できる(※なので、HDDの結構ボリュームが消費されます。それらを整理するためにGC機能があります。)

  • インストールパッケージは、ストア内の導出ハッシュ(一意に生成されたハッシュ)によって特定されたフォルダにインストールされる

  • この一意なハッシュ名フォルダにインストールパッケージのすべてのデータが格納される(以下はtreeコマンドの例)

    $ readlink $(which tree)
    /nix/store/zzljry2r5w14rmvg3p9lz7ym326rfcpp-tree-1.7.0/bin/tree
    
    $ tree /nix/store/zzljry2r5w14rmvg3p9lz7ym326rfcpp-tree-1.7.0/
    /nix/store/zzljry2r5w14rmvg3p9lz7ym326rfcpp-tree-1.7.0/
    ├── bin
    │   └── tree
    └── share
        └── man
            └── man1
                └── tree.1.gz

 

[Nixチャンネル(nix-channel)について]

  • Nixチャンネルは特定の条件を満たすnixpkgsのバージョンを提供する仕組み
    • Hydraでビルドとテストが成功している場合に提供される
  • Nixチャンネルの種類
    • 安定チャンネル、例 nixos-18.09
    • 不安定(開発): nixos-unstable
    • サーバ向け、例 nixos-18.09-small, nixos-unstable-small
  • Nixストア内にはNixチャンネルの最新nixpkgsが入っていてNixに利用される
  • Hydraでインストールパッケージがビルドされ、自動的にバイナリパッケージが生成される(これが特定のチャンネルとして提供される) 

 

NixOSの仕組み

NixOSは、このようにNixパッケージマネージャーを中心で構成されているOSなのでした。まとめるとNixOS自体はこのような外観をしています。

 

NixOS全体の外観 

f:id:long10:20181127140859p:plain

 

 

NixOSを使うと以下のような恩恵を受けられます。

  • モジュールシステム(宣言型設定)
  • Dependency Hellからの脱却
  • ロールバック
  • 軽量コンテナー (systemd-nspawn利用)
  • Dev/Opsツール (NixOps)

 

モジュールシステム(宣言型設定)

 

なんと言ってもNixOSといえばこれという機能です。

様々な設定やパッケージ管理、システムの自動化などを宣言的に管理することができます。モジュールシステムはnixpkgs内に含まれていてNix言語を使って記述します。

 

NixOSのメインとなる情報はこのファイルに宣言的に記載します。

 /etc/nixos/configuration.nix

 

宣言的に設定した例をいくつか紹介します。 

プロビジョニングの例

environment.systemPackages = with pkgs; [
  firefox
  termite
  tmux
];

 systemdのサービス

systemd.services.ircSession = {
  wantedBy = [ "multi-user.target" ];
  after = [ "network.target" ];
  serviceConfig = {
    Type = "forking";
    User = "username";
    ExecStart = ''${pkgs.tmux}/bin/tmux new-session -d -s irc -n irc ${pkgs.irssi}/bin/irssi'';
    ExecStop  = ''${pkgs.tmux}/bin/tmux kill-session -t irc'';
  };
};

 cronのサービス

systemd.services.cron = {
 enable = true;
 systemCronJobs = [
 "30 00 * * * sh hogehoge.sh"
 ];
};

 

Dependency Hellからの脱却

 

インストールパッケージは、ストア内の導出ハッシュによって特定されたフォルダにインストールされるので、たとえばバージョンの異なる2つのパッケージが上書きされることもなく(別のハッシュ値が生成されるため)、またパッケージが依存する別のパッケージについてもこの導出ハッシュを用いてビルドされるため、相互的に依存し合うパッケージは存在しません。ですので、インストール時に毎回、環境依存に怯えながらインストールすることはなくなります。(この点に関してはDockerを使えば問題は解決すると思いますが、NixOS上でもDockerはもちろん使えますので、ぼくの場合は用途によって適宜使い分けています。)

 

ロールバック

 

なんらかのsystem設定を間違えてしまったために、OSが起動しなくなってオワタという経験も、NixOSを使えばさっとロールバックできるから安心です。 

  • コマンドでロールバック可能

  • ブート画面でロールバック可能

 

 GRUB画面で前のリビジョンを選択して起動する

f:id:long10:20181127132620p:plain

 

NixOSの再構築

  • nixos-rebuild コマンドで変更した設定を適応する

    $ nixos-rebuild switch
  • 設定をビルドのみする場合(コンパイルチェック)

    $ nixos-rebuild build
  • 宣言的にOSを管理できるので、configuration.nixを別のPCにコピーし、この手順で適応すれば同じ環境を再現できる

 

NixOps Dev/Opsツール

 

NixOSのDev/OpsツールとしてNixOpsがあります。NixOSはDev/Opsを大変重要視した設計思想となっていて、NixOpsを使うことで、デプロイ先のハードウェアや仮想マシンに対してNixOSのセットを展開することができます。また、NixOSのシステム構成管理はクライアントPCとシームレスに管理でき、宣言的なアプローチをネットワークにも拡張し、さらにそのプロビジョニングを追加することができます。ぼくが所有している複数のNixOSマシンについてもNixOpsを用いて設定構成をネットワーク拡張して使用しています。

 

NixOSの不便なところ

 NixOSはとても便利かつ安心して利用できるOSなのですが、すべての人にとってそう言えるかといえばなかなかそれは難しい面もあります。

たとえばこんな点です。

  • クセが強いので慣れるまでに時間がかかる
  • 利用できるパッケージが他のパッケージより少ないので、ないパッケージは自分でビルドする必要がある(ものによっては結構難しい。。)
  • ちょっとしたことを試すのが面倒(そんなときこそDockerを使いましょう)

Dockerのサービス起動もたったのこれだけです。

{
  virtualisation = {
    docker.enable = true;
  };
}

 

終わりに

いかがでしたでしょうか?

NixOSの特性についてざっくりとご紹介させて頂きました。NixOSを使うことで、いわゆる”Dependency Hell”から開放され、依存関係の完結が保証されることで、堅牢な管理や安全なロールバックが可能となります。NixOSには今回ご紹介できなかった機能がまだまだたくさんありますので、もしご興味を持っていただけたらぜひお試し頂けると嬉しいです。

 

不定期開催ですがMeetupやってます!

www.meetup.com

 

余談ですが、今年のNix Conference(Nixcon 2018)で、Nixを使ったデータパイプラインの設定についてのセションがあって個人的にとても興味深かったです。機械学習やパイプライン基盤にもNixOSを使うと面白そうですね。

 

Georges Dubus - Nix for data pipeline configuration.pdf - Google ドライブ

 


Georges Dubus - Nix for data pipeline configuration

 

さて、dely Advent Calendarの明日のタイトルは、『超手軽に構築する!サーバレスなWEBパフォーマンス定点観測基盤』です。お楽しみに!