Subscribed unsubscribe Subscribe Subscribe

Hatena Developer Blog

はてな開発者ブログ

開発速度と品質のトレードオフの判断基準の合意

Webサービスの開発は、ユーザ/顧客へ価値を早く届けるため、競合より早くリリースするため、人的リソースを無駄使いしないためなど、とにかく素早く進めたいものですね。一方で、開発を急ぐあまり品質を犠牲にすればかえって価値が失われたり、技術的負債が溜まって長期的なコストが大幅に増大する可能性もあります。開発速度とプロダクト品質は基本的にはトレードオフの関係にあるのでしょう。

開発速度と品質のどちらを優先するかはプロダクトの性質や、チームもしくは会社の状況によって異なるとおもいます。この状況の認識がチームメンバー間でずれていると、チームのパフォーマンスを最大限に発揮できないばかりか、チーム内の関係悪化も招きかねません。エンジニアたちとプロダクトオーナーの間の対立のようなありがちな問題の原因の一つかもしれません。

そこで、開発速度と品質のトレードオフをどう判断すべきかの基準を明確にして、原則それに従うことをあらかじめチーム内で合意してみました。

この記事は、はてなエンジニアアドベントカレンダー2016の24日目で、担当はid:taraoです。昨日の担当はid:takuji31さんのAtomの自動補完プラグイン「autocomplete-plus」のPerl用Providerを書いている話でした。

Read more

文字列アルゴリズムの学びかた

こんにちは!はてなアプリケーションエンジニアの id:takuya-a です。

みなさんは、このような疑問をもったことはありませんか?

  • grep はどのように文字列を検索しているのか?
  • MeCab はどうやって辞書を高速にルックアップしているのか?
  • パーサやコンパイラを作りたいけど、何から始めればいいのか?

本稿では、「文字列アルゴリズムとはどんなものなのか?」「なぜ重要なのか?」「何を知っておくべきか?」「どうやって勉強すればいいのか?」といった疑問にお答えしていこうと思います。

文字列アルゴリズムの意外な応用や、モチベーションを保ちやすい勉強のしかた、文字列アルゴリズムを勉強するために行った社内での取り組み、実装するときのコツといったトピックについても触れています。

このエントリは、はてなエンジニアアドベントカレンダー2016の22日目の記事です。昨日は id:syou6162 さんによる はてな社内で行なっている機械学習勉強会について紹介します でした。

文字列アルゴリズムとは

文字列処理は、プログラミングにおいて日常的に使われるわりには、文字列アルゴリズムについてはあまり知られていないように思います。くわしい説明は後にして、ここでは文字列アルゴリズムの分野をごく簡単に紹介します。

もっとも基本的な文字列アルゴリズムは、部分文字列検索(パターンマッチ)でしょう。 複数のファイルから検索する場合はとくに全文検索と呼ばれます。 とくに grep などの文字列検索ツールへの応用が有名ですね。

その他にも、パターンの数が数百万以上になっても高速に検索するアルゴリズムや、いくつかの文字の違いを許容するあいまい検索などの複雑な検索にも対応できるアルゴリズムもあります。

さらに、音声認識やデータ圧縮、時系列パターンマイニングといった、単なる文字列検索にとどまらない応用があり、数々のアルゴリズムの分野のなかでも文字列アルゴリズムは特に重要であると考えています。

なぜ文字列アルゴリズムを学ぶのか

ここでは応用先を中心に、文字列アルゴリズムを学ぶことでできるようになることを紹介します。以下のように多様な応用があります。

  • 全文検索
    • 検索エンジンには様々な応用がある
    • 転置インデックスの辞書やあいまい検索など
  • パーサ・コンパイラ
    • オートマトンの理論を共通の基礎とする
    • 自分の DSL を作ったり、独自のパーサを書いたり
  • 自然言語処理
    • 自然言語処理は文字列を扱うことが多いので、文字列アルゴリズムの重要度は高い
    • 形態素解析などには直接応用されている
  • 機械学習
    • 文字列(単語など)を素性とする機械学習で応用可能
    • 精度や前処理のパフォーマンス向上が狙える
  • 音声認識
  • パターンマイニング
    • 時系列データ解析・ログ解析など
    • 類似パターンや繰り返しのパターンを発見する
  • データ圧縮
    • bzip2 などの圧縮アルゴリズムに応用されている(文字列アルゴリズムの一種である BW 変換を利用)

これら以外にも、基礎を知っていれば意外なアプリケーションに利用できることもあります。たとえば、ISUCON6 で出題されたことが記憶に新しいはてなキーワードのキーワードリンク作成(テキストのキーワード部分にリンクをつける処理)でも、後述するトライ木を利用することで高速に行うことができます。そのほか、テキスト中からn回以上現れるフレーズを高速に抽出する、などの応用もあります。

また、文字列アルゴリズムは、工夫すればバイナリにも適用することができます(「番兵とヌル文字」を参照)。つまり、アルゴリズムによっては、なんらかのデータの並び (sequence) でさえあれば適用できる可能性があります。あなたの発想次第で、意外なところが高速化できたり、アプリケーションをもっと大規模なデータに適用できるようになるかもしれません。

最初に知っておくべきこと

文字列アルゴリズムの世界は広大なので、そのすべてを一度に勉強するのは難しいと考えています。興味のある分野で関連領域をひとつずつ潰していくのがおすすめです。

基礎的な文字列アルゴリズムには、ざっと以下のようなものがあります。

  • 文字列のソート
  • 部分文字列検索(パターンマッチング)
  • 複数パターンマッチング・辞書データ構造
  • 正規表現マッチング
  • あいまい検索

まずは、具体的なアルゴリズムを勉強し始める前に、各アルゴリズムの特徴と使いどころをつかむところから始めましょう。それぞれに代表的なアルゴリズムを、後ほど特徴とともに簡単に説明します。また、思い立ったときにすぐ学習を始められるように、参考リンクなどのリソース集を併記しますので、興味が湧いたときにのぞいてみてください。

どうやって勉強するか

経験上、文字列アルゴリズムを勉強するモチベーションを保ち、理解していくうえで最も効果的だったのは、自分が作りたいものを実装してみる ことでした。

前職では全文検索エンジンを開発していたのですが、自分にとって一番のブラックボックスは形態素解析でした。形態素解析器の中身を知りたい!という興味から、Kuromoji という Java の形態素解析器のソースコードを読み始めたり、 NLP の資料や論文を読んでみたものの、「理解した!」と思うにはほど遠い状況でした。

そこで、自分が好きだった JavaScript で形態素解析器をゼロから書いてみようと思い立ち、ダブル配列(後述する、トライ木というデータ構造の一種)とビタビアルゴリズム(動的計画法の一種)による形態素解析器を実装しました。実装してみることで、処理のどこに時間がかかるのか、データはどのように保存されてどう読み出されるのかなど、形態素解析の動作が手に取るようにわかるようになりました(とはいえ、辞書のクセにはずっと翻弄され続けていますが…)。

目的とするアプリケーションの部品が組み上がっていくのは、とても楽しい経験でした。アルゴリズムだけを勉強しようとしていたとしたら、間違いなく挫折していたと思います。

ここで言いたかったのは、文字列アルゴリズムの教科書を開く前に、まず 作りたいものを探すことから始めるという勉強法もある 、ということです。 最初は簡単なアルゴリズムを応用したものでよいと思います。解きたい問題、作りたいアプリケーションによって、関連するアルゴリズム・分野を深掘りしていくと、教科書的に勉強するよりもはるかに深い理解が得られます(また、その副産物として新しい OSS ができることもあります)。

モチベーションを保つ上でもうひとつ重要なことは、「仲間を探す」ことです。ひとりで自分を高め続けるのはとても苦しいことですが、苦労や感動を共有できる人がひとりでもいれば、学習を継続できる確率は飛躍的に高まるでしょう。

社内勉強会の取り組みについて

「作りたいものを実装してみる」以外の、もうひとつの勉強法として、仲間を集めて勉強会を開催するという手があります。今回は、社内で実施している、文字列アルゴリズム勉強会の取り組みについて紹介します。

具体的には、カリフォルニア大学サンディエゴ校のオンライン講座 (MOOC) をみんなで受講しています。私は、文字列アルゴリズムのなかでもとくに接尾辞配列や、その関連アルゴリズムに苦手意識があったのですが、その勉強にちょうどいいオンライン講座をたまたま見つけました。それが Coursera で開講されている Algorithms on Strings です。社内で紹介したところ、幸い興味をもってくれた仲間を集めることができました。

機械学習の社内勉強会でも Coursera を活用していたこともあり、同じような形式で進めています。

  • 昼休みや夜に、講義ビデオをみんなで見る
  • わからないところは適宜ホワイトボードを使ったりして話し合う
  • 週末にプログラミング課題を実装する

この講座の一番いいところは、毎週プログラミング課題が出ることです。課題では、実装すべき関数の入力と出力が明確に決められており、本質的ではない部分はあらかじめ実装されている雛形が提供されています。これにより、 文字列アルゴリズムを実装するための心理的ハードルが極限まで下がっている状態 になっています。あとはやるだけです。

ひとつ残念な点は、無料で受講しているかぎり課題が採点されないことです!ただ、課題ができなくてずるずる遅れていく…といったことがないので、内容さえある程度理解していれば毎週気軽に参加できるというメリットもあります(もちろん実装したほうが理解は深まりますが)。

現在は(講座本来のペースからは遅れながらも)最終週までたどりつくことができています。なんとか年内には完走できそうです!

代表的な文字列アルゴリズム

ここでは、代表的なアルゴリズムについて、ひとこと解説と、参考になる学習リソースを示します。

最初は、この解説ではおそらく意味不明だと思いますので、興味が湧いたらぜひ参考リンクの記事や書籍を読んでみてください。

1. Brute Force による文字列マッチング

  • もっともシンプルな部分文字列検索アルゴリズム
  • 1文字ずつずらしながら部分文字列すべてとテキストの文字を比較する
  • 文字がマッチしなかったらテキストの次の位置をみる
  • Brute force algorithm

2. KMP による文字列マッチング

  • 文字位置をスキップすることで比較回数を減らす
  • 理論上は重要
  • 実用上は後述する BM 法、とくに Quick-Search (Sunday のアルゴリズム) が使われている
  • Algorithms on Strings の3週目で登場

3. BM (Boyer-Moore) による文字列マッチング

4. Bitap (Shift-And / Shift-Or) アルゴリズムによる文字列マッチング

5. Rabin-Karp (Rolling hash) による文字列マッチング

  • 工夫した多項式ハッシュ関数を用いることで、テキストの長さに対して線形時間で検索可能
  • 位置nからm文字の部分文字列のハッシュ値から、位置n+1(1つずらした)からm文字の部分文字列のハッシュ値を高速に( O(1) で)計算できる
  • 位置をずらしていった、すべて(|Text| 個)の部分文字列のハッシュ値を O(|Text|) で計算できる
  • 詳しくは蟻本ことプログラミングコンテストチャレンジブック 第2版「4-7 文字列を華麗に扱う」を参照

6. トライ木 (Trie) による複数パターンマッチング

7. 正規表現のマッチング

  • とても表現力の高いパターンマッチが可能
  • 有限オートマトン (FSA) の理論と関係が深い
  • 正規表現エンジンの実装は大きくわけると DFA 型と VM 型という2種類がある
    • DFA 型:正規表現を決定性有限オートマトン (DFA) に変換し、その上での状態遷移をシミュレートする
    • VM 型: 正規表現を仮想マシン (VM) 上のバイトコードに変換し、 VM 上でのバイトコード実行をシミュレートする
  • 正規表現エンジンをどう実装するかについては、正規表現技術入門を読むのがおすすめ

8. 接尾辞配列 (Suffix Array)

  • 検索対象のテキストを接尾辞配列というデータ構造(元テキストと同じ長さの配列)に変換する
  • 接尾辞配列を使うと、パターンに対して2分探索で検索できるようになる
    • 大きなテキストに対しても高速
    • http://www.slideshare.net/nobu_k/suffix-arraysolr
    • 単純な実装では O(|Pattern| log |Text|)
    • LCP (Longest Common Prefix) 配列をあらかじめ計算して持っておくことで O(|Pattern| + log |Text|) になる
    • LCP 配列の計算については接尾辞木の項目を参照
  • 線形オーダー O(|Text|) で接尾辞配列を構築するアルゴリズムがある

9. 接尾辞木 (Suffix Tree)

10. BW 変換 (Burrows-Wheeler Transform)

  • bzip2 の前処理として利用されている
  • BW 変換後の文字列 (BWT) はテキストと同じ長さの文字列になる
    • このとき、同じ文字が並びやすくなる、つまり元の文字列にくらべて圧縮が効きやすくなる
    • (英語の場合) nd の前には a が来やすい、みたいな性質を使っている(and という単語が頻出するので)
  • BWT から元のテキストが復元できる(逆変換が可能)
    • First-Last Property という性質を利用した LF-mapping を使う
    • Algorithms on Strings の2週目のビデオがわかりやすい
  • SA-IS を使うことで O(|Text|) で計算可能

11. FM-index

  • 圧縮インデックス
  • 通常の転置インデックスに比べて、動的な更新は難しい
  • 高速文字列解析の世界 7.5.2 に FM-index を使った検索についての解説がある

文字列アルゴリズムを実装するときのコツ

実用的なアプリケーションやライブラリを作るのは、試しに実装するよりもはるかに大変です。ここでは、実装のコツをいくつか紹介します。

テストを書く

基本的なことですが、テストを書きましょう。一般的にアルゴリズム・データ構造の実装は、バグが入り込みやすく、潰すのは難しいです。とくに境界条件のテストは念入りに行うようにしましょう。

文字列アルゴリズムは基本的には関数単位で実装でき、入力と出力が明確です。そのため、複雑なアプリケーションに比べてテストが書きやすいといえます。テストがしっかりあって、試行錯誤しやすい状態になっていると、パフォーマンスチューニングも格段にやりやすくなります。

簡単なものから実装する

これも基本的なことですが、まず思いつく最も簡単な実装を行い、そのあと効率的なコード・複雑なアルゴリズムを書きます。

文字列検索を考えたとき、まず最初に実装すべきは、力まかせ (Brute Force) による部分文字列検索です。複雑なアルゴリズムに進む前に、文字列アルゴリズムのもっとも基本的な道具である、以下の3つを使いこなせるようにしておきます。

  • 文字列から文字へのインデックスアクセス (charAt)
  • 文字列の長さ (length)
  • 文字の比較

「簡単なものから実装する」というルールは、他のアルゴリズムを実装するときにも使えます。「早すぎる最適化を避ける」というのは、ソフトウェア開発におけるベストプラクティスのひとつでもありますよね(なかなか実践できないことでも有名ですが)。

また、他の文字列検索アルゴリズムを実装するときのベースラインにもなります。「推測するな、計測せよ」という格言もありますが、より高度なアルゴリズムを実装するとき、ベースラインからどれくらい速くなったかは重要です。また、速いアルゴリズムを実装したはずなのにそんなに速くなってない、といったケースでバグを早期に発見できることがあります。

マルチバイト文字の扱い

  • 可変長の符号化方式を使っている場合は、文字単位でのインデックスアクセスが困難
  • Unicode であれば UTF-16/UTF-8 が主流
    • UTF-32 なら困難はない(固定長なので)
  • UTF-16/UTF-8 は可変長の符号体系
    • UTF-16: サロゲートペア
    • UTF-8: 1文字のサイズが可変(1バイト〜4バイト)
  • 検索結果として得られる位置は文字の位置ではない
    • UTF-16: サロゲートペアがない場合は文字の位置と一致
    • UTF-8: 位置はバイト配列の何バイト目かを表すにすぎない
  • UTF-16/UTF-8 であれば、文字の内部でヒットすることを避けられる
    • UTF-8: 文字の1バイト目かそうでないかが判定できる符号体系(ビット演算で判定可能)
    • UTF-16: 2バイトのコードポイントで判定できる符号体系
  • Unicode のサロゲートペアとは何か - ひだまりソケットは壊れない

番兵とヌル文字

アルゴリズムによっては番兵(何かしらの区切りや終端を表現する文字)が必要なものがあります。このとき、文字列中のどんな文字でもない文字を番兵として使うことになります。ヌル文字 \0 を使うことが多いようです。

しかし、バイナリに対して文字列アルゴリズムを適用したい場合、これが問題になります。バイナリを文字列として捉え、その中から検索したいとします。すると、バイナリには \0 が現れないことを保証できないため、番兵が用をなさなくなってしまいます。

つまり、バイナリも扱いたい場合は、番兵を陽に使わないような工夫が必要なのですが、アルゴリズムによって対処方法は異なりますし、実装難易度は跳ね上がります。頑張りましょう。

さらに学ぶために

まとめ

  • アルゴリズムを勉強するには、なにより実装してみることが大事
    • grep などの文字列検索アルゴリズムが具体的にイメージできるようになる
    • アルゴリズムをより深く理解できる
    • モチベーションを保ちやすい
  • 社内では文字列アルゴリズムの勉強にも Coursera を活用しています
    • わかりやすい講義ビデオ、雛形を用意してくれている課題
    • モチベーションまではコントロールしてくれない
    • 仲間を集めるなどの工夫もあるとよいですね
  • 抑えておくべき文字列アルゴリズムを簡単に紹介しました
    • 他にもおすすめのアルゴリズムがあれば教えてください!

はてなでは文字列が好きなエンジニアを募集しています。

hatenacorp.jp

明日のアドベントカレンダーは id:takuji31 さんです!

「Hatena Engineer Seminar #7 @ Tokyo」を開催しました & 資料を公開しました! #hatenatech

こんにちは、Web アプリケーションエンジニアの id:KGA です。

去る、12月6日(火) にはてな東京オフィスのイベントスペース SHIBAFU において Hatena Engineer Seminar #7 @ Tokyo を開催いたしました。平日夜の開催にもかかわらず多数の方にご来場いただき誠にありがとうございました。

今回は普段のエンジニアセミナーとは趣向を変え、はてなのエンジニアがどのように成長してきたか、これから先どのようなキャリアを描いているか、会社としてその成長やキャリアを支える制度や環境がどうなっているか、などについて発表をさせていただきました。また、トーク・LT の後にはプロデューサーを司会に据えパネルディスカッションを行いました。こちらも普段とは趣向を変えた催しでしたがトーク・LT だけでは伝えきれなかったはてなのエンジニアの本音や深い部分をうまく引き出してみなさまにお伝えできたと思います。

f:id:KGA:20161206205259j:plain

パネルディスカッションの最後にもお伝えしましたが、はてなでは中途採用、新卒採用を問わず各エンジニア職を鋭意募集していますので、少しでもご興味を持たれた方はぜひご応募ください!

hatenacorp.jp

hatenacorp.jp


最後になりましたが、当日の発表資料を掲載いたしますのでぜひご覧ください。この度は多数のご参加、誠にありがとうございました。次回も会場にてお会いできることを楽しみにお待ちしています!

トーク

はてなで一人前のエンジニアになる方法 id:hakobe932

speakerdeck.com

エンジニアがはてなでマネージャーをやるということ id:Songmu

blog.song.mu

LT

ウェブオペレーションエンジニアになるまでの思い出 id:y_uuki

blog.yuuk.io

ぼくがセールスエンジニアというキャリアを選んだ理由(わけ) id:a-know

www.slideshare.net

はてな社内で行なっている機械学習勉強会について紹介します

このエントリは、はてなエンジニアアドベントカレンダー2016の21日目の記事です。昨日は id:hakobe932さんによる次に何を勉強するかを決めるための作戦でした。

こんにちは、アプリケーションエンジニアのid:syou6162です。このエントリでは、今年の9月から社内で行なっている機械学習勉強会についての概要と、この数ヶ月でやってきた内容について紹介したいと思います。

最新の論文読み会、だけでいいのか?

私は今年の3月まで自然言語処理/機械学習の企業研究者として働いていて、転職して4月からはてなでエンジニアとして働いています。「転職してからあまり論文読めていないなぁー」と思い、秋から論文読み会をやろうと考えていました。しかし、(まだまだ期間は短かいですが)春からエンジニアとして働いてきたことを振り返ると、機械学習をサービスに展開するにあたって、論文を読むだけでは十分ではないとも思いました。例えば、少なくとも以下のような要素は考慮する必要がありました。

  • 学習データをどうやって集めるのがよいかは自明ではない
    • エンジニアはひたすら地道にアノテーションし続けるのが得意というわけでもない
  • はてなの場合、機械学習に強みを持つエンジニアはある程度いるが、多数派ではない
    • あまりに複雑すぎるものを採用すると技術的負債になる可能性が高い
    • 達人による謎のチューニングテクニックがないと精度が出ないようなものは困る
    • サポート担当から「ユーザーからこういう指摘があった」という報告を受けた際、精度はよいが挙動が掴みにくいものだと具体的にどう改善していいか分からず困る...
  • 機械学習を使った機能は機械学習を使わない機能よりもディレクターやプロデューサーが工数の見積りづらい
  • 大量のユーザーに機械学習を使った推薦や異常検知を行なうにはどういったインフラ/ミドルウェアが適しているのか

こうして列挙してみると、アプリケーションエンジニアだけでなく、インフラエンジニア、サポート、ディレクター、プロデューサーなど様々な職種の人と一緒に考えていくとよいのではないかと考え、社内で参加者を募りました。今日までに機械学習勉強会を7回開催しましたが、アプリケーションエンジニア、インフラエンジニア、ディレクターが定期的に参加してくれています。他業種が集まって機械学習を使ったサービスについてディスカッションしていくと自分にはなかった観点の発見がありました。

他社から学ぶ

勉強会の最初の時期はサービスに機械学習をうまく使っている事例を見ることによって知見を集めました。何社かの事例を見ましたが、サイバーエージェントさんの以下のテックレポートが特に参考になりました。

私としては、特に特徴量や精度の時系列変化をきちんと計測するという取り組みが特に参考になりました。Webシステムではユーザーのフィードバックを受けて教師データが追加/編集されることがありますし、あるデプロイのタイミングから特定の特徴量の抽出が失敗して精度が悪化している(しかし、分類器としては何かしらのラベルを返しているため、発見が遅れてしまう!)こともあります。spammerの傾向も変わります。

まずは身近なところから始めてみようということで、自分で運用しているエントリ推薦botの予測精度や教師データのラベルの割合をトラッキングするところから始めていますMackerelでグラフにすることでどの時期のデプロイから精度が落ちたかや、精度が閾値を下回ったらSlackに通知させることができるようになりました。

f:id:syou6162:20161217224857p:plain

機械学習の学習データをどうやって効率的に作るか

機械学習を使ったサービスを作るにあたって、学習データの作成は欠かせない工程です。しかし、淡々とデータを作っていくのはなかなか大変ですし、同じ精度を出せるならなるべく少量のデータで実現できるとハッピーです。これを実現する手法として能動学習が有名ですが、私自身試した経験がなく社内であまり試されていませんでした。いい機会だったので実際のタスクで実験し、結果を勉強会で共有しました。

結論としては、割と効果があり闇雲にアノテーションを頑張るより能動学習を積極的に使っていくとよさそうだという知見が得られました。論文で似たような事例を紹介してもよかったのかもしれませんが、はてなの実際のタスクを元に実験をしてどれくらいの作業時間が減らせそうかが(一例ですが)分かったということもあって、データ作成を含めた機械学習のプロセスをまだやったことがないエンジニアやディレクターの参考になっていると嬉しいです。

サービスへの事例や技術選定の基準をキーワードにまとめていく

はてなでは機械学習専門のチームはまだ存在しないため、社内で適用された機械学習に関する情報が各チームばらばらに存在している状態でした。情報がばらばらになっていると、例えば以下のようなときに困ります。

  • あるサービスで使われている機械学習の学習データがどこにあるか分からないため、改善しようと思っても気軽にはアイディアを試せない
  • 新しいサービスに機械学習を使おうとしているが、どの学習器を使うと安定して運用、メンテナンスしていけるのか指針が立てられない

こうした状況はあまり好ましくないため、技術グループ(はてなの技術グループに関してはid:motemenさんがまとめています)のはてなグループのキーワードに学習データのリソースの場所、使っている学習器、開発当時の担当者のログ(日報)などをまとめていきました。

また、はてなのサービスはリリースされると長く運用されることもあり、パフォーマンス要件を満たしつつ継続的にメンテナンスしやすい技術を選んでいく必要があります。プログラミング言語やミドルウェアに関しては技術グループで基準がありましたが、機械学習についての基準は特にありませんでした。機械学習勉強会をきっかけに機械学習をプロダクトに入れる際に考える採用基準について考え、一箇所にまとめる活動をしました。

ときには最近の機械学習技術の話題にもキャッチアップ

機械学習や自然言語処理界隈は皆さんご存知の通り、急速な勢いで発展していることもあるため、最近の研究開発の話題もときどき勉強会で取り扱っています。最初はランチタイムにGitHub Enterpirseのissueに書き込んだネタを共有していたのですが、社外に公開したほうがより情報も集まってくるだろうということで、定期的にまとめたものを公開しています。

これらの回では1つのネタは軽く紹介するものでしたが、そうでない回もあります。先月はGoogle翻訳が深層学習を使ってかなり大きな改善をしたというイベントがあったため

  • 深層学習以前はどのようなものが困難であったか
  • どのような技術を用いてそれらを解決したのか
  • 深層学習を用いたことによる新たな問題点にはどのようなものがあるか

を中心に説明しました。最先端の細かい技術は数年するとディファクトが変わっている可能性も大きいため、今後も(直接ではないにしろ)残っていくと思われるAttention付きのEncoder-Decoderについて説明しました。

また、機械学習の新たな応用先の1つと考えられているクラウドソーシングやhuman computationについて学んだ回もありました。

まとめ

社内で行なっている機械学習勉強会、その勉強会がきっかけになって行なっている活動についてまとめました。このような機械学習の様々な分野への適応事例、最新の手法の紹介などに興味があり、現在機械学習に取り組まれているエンジニアの方(現在は京都オフィスのみで勉強会をやっておりますので、京都でお会いできる方)は是非ランチタイムに遊びにきてください。ご連絡は @syou6162 までどうぞ!

hatenacorp.jp

明日のアドベントカレンダーはid:takuya-aさんです!

オープンソース活動への取り組み方

はじめまして。iOSとAndroidアプリの開発を行っている、アプリケーションエンジニアの id:ikesyo です。今年1月の入社後、初めての開発者ブログでの記事になります。最近の大きな出来事は、家族会議の結果、『ユーリ!!! on ICE』のBlu-ray全巻購入をしたことです。

この記事は、はてなエンジニアアドベントカレンダー2016の17日目の記事です。昨日は id:aereal によるはてなブログのデプロイを約6倍高速化したはなし - Sexually Knowingでした。

私は現在、自分のライブラリであるHimotokiやiOS開発用のパッケージマネージャのCarthageなど、全部で十数個のオープンソースソフトウェアの開発、メンテナンスを行っています。最初の頃はGitHubでPull Requestを出すのも怖かった私ですが、自身の活動を振り返ることで、どのようにオープンソース活動に取り組んでいくかという一つの事例をお見せしたいと思います。

目次

はじめに

私がオープンソース活動にのめり込んだのは、ReactiveCocoaというリアクティブプログラミングライブラリとの出会いがきっかけでした。GitHubが、自社で開発していたものをオープンソース化したそのライブラリは、当時の自分にとって未知の領域であり、その元となったRx.NETに関する記事やドキュメントをたくさん読んだり、関数型プログラミングの考え方に触れたりと、非常に興奮したのを覚えています。内部実装に興味を持ってコードリーディングもしていたと思います。

当時はフリーランスのiOSアプリエンジニアをしていたのですが、機会があればそれを仕事で使ってみたいと思っていました。2013年にある仕事で実際に使う機会に恵まれ、使い込んでいく中では色んなバグに遭遇しました。日本語の情報もほとんどない中で、使い続けるには自分でバグを直すしかないという気持ちになり、ここから本格的にGitHubにPull Requestを出していくことになります。

コードを直すこと自体はそれほど問題がなくても、タイトルや説明の英語をどう書いていいものかと悩み、ちょっとした修正を出すのに何十分、何時間と掛かっていたこともあったように思います。レビューのやり取りをするにも、こんな英語でいいだろうかと悩んだりもしていましたが、回数を重ねるうちに、コードの中身が明瞭であれば十分だと思ったり、他者の会話から言い回しを参考にすれば必要なコミュニケーションは取れるのだと、開き直ることができました。そうしてレビューをパスしてマージされた時にはとても興奮したものです。

フリーランスをしながらオープンソース活動をしていく中で、Cocoa勉強会関西関西モバイルアプリ研究会といった勉強会に出会いました。活動の経験や成果を発表していたところに声を掛けてもらい、はてなに入社する運びとなったのでした。オープンソース活動が高じて転職したことになりますね。

といったところで、ここからは実際の活動を時系列で振り返ってみようと思います。

活動遍歴

https://github.com/pulls を開くと、自分の過去のPull Requestを確認することができます。遡ってみると初めてのPull Requestは2012年でした。

2012年

  • Kiwi
    • https://github.com/kiwi-bdd/Kiwi/pull/124
    • 初めてのPull RequestはKiwiというBDDのテストライブラリでした。テスト名称に日本語を使うと挙動がおかしくなるというのを直していたようです
  • ReactiveCocoa
    • ReactiveCocoaを知ったのもこの年でした。まだまだモバイル開発での認知度は低かったですね……
    • この頃はまだcontributeはしていませんでした

この年はKiwiの1件のみでした。

2013年

  • ReactiveCocoa
    • 2013年に入ると、ReactiveCocoaへ時々Pull Requestを出すようになっていました
    • きっかけは仕事でこのライブラリを使い出したことでした。挙動を追うためにソースコードを読む中で気になった箇所を改善したり、遭遇したバグを修正するなどしていました

2014年

  • ReactiveCocoa
    • 前年末に集中的にPull Requestを出していたら、年明けすぐにCollaborator(いわゆるコミッター)の誘いを受け、コミット権を得ました
    • この辺りからGitHubでのオープンソース活動にハマっていった感がありますね
  • Quick (Nimble)
    • https://github.com/Quick/Quick/pull/57
    • WWDCでのSwift発表直後に登場したBDDテストライブラリで、Swiftのコードを書いてみたくていじっていた気がします
    • 後にCollaboratorになります
  • その他

2015年

2014年まではPull Requestの数は20〜30程度でしたが、この年にはなんと370程と爆増しました。Swift製のライブラリが増えてきて、Swiftのコードを書くのが楽しかったこと、Carthageに深くコミットし始めたこと、自分のライブラリの開発もPull Requestベースで進めていたことが増加の要因だと思います。

  • ObjectMapper
    • Swift製のJSONマッパーです。これも仕事で使っていたもので、機能追加やリファクタリングを多数行いました
    • が、Collaboratorにはなっていません
  • Himotoki
    • 自作のJSONマッパーです。自分のライブラリでちゃんと認知を得たのはこれが初めてでした
    • ObjectMapperのAPIや内部実装に不満が出てきた頃に、ならば自分で作ってしまえばいいと気付かせてもらって、作り始めたのがきっかけです。
  • APIKit
    • 設計が参考になるなーと思ってコードを読んでいる中で、ちょっとしたコード改善をたくさん積み上げていき、Collaboratorにもなっています
  • ReactiveCocoa
    • Swift対応が本格的に始まり、継続的にコミットしていました
  • Carthage
    • Swift製のパッケージマネージャです。ReactiveCocoaのSwift版を実プロジェクトで使ってみるという実地検証の役割もあったものでした
    • ReactiveCocoaのSwift版を洗練させていく過程でここのコードにも関わっていき、Collaboratorになりました
  • その他
    • ReactiveCocoaやCarthageの依存ライブラリである以下のライブラリも、芋づる式にメンテナンスするようになりました
    • Result
    • Commandant
    • ReactiveTask

2016年

昨年よりも手を広げた結果、以下のライブラリのCollaboratorにもなりました。現時点でのPull Request数は440程まで増えていました。こんなにやっていたのか……。

  • はてなに入社しました
    • 活動ペースは落ちていません
    • SwiftGenなど、業務に関係して業務時間中に作業したPull Requestもあります
  • ReactiveSwift
    • Objective-CのAPIも並存していたReactiveCocoaからSwiftのAPIを分離したもの
  • Tentacle
    • Carthageから分離したGitHubのAPIクライアント
  • OHHTTPStubs
    • APIKitやTentacleで使用していたネットワークのスタブライブラリ
  • SwiftGen
    • 画像やStoryboardなど各種リソース用のコード生成ツール
  • Quick, Nimble
    • 先述のテストライブラリで、ReactiveCocoa、Carthageでも使っています

何に取り組むか

いざオープンソース活動をしようと思っても、何に取り組むかを考える必要があります。これまでの経験で、私は以下のような探し方をしています。

  • 仕事で使っているもの
    • 使い込む中で機能不足やバグにぶつかることが多いので、一番モチベーションが湧きやすいのではないでしょうか
  • ライブラリの依存ライブラリ
    • OSや言語のバージョンアップへの追従の関係で、依存ライブラリのアップデートにも先手を打っていきたくなるものです
  • オーナー、メンテナーの別プロダクト
    • 人単位で見ていくのもよさそうです
    • 別のプロジェクトで既知の間柄だと、Pull Requestを出すのも気が楽になります
  • https://github.com/trending
    • 言語別に最近盛り上がっているリポジトリを探せます(たまに見る程度)

私の場合、ReactiveCocoaからCarthage、そしてその他諸々、と依存の依存を芋づる式に辿っていくことで関わる範囲が広がっていきました。逆に、自分の関わるライブラリに依存している依存元を辿っていく方向もありそうです。

どう貢献するか

  • 機能追加、バグ修正
    • 最も直接的な貢献方法ですね
  • リファクタリング
    • 既存コードのちょっとした整理だけでも、結構やる余地があるものです
  • バージョンアップ追従
    • 依存ライブラリ、言語バージョンなど
    • iOSやSwiftのようなバージョンアップ頻度が高いものは、貢献のチャンスも多いです
    • 今時のSwift界隈では、Swift Package ManagerやLinux対応も貢献チャンスです
  • Issueへのコメント、回答
    • GitHubやStack Overflowでのユーザーの質問に答えることも、メンテナーの負担を軽減することで大きな意味があります
  • ドキュメンテーション
    • 直接コードをいじらずとも、ドキュメンテーションコメントやREADMEなどを充実させていくのも立派な貢献です

気を付けること

  • CONTRIBUTING.md
    • Issue報告やPull Requestを出す上での注意点、新バージョンのリリース方法などが記載されていたりします
    • あったら一読してからアクションを起こしてみましょう
  • コーディング規約
    • 明確な規約がない場合もありますが、既存コードや過去のPull Requestでのレビュー内容をチェックして、雰囲気を掴むのも大事
    • 「郷に入っては郷に従え」
  • Issue, Pull Requestのテンプレート
    • テンプレートがある場合は無視して削除したりせず、きちんと埋めてあげましょう

気にし過ぎないこと

  • 英語
    • ネイティブじゃない人も多く、多少文法がおかしくても伝わるし、そんなに気にされないはずです
    • チャットではないので、コメントを書くのに多少時間が掛かっても問題ありません
    • Google翻訳の精度が上がったのも助けになりそうです
    • リポジトリをWatchしてIssueやPull Requestのコメント、議論をなんとなく眺めていると、言い回しや語彙のパターンが掴めるかもしれません
  • 焦らない
    • 時差の関係で、コメントの一往復に1日掛かることも珍しくありません
    • 空き時間の対応だったりするので、回答やレビューに時間が掛かることも多いです
    • 気長に待ちましょう

おわりに

オープンソースソフトウェアへの貢献の種は色んなところに落ちています。身近なものや簡単に出来ることから始めて、気楽にオープンソース活動に親しんでいってみてください。

とはいえ、それが行き過ぎて負担になって消耗しては元も子もないですし、GitHubに草が生えていないとダメというわけでもありません。とにかく気楽にやっていきましょう(私も最近はFINAL FANTASY XVに時間を奪われがちです)。

hatenacorp.jp

明日のアドベントカレンダーは id:taketo957 です!

P.S.

来週12月23日(金)発売のWEB+DB PRESS Vol.96(なんと16周年!)で「Swift 3開発最前線」という特集記事を執筆しました。こちらもぜひお楽しみください!

gihyo.jp

フロントエンドPodcastはじめました

こんにちは! id:amagitakayosi です。
はてなブログチームで JavaScript と Perl を書いています。

最近はてなでは Frontend Lunch Podcast を配信しています。

(2017-02-07 15:38 追記) Podcast配信サイトを移転したので、配信URLを変更しました。

この記事では、Podcast を始めた経緯を通じて、はてなのフロントエンド会の取り組みを紹介したいと思います。

目次

フロントエンド会の発足

はてなには「フロントエンドエンジニア」という肩書のメンバーはいません。
人によって得手不得手はありますが、サーバー側のアプリケーションもクライアント側のJavaScriptもアプリケーションエンジニアが開発する、というスタイルです。

このスタイルのメリットとしては、タスクを柔軟にアサインできる事、チーム間でのメンバー入れ替えがしやすい事や、機能ごとの属人性を抑える事ができるといった点が挙げられます。
反面、フロントエンドに特化した設計やライブラリなどの知見をまとめるのが難しいという課題があります。
また、フロントエンド界でのニュースや流行の変化にキャッチアップするための場が欲しいという要望もありました。

こうした背景から、はてなでは本年度「フロントエンド会」というグループを設置し、フロントエンドに関する話題を引き受ける活動を始めました。

フロントエンドMTG

まずは、社内のプロジェクトの状況を把握するためのMTGを行ないました。
各チームからフロントエンドに明るいメンバーを一人ずつ選出し、毎回順番にチームの現状について報告してもらいます。
主な議題は以下のとおりです。

  • プロジェクトのフロントエンドまわりの構成
  • 最近の取り組み
  • 課題 (パフォーマンス、ビルドツール、ファイルサイズ等)

MTGで出た話題については、議事録でいつでも参照できるようにしました。

f:id:amagitakayosi:20161215183130p:plain:w600

これにより、設計の知見やライブラリの選定など、ある程度社内で共通した方針が得られました。
また、実際に他チームのコードを読んで議論することで、具体的な改善のアドバイスをしたり、自分のチームとの実装の差異を知ることもできました。

一方で、このMTGでは報告を行なうメンバーの負担が大きく、開催間隔があいてしまう為、技術共有の頻度が低くなってしまいました。
また、選ばれたメンバー以外にとっては参加の敷居が高く、裾野を広げられないという問題もありました。

そのため、一通り現状共有した時点でMTGは終了とし、別の方法を探ることにしました。

フロントエンドランチ

9月初旬より、毎週水曜日の昼休みに「フロントエンドランチ」を行なっています。
なるべく気軽に参加できるよう、誰でも参加OKとし、昼食を食べつつ雑談できるような雰囲気を目指しました。

ランチタイムになると会議室に集まり、 Google Hangouts を開始します。
はてなのオフィスは東京/京都の2箇所に分散しているので、 Hangouts で画面共有するようにしています。

フロントエンドランチは以下のような流れで行なっています。

  • JavaScript関連のニュース閲覧
  • 勉強会チェック
  • 近況報告/相談

まずは一週間分のJavaScript関連ニュースをチェックします。
JSer.infoJavaScript Weekly, Frontend Weekly といったメールマガジンが主な情報源です。
時間に余裕があれば Echo JS で新しい話題を探したりもします。

続いて、勉強会の情報をチェックします。
主に dotsJavaScript タグで絞り込んで見ています。
(例: https://eventdots.jp/event/search?keyword=javascript&pref=&from=2016-09-28 )

最後に近況報告タイムです。
参加者同士で、最近書いたコード、お悩み相談、気になったニュース等を共有します。
いくつか話題がある場合、あらかじめ GitHub issue に近況をまとめておいたりもします。

f:id:amagitakayosi:20161216084210p:plain:w640

フロントエンドランチを始めたのは、以下のツイートを見て「ランチタイムなら気軽に続けられるのでは……?」と思ったのがきっかけでした。

twitter.com

「とにかく気軽に続ける」をモットーに始めましたが、今のところ毎回3〜7人程の社員が参加してくれています。
あまりフロントエンドを触らないメンバーも参加してくれているので、当初の目論見はある程度実現できたと思っています。

Frontend Lunch Podcast

フロントエンドを続けていく内に、「参加していない人も会話の記録を聴けるようにしたい」と思うようになりました。
せっかくのランチタイムに毎回出席するのもしんどいし、用事が入って出席できない人もいますからね。

議事録をとったり、 Hangouts を録画するという方法も考えましたが、気軽に共有するのが難しそうです。
そんなわけで、フロントエンドランチの様子を録音し、 Podcast 形式で社内配信することにしました。

実は、はてなには社内で Podcast 配信を行なう社内サービスがあります。
配信当初はこれを利用して社員向けにのみ配信していました。

f:id:amagitakayosi:20161216090749p:plain:w640

ただ、続けるうちに、せっかくなので社外に配信したいという気持ちが強くなってきました。
はてなのフロントエンド方面でのアウトプットを増やすチャンスです。

社内の広報やチーフエンジニアに確認をとり、2016/11/07 から社外への公開をはじめました。
現在では、毎週できあがった音源をチーフエンジニアにレビューしてもらって公開する、というフローで運用しています。

配信技術について

この章では、 Frontend Lunch Podcast の配信に利用しているサービスや機材を紹介します。

録音には Chromebox for meetings のマイクを利用しています。
Chromebox for meetings は Google Hangouts でリモート会議を行なうためのシステムです。
このマイクはノイズを良い感じに軽減してくれます。
もともと画面共有のために Hangouts を利用していたので、ついでに録音もできて便利。

Soundflower を利用すると手元の PC の音声出力を音声入力にループバックできるので、これを手元の録音ソフトで録音します。

マイク -> (Google Hangouts) -> MacBookPro の音声出力 -> (soundflower) -> MacBookPro の音声入力 -> 録音ソフト

本家 Soundflower は最近の mac で動かないので、 fork 版を使うと良いです。
https://github.com/mattingalls/Soundflower

編集には Logic Pro X を使っています。
マイクとの距離によって声の大きさがバラついてしまったり、食器やエアコンの音が入ってしまうので、いくつかエフェクトをかけています。
具体的には、 EQ で 300Hz 以下をカットしたり 1kHz 辺りを上げて声を聞き取りやすくしています。
また、 Adaptive Limiter を重ねることで音量をそろえています。

普通に会話していると、どうしても間があいてしまったり噛んだりしてしまうので、そういった部分はカットするようにしています。

f:id:amagitakayosi:20161216094322p:plain:w640

配信には SoundCloud を利用しています。
SoundCloud は自動で RSS フィードを生成してくれるので、フィード URL を iTunes に登録するだけで Podcast を配信できます。
ただ、アップロード容量の制限もあるので、将来別のサービスに移行することも考えています。

f:id:amagitakayosi:20161216095841p:plain:w640

(2017-02-07 15:52 追記)
Podcast配信用Webサイトを作成し、SoundCloudから移行しました。
サイト作成にはr7kamura/yattecastを利用し、GitHub Pagesでホスティングしています。

hatena.github.io

おわりに

もし Podcast を聴いてはてなに興味をもってくれた方がいらしたら、是非遊びにきてください!
ランチを食べながらお話ししましょう!

ご連絡は @amagitakayosi までどうぞ!


株式会社はてなでは、社内外を問わず技術情報の共有に積極的なエンジニアを募集しています。

hatenacorp.jp

本記事は はてなエンジニアアドベントカレンダー2016 の15日目の記事です。
昨日は id:itchynyMackerelにおけるフロントエンドのパフォーマンス改善の取り組み - Hatena Developer Blog でした。 次の担当は id:aereal です。

Mackerelにおけるフロントエンドのパフォーマンス改善の取り組み

この記事は、はてなエンジニアアドベントカレンダー2016の14日目の記事です。13日は id:astj による『Perl 6 のモジュールエコシステムの話とモジュールを公開する話 (2016年12月版) - 平常運転』でした。

こんにちは。Mackerelチームでアプリケーションエンジニアをやっている id:itchyny です。

Mackerelは、同じ役割を持つホストを束ねた「ロール」、そしてロールを束ねた「サービス」というまとまりでホストを管理し、一覧性の良いグラフ画面を提供しています。 ロールあたりのホスト数、そしてサービスあたりのロール数が増えると、グラフの画面のパフォーマンスに大きく影響します。 Mackerelチームでは大規模なサービスでも快適にグラフを閲覧できるように、継続的に画面のパフォーマンスを改善してきました。

本記事では、Mackerelのフロントエンドのパフォーマンスを改善する過程で得られた知見をご紹介します。

パフォーマンスを改善するためにやったこと

計測する

計測することはパフォーマンス改善の最初の一歩として、とても大切です。 計測しなければ、なぜその箇所を改善するのかを他の人に伝えることができません。

JavaScriptのパフォーマンスを計測するには、ブラウザーのプロファイラーを使うのが手軽だと思います。 実行している全てのコードに対して、実行時間やコールスタックを表示してくれます。

ところが、重厚なWebフレームワークの上に乗っていると、本当に重いところになかなかたどり着くことができません。

f:id:itchyny:20161215135409p:plain

複雑なコールスタックを展開していくと、実行時間が支配的ではなくなって本当にチューニングする必要があるかよくわからなくなります。 表示されるのはフレームワークの関数なのだけど、その関数が呼ばれるきっかけを作っているのは自分たちのコードである、そういう場合には本当に悪い箇所にたどり着くのが困難になります。 フレームワークのとある関数が重いと分かっても、自分が渡している引数のせいで重くなっていると表示されているケースでは引数毎に実行時間を集計したくなるのですが、そういうことはできません。

フレームワークを使っていると、JavaScriptの処理系からはまずフレームワークの関数が見える状態になり、私たちが書いたコードはフレームワークの複雑な処理に隠れてしまいます。 運良くコールスタックを辿ることができて、自分たちのコードの重いところや、必要以上にフレームワークの関数を呼びすぎている箇所を特定できれば、うまく改善できるでしょう。 しかし、私たちがやりたいことは、そんなに苦労してブラウザーのプロファイル結果を掘り起こして原因を頑張って見つけるということではありません。

改善したいコードが自分たちのコードならば、自分たちのコードに限ってプロファイルをとればよいのです。

パフォーマンスのプロファイラーを自分で書く

JavaScriptのパフォーマンス改善に本気で取り組んでくださいと言われたその日のうちに、ブラウザーのプロファイラーでは知りたいことが何もわからないということに気が付き、その夜にはHaskellでプロファイラーを書き始めていました。

人に伝えやすいのでプロファイラーと言っていますが、正確には「プロファイルをとるコードを注入するトランスパイラー」です。 JavaScriptのコードをパースして構文木にし、全ての関数の最初と最後に、実行にかかった時間を計測するコードを挿し込んでくれます。 変換したコードを読み込めば、console.logで重い関数を表示してくれます。 やってることは単純なので半日くらいで書くことができました。 実際に、次の日からsjspでプロファイルを取り、いくつも重い処理を改善してきました。

ブラウザーのプロファイラーで重いと分かるのがフレームワークの関数だったとしても、実際に重い処理が自分が書いたコードというのはよくあることです。 私たちが書いたコードは複雑なコールスタックの波に飲み込まれ、ブラウザーのプロファイラーの中では霞んでしまうかもしれません。 フレームワークの作者ならいざ知らず、自分のコードのチューニングしたいなら自分のコードだけのプロファイルを取るべきです。 実行時間で支配的なコードが自分が書いたコードだと気がつかずに、なぜかフレームワークの関数ばかり表示するブラウザーのプロファイル結果ばかり眺めてフレームワークの批判をするのは的はずれです。

  • どのコードのプロファイルを取りたいのかきちんと考えよう
  • 計測は大事。よい計測ができないならば、よい計測ができるツールを作ればよい

LIMITする・グラフの簡略化

Mackerelでは1ロールに何ホストでも登録できるようになっており、特に制限は設けていません。 ロールに属する全てのホストの情報を返すようになっていると、数百ホストを超えるロールが複数存在するような大規模な使い方に対応できません。 ロールのグラフのホストの一覧では、現在では一定数以上を超えると全ては返さずに、ページングのあるホスト一覧画面へのリンクが表示されます。

また、1つのロールのホストが1000を超えるとメトリックの線が重なりグラフが見にくくなり、ブラウザーの描画も負荷がかかるようになります。 かつては全て描画していたのですが、グラフの描画に時間がかかるという問題があったため、今はグラフを簡略化しています。 ホストの数が多い場合、折れ線グラフでは最大と最小と平均の三本が、積み重ねグラフでは合計の一本が表示されるようになっています。 ユーザーは必要に応じて簡略グラフと全てのメトリックを表示するグラフを切り替えることができます。

  • 仕様で上限がないものがあれば、一定数以上はページングのある画面で見てもらおう
  • 情報が落ちることは覚悟して、最小でどういう情報があれば役に立つかを考えよう

JSONの構造を変更する

Mackerelのフロントエンドは、できるだけサーバーサイドよりもクライアントサイドでレンダリングするという方針で設計しています。 ホストのステータスや表示するグラフや期間の変更、監視ルールの作成や更新・削除、グラフのチャットツールへの共有など、多くの操作が画面遷移せずにXHRで通信し、画面を動的に更新するからです。 最初からクライアントサイドでレンダリングするようにしておくと、定期的にアラートの状態を取得して画面を更新して欲しいといった要求にも簡単に対応できます。

Mackerelではページを開いた時に多くの情報をJSONで取得しています。 特に、サービスのグラフ一覧画面では画面の描画に必要な情報がたくさんあり、そこで叩いているJSONが肥大化するという問題がありました。

JSONが肥大化した時に考えることは3つあります。 まずは不要なデータがないか調べること。 開発が何年も続く中で、かつて画面に表示していたものをいつの間にか出さなくなったのに、まだクエリを叩いてJSONに入れていた、そういうものが残っている場合があります。 昔から叩いているJSONは特に注意して調べて、不要な情報があれば削りましょう。

もう1つは分割すること。 XHRの中である一つのJSONが飛び抜けて大きく、そのサイズの半分を超えるまとまった情報があるならば、それを単体のJSONとしてAPIを分けると良いでしょう。 データを並行に取りに行くとXHRにかかる時間が半減します。 実際、グラフの一覧画面ではホストのデータのJSONを別のAPIに切り出しました。 ただし、分割しすぎると共通の処理で無駄にクエリを叩くことになり、またXHRが増えてかえって遅くなることもありますので、全てを細かく分割しろと言っているわけではありません。

そして、重複しているデータがないか考えるということです。 例えば次のような構造のデータがあったとします。

{
  "monitorsByRoleId": {
    "roleId0": [ { monitor0 }, { monitor1 }, { monitor3 } ],
    "roleId1": [ { monitor1 }, { monitor2 }, { monitor3 } ],
    "roleId2": [ { monitor1 }, { monitor2 }, { monitor3 }, { monitor4 } ],
    // etc.
  },
  // etc.
}

monitorN のデータが大きくて、かつ重複も多かったので、次のようにJSONの構造を変更しました。

{
  "monitorsById": {
    "monitor0Id": { monitor0 },
    "monitor1Id": { monitor1 },
    "monitor2Id": { monitor2 },
    "monitor3Id": { monitor3 },
    "monitor4Id": { monitor4 },
    // etc.
  },
  "monitorIdsByRoleId": {
    "roleId0": [ "monitor0Id", "monitor1Id", "monitor3Id" ],
    "roleId1": [ "monitor1Id", "monitor2Id", "monitor3Id" ],
    "roleId2": [ "monitor1Id", "monitor2Id", "monitor3Id", "monitor4Id" ],
    // etc.
  },
  // etc.
}

この変更によって、監視ルールを多く設定している場合にJSONのサイズを大きく減らすことができました。 しかも元と全く同じ情報を簡単に再構築することができます。

  • 昔からあるAPIには注意し、現在のテンプレートに不要なデータを引いてないか調べよう。
  • JSONのサイズを注視して必要であれば分割しよう。サーバー側のアプリケーションのシリアライズ、サーバーの転送量、ネットワークの帯域、ブラウザーでの展開処理速度など、様々な場所に影響します。
  • JSONのデータが冗長ではないかを確認すること。重複があればまとめる。一覧で全ての中身を返している時は、idの一覧にして中身は別に持つことを検討する。それでデータサイズが減るならば、JavaScriptの処理が複雑になりすぎない範囲で改善する。

フレームワークに対する理解を深める

MackerelではAngularJS 1を採用しています。 パフォーマンスについて語っているのにAngularJS 1を使っているのかと首をかしげるかもしれませんが、Mackerelの開発が始まった頃は今を時めくReactもそこまで有名ではなく、当時の状況を考えれば良い選択をしたと思います。 おかげさまでこれまで簡単にテンプレートを作ったり、インタラクティブなフォームを効率よく作ることができました。 フレームワークというのはロックインが怖いもので、何万行もコードが積み上がったらやめるのが大変になります。 そういう状況になると、何か月もかかるであろう移行を考えるよりも、まずは使っているフレームワークと真摯に向き合う姿勢が大事なのです。

フレームワークに重い処理をさせている大元は、私たちが書いたコードです。 AngularJS 1を使っているとついつい重い処理のコードを書きがちという主張には同意しますが、行儀よくコードを書いてパフォーマンスを引き出す手法はいくつもあります。 ハマりどころが多くて重くなりがちだという批判は理解できますが、フレームワークの特性も知らずに何のチューニングもしていないのに、フレームワーク自体のコードが重いと思ってしまうのはよくありません。

AngularJS 1を使ったアプリケーションのパフォーマンス改善は、$rootScope.$digestが呼ばれる回数をどれだけ減らすかにかかっています。 AngularJS 1のコードを読んだり、sjspを使って何度もプロファイルを取る過程で (angular.jsに限ってプロファイルを取ることもできる、その柔軟さが好きです)、ようやくこのことに気が付きました。 一般に$watchの数は2000におさえるべしと言われますが、仮にそれを守ったとしても、ページの初期化時に何十回も$rootScope.$digestが呼ばれるようでは結局遅くなってしまいます (もちろん$watchの数をおさえるのも大事です)。

では、$rootScope.$digestはどういう場面で呼ばれるのでしょうか。そしてどうすればいいのでしょうか (AngularJS 1に興味がなければ読み飛ばして頂いて構いません)。

  • $scope.$apply: $scope.$digestで済むならばそれを使いましょう (もちろんこれらの違いをきちんと理解する必要はあります)。どうしても$rootScopeで更新したい、しかもそれが数十個あるという場合は、$applyAsyncを使えば1回になるかもしれません。
  • $timeout: タイムアウト解決時に$rootScope.$digestが呼ばれます。setTimeout + $scope.$digst で代用できないか考えましょう。特にscopeの変数に関係なければ、setTimeoutで十分な場合もあります。
  • $http: レスポンスを受け取った後に呼ばれます。これによって、scopeの変数にデータを代入してDOMが更新されるので、必要な場面もあります。しかし、複数のJSONをそれぞれ$httpで取得してscopeに代入するよりも、window.fetch$qでまとめたほうがパフォーマンスは良いでしょう。$httpを沢山使っているけど全て変更するのは大変という場合は、$httpProvider.useApplyAsync(true);を呼んでおけば少し良くなるかもしれません。
  • $q: 解決する時に$rootScope.$digestが呼ばれます。これが呼ばれることによってDOMが更新されるので、必要だという場面もあります。必要でない場合はPromiseを使えばいいかもしれません。

$rootScope.$digestが呼ばれるきっかけはたくさんあります。 DBを叩く時にN+1を避ける慎重さがあるならば、「N+1 $rootScope.$digest」を避けるくらい慎重にコードを書くこともできると思います。 上記のように$rootScope.$digestがどこから呼ばれるかをリストアップできるのは、丁寧にconsole.traceなどでコードを追ったからです。 内部で$scope.$apply$timeoutを多用しているプラグインは避けるほうが良いでしょう。 特にキー入力のたびに$scope.$applyを呼ぶようなものは要注意です。

他にも細かい技はいろいろあります。 AngularJS 1を使う上ではどれも基本的なことですが、思いつく限り書いておきます。

  • deep watchが必要なければshallow watchにする (データ変更がオブジェクトの参照が変わるときのみである場合)
  • $watchする必要があるのが一時的ならば、丁寧にunwatchする
  • one time bindingを検討する
  • 複雑なテンプレートで、ユーザーがボタンを押して表示をトグルするものがあれば、最初はng-ifで隠しておく (scopeには気をつけつつ)
  • 属性 (attrs) を$watchするときは、linkで$watchするのではなくてcompileで$parseした結果を$watchする

以下の動画はAngularのパフォーマンスについて理解を深める上で役に立ちます。

つらつらと書いてきましたが、いまさらAngularJS 1を使うという選択肢はまずありえないでしょう。 パフォーマンスが要求される複雑な業務アプリケーションならば尚更です。 パフォーマンスを重視したフレームワークが色々とあるのも知っています。 これまで積み上げてきたAngularJS 1ベースのたくさんのコードやテンプレートを移行するのは苦労することが予想されるうえに、特にこれといった決定打となる移行先もないため、どういうふうに脱出するかは検討している段階です。 これまで開発スピードを維持してこれたフレームワークを選んだ先人に感謝しつつ、冷静に次へのステップを考えましょう。

  • 世間で重いと言われているからとそうなのだと諦めるのではなく、きちんとフレームワークと向き合い、特性を理解した上でチューニングしよう
  • 先人には感謝しながら、より良い道を模索しよう

スタイルはCSSに任せる

スタイルを設定するのはCSSの十八番です。 画面の初期化時に要素の高さをJavaScriptで取得して、別の要素のサイズを設定するというのはあまりいいやり方ではありません。 そういう処理が、ページの初期化時に数十回走るようであれば、まず見直したほうがよいでしょう。 少しデザインを変更してCSSでやれば初期化のパフォーマンスが改善するという場合は、仕様変更を検討してみましょう。 私はcalc()vhが好きです。

  • CSSでできることはCSSでやりましょう

V8の最適化アルゴリズムを調べる

JavaScriptのパフォーマンスについて考える上で、その実行処理系に目を向けるのは自然なことです。 JavaScriptはゆるふわな言語で色々なことができますが、きちんと節度を持ってコードを書けば、実行処理系の最適化の恩恵を受けられる可能性があります。 例えばHidden classを意識して後からプロパティーを付け加えることはしないとか、頻繁に呼ばれる関数の中ではtry catchを使わないといったような話です。 以下の動画はとても刺激的です。

機能がもたらす利点と欠点を考える

パフォーマンスの悪い機能があって、チューニングではどうにもならないとしましょう。 その機能を取るかどうかはどうやって判断すればいいのでしょうか。

そういう時は、どれくらいの人たちがその機能を便利だと思い、どれくらいの人の利便さを損なっているかを考えます。 あるプロダクトにおいて、そこまで重要ではない機能、あるいは他の機能で補えるものを考えます。 5%の人が便利に思っているものでも、95%の人にはなくてよい機能であり、その機能によって全員のページロード時間が倍以上かかっているとします。 パフォーマンスチューニングでなんともならないのであれば、その機能は削るのがよいでしょう。 なおこれはあくまで私見であり、チームの意思決定や開発体制、プロダクトオーナーの意向など、様々な要因によって左右されます。

  • 重くてどうにもならない機能は、どれくらいの人が便利に使っているかを考えた上で削るかどうか考えましょう

データの特徴を使ってよいアルゴリズムを模索する

何らかの重い関数がある時は、その典型的な入力データにどういう性質があるかを考えましょう。 実はデータストアから引いた時点でソートされていたりしませんか。 効率の良い探索方法があるかもしれません。 実はその行列の要素はほとんど0だったりしませんか。 行列演算が速くなるかもしれません。

コードを書く上で、見ている変数にどういう性質があるのかを考えるのは大事なことです。 典型的なデータのサイズやコードが呼ばれる頻度、アルゴリズムを変えることで得られる恩恵の大きさ、コードが必要以上に複雑にならないか、これらのバランスを考えながら改善していくとよいでしょう。

よいアルゴリズムはよいデータ構造から、そしてデータ構造は要素の性質から作られます。 順番の揃った配列から素早く要素を見つけられるのは、要素間で大小を決定できるからです。 ハッシュを作って高速にアクセスできるのは、各要素に対して性質のよいハッシュ関数が存在するからです。 集合に付与される性質と、それによって作られるデータ構造があるから、よいアルゴリズムがあるのです。

まとめ

パフォーマンス改善の基本はどんな言語でも、どんなレイヤーでも同じです。 計測することと、支配的な箇所から改善することです。 その手法は様々ですが、既存のプロファイル手法に囚われずに、本当に計測したいものを計測してくれるツールを作るという勇気を持つことは大事なことです。 既存の挙動を変えずに行える改善もありますが、同じ挙動を維持することが難しい場合もあります。 開発期間が長くなり、機能が増えれば重くなるのはあたりまえのことです。 時には仕様やデザインを変え、また時には表示する情報を落とし、先輩の書いたコードを削ったりしながらサービスを使いやすいものに育てていくという姿勢こそ、そのサービスを持続的に発展させて行くのではないでしょうか。

次のアドベントカレンダーの担当は id:amagitakayosi です。