Quantcast
Channel: pixiv inside [archive]
Viewing all 111 articles
Browse latest View live

大量接続に耐えるWebSocketアプリケーションサーバ構築のコツ

$
0
0

WebSocketの扱うサービスでは、長時間のコネクション、再接続処理、プロキシ、ロードバランサなど、インフラの面で多くの問題を抱えがちです。弊社のサービス「pixiv」の9周年企画でも、この問題に直面しました。

実際にそこで構築したインフラの事例をもとに、本運用に使えるWebSocketサーバの構成について、pixivインフラ部の南川からご紹介します。

f:id:devpixiv:20161129204837p:plain * 9周年企画 “黒歴史”をロケットで宇宙に飛ばす pixiv黒歴史

そもそも WebSocket とは?

WebSocketはTCP上で動く双方向通信のための通信規格です。

Webページの読み込みで行われているような、クライアントがサーバにデータを要求し、サーバはクライアントにレスポンスを返すというHTTPの通信ルールとは違います。サーバと長時間コネクションを確立し、Socketのようにデータのやり取りを行います。そして、コネクションを確立した後は、サーバ・クライアントのどちらからも通信を始めることができます。

HTTPと比較すると、コネクションを張り続けるため通信のオーバーヘッドが少ないという利点があります。一方で、コネクションが切断された場合の再接続処理などの面がやや手間です。コネクションを張り続けることでの注意点もあり、HTTPとは違った知識が要求されます。

TCP/IPでのソケット通信の知識があれば理解は進みやすいでしょう。

WebSocketを有効に使えるケース

WebSocketが最も活躍するのは、クライアントとサーバ間で多くの双方向通信を行うケースです。

テキストチャットのように配信量が少ない場合は、Ajaxを使ったポーリングでも賄える場合が多いです。WebSocketの用途は、双方向で大量のデータをやり取りする必要がある場合に限られるでしょう。

また、移動通信中はコネクションの切断について意識しなければいけません。モバイル端末で使われるような場合には注意が必要でしょう。

WebSocketを使う上で注意する点

Webエンジニアとして気を付けておきたいWebSocketの注意点の一つがコネクションを張り続ける点でしょう。

HTTPはクライアントがサーバにデータを要求し、サーバはクライアントにレスポンスを返すまでが一連の処理です。レスポンスを返し終われば、コネクションを切断できます。

一方でコネクションを張り続けているWebSocketの場合、切断時には適切な終了処理を記述する必要があります。

サーバ側プログラムの更新のタイミングではコネクションを一度リセットする必要があるため、クライアント側でもコネクションの切断時に呼び出されるWebSocket.onclose属性にハンドラーを設定し、適切な復帰処理を記述する必要があります。

ws = WebSocket(...);


// e: CloseEvent
ws.onclose = function (e) {
    // 適切な復帰処理
    if (e.code == ...) {


    }
}

もう一点、HTTP通信に付きまとう問題としてプロキシサーバの存在があります。

WebSocketの通信は80番ポートを使用するため、WebSocketの動作に問題のあるHTTPキャッシュサーバと通るケースがあります。

これはサーバ側でコントロールできないこともあり厄介な問題となりえます。

例えばコネクションが生存しているか確認するために行う PING/PONG フレームがキャッシュサーバによって破壊されてしまうと、クライアントがコネクションが確立されていると錯覚し、奇妙な動作を引き起こすことがあります。

このような問題を避けるため、WebSocketを使う場合、必ずTLSを通さなくてはいけません。

HTTPS接続を行う場合と同じように証明書の設定をし、wss://で接続するようにすればこの問題を回避できます。

コネクション数制限についてもサーバ、クライアントで注意する必要があります。

WebSocketはブラウザのタブごとに違うコネクションを張ることができるので、クライアントあたりのコネクション数が大量になる恐れがあります。

サーバは多くのコネクションを張らなければならない場合がありますが、例えばNginxなどのHTTPサーバをTLSの終端としておく場合、プロキシとして使えるTCP/IPでのポート数には65535の制限があるため注意する必要があります。

データベースへの接続も同様に問題となるためコネクションプールを用意する方法などを使いポートの枯渇を避ける必要があります。

これは例えばタブがアクティブではない場合にはコネクションを切断し、数分に1度のポーリングに切り替えることで軽減できますがクライアントの実装は複雑になります。

コネクションフル通信

コネクションを確立する通信のため、サーバ・クライアントで多くのデータをやり取りする場合に新規接続を張る際のオーバーヘッドを減らすことができます。

一方で移動通信環境では度々切断が発生するため、再接続時にデータの再取得を行うような設計になっているとデータのやり取りは想定しているよりも多くなってしまうこともあります。

より有効に利用するには、PCやWifi環境化でのほぼ常時コネクションを維持できる環境を対象にするのがよいでしょう。

アクティブでないタブでもコネクションを張り続けてしまうことが問題になる場合がある

WebSocketでコネクションを確立するとアクティブではないタブでもWebSocketのコネクションは有効のままになります。

これはサーバからのプッシュで便利な面もありますが、多くの非アクティブなユーザのブラウザでもコネクションが張り続けられてしまい、サーバのコネクションを占有し続けてしまうという問題もあります。

サーバが受け付けられるコネクションの数には制限があるため、接続と切断を繰り返すHTTPと比較すると1サーバで処理できるユーザ数は少なってしまいます。

アクティブでないユーザのためにマシンリソースを割り当てられないよう、クライアント側にも工夫が必要です。

再接続処理

WebSocketの再接続処理についてはクライアントが考慮しておく必要があるでしょう。

サーバ再起動時など、大量のクライアントが同時に切断される場合、再接続処理も同時に大量発生するため、サーバ側が一時的に高負荷になってしまう問題があります。

デプロイ時など同時に複数のサーバが再起動される場合にこの負荷が無視できない大きさになることが予想できます。

9周年企画サーバの事例では、再接続時に画面描画に必要なデータを全てまとめて要求するという非常に高負荷な処理を行う設計になっていました。このため、サーバの再起動時に発生する切断でクライアントからのアクセスが集中し、データベースサーバが高負荷になるという問題が発生し、デプロイが容易にできなくなりました。

サーバの数を増やし、緩やかに再起動することである程度緩和できますが、クライアント側でも切断を検知した後、すぐに再接続するのではなく、ランダムな時間待ってから接続したり、再接続処理でサーバに大きなデータを要求することを避けることである程度サーバ側の負荷を分散することができます。

ロードバランサー

HTTPサーバの構成ではよくあるL7ロードバランサーはWebSocketではあまり意味がありません。

HTTPロードバランサーは接続のたびに異なるバックエンドサーバに接続することで負荷分散を行うことを目的としますが、WebSocketを使う場合、接続が長期間維持されるため異なるサーバに接続されることはありません。

また、ロードバランサーとしてつかうサーバのコネクション数がボトルネックとなるため、配置しないほうがよいでしょう。

NginxやHAProxy、AWSのELBなどはTCPロードバランサーとしての機能もあり、簡単な設定で使うこともできますが、上記の問題により接続が偏る問題は付きまといます。

HTTPとの通信と異なり、接続元情報をアクセスログに記録するには proxy_protocol を使うなどの工夫も必要になります。

いずれにせよ、サーバ側でのロードバランシングは実装の難しさに比較してメリットが小さいので、負荷分散を目的とした接続先のバランシングはクライアント側で行うようにできればよいでしょう。

DNSラウンドロビンを使う方法は簡単にある程度効果が見込めますが、定期的な切断処理がない場合、一度確立したコネクションが切断されるには時間を要するためより効果的に分散するためにはサーバ、クライアント双方で専用の仕組みを用意する必要があります。

構成例

以上のことを踏まえた現実的なWebSocketサーバの構築例は以下の図のようになります。

LUNCHERf:id:devpixiv:20161129205205p:plain

WebSocketサーバがインターネットに面したIPアドレスを持ち、DNSラウンドロビンで接続先を分散します。

サーバクライアント間での細かな制御ができると理想ではありますが、クライアント側でのコード変更が不要なため、DNSラウンドロビンを使っています。

ただし、DNSラウンドロビンによる接続先の分散は偏りを生みやすいため少ないサーバ台数での運用ではうまく分散するのは難しいので注意が必要です。

サーバ側の構築はHTTPサーバと比べシンプルになります。

終わりに

WebSocketを活かしたサービスを本運用に乗せるには、気をつけるべき点がたくさんあります。

HTTPと違う点も多く慣れていないと扱いづらいと思うこともありますが、使いこなせば強力なツールとなりえます。

実際に動かすプログラムのコードは省略しているので、各フレームワークのドキュメントを参照してみてください。


【pixivの生APIを叩けます】ピクシブ主催ハッカソン「CODE HAMMER#02」を12月3〜4日に開催!

$
0
0

弊社のサービスであるイラスト投稿SNS「pixiv」には、パブリックなAPIが提供されていないため、サードパーティのアプリを開発することができません。pixivの投稿は作品だったりするので気軽にAPIを公開するわけにもいかなかったりします。

…とはいえ、pixivには現在6,500万を超える作品が登録されています。これだけのイラストが投稿されているサービスが、この世界のどこにあるでしょう。そしてこれらの作品が、今のpixivとは違う別の形で表現されたらどうなるでしょう。想像したことはありませんか?

  • もっと違う、イラストのコミュニケーション方法があればどうなるだろうか?
  • 機械学習の力を借りて、新しい体験を与えることができればどうなるだろうか?
  • モバイルだからこそ、もっと違う投稿体験を作り出せるとすればどんな形になるだろうか?

「CODE HAMMER#02」は、そんな想像を形にして競い合う大会。pixiv発、特別にAPIを公開したハッカソンです。優勝賞金は10万円!!2016/12/3(土)〜4(日)に開催していますので、お友達と一緒にご参加ください!

code-hammer.tech-salon.com

前回の様子

東大生プログラマチームから高校生プログラマチームまで。美大生チームから未踏プロジェクトチームまで、いろんな経歴を持った人が集まり、もはやカオスな空間が広がっています。

▼ APIで何ができるか探っている様子 f:id:devpixiv:20161022114424j:plain

▼ いやいや、まずは仕様だろと議論している様子 f:id:devpixiv:20161022114520j:plain

▼ アイデアだしあってます f:id:devpixiv:20161022204055j:plain

▼ …イラストを扱うアプリ開発で、なんでこんな数式がでてくるのか!? f:id:devpixiv:20161022160358j:plain

▼ お菓子やジュースに加えて、pixiv発のエナジードリンク「pixiv DORADO」も提供! f:id:devpixiv:20161022215913j:plain

▼ みんな、ギリギリまで粘ってます f:id:devpixiv:20161022204043j:plain

▼ いよいよ発表、徹夜明けの人もいて眠そう f:id:devpixiv:20161023181412j:plain

▼ 悩む審査委員たち f:id:devpixiv:20161023182721j:plain

▼ 優勝チームは、ネット上で、3D空間に作品を並べるサービスを提案! f:id:devpixiv:20161023192340j:plain

▼ みなさん、2日間ほんとうにお疲れ様でした! f:id:devpixiv:20161023195332j:plain

なお、ハッカソンの様子については、gihyo.jpさんにもまとめ記事が掲載されています!興味があればどうぞ! gihyo.jp

この記事を呼んで興味が湧いた方、ぜひぜひご参加ください!お友達と一緒に参加すると、いい感じで盛り上がれます!

code-hammer.tech-salon.com

ピクシブの新執行役員に直撃!CTO&CCO就任記念インタビュー

$
0
0

pixiv Advent Calendarが今年も始まりました!

この12月、ピクシブ株式会社に新しい執行役員が2人誕生しました。今回はCTO(最高技術責任者)の高山とCCO(最高文化責任者)の川田に、今後の抱負などをうかがってみました。

f:id:devpixiv:20161130104917j:plain(※ 左: CCO 川田 寛、右: CTO 高山 温)

――まずはお二人の自己紹介をお願いします。まずは高山CTOから。

高山 このたびピクシブ株式会社CTOに就任しました、高山(@edvakf)です。僕はpixivに入る前はイギリスとカナダで物理を学んでいて、26歳で大学院中退という、かなり異色な経歴でした。当時は大学をサボってJavaScriptばかり書いていまして、Webプログラミングを仕事にしようと思い、2012年3月に入社しました。

――入社してからはどんなことを担当されていたのでしょうか。

高山 入社後はpixiv本体のPHPを書いたり、「pixivコミック」というWeb漫画サービスでRuby on RailsやScalaを書いたりする、いわゆるサーバーサイドWebエンジニアで、昨年「リードエンジニア」というエンジニアのスペシャリスト職に就いていました。ただ、ここ最近はエンジニアの組織づくりなどのマネジメントが主な仕事になっていましたので、その流れでCTOを任されることになりました。

――それでは続いて川田CCOお願いします。

川田 同じくピクシブ株式会社CCOに就任しました、川田(@_furoshiki)です。私もやや経歴は変わっていますね。高校卒業後はフリーターをしたり、カナダやアメリカあたりをブラブラしていたり。そんななかインターネットやエンジニアという仕事の可能性に惹かれまして、とっとと現場に入ろうと思い、IT系の短期の専門学校を経てNTT系SIerに入社しました。

――安定した職場から転職されたとうかがっていますが、どんなきっかけで?

川田 受託案件で技術を提供するだけというのはちょっと違うかなぁ、なんて。ビジネス領域にも踏み込んでサービスを作って育てていきたいと感じるようになり、6年ほど働いていたんですが一大決心して、2015年8月に入社しました。私は「創作活動をしている人の人生を変える!」という信念のもと、サービス作りから組織まで、さまざまなことに関わるようにしました。

――入社後のご担当は?

川田 職種としてはエンジニアですが、「BOOTH」という通販サービスで新規機能の企画を立てたり、グロースハックしたり、実際にコードを書いて実装したり。また、運営やサポートにも関わったり、チームの管理方針を変えたりと、サービスを良くするために必要なことは、エンジニアという枠にとらわれず、なんでもやっていました。最近は社内のほかのプロジェクトでもそんな協力をしていて、採用活動などにも深く関わったりしています。そんな流れもあって、このたびCCOを任されることになりました。

――高山さんは実態としてCTO的な業務が増えていたとのことでしたが。

高山 「リードエンジニア」という肩書は、技術的な挑戦についての自由度を大きく与えられていた立場です。ただ最近は、自分が立ち上げから携わっていた「pixivコミック」の技術責任者としての役割や、エンジニアの面談や評価制度を整備する仕事が増えていました。エンジニアの数も増えていくなか、片手間でやっていた組織づくりがこれまで以上に重要になってくると考えていたところでCTO就任のお話をいただいた形です。今後は特定のサービスに関わるよりは、会社全体を見ることが増えるはずです。

――組織づくりというお話ですが、CTOとしての抱負をお聞かせください。

高山 会社の技術的プレゼンスを高めることであったり、エンジニアの評価や働きやすさの向上などにコミットできればと考えています。まだあまり具体的に何をやるかは考えられていませんが、変化に強い組織というのをキーワードにしていきたいと思っています。pixivは生まれてまだ9年しか経っていません。今までの技術が次の10年に通用するかは怪しいと考えて、起こりうる今後の変化に対応できる状態にしておきたいという漠然とした思いがあります。

川田 インターネットを取り巻く環境も変わってきましたからね。PCもモバイルも、あるいは、デバイスそのものも、9年前とはまったく違う形で人々の生活に浸透しているように思えますし。今までと同じビジネスモデルも、いつまで通用し続けるのか……。

――そもそも川田さんの「CCO」って、ちょっと聞き慣れない肩書ですが。

川田 ネットで調べると「Chief Content Officer」とか「Chief Customer Officer」とか「志々雄真実」とか出てきますが(笑)。「Chief Culture Officer(最高文化責任者)」という意味になります。

――どういうミッションを担当される形でしょうか。

川田 たとえば高山のCTOなら、今後の世の中の変化に応えられるような新しい技術を組織として常に持ち続ける、そのために必要なことを担当することになりますが、私のCCOというのは、社員の「スキル」というより「マインド」に対する最高責任者ということになります。

pixivには「クリエイターやアーティスト、ものづくりをしたい人たちの活動を、もっともっと楽しくする」という大きなビジョンがあります。技術力だけでなく、ものを作っている人たちをもっと喜ばせたいとか、そうしたモチベーションを高めるため、組織の風土を変えたり、仲間を集めたり。そうした文化を醸成していくのがCCOの役割です。

――お二人とも執行役員ということで、経営的な側面でも会社にコミットしていくお立場なのかなと思いますが。

高山 CTOの役割って会社によって全然違っていると思いますが、「技術で経営を支える」というところは共通しているはずです。そういう意味で、経営的な観点で会社で採用する技術のことを考えますし、逆に技術的観点から経営に対して提案もしていければと考えています。

川田 CCOの役割で考えるなら、企業ビジョンを深く浸透させたり、そのための環境を整えたり、ビジョンに則した活動をしている人をきちんと評価できるようにしたり、同じビジョンを持つ仲間を増やすための企業ブランディングを進めたり。全社のリソースを大きく投入しなければできないことですし、失敗したらダメージは大きい。しかし、そういうリスクの大きな判断を、会社を継続成長させて価値を高めるために進めて行くことと捉えてます。

高山 ピクシブの社員には「クリエイターの活動を楽しくするために仕事をしている」という人がたくさんいて、みんなの目指しているものが一致していることが強みです。だからといってエンジニア組織として当たり前にやるべきことがおろそかになってはいけないので、そのバランスを取りながら会社もサービスも良いものにしていきたいです。

川田 私も同意見です。「クリエイターの活動を楽しくしたい」という同じ想いを持っている高い専門性を持った人たちが集まっている。日本のどこを探したってそんな集まりはなかなかないと思っています。実際にpixivのサービスにもたくさんのクリエイターがいて、しかもそれは日本にとどまらず世界中から集まっている。

高山 そういえば僕が1985年3月生まれなので、同級生かも。

川田 1985年の2月生まれです! 一緒にもっともっと面白いこと、できると思っていますよ。

――本日はありがとうございました!

Railsのアップグレードでもう苦しまない

$
0
0

pixiv Advent Calendar 2日目(つ∀-)

Ruby on Railsは世界中のプロジェクトで採用されている最も有名なWebフレームワークのひとつである。
これほど有名なフレームワークでありながら、約1年ごとに訪れるアップグレードでは皆揃って頭を抱えることになる。

そう。
Ruby on Railsを使うプロジェクトにとって、アップグレードはひとつの鬼門である。

一定以上の規模のプロジェクトになると
専任のエンジニアがブランチを作って、つきっきりでアップグレード作業をすることになる。(あなたの会社もそうじゃない?)

あんな退屈で辛い作業はない...

負債を減らすにはコツがいる

どうすればRailsのアップグレードを楽にできるか。

アップグレードに時間がかかるのは「最新バージョンとの差分」が大きいことが原因なので、
差分が少なければ、それだけアップグレードが楽になる。

今回は、Railsのアップグレードで苦しまないために差分を少なくするコツをいくつか紹介する。

1. コードを書かない

YAGNI(You Ain't Gonna Need it)(機能は必要になるまで追加しない。)という原則の元に開発する。
当たり前だけどコードが少ないほどアップデートは楽になる。

また、コードを追加するのと同様に、不要になったコードはガンガン消していく。
あなたのプロジェクトには不要なコードが残っていないだろうか?

2. Rails-Wayに乗ってコードを書く

Rails Guideの始めに記載されているように、Rails-WayはRails開発の唯一解である。

「1. コードを書かない」にも通じる話であるが、Rails-Wayに乗った開発をしていれば、余分なオプションや実装がグッと削られてコード量を少なくできる
そうすれば、当然アップグレードは楽になる。

Railsは、最善の開発方法というものを1つに定めるという、ある意味大胆な判断に基いて設計されています。Railsは、何かをなすうえで最善の方法というものが1つだけあると仮定し、それに沿った開発を全面的に支援します。言い換えれば、ここで仮定されている理想の開発手法に沿わない別の開発手法は行いにくくなるようにしています。...

(Rails Guide: 2. Railsとは何か)

また、Rails Guideに乗っ取っていれば、アップグレード方法も明示されるため、プロジェクト固有の問題に悩まされにくくなる。

3. Deprecatedなコードを明示する

認識できない(していない)負債ほど厄介なものはない。

次のバージョンで変更されることが明らかだったり、Railsをハックしていて不安な箇所があったりするならば
それを明示しておくだけでアップグレード作業が楽になる。

# コードかテストに直接記述する
if Rails.gem_version > Gem::Version.new('5.0.1')
  ActiveSupport::Deprecation.warn('廃止予定')
  raise '5.1.0で動かなくなるかもしれない'
end

これの良いところは「負債を認識できる」ようになることである。
こうしておけば、この負債は次回のRailsのアップグレード作業の時に確実に返済される。

4. Railsの変更をbackportする

普段からRailsの更新を細かくbackportする。
例えば config/initializers/backport_rails5_1.rbを作る。

語弊を恐れずに言うなら、Railsは壊れているため
プロジェクトでは幾つかのbackportをいれることがある。(例 #25834, #25146, #25364)

そこに、新機能(例 #25991)もbackportすることで次期バージョンとの差分を少なくすることができる。

とはいえ、これは積極的にやるものではなくて次の条件に当てはまるものだけ導入する。

  • 変更が小さい
  • テスト可能なこと
  • 導入にメリットがあること

This Week in RailsのRSSをSlackに流しておいて、必要そうなものだけピックアップしてbackportすると良いと思う。

まとめ

いくつか紹介したが、アップグレードを待たず、普段から小さく差分を減らしていく心がけが大事。
しかし、わざわざ時間をかけると本末転倒なので、あくまで開発に集中してついでに差分を減らせればベストだと思う。

Railsの開発は楽しくも少し大変。各プロジェクトのやり方で、うまく付き合っていければ嬉しい。


今更ながらの自己紹介

pixivFACTORYの開発リーダー や 日本酒の管理 をしている 2015年入社の @alpaca-tc だ。

twitter.com

普段は、pixivFACTORYでRuby/JavaScript/Goを書いている。
若手でもガンガン開発ができるこんな弊社に興味がある方は、ぜひオフィスに遊びに来てくれヾ(〃><)ノ゙

Firebase Cloud Messaging (FCM) を利用してお手軽に通知を送る

$
0
0

ピクシブ株式会社 Advent Calendarの@3日目です。今日はスマホアプリエンジニアの chocomelonがお送りします。

最近、弊社のプロジェクトではネイティブアプリのプッシュ通知に Firebaseを使っています。導入方法は色々な方々が紹介しているので今さら感はありますが、弊社ではこんな感じで使っていますという事例を絡めて紹介できればと思います。

記事の内容的には Firebase で送るプッシュ通知の概要は知っていて、これから導入を検討している方や、Firebase を使ったプッシュ通知が実際どんな形で使われているか興味がある方にオススメです。

Firebase Notification と Firebase Cloud Messaging

Firebase で送る通知は、Firebase Notificationを使ってGUIから送る方法と Firebase Cloud Messaging (以下FCM) を使って送る2つの方法があります。弊社では FCM を使って通知を送っています。

Firebase Notifiation は送る内容、ターゲット、タイミング等を Notifications Console GUI からの操作でき、お手軽に通知を送ることができます。

画像:Notifications Console GUI f:id:chocomelonchan:20161201104525p:plain

ただ Firebase Notification はAPIは存在せず、定期実行をしたい場合などに不便です。現状API経由で通知したい場合は、FCM しかないので FCM を使っています。

FCM には Notification Message と Data Message があります。Notification Message の場合、Android は端末がバッググラウンドの状態にある時に処理のハンドリングができず、正確な通知の開封率が計測できなかったため Data Message を使用しています。詳しくは Firebase のドキュメントを参照してください。

特定のトピックを購読しているユーザーに対して通知を送る

pixiv アプリでは、新規ユーザー向けにどの通知が刺さるかちょこちょこABテストしています。そのうちのひとつに pixiv のイラストデイリーランキングが更新されたタイミングで通知を送っているので例として紹介します。

サーバ側の処理

サーバ側ではランキングが更新されたタイミングで FCM に以下のデータを投げつけてアプリに通知を送っています。通知を受信したアプリは"data"内の値にもとづいて通知を生成します。

data.json

{"to": "/topics/illust_daily_ranking",
    "data": {"title": "pixiv",
        "body": "ランキング更新!トレンド作品をチェック!",
        "target_url": "pixiv://ranking/illusts"
    },
    "content_available": true}

content_availableに関しては、iOS のために設定しています。content_availableは APNs でいう content-availableに相当しています。iOSアプリでは、この値が trueの場合、アプリがバックグラウンドにいたり、終了した状態でも通知を受け取ることができます。Android では、デフォルトでアプリがバッググラウンドの状態でも通知が届きます。

実際の送信処理は以下のようになります。さきほどの json を FCM に投げます。

curl -H "Authorization: key=XXXXXAPIKEYXXXXX" -H "Content-Type: application/json" -d @data.json https://fcm.googleapis.com/fcm/send

アプリ側の処理 (Androidの場合)

アプリ側ではトピックを購読する処理と受信したときの処理を書きます。以下はAndroidの例です。長くなるので細かい設定の話は省きます。詳しくは Firebase のドキュメントを参照ください。

トピックの購読処理

FirebaseMessaging.getInstance().subscribeToTopic("illust_daily_ranking");

受信時の処理。

@Overridepublicvoid onMessageReceived(RemoteMessage remoteMessage) {
    Map<String, String> data = remoteMessage.getData();
    RemoteMessage.Notification notification = remoteMessage.getNotification();
    String title = data.get("title");
    String body = data.get("body");
    String targetUrl = data.get("target_url");
    // 略... NotificationManagerを使って通知をnotifyしたりする。
}

jsonで渡したデータはRemoteMessageのgetData()でKey-ValueのペアでMapされた形で取得できます。 取得したタイトルやメッセージbodyのデータ使って、NotificationManager経由で通知を送ります。

ユーザー個別に通知を送る

pixiv のアプリで新規ユーザー向け通知の最適化のために、ユーザーがしたアクション(ブックマークやフォロー)をもとにオススメ作品を送るという通知を試験的にしています。

これを実現するために、新規ユーザー個別の通知にユーザー毎に一意になるようなトピックを使っています。トピックの生成がおそらく上限が設定されていないので、トピックで購読させた方がトークン管理やユーザーが別の端末を使っている場合も特に気にしなくて良いです。

トピックが被って別のユーザーの通知がきてしまわないようにサーバ側で生成したトピックを受け取って購読しています。

サーバ側の処理

  • 準備
    • ユーザー毎に一意のトピックを返すAPI
  • 通知処理
    • 通知対象のユーザーをデータベースから抽出
    • 抽出したユーザーからオススメを生成
    • FCM 経由でアプリに通知

アプリ側の処理

  • サーバ側のAPIからトピックをもらい購読

まとめ

通知をお手軽に試すのに Firebase Notification は非常に良いかなと思います。ただ少しカスタマイズしたい場合などは FCM を使うと良いかと思います。ぜひみなさんも試してみてください!

pixivコミックをおすすめしてくれる司書チャットボットを試作しました

$
0
0

ピクシブ株式会社 Advent Calendar 2016 4日目です。16卒エンジニアの yasu がお送りします。好きな言語はLISPです*1

先日、Google Apps ScriptとLINE Messaging APIを利用して、会話BOTを開発した記事を公開しました

今回はLINE上で動くコミック司書ボットを試作しました。チャット上で、インタラクティブにやりとりするために、LINE Messaging APIのテンプレート機能を使っています。また、ユーザの発言解析にMicrosoft Cognitive Service LUISを使っています。

f:id:tamanobi:20161202173324j:plain

pixivコミックのおすすめを教えてくれる司書を作りました

弊社には、pixivコミックというコミック総合サイトがあります。pixivコミックで読める作品の数は約2300件(2016/12/02現在)にも及びます。

大量のコミックの中から、お気に入りの作品を見つけるのはなかなか骨が折れます。私のために、おすすめコミックを紹介してくれる司書がいたらどんなに良いことでしょう。そのような司書を夢見てチャットボットを試作しました。ユーザの発言に応じて、pixivコミックに載っているコミックを紹介してくれるチャットボットです。残念なことに試作品なので、外部公開はしていません。

f:id:tamanobi:20161202171113p:plain

チャットボットがコミックをおすすめしてくれる様子

作成したチャットボットはユーザの発言を解析して、作品をおすすめしてくれます。司書に話しかけるように、「怖い話が読みたい」と言ってみます。ボットが「こんなのはどう?」といって怖い話を推薦してくれました。表示されたカードから、作品ページへ直接移動できるためスムーズにコミックを読むことができます。

なぜチャットボットを作るのか? チャットボットを作る動きが盛ん

今年は、チャットボットに関わる発表やハッカソンが非常に多かった年です。例えば、Microsoft Bot Frameworkや、LINE Messaging APIhachidoirなどがリリースされました。ボットと会話をしたいという夢をもつ僕には最高の年です。

私たちがコミュニケーションを取るときには、基本的に自然言語を使います。いままでコンピュータに何か作業をさせる場合には、用意されたプログラミング言語を使う必要がありました。一方チャットボットでは、限られた範囲ではありますが自然言語による会話を通じて、コンピュータに作業を依頼することができます。

限られた範囲でも、チャットを用いてさまざまな操作(例:自然言語で検索)ができるとすれば、利用するユーザを引き込める可能性があります。たとえば、ウェブサイトのどこにボタンがあるのかわからないとか、使い方がわからないユーザがいるでしょう。

具体的には、「◯◯さんのアポ、何時から何時までとっておいて!」とか、今日の人気コミック、何かなー?と、チャットボットに話しかけることで、ボットがその裏の煩雑な作業をやってくれることが期待できます。

世界的にも徐々に関心を集めつつあるチャットボット

チャットボットは、まだまだ人気とは言えませんが、去年から少しずつ関心を集めているようです。以下のグラフは、Google Trendsで、チャットボットとボットフレームワークについて調べてみた結果です。つい先月、MicrosoftがAzure Bot Servicesを発表したことから、チャットボットのプレゼンスはもっと高くなるでしょう。

チャットボットは、誰でも使える新しいインタフェースか

日常的に行っている会話を介して、コンピュータにさまざまなことを依頼できるボットは、お問い合わせのシステムとして活躍が見込まれています。日本の銀行でもチャットボットが導入されているようです。

まだまだ音声応対をすべてボットで行うという施策は浸透していないようですが、テキストベースでの応対を行ったり、音声応対は人間のオペレーターにまかせて、リアルタイム会話分析で、オペレーターを支援する情報を画面に表示したりする運用が行われています。

今回試作したチャットボットでは、ユーザとボットが直接会話を行います。あくまでもボットの応対になるため、誰でも違和感なく使えるものではありませんが、スマートフォンを持っていて、LINEでテキストを送信できる人には使えるシステムです。

チャットボット試作で困ったこと:自然言語を解析するならLUISを使おう

チャットボットは、日常的に使う言葉で利用できることがメリットなのですが、チャットボットを作る側としては悩みの種になります。自然言語の構造は単純ではないため、正規表現や形態素解析だけでは意図を把握するのに限界があります。

そんなときは、Microsoft Cognitive Services*2の一つであるLUIS: Language Understanding Intelligent Serviceを使うと便利です。LUISというのは、自然言語を解析して意図の種類を示すタグにディスパッチしてくれるツールです。たとえば「天気を教えて!」や「天候はどうだろう?」という自然言語をgetWeatherというタグにマップできます。自分で正例を覚え込ませたり、誤判定を修正したりすることがウェブ上でできるため、気軽に試せます。

今回作ったチャットボットでは、「ランキング見せてください」や「○○っていう漫画ありませんか?」という言葉がそれぞれ意図を表すタグにマップされます。

LUISでは、GETパラメタにユーザの入力に載せてHTTPリクエストするとJSONで該当するタグの尤度を返します。非常にシンプルなAPIなので、cURLやブラウザから簡単にデバッグできて便利でした。

このLUISを用いることで、自然言語を意図に変換することができます。ちなみに、LUISの利用には無料枠が用意されているので、試しに使ってみる程度であれば料金を請求されません。利用される方は、Cognitive Servicesの料金を必ず参照し、ご自身の責任で利用してください。

LUISの試し方は、Microsoft Bot Frameworkと簡単連携! LUISで自分だけのAIを作ってみた - BITA デジマラボLUIS (Language Understanding Intelligent Service) 日本語対応 ~ 解析エンジン作成&利用方法 – 青い空の向こうへにわかりやすくまとまっているので、参照してみてください。

チャットボットのプラットフォーム:LINE Messaging APIを使う

チャットボットを動かすには、当然プラットフォームが必要です。今回はカルーセルが使えるLINEを使いました。LINEは通常上から下へテキストがレイアウトされますが、カルーセルを使うと横に展開されます。いくつかある候補の中からユーザに選んでもらうときに一覧性が高く、有用です。

LINE Messaging API

LINE Messaging APIは、カルーセルを始めとするテンプレートが充実しています。確認ボタンや画像付きカードなどは表現力が高く使いやすいです。詳細な説明は公式ドキュメントに譲ります。

個人的には、普段LINEを友人同士で使っているときには見られないカルーセルがとてもお気に入りです。

pixivコミックおすすめチャットボットの機能

LINE Messaging APIとMicrosoft Cognitive Services LUISを使って作られたチャットボットは以下の機能があります。最後にデモ動画を紹介します。

  • コミック検索
  • 注目漫画
  • ランキング
  • 作品の詳細情報取得

司書チャットボットで、コミックを探すデモンストレーション

まとめ

本記事では、LINEをプラットフォームとして、LUISという自然言語解析APIを使って、pixivコミックの作品を勧めてくれるチャットボットを作った話を紹介しました。インタフェースとしてのチャットボットは利用に特別な操作が必要ないため、幅広く人々にリーチできる可能性を秘めていると思っています。

LINE Messaging APIや、Microsoft Cognitive Servicesに興味が湧いた人は下の公式ドキュメントを参照してみてください。

参考資料

引き続きAdvent Calendarお楽しみください!

明日のピクシブ株式会社 Advent Calendar 2016の担当者は、私と同じ16卒エンジニアのkpです。お楽しみに!

*1:shibuya.lispが終わってしまうと聞いて残念な思いでいっぱいです

*2:Microsoftの提供する Cognitive Servicesは、「人工知能アルゴリズムのコレクション」と紹介されている

Swift+CocoaPodsではじめてのOSS(ハンバーガーメニューを作ってみた)

$
0
0

ピクシブ株式会社 Advent Calendar 2016 @5日目です。

新卒のkpです。昨年までアルバイトとしてpixiv、pixiv touch、pixiv Spotlight (現 pixivision)に、現在は創作したものを販売できるWebサービスのBOOTHに携わっています。

ハンバーガーメニューをライブラリ化してCocoaPodsに登録した

Webサービスのスマートフォン版表示や、iOS/Androidアプリでよく見るUIの一つにハンバーガーメニューがあります。三本の水平線で表され、画面の隅にあり、タップすることで隠れているメニューを呼び出すものです。

しかし、SwiftのUIKitには、ハンバーガーメニューと呼ばれるコンポーネントの実装がありません。 解決方法は大きく分けて2種類あります。 一つは、UIPresentationControllerや、UIViewControllerAnimatedTransitioningなどを使って手作業で実装していく方法です。 もう一つは、ライブラリによる解決です。Swiftには、CocoaPodsというライブラリ管理プラットフォームがあります。しかし、登録してあるライブラリはトランジションが派手なライブラリが多いように感じました。シンプルな実装を求めていたので、この機会に、既存のアプリケーションから該当部分を切り出してライブラリ化し、CocoaPodsに登録してみました。

ライブラリ化することで、プロジェクトを分割して小さくでき、再利用が容易になり、更にビルド時間の短縮にもつながります。本エントリでは自作ライブラリをCocoaPodsに登録する方法について紹介させて頂きます。

今回開発したライブラリはGitHubで公開しています。 keisei1092/PlainMenuController

空のCocoaPodライブラリを作る

ターミナルで、任意のディレクトリに移動して以下のコマンドを実行します。

gem update --system
gem install cocoapods
pod lib create PlainMenuController

pod lib createを実行すると言語やテストフレームワークの有無についていろいろ聞かれます。

f:id:keisei_1092:20161205142008p:plain

終了すると自動でXcodeで該当ディレクトリがオープンします。 現時点(2016/12/04)だとテンプレートが古く (~ Swift 3.0) 、Convert Syntaxダイアログが出てきます。ダイアログが出た場合は、メニューからConvert→Swift 3.0→Next→Saveを選択することで、警告を解消できます。

コードを書く場所は、

Pods/Development Pods/(ライブラリ名)/(ライブラリ名)/Classes/

になります。ここにReplaceMe.swiftというファイルがありますがこれはクラスの置き場所を示しているだけの空ファイルのため、削除しても問題ありません。

既存アプリケーションからコードを切り出す 今回は最小のライブラリ化として、UIPresentationControllerクラス、UIViewControllerAnimatedTransitioningプロトコルを切り出しました。 動いている既存アプリケーションから該当クラスをライブラリ用のディレクトリに移動しました。

ハマりそうなところとしては、モジュール外からもアクセスできるようにクラスやメソッドのaccess levelにopenを付与する必要があります。この手順を踏まないとExamples配下のクラスから呼ぶ際にunresolved identifierエラーになります。

(実際のクラスはこちら: MenuPresentationController, MenuPresentationAnimator

コードを切り出したら、メニューを呼び出すViewControllerに

import (ライブラリ名)

を追加し、呼び出し先のUINavigationControllerのtransitioningDelegateをselfにします。

letstoryboard= UIStoryboard.init(name:"MenuTableViewController", bundle:nil)
letmenuTableViewController= storyboard.instantiateInitialViewController() as! MenuTableViewController
letpresentingNavigationController= UINavigationController(rootViewController:menuTableViewController)
presentingNavigationController.modalPresentationStyle = .custom
presentingNavigationController.transitioningDelegate =self
navigationController?.present(presentingNavigationController, animated:true, completion:nil)

最後に、このViewControllerにextensionとしてUIViewControllerTransitioningDelegateを実装し、MenuPresentationControllerを返すようにします。

extensionViewController:UIViewControllerTransitioningDelegate {


    funcpresentationController(forPresented presented:UIViewController, presenting:UIViewController?, source:UIViewController) ->UIPresentationController? {
        return MenuPresentationController(presentedViewController:presented, presenting:presenting)
    }


    funcanimationController(forPresented presented:UIViewController,
                             presenting:UIViewController,
                             source:UIViewController) ->UIViewControllerAnimatedTransitioning? {
        return MenuPresentationAnimator(isPresentation:true)
    }


    funcanimationController(forDismissed dismissed:UIViewController)
        ->UIViewControllerAnimatedTransitioning? {
            return MenuPresentationAnimator(isPresentation:false)
    }


}

下のGIFは、ここまでで実装したハンバーガーメニューの動作です。

f:id:keisei_1092:20161205142028g:plain

一旦実装はここまでとします。

ライブラリをCocoaPodsに登録する

CocoaPodsにこのライブラリを登録してみます。登録する前に、ライブラリの説明文をデフォルトから書き換えておきます。プロジェクトディレクトリ配下のPlainMenuController.podspecのsummaryを任意の文に置き換え、authorのURL内にGitHubのidを入れておきます。

Pod::Spec.new do |s|
  s.name             = 'PlainMenuController'
  s.version          = '0.1.0'
  s.summary          = 'Basic, and essential slide menu.'# changed…


  s.homepage         = 'https://github.com/keisei1092/PlainMenuController'# changed
  s.source           = { :git => 'https://github.com/keisei1092/PlainMenuController.git', :tag => s.version.to_s } # changed


...

ファイルを保存したら、このpodspecの検証を走らせます。

pod lib lint PlainMenuController.podspec

 -> PlainMenuController (0.1.0)

PlainMenuController passed validation.

検証が通ったら、いよいよリリース作業です。最新コミットにtagをつけ、CocoaPodsユーザ登録を済ませると、pushコマンドが通るようになります。

pod trunk register (メールアドレス) '(名前)’ # => リンクが送信されるのでCocoaPodsに登録
pod trunk push PlainMenuController.podspec


Updating spec repo `master`


Validating podspec
 -> PlainMenuController (0.1.0)


--------------------------------------------------------------------------------
 🎉  Congrats


 🚀  PlainMenuController (0.1.0) successfully published
 📅  December 4th, 00:16
 🌎  https://cocoapods.org/pods/PlainMenuController
 👍  Tell your friends!
--------------------------------------------------------------------------------

今回開発したライブラリはGitHubで公開しています。 keisei1092/PlainMenuController

今後

現時点では、メニューが左からしか出せなかったり、まだ実装側のViewControllerでUIViewControllerTransitioningDelegateプロトコルを実装しなければいけなかったりと使いにくい点が多々あります。またライブラリとしてテストフレームワーク (Quickなど) の導入もやりたいところです。自分の開発を進めながら他の開発者の役に立てるというのは素晴らしいことなので、これからもガンガンコミットを生やして使ってもらえるものにしていきます。

CMSの編集ロックUI部分で元気に走り回るRiot.js

$
0
0

はじめに

ピクシブ株式会社 Advent Calendar 2016 6日目です。 新卒(16卒)エンジニアの@shocotaです。 会社では主にPHPを、趣味でRuby on RailsでWebアプリを開発しています。

今回は弊社メディアpixivisionの記事編集画面にRiot.jsを導入して、編集ロック機能を実装したことについてお話します。

多言語対応メディアを支える編集画面

pixivisionは現在4ヶ国語で記事を展開しており、各国語での記事を同時に公開することも少なくありません。この編集作業を支援するため、弊社では専用の編集システムを社内で開発しています。

f:id:devpixiv:20161206232954p:plain

多人数で編集される記事にふりかかる困った問題点

pixivisionを支えるために多くの作業者が関わっており、1つの記事上で複数の作業が同時に発生し、他者の編集内容を上書きしてしまう場面が出てきます。 これまでは作業フローの工夫によって衝突を防いできました。しかし、人手が関わるため、どうしても衝突が生じることがあります。そのようなときにはエンジニアが履歴データから頑張って復旧することで対処していました。このような応急処置では、エンジニアの開発も、記事作成者の作業も止まってしまいます。また、クリエイターを応援するべき弊社が、使いづらい編集システムによって、記事作成という一種の創作活動を阻害してしまうことはメンタリティとしてもよろしくありません。

そうして、私は編集ロック機能の開発に踏み切りました。

編集ロック機能の開発

今の状況を打開すべく、複数の編集者が記事を同時に編集できない仕組み、編集ロック機能を開発しようと思いました。 サーバー側の実装は比較的単純で、キャッシュを使って誰がどの言語をロックしているかの情報を保持して返すだけです。

しかし、物事はそうはうまく進みません。フロント側では複雑に絡み合う状態遷移をjQueryでなんとかする必要がありました。

複雑な状態遷移を乗り越えるべく導入したRiot.js

pixivisionのフロントは、jQueryによるDOM操作を駆使して書かれていました。今の書き方では、多様なロック状態に合わせてUIの状態を更新コードをメンテナンスし易く書くのが難しそうだと判断しました。

そこで、今回採用したのはRiot.jsと、RiotControlです。

f:id:devpixiv:20161206233430p:plain

Riot.jsの特徴

Riot.jsはWebページ上のDOM要素の生成と制御を支援するView用のライブラリで、コンポーネント指向を採用するなどReactに大きく影響を受けています。 容量が非常にコンパクトで、APIの数も少なく覚えやすいなどの特徴があります。最も大きな利点はWeb Componentsのように「HTMLのカスタムタグを新しく定義する」思想で作られていることだと考えています。 あるUI要素に関連するJSのロジックやCSSなどを1ファイル内に封じ込めることができるため、コンポーネントごとに管理しやすくなります。その上、<style><script>タグを持った小さなHTML(-likeな)文書として各カスタムタグを記述できます。

<list-items>
  <h3> Fruits </h3>

  <ul>
    <li each={ item in items }>{ item }</li>
  </ul>

  <style scoped>
    h3 {
      color: red;
    }
  </style>

  <script>
    this.items = ['Orange', 'Grape', 'Apple', 'Strawberry']
  </script>
</list-items>

表示結果: f:id:devpixiv:20161206233053p:plain

riot.observe()で、複数タグの協調動作を実現する

編集ロックでは、「編集をはじめる」ボタンを押すことでフォーム上のオーバーレイが消えるなど、複数のUIが状態によって変化する挙動がたくさんあります。

f:id:devpixiv:20161206233258g:plain

しかしRiot.jsには、マウントされた特定のタグのメソッドを外部から直接呼び出す仕組みがありません。その代わりにriot.observe()を利用し、イベントを呼び出すことによって状態の更新処理を発火することにより、複数のカスタムタグを協調して動かすことができます。 ただし、これはきちんと使い方を決めて共有しておかないと、あっという間に処理の流れが追えないコードになってしまいます。実装には、RiotControlを使用しました。

Riot.jsでFluxを実現する軽量ライブラリRiotControl

RiotControlは、Fluxにインスパイアされたriot.observe()を用いる簡素なライブラリです。このライブラリを使用すると、処理の流れをAction・Store・Viewに分離して責務を分けることによって、見通しのよいコードを書くことができます。

Riot.jsにした決め手は、学習コストの圧倒的低さ

今回、Riot.jsとRiotControlを採用した経緯には、私にある程度の知見があったこともあるのですが、以下の点も考慮しました。

  • 今回は機能追加が一番の目的であり、時間をかけてビルド環境をがっつり用意しなくても導入できる
  • pixivisionのエンジニアはJavaScriptを触る機会はフロントエンドエンジニアと比べると少なく、私を含めて最新のJavaScriptの文化にそんなに慣れていない
    • Riot.jsのタグ記法はSmartyでのHTML制御と較べて違和感なく書ける
  • 現在の構成に不満が出たとき、Reactなどの別構成への移行も容易
    • Fluxによってきちんと疎結合を保つようにコーディングしていれば、コンポーネントごとの段階的な移行も可能

編集者にもわかりやすい、状態が的確に反映されるUI

実作業をしている編集者に意見を聞いたところ、かなり好評でした。

  • 編集ロックが導入されたことにより、編集がぶつからなくなった
  • 現在誰が編集してるのか表示され、急ぎで編集を行いたいときは声かけで対応できた
  • エラー時にアラートが飛ぶだけでなく、次の「再読み込み」という行動に誘導され、わかりやすい(下図) f:id:devpixiv:20161206233341p:plain

以上のように、Riot.js導入によって、現在の状態がUIに反映され、編集者に見えるようになりました。jQueryだけで実現しようと思ったら、これほど短時間で実装は終わらなかったと思います。

まとめ

本記事では、既存のプロダクトのUIとロジックをRiot.jsとRiotControlによって部分的に拡張し、置き換える事例について紹介しました。Riot.jsは既存の画面に対して部分的に適用できますし、昨今のJavaScriptのフレームワーク事情に明るくなくても学習コストが少ないために導入しやすいのが特徴です。 既存システムに新しい思想のフレームワークを加えるときは大げさな変更を考えがちですが、まずはミニマルに変更して、チームで学習しながら広げていくのも重要だと考えています。そういう意味では、このライブラリ導入は成功でした。

みなさんもぜひ使ってみましょう。


BlenderとPythonで3Dモデルを動的生成してレンダリングする

$
0
0

ピクシブ株式会社 Advent Calendar 2016 - 7日目

本日は新卒エンジニアのhayaがインターネットにつながってなくてもできるお話をします。
具体的には、PythonでBlenderを操作し、入力データにしたがって3Dモデルを生成したり変形したりすることで、任意形状オブジェクトのレンダリングをGUIなしで実行します。

もちろんインターネットにつながっているとなお良く、ものづくりがもっと楽しくなるアイテム制作サービス - pixivFACTORYでは、Blenderと画像変換を駆使して、様々なオリジナルグッスの仕上がりイメージをユーザーに提供しています。

コードで3Dオブジェクトをつくる

基本的に、BlenderのUIで操作できることは後述のPythonコンソールからでも同様の操作を行うことができます。
レンダリング画像の見栄えを良くするために必要な要素はたくさんありますが、この記事ではオブジェクトの生成と形状調整に関連することを紹介します。

まずは一旦、Blenderの画面を見ながら順を追って。

Pythonコンソールを使用する

Blenderを起動した直後は3D Viewが表示されているはずです。
Blenderには様々な作業に特化したエディタ画面が用意されており、画面左下のボタンからエディタタイプ変更できます。

Python Consoleを選択するか、ショートカットShitf+F4でPython Consoleを表示、 戻るときは同様に3D Viewを選択するか、ショートカットShift+F5

Blenderの画面はこのような切り替え可能なエディタの組み合わせで構成されていますが、あらかじめ、いくつかのエディタの組み合わせたプリセットも用意されています。

コンソールを使用する場合、画面上部のレイアウト切り替えボタンからScriptingを選択すると、3D View、Python Consoleに加えて、InfoとText Editorのペインが表示されます。 PCの画面が大きいなら、こちらを使うのが良いかもしれません。

InfoペインにはGUIで操作した際のコマンドなどが流れるので、一度手動で行った作業を自動化する際の参考に。
また、どのレイアウトでも、GUIの各要素にカーソルをのせていれば、コンソール上での操作の仕方が表示されるようになっているので、GUIで自分ができる操作であれば、独学でもコンソールで同様の操作を再現しやすいと思います。

f:id:devpixiv:20161204144410j:plain

コンソール上でCtrl+Spaceを押すと自動補完してくれます。出てくる候補を見るだけでわかる情報も多いので、とにかく補完してみると良いでしょう。

f:id:devpixiv:20161206195115p:plain

※Macの場合、Ctrl+SpaceがSpotlight検索に割り当てられていると思われるので、Autocompleteのボタンを右クリックしてChange Shortcutから好きなキーに割り当て直すと良い。

3Dオブジェクトの生成

コンソールからオブジェクトを生成する方法はたくさんあるので、ここではそのうちいくつかを抜粋して紹介します。

1. 頂点データから任意のポリゴンを生成する

頂点をすべて指定すれば任意の形状を作ることができます。当たり前ですね。
それでも頂点が大量にある場合、これはむしろ手動では実現が難しくなりますから、コードで頂点データを生成してオブジェクトを生成したい場面は、結構あるのではないでしょうか。

例えば、フラクタル図形や数列に基づいた規則的な図形を生成する場合など、GUIではやりたくないような気分になります。

import mathutils
from mathutils import Vector

# 頂点
verts = []
verts.append(Vector((0,0,0)))
verts.append(Vector((1,0,0)))
verts.append(Vector((1,1,0)))
verts.append(Vector((0,1,0)))

# 面を構成する頂点は、vertsのインデックスで指定できる
faces = [[0, 1, 2, 3]]
edges = []

mesh = bpy.data.meshes.new(name='Mesh')
mesh.from_pydata(verts, edges, faces)
obj = bpy.data.objects.new('Object', mesh)
obj.location = (0,0,0)
bpy.context.scene.objects.link(obj)

# 面を張らず、辺だけを追加する場合# 面同様にvretsのインデックスを指定して辺を指定できる
faces = []
edges = [[0,1],[1,2],[2,3],[3,0]]

mesh = bpy.data.meshes.new(name='Mesh')
mesh.from_pydata(verts, edges, faces)
obj = bpy.data.objects.new('Object', mesh)
obj.location = (0.5,0.5,0)
bpy.context.scene.objects.link(obj)

上記以外にもいくつかの方法で、頂点データからオブジェクトを生成できます。

参考: Dev:Py/Scripts/Cookbook/Code snippets/Three ways to create objects - BlenderWiki

2. ファイルからインポート

Blenderはいくつかの3Dモデル/シーンファイルのインポートに対応しているので、入力として与えるファイルの形式次第では、1行のコードでレンダリング内容の大半を準備することができます。

bpy.ops.import_ ...

f:id:devpixiv:20161204141916j:plain

SVGから2Dのオブジェクトを生成することもできます。SVGはCurveとしてインポートされるので、場合によってはMeshに変換する必要があります。
なお、Blender内の長さ単位1BUは1mに対応するので、SVGのwidth/heightを設定しておくと、指定したサイズでインポートしてくれます。

<!-- circle.svg --><svg xmlns="http://www.w3.org/2000/svg"xmlns:xlink="http://www.w3.org/1999/xlink"viewBox="0 0 10 10"width="1000mm"height="1000mm"><circle cx="5"cy="5"r="5"fill="#fff" /></svg>
bpy.ops.import_curve.svg(filepath='circle.svg')

ちなみに、オブジェクト名を指定してインポートしたり、インポートしたオブジェクト名を取得する方法が見つからないため、 インポート前のオブジェクト一覧を記録しておくか、先に存在するオブジェクトを選択不可にしておくなりして、追加したオブジェクトだけを取得できるようにする必要がある。 (何か良い方法はないものか・・・)

# 一旦すべて選択不可能にする
bpy.ops.object.select_all(action='DESELECT')
for o in bpy.context.scene.objects:
  o.hide_select = True

bpy.ops.import_curve.svg(filepath='circle.svg')
# 選択不可能なもの以外すべて=先ほど追加したものだけ選択
bpy.ops.object.select_all(action='SELECT')
obj = bpy.context.selected_objects[0]
bpy.data.objects[obj.name]
obj.select = True
bpy.context.scene.objects.active = obj

# インポートしたオブジェクトに対して何か処理する# 例えばメッシュに変換
bpy.ops.object.convert(target='MESH')

# すべて選択可能に戻すfor o in bpy.context.scene.objects:
  o.hide_select = False

3. プリミティブから生成

自由度は高くないのですが、基本図形は簡単に生成できます。
ブロックが積み上がっている、謎の球体がたくさん浮かんでいるといった表現、 マテリアルを工夫したプリミティブをたくさん生成して、爆発や魔法エフェクトなどの用途には使えそうだとか、 簡単に思い浮かぶ程度には実用性がありそうです。

bpy.ops.mesh.primitive_ ...

f:id:devpixiv:20161204150336j:plain

オブジェクトの変形

ここまでは、新しいオブジェクトを生成する方法を紹介してきましたが、ここからは既存のオブジェクトを変形して任意形状を作る方法を紹介します。
新しいオブジェクトを生成する場合も、オブジェクトの移動/回転を行うことは多いでしょう。

1. 移動・回転・拡大縮小

生成したオブジェクトを移動・回転・拡大縮小する。
Blender上では角度をラジアンで扱うことが多いので注意。

import math

obj = bpy.data.objects['Cube']
obj.location.x += 1
obj.rotation_euler = (math.radians(15), math.radians(30), math.radians(45))
obj.scale.x = 2

2. モディファイヤーで変形する

工夫次第ですが、モディファイヤーでできることはかなり多く、少しのパラメータ設定でも表現の幅を大きく広げることができます。
これといった工夫をしない場合でも、自動で生成したオブジェクトにSubsurf Divisionを適用するなど、わりと使用する印象。

bpy.ops.mesh.primitive_uv_sphere_add()
obj = bpy.context.selected_objects[0]

mods = obj.modifiers
mod = mods.new(name='subsurf', type='SUBSURF')
mod.render_levels = 4
mod.levels = 4# 3D View用

f:id:devpixiv:20161204145536j:plain

3. シェイプキーで変形する

使いどころは限られてきますが、シェイプキーを使った変形も自動化に向いていると思います。
主にアニメーションの作成に使用する機能ですが、複数の頂点の移動からなるオブジェクトの移動・変形を、1つのパラメータを介して滑らかに調整できるため、 少ないパラメータでもなかなか複雑な形状を作ることができます。複数のシェイプキーを設定すれば、かなり複雑な変形も実現可能。

動的にシェイプキーを設定しようとすると変形前後の形状に関する頂点データなどを与える必要があり、わざわざシェイプキーを介する必要性が薄くなるので、 モデルにシェイプキーを設定したものを事前に用意することになるでしょうか。

# Cylinderにシェイプキーが設定されているとして
obj = bpy.data.objects['Cylinder']
# 0番目は通常Baseの形状になる。キー名で指定しても良い
obj.data.shape_keys.key_blocks[1].value = 0.2
obj.data.shape_keys.key_blocks['Key 1'].value = 0.8

f:id:devpixiv:20161206014448g:plain

コマンドラインからBlenderを動かす

BlenderはGUIなしで動作させることが可能です。
Blenderの実行ファイルへパスを通すか、実行ファイルパスを指定してコマンドラインから起動できます。 その際、任意であらかじめ作成しておいたblendファイルを読み込むことができ、 さらにpythonスクリプトを指定すれば、起動後に自動で行う操作を自由に設定できます。
いままで説明した内容を含むコンソール上での操作手順を外部から指定できるわけですから、入力データはもちろん、操作の内容もその都度自由に変更できます。

blender [blendファイル] --background --python [pythonスクリプト]

スクリプトにレンダリングを行う処理を書いておけば、レンダリング結果をファイルに書き出すことができるので、 あとは入力データを準備すれば、コマンド1行でシーンを構築してレンダリング結果を得ることができます。

Blender内で実行するpythonスクリプトに入力データを渡す

常に固定された入力データを使用するということはあまりないでしょうから、 入力データ文字列や入力データファイルのパスをスクリプトの引数に渡すことを考えます。

実行コマンドのオプションを終了したあとに続けて文字列でパラメータを指定する方法と、環境変数で指定する方法があり、 どちらの場合も、スクリプト内でパラメータを受け取る処理を自分で記述します。

オプションを抜けて文字列で渡す
# test_argv.pyimport sys

offset = sys.argv.index('--') + 1print(sys.argv[offset:])
blender --background --python test_argv.py -- args0 args1
環境変数で渡す
# test_env.pyimport os
print(os.environ['BLENDER_PARAM'])
BLENDER_PARAM=parameter blender --background --python test_env.py

# Windowsではsetを使用できる
# set BLENDER_PARAM=parameter
# blender --background --python test_env.py

デモンストレーション

個人気に試してみたかったのでシェイプキーによる変形を使ったデモをやってみます。
(3Dプリンタ用データをインポートしてレンダリングするとかだと説明の必要もないですし・・・)

シェイプキーで再現しやすそうな、轆轤回しか旋盤加工っぽい何かで面白いものを探したところ、良さげなのを発見。

Personalized Jewelry. The Original Soundwave Bracelet / Necklace

音声データから柱状のアクセサリーを作ってくれるそうです。
テカテカのステンレス製なら、マテリアルはさほど凝ってなくても見栄えしますね。

準備

モデルはあらかじめ準備しておき、それを変形してお目当ての形状を得る方向で作業します。
(記事の都合上、ここではモデルの編集方法は詳しく説明しません。)

まず、ループカットで細かく輪切りに分割した円柱を作成し、 分割した各円周に対して、シェイプキーを割り当てておきます。
旋盤加工っぽいことをやりたいので、値が1に近いほど削れて細くなるように、 Value=0でそのまま、Value=1で半径がもとの10%になるようなシェイプキーをKey 1 ~ Key 32まで設定してみました。
ここままだとカクカクしているので、Subsurf Divisionモーディファイヤーを追加して滑らかにしておきます。 ワールド、光源、カメラ、マテリアル、レンダリング設定などもお好みで適宜設定。

f:id:devpixiv:20161206122259j:plain

pythonスクリプト

blendファイルとwaveファイルを読み込み、 あらかじめ用意した円柱に対して、wavの振幅をシェイプキーとして割り当ててみます。

# rendering.pyimport bpy
import sys
import os
import wave
import numpy as np

defshape_key_values_from_wave(wave_file, step_num):
  data = wave.open(wave_file)
  nchannels, sampwidth, framerate, nframes, comptype, compname = data.getparams()
  step = int(nframes / step_num)
  shape_key_values = []

  for i inrange(step_num):
    buff = data.readframes(step)
    buff = np.frombuffer(buff, 'int16')
    # あまり滑らかでないほうが見た目が良かったので、思い切ってざっくりとサンプリングする
    buff = [abs(v) for v in buff[:50]]
    shape_key_values.append(np.average(buff))

  data.close()

  # 正規化して見栄えを良くする
  max_amp = max(shape_key_values)
  return [1.0 - (float(v) / max_amp) for v in shape_key_values]

defmain():
  # パラメータを取得する
  offset = sys.argv.index('--') + 1
  wave_file = sys.argv[offset:][0]

  # 開いた.blendファイルのファイル名とディレクトリの取得
  d_name, f_name = os.path.split(bpy.data.filepath)

  # 既存のシーンとオブジェクトを選択する
  scene = bpy.context.scene
  obj = bpy.data.objects['Cylinder']

  # waveからシェイプキーを生成してセットする
  shape_key_values = shape_key_values_from_wave(wave_file, 32)
  for i inrange(32):
    obj.data.shape_keys.key_blocks[i+1].value = shape_key_values[i]

  # .blendファイルと同じディレクトリに出力するように設定
  scene.render.filepath = os.path.join(d_name, 'output.png')
  # レンダリングしてファイルに書きだす
  bpy.ops.render.render(write_still=True)

try:
  main()
exceptException:
  print(traceback.format_exc())
  sys.exit(1)

参考:
【python】波形の表示(モノラル・ステレオ)【サウンドプログラミング】 - すこしふしぎ.
PythonでWAVファイルを読み込む - 音楽プログラミングの超入門(仮)
Pythonで音の波形を表示(Wavファイル)

レンダリング

あらかじめ要したblendファイル(wave_cylinder.blend)と、上記pythonスクリプト、適当な音声ファイル(sound.wav)を指定してBlenderを起動すると、自動的にレンダリングが開始されます。
今回し要したスクリプトではblendファイルと同じディレクトリにレンダリング結果が出力されるようになっています。

blender wave_cylinder.blend --background --python rendering.py -- sound.wav

f:id:devpixiv:20161206174328p:plain

入力に使用したwavファイルによって、いい感じの形状になったり、イマイチだったり、ただの棒になったりすると思います。

あとは、お風呂の栓についているようなチェーンのモデルを追加して、背景設定を工夫すれば、オシャレアイテムの仕上がりイメージの完成です。
もう少し分割数に関しては、もう少し増やした方が良かったかもしれない。

お疲れ様でした。
引き続き、ピクシブ株式会社 Advent Calendar 2016をお楽しみください。

割れ窓理論を導入してWebサービスのクオリティに直結した話

$
0
0

ピクシブ株式会社 Advent Calendar 2016 @8日目の記事です。

こんにちは、エンジニアのdo7beです。pixivFANBOXの開発などに携わっています。

さて、今回は新規開発プロジェクトに「割れ窓理論」を導入してサービスのクオリティ向上に繋がった話をご紹介したいと思います。

割れ窓理論とは

「割れ窓理論」とは、アメリカの犯罪学者ジョージ・ケリング氏が提唱した「建物の窓ガラスが割れたまま放置されると住民もゴミを捨てるようになり、治安が悪化し、より重大な犯罪が発生してしまう」という理論です。

つまり軽犯罪を取り締まることこそが、重大な犯罪を防ぐために重要であることを指しています。

私たちのチームではこれをWebサービスの新規開発プロジェクトに取り入れています。

WEBサービスにおける割れ窓は「軽度のデザイン崩れ」や「表記ゆれ」に、重大な犯罪は「バグ」や「全体のクオリティ低下」に該当します。つまり割れ窓理論をプロジェクトに取り入れるということは、全体のクオリティ向上のため、優先度が低いタスクに”優先的に”取り掛かる体制を作ることに繋がります。

チームでの活用事例

新規開発を進めるにつれて軽微なバグやデザイン崩れが積み重なり放置されてしまうという悩みを抱えていました。そんな時に割れ窓理論の記事を見つけ、その日のうちに導入してみました。

参考記事:デザインとプロダクト開発における「割れ窓理論」 – Medium Japan – Medium

f:id:devpixiv:20161208130750j:plain

私たちのチームではタスク管理ツールとしてTrelloを採用しており、各人がプロダクトを触りながら少しでもデザイン崩れに気づいたら「割れ窓リスト」にタスクを追加しています。そして週に一度設けている「割れ窓デー」では下記のようなことを行っています。

  • 全員で30分ほどサービスに触れて割れ窓やバグを探す
  • 現状判明している割れ窓タスクが妥当かどうか議論する
  • チームメンバー全員で集中的にタスクを片付ける

f:id:devpixiv:20161206144002p:plain

やってみた結果

割れ窓デーとして週に一日を費やしているため、重要なタスクに取り掛かる工数が減ってしまいます。しかし、割れ窓理論を導入し様々な良い結果を得られることができました!

  • デザイン崩れがなくなることでサービスに統一感が生まれ、重大な犯罪に該当した「全体のクオリティ低下」を継続的に防ぐ仕組みを作ることができた
  • 割れ窓について普段から意識するため、チームメンバー間でサービスの正しい姿について自然と共有されるようになった
  • 新規開発において優先度が低く溜まりがちな「デザイン崩れ」のタスクが片付くため、本来やるべきタスクに集中できるようになった

現に、デザインについて意見を述べることのなかったチームメンバーが「ここのボタンの色、ルールおかしくないですか?」等の指摘をしてくれるようになりました。

まとめ

新規開発中やリリース直後はどうしても些細なタスクを多く抱えてしまいがちです。このような問題に立ち向かうことでサービスのクオリティ向上に繋がる割れ窓理論、ぜひ試してみてはいかがでしょうか?

AndroidのBottomNavigationってどうなのよ?pixivコミックのリニューアルを通じて考えてみた

$
0
0

こちらはピクシブ株式会社 Advent Calendar 2016、9日目の記事です。

こんにちは。Androidアプリエンジニアの @rooandqoo です。普段は「pixivコミック」というアプリの開発や運用をしています。

f:id:rooandqoo32:20161208194940p:plain:w200

↑こんなやつです

pixivコミックアプリのリニューアル

さて、先日アプリ版pixivコミックがリニューアルされ、カテゴリごとに「注目」「ランキング」「新着」と分類されたマンガが並ぶトップページが、「特集」というかたちで、まるで図書館の本棚のようにさまざまなジャンルのマンガが並ぶページに生まれ変わりました。

f:id:rooandqoo32:20161209155046p:plain:w240

例を挙げると、「胃袋を刺激するグルメマンガ」「男子禁制?女の園特集!」といった具合です。 pixivコミックならではの作品たちと出会える非常に素敵なアプリとなっておりますので、ぜひダウンロードしてみてくださいね。

この「特集」はpixivコミック編集部が1,800を超える作品の中から毎週ピックアップしてつくっているのですが、その話はまた別の機会に。

さて、このリニューアルはAndroid, iOS両プラットフォームにおいて行われたのですが、特に大きく変わったのはAndroid版です。 どこが大きく変わったのか、まずはこちらをご覧ください。

before

f:id:rooandqoo32:20161208200001g:plain

after

f:id:rooandqoo32:20161208201105g:plain

Androidアプリには珍しい「画面下部のナビゲーション」がついていることがわかるかと思います。 今日はこちらの実装にあたって、採用の方針や苦労したところをお話します。 さっくり結果だけ言うと、「実装して良かった」です。

画面下部ナビゲーションを実現するBottom Navigationとは

Bottom Navigationは、Material Designのガイドラインの今年の3月頃に追加されたコンポーネントです。 Google+など一部のGoogle製アプリにはしばらく前から実装されていましたが、Material Design的には非推奨のデザインでした。

ドキュメントはこちら

iOSではよく見るUIですが、Androidアプリにおいて、トップレベルの要素を行き来するナビゲーションは NavigationDrawer で実装するのがスタンダードなものでした。

f:id:rooandqoo32:20161208195715p:plain:w240

(NavigationDrawerの様子です)

NavigationDrawer は閉じている間、画面を専有する面積が狭いので、コンテンツに使える領域が広いというメリットがあります。しかし、トップレベルの要素が複数存在する際、わざわざ左上のボタンを押してメニューを開かなくてはならないため、ユーザーに「このアプリで何ができるか」を一発で伝えるための手段がかなり制限されてしまう、というデメリットがありました。

pixivコミックでは、トップレベルの要素として「ホーム」「検索」「フォロー新着」の3つを持ち、さらにその中で「公式作品」と「投稿作品」に世界が分かれています。 (どちらを先に分割するかで実装の方針も変化しますが、先に機能から分けていく方針で実装しています) マテリアルデザインの Navigation ガイドラインに則ると、「ホーム」以外の要素は NavigationDrawer 内に隠すことになってしまい、アプリでよく使われがちな「検索」と「フォロー新着」に辿り着く距離が遠くなってしまいます。 タブを使うこともできるのですが、各タブの中で更に「公式作品」「投稿作品」の分岐を見せねばならないのがネックとなります。

リニューアル前は、苦肉の策として ActionBar にそれぞれの導線を置いている状況でした。(※本来 ActionBar はコンテンツに対するアクションを設置する場所なので、不適切)

実装

公式のデザインサポートライブラリ(https://developer.android.com/topic/libraries/support-library/revisions.html)をはじめ、GitHub にもたくさんのライブラリがあります。

サポートライブラリの BottomNavigation は、アイテムが4つ以上になったときのアニメーションをオフにできなかったので、こちらは一旦考えないことに。 結局、実装がシンプルかつ、アイテムの上にバッヂを付加する機能をサポートしていたため、AHBottomNavigationを採用しました。

実装はおおよそGitHubに記載してある通りにしただけですが、実際のトップページのレイアウト構成は図のようになっています。

f:id:rooandqoo32:20161208201314p:plain:w240

実はこの機会に初めてCoordinatorLayoutも導入しています。 コンテナ内部のFragmentがRecyclerViewになっていると、ToolBarやBottomNavigationがスクロール時に自動で隠れるのは驚きでした。CoordinatorLayout恐るべし……。

よかったこと

まず、ひと目で「何ができるか」わかりやすくなったように思います。 手の届きやすいところに「ランキング(こちらは追加で実装されたコンテンツです)」「検索」「フォロー新着」が並ぶことで、各画面へのアクセスが容易になりました。 よく使われる機能までの距離が近くなったことが寄与したのか、それぞれのスクリーンビュー数も大幅に増加。

そしてActionBarがスッキリしたことも個人的に嬉しかったポイントです。

つらかったこと

それまで、トップページを始めとした画面はほぼすべてListViewGridViewで実装していたので、それらをすべてRecyclerViewに置き換えるのが大変骨の折れる作業でした。 また、画面遷移にも気を使ってあげなければなりません。 ビューは基本的にFragmentで実装していたのですが、好き勝手移動できる分、Fragmentの重ね順の制御で泣きを見ることに。 遷移先をActivityで定義し直すことで対応しましたが、もっと綺麗な運用方法があれば知りたいところです。

まとめ

Android版pixivコミックアプリのリニューアルに BottomNavigation を導入した話でした。 3つ以上のトップレベルの要素を NavigationDrawer に仕舞いたくない、という場合には非常に有用なコンポーネントであると感じました。

おわりに

本日は書けませんでしたが、pixivコミックアプリでは Firebase Dynamic Links を社内でいち早く導入したり、Swift 3 への対応を最速で行ったりと、非常にフレキシブルな現場で開発されています。そのあたりの話もいずれできればと思っています。

pixivコミックは沢山のマンガとの出会いを提供するためまだまだ進化していきます。そんな発展途上のpixivコミックをぜひぜひよろしくお願いいたします。 なお弊社ではマンガ大好きなエンジニアも大募集してます!!

明日は @がVimの濃厚な?話をしてくれるはずです。

pixiv開発を支えるVim (タグジャンプ編)

$
0
0

こんにちは、 ピクシブ株式会社 Advent Calendar 2016の10日目の記事を担当します、エンジニアのkanaです。弊社は様々なサービスを開発・運営していますが、私はその中でもイラストコミュニケーションサービスのpixivの開発に携わっています。

今回は日々の開発の中で気になったちょっとしたVimの話をします。

発端

コードを読み書きしてると「この便利メソッドが中でやってる処理がどうにも臭うぞ……」という場面にしばしば遭遇します。そういう時はタグジャンプを使います。

  1. universal-ctagsをインストールする
  2. プロジェクトのルートディレクトリで ctags -Rを実行して tagsファイルを生成する

という前準備を済ませたら、後は

  • <C-]>で定義に飛ぶ
  • <C-t>で元の位置に戻る

というキーバインドを覚えるだけでコードツリーを高速で飛び回る事ができます。これで毎回 grep NankaAyashiiMethodしなくても一発で目的の位置まで飛べるようになりました。

問題

タグジャンプを覚えると生産性は2000倍くらいに上昇するのですが、日々使っていると問題がある事に気付きます。 同名の定義に弱いのです。

例えばプロジェクトに以下のファイルがあるとしましょう:

<?phpfinalclass Illust
{...publicstaticfunction getByIds(array$illust_ids){...}...}
<?phpfinalclass Novel
{...publicstaticfunction getByIds(array$novel_ids){...}...}
<?phpfinalclass User
{...publicstaticfunction getByIds(array$user_ids){...}...}

そして小説の検索画面のコードを読んでいて以下のコードに遭遇したとしましょう:

<?php...$query=[/* ... 何だか凄そうなクエリー ... */];
$novel_ids= SearchEngine::get($query);
$novels= Novel::getByIds($novel_ids);  // ←何か気になる...

ここで getByIdsにカーソルを合わせて <C-]>を押下すると Novel::getByIdsの定義に飛……びません。 Illust::getByIdsの定義に飛びます。

タグジャンプはカーソル下の識別子の定義位置を tagsファイルから探します。この例だと getByIdsの定義位置を tagsファイルから探します。しかし getByIdsの定義は3つあります。このような場合、(ファイル名の辞書順で)最初の定義に飛びます。 Novel::getByIds(...)とクラス名が自明な呼び出し方なので Novel::getByIdsの定義に飛んで欲しいところですが、そういう文脈は読み取ってくれません。

こういう場合は :tnext:tpreviousで他の同名の定義位置を行き来する事が出来ります。これで当座は凌げるのですが、何回も同じ状況に遭遇するにつれてストレスが溜まっていきます。

しかも、この位の問題なら既に誰かが解決してるだろうと思い検索してみても全然出てきません。PHP限定ならどうにかなるかと思って調べてみると、 phpcomplete.vimそれっぽいオプションがある事は分かりましたが、実際に試してみるとイマイチでした(self::hoge()$this->hoge()に対しては現在編集中のクラスと関係なく最初の定義に飛び、 $var->hoge()に対しては $varの型を無視して現在のファイル内の定義が優先される。 ClassName::hoge()のみ適切な定義位置に飛ぶ)。これは困りました。

実装方針

出来合いの物が無いなら無いで仕方がないので自分でどうにかするしかありません。ざっくり実装してしまいましょう。完璧な精度を追い求めるとキリが無いので、実用上困らないレベルで押さえておくことにします。具体的には以下の条件にします:

  • 対応する言語はPHPに限定する
    • pixivの開発ではPHPを読み書きする頻度が圧倒的に高い為
  • クラス名が比較的自明な self::method()$this->method()ClassName::method()なら該当するクラスのメソッド定義に飛べるようにする
    • pixivのコーディング規約ではインスタンス作成は極力避ける事になっており、上記3点に対応すれば実務で困る事はまず起きない為

後はどう実装するかですが、これはざっくり3ステップに分解できます:

  • tagsファイルの生成時にメソッドの定義位置だけでなくクラス名も含める
  • カーソル位置の識別子にマッチする全てのタグ情報を tagsから取得する
  • 呼び出し方とどのクラス名を使うかを判定して適切な位置へジャンプする

これで方針が決まったので一つ一つ実装していきましょう。

実装

tagsファイルの生成時にメソッドの定義位置だけでなくクラス名も含める

これは楽勝です。universal-ctagsを使っていれば何もする必要がありません。

$ ctags *.php

$ grep getByIds tags
getByIds        lib/Illust.php  /^    public static function getByIds($illust_ids)$/;"  f       class:Illust
getByIds        lib/Novel.php   /^    public static function getByIds($novel_ids)$/;"   f       class:Novel
getByIds        lib/User.php    /^    public static function getByIds($user_ids)$/;"    f       class:User

カーソル位置の識別子にマッチする全てのタグ情報を tagsから取得する

これも楽勝です。Vimでは

  • taglist()で識別子にマッチする全てのタグ情報を tagsファイルから取得でき、
  • <cword>でカーソル下の識別子を取得できる

ので、

taglist(expand('<cword>'))

で目的のものが得られます。

呼び出し方とどのクラス名を使うかを判定して適切な位置へジャンプする

これは……楽勝ではなさそうです。とはいえ一手一手詰めて行けば直ぐできるでしょう。 必要そうな処理に分解して実装していく事にします。

対象の識別子の位置を特定する

function! s:_GetCwordStartPos()letcword=expand('<cword>')letcword_pattern='\V'.escape(cword, '\')letcword_end_pos=searchpos(cword_pattern, 'ceW', line('.'))letcword_start_pos=searchpos(cword_pattern, 'bcW', line('.'))return cword_start_pos
endfunction

カーソル下の識別子をそのまま検索して、結果のカーソル位置から識別子の位置を割り出そうという試みです。

識別子の直前のコードから呼び出し方を判定する

function! s:GuessClassName()letcursor_pos=getpos('.')letclass_name= s:_GuessClassName()callsetpos('.', cursor_pos)return class_name
endfunctionfunction! s:_GuessClassName()letline=getline('.')letprefix_end_index= s:_GetCwordStartPos()[1] -2letprefix= prefix_end_index >=0 ? line[:prefix_end_index] : ''" 識別子の直前のコードがif prefix =~#'\<self::$'|| prefix =~#'$this->$'" self:: または $this->return s:_GetCurrentClassName()elseif prefix =~#'\<\k\+::$'" 多分クラス名returnmatchstr(prefix, '\<\zs\k\+\ze::$')else" 不明return''endifendfunction

これは軽くパターンマッチするだけなので簡単ですね。

現在のクラス名を取得する

functions:_GetCurrentClassName()normal! 999[{
  ifsearch('\<class\>', 'bW')==0return''endifnormal! W
  returnexpand('<cword>')endif

[{で釣り合いの取れた {に移動できるので、これを繰り返せば一番外側 = クラス定義の {にまで辿り着けるだろうという試みです。

複数ある候補の優先順位を調整する

function! s:ReorderTags(tags)letcword=expand('<cword>')letcurrent_filename=expand('%:p')letexact_tags_in_current_file= []
  letother_tags= []
  fortagina:tagsiftag['name'] ==# cword &&fnamemodify(tag['filename'], ':p')==# current_filename
      calladd(exact_tags_in_current_file, tag)elsecalladd(other_tags, tag)endifendforreturn exact_tags_in_current_file + other_tags
endfunction

<C-]>は現在のファイルに存在する定義に優先して飛ぼうとします。 tagsファイル内の出現順序はジャンプ先の優先順序と一致しません。 なので taglist()の並び順が実際の優先順序と一致するよう調整する必要があります。

複数ある候補の何番目に飛べば良いか判定して実際に飛ぶ

function! s:IikanjiNiJumpShite()letclass_name= s:GuessClassName()letjump_count=''if class_name !=''lettags= s:ReorderTags(taglist(expand('<cword>')))fortagintagsifhas_key(tag, 'class')&&tag['class'] ==# class_name
        letjump_count=index(tags, tag)+1breakendifendforendifexecute'normal!'jump_count."\<C-]>"endfunction
  • 文脈から飛びたいクラス名が判明している
  • 実際に tagsに該当クラスのメソッド定義がある

という状況ならその定義に飛ぶという試みです。 確信が持てない状況ならデフォルトの <C-]>に任せます。

仕上げ

後は一連のコードを ~/.vim/after/ftplugin/php.vim辺りに貼り付けて

nnoremap<buffer><silent><C-]>  :<C-u>call <SID>IikanjiNiJumpShite()<CR>

も追加すれば良い感じにジャンプできるようになります。

(→ 直ぐ使えるようにプラグイン化しておきました)

動作確認

実際に試してみましょう:

youtu.be

ちゃんと動いてるようです。よかったよかった。

まとめ

これで明後日の定義に飛ぶ事が無くなって、より快適に開発ができるようになりました。やったぜ!

ピクシブ株式会社では、このように日々の開発効率を地道に上げるのが好きなエンジニア・アルバイトを募集しています。使用エディタは問いません。

recruit.pixiv.net

明日はtadsanさんによるpixiv開発を支えるEmacsの話です。ご期待ください。

PHP開発のためのEmacs 2016 (pixiv

$
0
0

こんにちは、今年もピクシブ株式会社 Advent Calendar 2016です。

最近は社内にPhpStormを浸透させようと暗躍*1してますうさみ @tadsanです ヾ(〃><)ノ゙ 今年は.emacs Advent Calendar 2016も書いてます!

2014年はEmacsでpixiv-novel-modeを作ったを書きましたが、今回は私がどうやってEmacsで仕事をするのか、そしてどのようにPHP開発環境を効率化するかについて書きます。

ちなみに昨日はkanaによるpixiv開発を支えるVim (タグジャンプ編)なので、Vimをご利用の型はこちらも読んでみてください。

なんとかStormに負けたくない気持ち

今回の内容の要約は、今年3月にPHPの知見などを共有するLT会・PHP BLT #3で発表したので、長い文章を読むのがだるいひとは見といてください。

SSHとTRAMP

pixivの開発環境は現在のところローカルマシンで動かすことが容易ではないため、共用のDebian GNU/Linuxサーバーにログインして作業する必要があります。

EmacsにはTRAMP(Transparent Remote (file) Access, Multiple Protocol)機能があります。これは、リモートサーバーや別ユーザー権限でファイルを透過的に編集するための機能です*2。PC-UNIX(Linux/macOSなど)環境であれば、~/.ssh/configの設定を読んでくれるので、SSH公開鍵でログインできる環境ならば設定は非常に簡単です。ssh hoge-servのようにログインできるサーバならば/ssh:hoge-serv:/または/scp:hoge-serv:/で開くことができます。

私はrecentfを使って履歴からファイルを開くのが好みなのですが、ローカルファイルもリモートファイルもまったく同じように絞り込んで検索できるので、とても捗って便利が強いです。

タグジャンプ

昨日のVimの記事で言及されてたタグジャンプ(クラスやメソッドの定義位置に移動する機能)ですが、私も最近まで同じ問題に悩まされてました。しかし、最近リリースされたEmacs 25.1で既存のタグジャンプの代替として搭載された新機能Xrefは定義の重複があったときには選択できるようになったので、実はあまり困ってません。

f:id:zonu_exe:20161211233855p:plain

私はctags日本語対応版を自分でビルドして、こんなシェル関数でTAGSファイルを出力します。

phptags(){
    ctags -e--php-types=c+i+d+f$(git ls-files '*.php')}

Emacs Lispパッケージ

Emacs 24.1以降、package.elと呼ばれるパッケージ管理システムが標準機能に取り入れられました。これに更新が活溌なMELPAリポジトリを追加して利用するのが一般的です。

今回紹介するパッケージは基本的にMELPAからインストールできます。まだ利用してなければ、package.el - Emacs JPを参考にして設定してみてください*3

インターフェイス

デフォルトの状態よりもEmacsの各機能を利用しやすくために、補完インターフェイスを設定することは有効です。詳細は君は誰とEmacsる? (補完インターフェイス紹介篇)をお読みください。

php-mode

ことあるごとに紹介してるのですが、php-modeは現在でも更新が続いてますので、「むかしダウンロードしたけど更新してない」ってひとは、最新版を使ってみてください。最近ではPHP 7.1で追加された文法のサポートなども入ってます。

web-mode

web-mode.elはHTMLテンプレートを編集するためのメジャーモードです。pixivで利用するSmartyのほか、数々のテンプレート言語に対応します。

web-modeはPHPにも対応しますが、pixivではPHPをHTMLテンプレートとしては利用することはないため、私はphp-modeを利用します。

(add-to-list 'auto-mode-alist'("\\.tpl\\'" . web-mode))(add-to-list 'auto-mode-alist'("\\.html?\\'" . web-mode))

EditorConfig

EditorConfigは各種テキストエディタに対応した設定ファイルです。これを利用するとプロジェクト固有のインデントルールなどが自動で設定されるので、わざわざ設定することがなくなります。

各エディタの拡張プラグインなどがありますが、Emacsのパッケージとしてもeditorconfig-emacsがあります。

せっかくなので、pixivで使ってる.editorconfigファイルを置いておきますね。単なる設定ファイルなので著作権などはありませんから、勝手にコピペして使ってください。

Magit!

MagitはEmacs上からGitの機能を利用するためのパッケージです。Gitのほとんどの機能をEmacsと統合することができます。Gitの差分から部分ステージングできる機能(git add -p相当)、編集中のバッファのgit blameを表示する機能(M-x magit-blame)、同じくファイルの履歴を表示する機能(M-x magit-logM-x magit-log-buufer-file)などが革命的にべんりです。

TRAMP経由のリモートサーバでも、ローカルファイルと同様に操作できるのは圧倒的なメリットです。デメリットとしては… リモートファイルおよび巨大なリポジトリではちょっと遅いので、打鍵してから数秒待たされることを許容できないひとにはおすすめできません。

magit-find-file

最近のエディタではプロジェクト内のファイル名を検索して開く機能があります。magit-find-file.elはGit管理下のファイルを簡単に絞り込むことができます。

php-eldoc

PHPには「array_maparray_walkの引数が逆!」「implodeexplodeの引数が覚えられない」のように初心者がうんざりする仕様があります。大丈夫、そんなの私も覚えてません。

ElDocは、関数/メソッドの入力中引数を表示するEmacs標準の機能です。sabof/php-eldocはPHPの標準関数の引数に対してElDocを表示してくれます。

f:id:zonu_exe:20161211233942p:plain

余談

array_map()array_walk()はそもそも役割が異なるのです。array_map()は配列の写像をとる関数ですが、入力には複数の配列をとることもできます。つまり、array_map(function($name, $work){ return ['NAME' => $name, 'WORK' => $work]; }, $names, $works)のような処理が書けます。一方で、array_walkはリファレンスをとって、配列を走査・操作することができます。つまり、$ids = [[7, 5, 3], ['3', '1', '5']]; array_walk($ids, function(&$a){ sort($a); });またはarray_walk($ids, 'sort');でソートできることを意味します。後者の用途にarray_map()は適しません。

phpunit.el

phpunit.elはEmacs上からPHPUnitをEmacs上から利用するためのパッケージです。私もバグ修正やTRAMP対応など開発に参加してます。

phpunit-current-projectでテスト全体(正確にはphpunitを引数なしで起動したときに実行されるテスト)を、phpunit-current-classで現在編集中のテストクラスにあるテストを実行できます。また、phpunit-group@groupが登録されたテストを選択的に実行できます。

composer.el

composer.elはPHPの依存管理性ソフトウェアであるComposerをEmacs上から透過的に利用するためのツールです。M-x composerではComposerの各種サブコマンドを対話的に実行できます。

EmacsからComposerを叩けて嬉しい場面もそんなに多くはありませんが、設定確認のためにM-x composer-find-json-fileで現在作業中のプロジェクトのcomposer.jsonを開けたり、M-x composer-view-lock-filecomposer.lockを読み込み専用モードで開けたりするのは、それなりにべんりなことがあるかもしれません。

Flycheck

Flycheckは編集中のファイルのsyntax checkを実行できるツールです。Emacs標準機能のflymakeよりも高機能・高速で、数多くの言語に対応します。容易に拡張もできるため、後述するように社内独自のコーディングルール違反を検知するlint checker機能を統合できます。

基本的な機能としてはPHPのSyntax Errorをその場で検出できるので、GitHubにpushしてからSyntax Errorに気付く、といったかっこわるいミスを避けられます。

f:id:zonu_exe:20161211234943p:plain

PsySH / psysh.el

PsySHはPHPのREPL/対話シェル実装です。要は、RubyのIRBとかPryとかrails console、Pythonのpythonコマンドを引数なしで実行したときに起動するアレです。

psysh.elはEmacs上からPsySHを起動したり、php-modeやphp-eldocとの連携もできます。

現在のクラス名を入力

pixiv開発を支えるVim (タグジャンプ編)にもある通り、現在pixivのコーディング規約ではクラスの静的メソッドの多用とクラス名の明示が推奨されてますので、編集中のクラス名が一発で入力できますと、大変捗ります。

この機能は現在php-modeに機能追加提案してますが、まだ取り込まれてないのでphp-util.elから、自身の.emacsファイル(~/.emacs.d/init.el)などにコピペすると良いです。

;; php-mode のキーバインドに登録する(with-eval-after-load 'php-mode(define-key php-mode-map (kbd "C-c C--")'php-util-insert-current-class)(define-key php-mode-map (kbd "C-c C-=")'php-util-insert-current-namespace))

pixiv-dev.el

これはpixiv開発にべんりな設定をまとめたパッケージです。コードはzonuexe/dotfiles/pixiv-dev.elにあります。

f:id:zonu_exe:20161211234022p:plain

M-x pixiv-dev-copy-file-url

これは編集中のファイルのWeb上のURLをコピペするコマンドです。GitLabでもGitHubでもいいのですが、「このファイルのここ見といて」とチームメンバーにリポジトリ内のファイルを共有する際に、わざわざブラウザでファイルの位置を探してから共有するのは明らかに非効率です。

このコマンドがあればファイルパスからWeb上のURLを一発でコピペできるので、時間を大幅に節約できます。現在は完全にURLをべた書きをしてますが汎用化はできそうなので、もっと改善して、そのうちライブラリにしたいですね。

M-x pixiv-dev-shell

開発サーバーでPsySHを起動できます。カレントディレクトリがどこでも起動できるので超べんりです。

M-x pixiv-dev-find-file

リモートサーバー上のpixivのディレクトリから(わざわざカレントディレクトリを移動しなくても)ファイル選択ができます。作ったは良いのですが、あまり使ってません。

pixiv-dev-mode

マイナーモードです。このマイナーモードの機能として、psyshpixiv-dev-shellのために設定したり、プロジェクト専用のFlycheckのチェッカーを定義したりします。

pixiv-lintはファイルに対してコーディング規約のチェックを実行し、以下のようなログを標準出力します。

file:pixiv-lib/TagCounter.php   line:3  col:0-5 level:warn      desc:クラス定義には final を明示すること

これを解釈するチェッカーは以下のように定義できます。 (同じ記述が反復してるので、もっと簡潔に書けないのかなあ……)

(flycheck-define-checker pixiv-dev-lint
  "Lint for pixiv.git"
  :command ("pixiv-lint" source)
  :error-patterns
  ((info line-start "file:"(file-name)"\tline:" line "\tcol:"(+(or"-" num))"\tlevel:info""\tdesc:"(message) line-end)(error line-start "file:"(file-name)"\tline:" line "\tcol:"(+(or"-" num))"\tlevel:error""\tdesc:"(message) line-end)(warning line-start "file:"(file-name)"\tline:" line "\tcol:"(+(or"-" num))"\tlevel:"(+ alnum)"\tdesc:"(message) line-end))
  :modes (php-mode)
  :next-checkers (php))

f:id:zonu_exe:20161211234606p:plain

弱点なのですが、私がまだFlycheckに詳しくなくてリモートサーバーにあるスクリプトを直接利用する方法がわからず、ローカルのPATH環境変数が通った場所にスクリプトをコピーしてくる、みたいなことをやってます。ルール更新のたびにコピーしてくるのもちょっとめんどくさいので、もっと効率化したいところです。

肝腎のlintスクリプトの件についても紹介したいところですが、Emacsの話題からは少し外れるので別の機会に紹介いたします。

まとめ

今回はEmacsで(PHPをはじめとした)コーディングのために便利なパッケージと自作の定義について説明しました。

もしわからないことがあれば、php-users-ja slackemacs-jp slackなどのコミュニティに常駐してますので質問していただければ解決できるかもしれません ヾ(〃><)ノ゙


さて、明日のピクシブ株式会社 Advent Calendar 2016は、弊社随一のJavaScript野郎のgeta6です。

脚注

*1:もともと経費で購入できるのですが、なまじ手慣れたエディタでどうにかできちゃってる人の方が多いのが逆に悩みなのです。

*2:概念について知りたい型は、TRAMP User Manualをお読みください。(2001年頃の訳ですが、TRAMP User Manual(日本語訳)もあります)

*3:もっと詳しく知りたい型は2015年Emacsパッケージ事情をお読みください

pixiv Sketchのフロントアーキテクチャ

$
0
0

(ピクシブ株式会社 Advent Calendar 2016 12日目の記事です)

qiita.com

こんにちは、エンジニアの id:geta6です。普段は主にpixiv Sketchの開発などに携わっています。

さて、先日ISUCON6の本選が開催され、ISUketchというReactを使ったお絵かきサービスが話題になったかと思います。

この問題で採用されたアーキテクチャは『pixiv Sketch』のアーキテクチャを参考にしたものとなっています。

パフォーマンス等に関しましては本選の感想として良質な記事がいくつも執筆されていますのでそちらに譲るとして、今回はこのアーキテクチャの設計意図や利点について話します。

どういうアーキテクチャか

pixiv SketchのWeb版は、2つのサーバーから構成されています。

1つ目はバックエンドでAPIを担当する『APIサーバー』で、Railsで作られています。APIサーバーはViewを持たず、主にJSONのみを出力します。

2つ目はフロントでViewのレンダリングを担当する『レンダリングサーバー』で、expressで作られています。httpクライアントを用いてAPIサーバーにリクエストを送り、かき集めた情報を元にReactをサーバーサイドレンダリングし、ユーザーへレスポンスを返します。

レンダリングサーバーのアプリケーションコードは、クライアントコードとしてそのまま利用されます。初回リクエスト以降はHistoryAPIを使ってリロード無しでURLを書き換え、APIサーバーから直接データを入手してViewを構築します。

バックエンドのAPIサーバーは、iOSアプリケーションやAndroidアプリケーションからも利用されます。iOSやAndroidのアプリケーションからフロントのレンダリングサーバーへリクエストが飛ぶことは基本的に無く、レンダリングサーバーは『WebBrowser利用者向けの1クライアント』という位置付けになっています。

f:id:geta6:20161212144353p:plain

設計の意図

『レンダリングサーバー』と『APIサーバー』を分離するというアーキテクチャは、主に3点の意図から設計されています。

マルチプラットフォーム前提

1点目は、pixiv Sketchでは開発開始当初からiOS版やAndroid版など、複数のプラットフォームからサービスを提供する予定があった点です。

Web版の開発開始時点で「Web版とiOS版を同時にリリースする」という目標があったこと、また「いずれAndroid版もリリースする」という予定があったため、設計時点でAPIベースの設計を強く意識しました。具体的には『Web版とiOS版が同時かつ新規に開発できる』という要件を重視していました。

「テンプレートを部分的にレンダリングしたhtmlをRailsから返し、DOMに挿入する」というアイデアもあったのですが、やはりiOS版の開発を同時かつ新規に行えるという要件から『ViewもAPIを利用することにして、データのインターフェースを統一した方がよい』という結論に至りました。

テンプレートの二重実装を防ぐ

2点目は、過去にRails側のテンプレートとJavaScript側のテンプレートを二重に実装する必要に迫られ、非常に辛い思いをしたことがあった点です。

Railsから出力されたView上で『ユーザーの入力内容に応じて内容が変化したり、要素が増減してプレビューができる』という機能を実装したことがあったのですが、JSが要素を挿入する際、Railsで実装したテンプレートとは別途にJSが認識できるテンプレートを実装する必要ができてしまいました。

このつらみを回避するため『ViewはView、ロジックはロジック、アーキテクチャーレベルで2者を完全に分離し、テンプレート実装を一度のみにしたい』という強い思いがありました。

個人的にやりたかった

3点目は、Node.jsを使ったReactのサーバーサイドレンダリングを実運用でやってみたかった、という個人的な理由によるものです。今までNode.jsをメインに据えたプロダクトは、期間を限定して公開したプロダクトを含めても数えるほどしかなく、よい挑戦の機会になりました。

設計の利点

この設計にしてよかったな、と思った代表的な利点を3つ挙げます。

コードの責任範囲が有限になる

アーキテクチャを分離しているので、どんなに頑張ってもViewがロジックを持つことはできません。「なんかテンプレートだと使えるけどAPIには乗っていない値があるらしい」といったような怪しさ満点の運用もできなくなります。

また、Viewへの変更はAPIサーバーのコードとは無関係なため、例えばエラーを含んだコードをデプロイしてしまいレンダリングサーバー全体が応答できなくなったとしても、APIサーバーの安定性に影響を及ぼしません。つまり、他のプラットフォームに影響を及ぼす可能性を限りなく低くすることができます。

f:id:geta6:20161212171113p:plain

コードの責任範囲が限定的であることは、コードレビューを効率化するという側面もあります。レンダリングサーバーのコードレビューは『js的に悪い書き方をしていないか・パフォーマンスは悪くなっていないか』などといった、単純なクライアントコードとしての良し悪しに集中して費やされることになるからです。

メンバーと共通の言語で会話できる

MVCが一体となったアプリケーションを開発していた時と比べて、例えば同じプロジェクトのiOSエンジニアと「このデータってどうやって表現する?」というような会話が生まれやすくなりました(もちろん、こちらから聞くこともあります)。

イテレートするオブジェクトをどのように保持するか・データ単位をどこで切るか・モジュールにどう命名するか・デザインにどう落とし込むかなど、会話は多岐に渡ります。

Viewの実装者が『JSONのスキーマ』という他プラットフォームのエンジニアと共通の言語を得ることで、同じデータを似たように扱うことになり、同じような悩みにぶつかりやすくなり、実装を先行した人に悩みを共有できる環境が整ったことによる効用だと思っています。

プラットフォームで足並みを揃える必要がない

新たに機能を開発するとき、APIの実装さえ終わっていれば全てのプラットフォームで実装を始めることができ、実装が完了すればリリースすることができます。取り決めさえしておけば、APIの実装完了を待たずにレスポンスをモック化して実装を始めることもできます。

重要なのは、Web版の実装が最も遅れた実装になっていても開発上で問題が起きないという点です(ユーザーやプロジェクトとしては問題ですが)。

『Web版もクライアントの1つ』というスタンスで開発されているため『テンプレートのみで実現されている機能』が存在しません。他プラットフォームのエンジニアの言葉を借りれば、「すでにWeb版で提供されているが、APIとしては存在しないので実装を待つしかない機能」が存在しません。

ある機能がプラットフォーム利用者にいつ提供されるかは各プラットフォームのエンジニア自身にかかっており、高いモチベーションの維持につながっています。

f:id:geta6:20161212172701p:plain

やっててよかったこと

このアーキテクチャでは複数のプラットフォーム・プログラミング言語から同一のAPIを頻繁に参照することになるため、APIドキュメントの整備が必須になると思います。

pixiv SketchではSwaggerを利用したドキュメントの自動生成を行なっており、あるエンドポイントから提供されるJSONのスキーマやモデル定義をいつでも参照できるようになっています。APIの実装が完了したら「pullして、読んで」と伝えるだけで各実装者が動けるようになるため、非常に有用です。これが無かったらリリースはもっと遅れていて、バグももっと多かったかもしれません。

APIドキュメントが開発の初期段階から整備済であったことはこのアーキテクチャにとって非常に大きな助けとなりました(実際に提案から導入までやってくれたのは id:walf443でした、ありがとうございます)。

まとめ

ここまでアーキテクチャの利点を話しましたが、もちろん全てのアプリケーションや場面に対して汎用的に適用できるものではありません。例えば『Webで表示するだけなのにリポジトリを2つ管理しなければならない』『Node.jsとRailsと2つのインスタンスを立ち上げる必要がある』等のデメリットもあります。

ISUCON6では「問題として出題する」という前提があったため、アーキテクチャに対して『とっつきにくい』『めんどくせぇ』といった印象を持たれた方もいるかもしれませんが、条件が合致すれば非常に快適な開発パフォーマンスを提供してくれます。

ちなみに、現在の構成だとAPIサーバーはJSONを返すだけのサーバーになっています。当初は開発速度やメンバーの知見の有無を優先してRailsが採用されましたが、処理速度等を優先してGo言語など、より高速な言語を選定するのもアリかもしれません。

この構成を考えついた時の俺は非常に神がかっていたと思うので、参考にしていただければ幸いです。


明日は新卒JSマン期待の星、RaggがJavaScriptかなんかの話をするみたいです、お楽しみに。

まだシングルスレッドでレンダリングしてるの? HTML5 CanvasとWeb Workerの最新技術

$
0
0

こちらは ピクシブ株式会社 Advent Calendar 2016、13日目の記事です。

こんにちは!4月からピクシブに入社したエンジニアの@_ragg_です✨
メンテナンスチーム・pixivFACTORYチーム・pixivFANBOXチームを旅して、デザインをかじったりフロントエンドを触ったりしています、3代目社内旅行エンジニアですね!

さて、今回はHTML5 Canvasに実装されつつあるOffscreenCanvasと、Web Workerについてお話しします。まだ日本語文献の少ないアツアツのネタです🔥🔥

OffscreenCanvas #とは

OffscreenCanvasは、「画面に表示されないCanvas」です。
かつて CanvasProxy と呼ばれていたのをご存じの方もいると思います、まさにそれです。

「画面に表示されないCanvas」は、「表示前に何段階か画像の加工が必要だけど、加工が終わるまでは画面に出したくない」とか「毎フレーム同じものをレンダリングしないように背景だけキャッシュしておきたい」という場合に有効です。

この説明だけだと「それってCanvasでもできるよね?」というのが率直な意見でしょう。
しかしOffscreenCanvasにはTransferableWorkerスレッド単体でも利用可能という興味深い特徴があります。(これについては後述します)

Web Worker #とは

Web Workerはブラウザ上でマルチスレッド処理を行える機能です。

Web WorkerはJavaScript上でWorkerクラスとして表現されており、new Worker('worker-script.js')のようにWorker用のスクリプトを与えることで、別のスレッド上で処理を行うことができます。 (詳しくは MDN: Web Worker を使用するをご参照ください)

Worker.postMessage()を経由することでメインスレッドからWorkerスレッドへデータ(メッセージ)を送信し、これによってメインスレッドとWorkerスレッドで協調動作を行います。

Transferable

先にお話したTransferableとは、このpostMessageメソッドを介して転送可能なオブジェクトの種類のことです。
postMessageメソッドで転送できるデータには、ObjectやStringなどのJSONにシリアライズできる値と、Transferable Objectsの2種類があります。

具体的には、JavaScriptの以下のオブジェクトがTransferableに当たります。

  • ArrayBuffer
  • MessagePort
  • OffscreenCanvas
  • ImageBitmap

OffscreenCanvasの利点

今までは、メインスレッド内であればCanvasを用いて曲線を書いたり、img要素越しに読み込んでImageData(RGBA配列)にデコードしたりできましたが、Workerスレッド内ではそのような高レベルな画像の取扱いが出来ませんでした。

そのため、Workerはメインスレッドからレンダリング途中のImageDataを受けとり、その中の数値データを順繰りに舐める類の重い処理を行うことが一般的で、Worker自身で画像をレンダリングするのは厳しみがありました。

しかし、OffscreenCanvasによってWorkerだけでも曲線を描いたり、画像をライブラリなしでデコードしたり、画面外でWebGLを触ったり、ということが出来るようになります。 *1
最近登場したServiceWorker内でも利用できるようです。

少々複雑な実装を行えば、ゲームなどでレイヤー別にWorkerを立てて並列レンダリングする、ということも可能になるかと思います🔥

OffscreenCanvasを触ってみる

今回はサンプルとして、Workerスレッド上でイラストにエフェクトを適用してCanvas上に表示してみます。

利用可能なブラウザ

サンプルを実行するのに必要な機能が実装されているのは、記事執筆時点でChrome Canaryに限られます。 またOffscreenCanvasはまだ安定版として提供されていない機能なので、以下の設定を行う必要があります。

  1. chrome://flags/#enable-experimental-canvas-featuresを開く
  2. 試験運用版の canvas 機能を有効にする
  3. Chromeを再起動する

実演

お待たせしました!いよいよ実際のコードです!

OffscreenCanvasは2つの方法で生成することができます。

  • HTMLCanvasElement#transferControlToOffscreenで生成する
    • Worker上で生成した画像をそのままCanvasに反映させたい場合はこちら
  • new OffscreenCanvas(width, height)で生成する
    • バッファーとして利用して直接画面に表示しない・Worker内でとりあえずCanvasを使いたい場合

今回はcanvas要素からOffscreenCanvasを生成してWorkerから直接レンダリングを行います。 まずはメインスレッド側のコードを見てみましょう。

// main.jswindow.addEventListener('DOMContentLoaded', async e => {const worker = new Worker('worker.js')

  // Workerの処理完了を待つためのヘルパconst waitResponse = () => new Promise(resolve => {
      worker.addEventListener('message', ({data}) => {
          data.action === 'resolve'&& resolve(data)
      }, {once: true})
  })

  const dest = document.createElement('canvas')
  dest.width = 640
  dest.height = 360
  document.body.appendChild(dest)

  // canvas要素からOffscreenCanvasを生成して、Workerスレッドへ転送するconst offscreen = dest.transferControlToOffscreen()
  worker.postMessage({action: 'attachCanvas', canvas: offscreen}, [offscreen])
  await waitResponse()

  const render = async () => {
    worker.postMessage({action: 'render'})
    await waitResponse()
    requestAnimationFrame(render)
  }

  requestAnimationFrame(render)
})

次にWorker側のコードです。

// worker.jsimport Filters from 'canvasfilters'import blur from './filters/blur'import highpass from './filters/highpass'import loadAsBlob from './utils/load-as-blob'newclass WorkerProcess {
  constructor() 
  {this.handleMessage()
  }

  handleMessage()
  {self.onmessage = async ({data: {action, ...props}}) => {switch (action) {case'attachCanvas':
            await this.attachCanvas(props)
            break;

          case'render':
            await this.render()
            break;
        }self.postMessage({action: 'resolve'})
    }}

  async _preload()
  {const imageBlob = await loadAsBlob('/src/images/example.png')
    this.sourceImage = await createImageBitmap(imageBlob)
  }

  async attachCanvas({canvas})
  {this.canvas = canvas
    this.ctx = canvas.getContext('2d')
    await this._preload()
  }

  async render()
  {const{ctx, sourceImage, canvas: {width, height}} = this
    ctx.clearRect(0, 0, width, height)
    ctx.drawImage(sourceImage, 0, 0)

    const srcImageBuffer = ctx.getImageData(0, 0, width, height)

    const imageBlurred = blur(srcImageBuffer, 10)
    const imageLowPass = blur(highpass(srcImageBuffer, 230), 80)
    const destinate = Filters.screenBlend(
        Filters.multiplyBlend(srcImageBuffer, imageBlurred), 
        imageLowPass
    )

    ctx.putImageData(new ImageData(destinate.data, destinate.width, destinate.height), 0, 0)
    ctx.commit()
  }}

解説

かいつまんで大切そうな所だけ解説します。

まずは以下のコードでcanvas要素とOffscreenCanvasを生成します。

// main.js内// canvas要素を作るconst dest = document.createElement('canvas')
// canvas要素に紐付いたOffscreenCanvasオブジェクトを取得するconst offscreen = dest.transferControlToOffscreen() 

このとき、transferControlToOffscreenを呼ばれたcanvas要素は、canvas要素から直接getContextメソッドをコールできなくなります。処理をOffscreenCanvasへ委譲したためです。以降はoffscreenを経由してのみ操作が可能です。

⭕ offscreen.getContext('2d')
❌ dest.getContext('2d')

そして取得したOffscreenCanvasをworker.postMessageでworkerへ転送します。

// main.js内
worker.postMessage({action: 'attachCanvas', canvas: offscreen}, [offscreen])

postMessageメソッドのインターフェースは、MDN: Worker.postMessage()では以下のように示されています。

myWorker.postMessage(aMessage, transferList);

第1引数(aMessage)にはJSONにシリアライズ可能な任意のオブジェクトを渡し、第2引数(transferList)にはaMessage内に含まれる、workerへ転送したいTransferableなオブジェクトを配列で渡します。

ここでtransferListに指定され、転送されたオブジェクトはメインスレッド上からは触れなくなります。 Chromeの場合、転送したのがArrayBufferであればその中身はbyteLength=0の空データに、OffscreenCanvasであれば幅と高さが0のキャンバスになり、メソッドをコールするとエラーになります。(転送と呼んでいるのはそのためです。)

転送されたワーカー側では、OffscreenCanvasから2Dレンダリング用のコンテキストを取得しています。ここは通常のCanvasと変わらないですね

// worker.js内this.ctx = canvas.getContext('2d')

そしてメインスレッド側からレンダリングの要求が投げられ、Workerスレッド側でレンダリングが実行されます。

// worker.js内
async render()
{const{canvas, ctx, sourceImage, canvas: {width, height}} = this
    ctx.clearRect(0, 0, width, height)

    // レンダリング処理...

    ctx.putImageData(new ImageData(destinate.data, destinate.width, destinate.height), 0, 0)
    ctx.commit()
}

この中のctx.commit()が通常のCanvasと異なる点です。 commitメソッドを呼ぶことにより、現在OffscreenCanvasにレンダリングされている内容を、生成元のCanvasへ反映します。これでメインスレッド側のCanvasへのレンダリングが完了です。
(commitメソッドはcanvas要素から生成されたOffscreenCanvasでのみ利用可能です。new OffscreenCanvasで生成されたインスタンスでは使用できません。)


ここまでのサンプルの動作はこちらから確認していただけます。
(先に述べた通り、experimental-canvas-featuresが有効なChrome Canaryでのみ動作します。それ以外では真っ白な画面になると思いますのでご了承ください😫)
ソースコードはGitHubで公開しています。

正常に動作する環境であればOffscreenCanvasによるそこそこ重めのエフェクト処理を行うサンプルが表示されるはずです、OffscreenCanvasでテキストレンダリングが使えない関係で開発者コンソールに何FPSでレンダリングされているか表示させています。 おおよそどんな流れでOffscreenCanvasが利用できるか参考になれば幸いです!

f:id:devpixiv:20161213162229p:plain

まとめ

OffscreenCanvasはパフォーマンス目的に使うとかなり使い所が難しい(今回のサンプルではシングルスレッドと目立った差がなかった)ですが、WHATWGのWikiを見ると「ServiceWorker内でのアイコン生成に使いたい」というユースケースがあるようで、パフォーマンスに限らず、いい感じに使えるポイントがありそうです。

新しすぎてまだ普通の環境では使えないものですが、pixiv SketchpixivFACTORYのプレビュー生成で使える日を首を長くして待っています。

本日は首が長くなりすぎた @_ragg_がお送りしました、明日は @syoichi がお送りします。 引き続きピクシブ株式会社 Advent Calendar 2016をお楽しみください🔥🔥

参考

*1: fillTextなど一部のメソッドが使えないようです(´・ω・`)


Slackを情報収集に使おう!SlackにRSS/Atomを簡単に追加できるChrome拡張機能の紹介

$
0
0

qiita.com

ピクシブ株式会社 Advent Calendar 2016の14日目担当のsyoichiです。
今年の2月に入社しました。現在はプレミアムチームに所属し、主にPHP/JavaScript/CSSで開発しています。個人ではブラウザの拡張機能やUser Script/User Styleをよく弄っています。

さて、私は入社する前まではプログラミングに関わるチャットサービスとしてはGitterぐらいしかまともに触ったことがなかったのですが、入社直後は社内でIdobataを主に利用し、数週間後には社内全体でSlackに移行と、一気にチャットサービスを常用することになりました。当時のSlackに対しては巷で話題のイケてるサービスという印象だったので、移行の時はワクワクしたものでした。それから数ヶ月経ち、すっかりSlack漬けの生活になっています。

フィードリーダーとしても活用できるSlack

最近になって知ったのですが、このSlackをフィードリーダーとして利用する事例がいくつかありました。

なるほど、確かにSlackは慣れ親しんでいるから投稿の読み方も身に付いているし、フィードだけでなくTwitterなどのより多様なコンテンツストリームを扱うこともできる…。Google Readerに始まり、feedlyLive Dwango Readerを常用してきた自分にとっては目から鱗でした。

こうした事例に触発されて私も個人用Slack Teamを作り現在試験運用中です。その際、RSS integrationにフィードを追加するのがスムーズにできないことに気付きました。フィードURLがわかっていればSlack上でコマンドから登録することはできますが、例えばブラウザでブログを読んでいる時にSlackに登録したいと思った時には自分でフィードURLを探さねばなりません。
こうした問題は、通常のフィードリーダーのようにフィード登録用のURLが無いことが大きな原因であると感じています。それさえあれば、既存の仕組み(ブラウザ内蔵のフィード登録ボタン、または拡張機能)で簡単にフィードを登録できるのですが…。

残念ながら、この「ブラウザからSlackに手軽にフィードを登録できるツール」は無さそうだったので、Chrome拡張機能として自分で作ってみました。以下はその紹介です。

github.com

ブラウザからSlackにフィード登録が簡単にできる「Add feed to Slack」の紹介

そのものズバリな「Add feed to Slack」は、フィードがあるブログなどに反応したPage Actionを押すと、SlackのRSS integrationのページを開いてフィードURLを自動的に登録フォームに入力してくれるChrome拡張機能です。

f:id:Syoichi:20161213154541p:plainフィードがあるブログなどにアクセスするとPage Actionが反応

f:id:Syoichi:20161213155029p:plain反応したPage Actionを押すとSlackのRSS integrationのページが開いてフィードURLを自動的に登録フォームに入力
フィードが見つからない場合はPage Actionのアイコンはグレースケール化

Chrome拡張機能を作ったのはかなり久しぶり(いつの間にかPage ActionがOmniboxに表示されず、Browser Actionと変わらないような形になっていたことに驚いた)でしたが、Chrome ウェブストアに公開するのはこれが初めてです。

最初はBookmarkletかUser Scriptでできないか考えたのですが、拡張機能の形で落ち着きました。

拡張機能の実装について

ここからは具体的な実装の解説をしてみます。シンプルさを重視した為、かなりコンパクトなものになりました。コアとなる実装は以下の2つのファイルだけです。

background.js

'use strict';

const SLACK_RSS_APP_URL = 'https://slack.com/apps/A0F81R7U7-rss';

let urlMap = new Map();

chrome.runtime.onMessage.addListener(({url}, {tab}) => {
  urlMap.set(tab.id, url);
  chrome.pageAction.show(tab.id);
});

chrome.pageAction.onClicked.addListener(tab => {
  chrome.tabs.create({
    url: SLACK_RSS_APP_URL
  }, () => {
    chrome.tabs.executeScript(null, {
      code: `
        document.getElementById('feed_url').value = '${urlMap.get(tab.id)}';
        document.querySelector('#settings + .card').scrollIntoView()
      `,
      runAt: 'document_end'});
  });
});

content.js

'use strict';

try{
  chrome.runtime.sendMessage(null, {
    url: (new URL(
      document.querySelector(`
        link[rel="alternate"][type="application/rss+xml"][href],
        link[rel="alternate"][type="application/atom+xml"][href]
      `).href
    )).toString()
  });
}catch (err) {// do nothing}

処理の流れについて

  • Content Script(content.js)で、適切なフィードURLを取得できたらBackground Page(background.js)に送る
    • 不適切なURLをURL constructorに渡すとエラーになるので、エラーハンドリングをする必要がある。ここでは、簡略化の為、処理全体をtry-catchで囲んでいる。
  • Background Pageで、送られてきたフィードURLを一旦保持してPage Actionを反応させる
  • Page Actionが押されたらSlackのRSS integrationのページを開く
  • 開かれたページでフィードURLを登録フォームに挿入する
  • 多くのフィードが登録されていた場合を考慮して登録フォームまでスクロールする

というのが処理の流れです。コード的にはES2015を使用していてモダンなように見えますが、実装的には昔ながらのChrome拡張機能という感じになりました。

使用している拡張機能のAPIについては以下を参照すると良いでしょう。

chrome.declarativeContentを使ってみたかったのですが…

可能であれば、chrome.declarativeContentを使用してみたかったです。このAPIは特定のページで特定のCSS Selectorにマッチする要素があればPage Actionを反応させるなどの特定のアクションを取ることができるのですが、なんと、このAPIは表示されているコンテンツのみにしかマッチしない方針になっていました。

CSS conditions only match displayed elements

残念ながら、非表示コンテンツのlink要素を扱うRSS Subscriber的な拡張機能とは縁の無いAPIとなります。ただ、これを活用すればContent Scriptが不要になるケースがありそうなので、いつか別の拡張機能で試してみたいですね。

今後の改善について

実装的にいくつか改善できそうな点があるので、それらはIssuesにまとめています。ご興味のある方はPull Requestをどうぞ!


この拡張機能を作成したことで、ブラウザでブログを読んでいるところからフィード登録ボタンを使用する感覚でSlackにフィードを登録できるようになりました!
フィードリーダーとしてSlackを利用している方、利用しようと考えている方は是非インストールしてみてください。

chrome.google.com


それでは、明日からも引き続きピクシブ株式会社 Advent Calendar 2016をお楽しみください。
明日の担当は@f_subalさんです。今日に引き続きSlackに関するもので、Slack Botについて書いてくださるそうです。

ES2016+ on herokuでSlackにイラストを流してユーザ像を感じ取る話

$
0
0

ピクシブ株式会社 Advent Calendar 2016、15日目の記事です。

はじめまして。4月より入社した新卒エンジニアの@f_subalです。普段は3分動画でお絵かき上達ができるサイトsensei by pixivの開発などに携わっています。今回は、senseiの新機能である お題投稿機能をリリースした際に、ユーザーの皆様が投稿したイラストをSlackに流すbotを開発した話をします。

senseiにおけるお題投稿

senseiは3分間でイラストの描き方を学べる動画を100件以上提供しています。人体・背景・キャラクターの講座をはじめ、有名クリエイターのメイキング動画もあり、手軽に描き方を学ぶことができます。

が、講座動画を見た後で実際に絵の練習をするというアクションにつなげるための仕組みはいささか不足していました。そこで開発されたのがこのお題投稿機能です。下図のようなお題のテンプレートがあり、これを元に弊社サービスの pixiv Sketchに練習イラストを投稿してもらえるという機能になっています(実際にはpixiv Sketchの機能である「描いてリプライ」を用いて、sensei公式アカウントの投稿にリプライする形で投稿します)。

f:id:devpixiv:20161215170121p:plainf:id:devpixiv:20161215170130j:plain

ちなみにこれは私の絵です。

sketch.pixiv.net

お題には2種類あります。各講座に紐付くお題と、日替わりで提供される「今日のお題」です*1。前者は例えば「横顔を描く」講座なら「横顔のアタリを模写してみよう」といった内容で、基本的に変わりません。一方で、今日のお題は毎日違うものが増えていきます。Twitterの@pixivsenseiでも毎晩20:00に今日のお題告知を流しているので、お気軽に参加してみてください。

Slackbotでお題投稿の様子を知る

さて、こうしてリリースされたお題投稿機能でしたが、実際にどういうイラストが投稿されるのか、またどういうお題が盛り上がるのかをチームとして把握したい気持ちがありました。もちろんDBを叩いて後から各お題への投稿数を見ることはできますが、どうせなら投稿ピーク時の盛り上がりをリアルタイムに感じたいですし、魅力的な投稿がすぐに捕捉できるに越したことはありません。

そこで、社内のSlackに専用のチャンネルを用意して、pixiv Sketchにお題テンプレートへのリプライがあるたびに通知するbotを作りました。実装方法は色々ありますが、今回はHerokuにexpressJSベースのアプリケーションを立ち上げ、Heroku Schedulerで定期実行する方式にしました。

アプリケーションの構成

今回は Babel + webpack + Node.js を用いてbotを作成しました。選択の理由としては、Node.jsが単純に他の言語より手に馴染んでいたこと、あとは仕様上不定回数の非同期処理が発生するので、async/awaitが欲しかったというのが大きいです(後述)。

Babelとwebpackの用意については弊社の@geta6のブログ記事が参考になります(ちょうど本日 webpack v2 が rc になりましたが、以下ではwebpack v1を前提に記述します。async/awaitも現在は stage-4 に格上げされましたが、一部記述に stage-3 であった頃の設定が含まれています。こちらも注記しつつ説明いたします)。

さて、今回のbotには4つの要素が必要になります。

  1. pixiv SketchのAPIを叩いて、お題テンプレートやそのリプライ作品を取得するクラス
  2. DBを叩いて、すでに送信済みの作品IDを取得/格納するDAOクラス
  3. 1.で取った作品IDと2.で取った作品IDの差分を取り、まだ送ってない作品を調べるクラス
  4. 3.の結果をSlackに送信するクラス(こいつが上の3つのクラスを叩くエントリーポイントになる)

各クラスをES classで書いて importします。DBはHeroku Postgresを用いました。

async/awaitを用いたAPIリクエスト

さて、senseiのお題は毎日増えます。それはつまり、お題テンプレートが毎日増えるということであり、したがって各お題テンプレートへのリプライを取得するAPIリクエストの回数も増える(=一定値にならない)ということです。

一般に、APIを外から叩く際には、連続で叩いて相手先に負荷をかけないように間隔を空ける必要があります。そのためにはいわゆる sleep関数を挟みつつ、リクエスト処理を直列に実行できなければなりません。

かつてJavaScriptにおいて、非同期処理を直列に不定回数実行するのは難しいことでした*2。しかし ES2017 に導入予定の async/await構文を用いれば、非同期処理の直列実行が簡単に実装できます(今回botの実装にBabel + webpackを選択した主要な理由がこれです)*3

.babelrcの presets に stage-0を加えると、ES.nextにて提案段階の機能を用いることができます(実装当初は presetsに es2015stage-0を入れていました)。ですが async/awaitは今夏に Stage 4(finished)に上がりましたので、これだけを目当てにするなら現在は必要はありません。

$ npm install --save-dev babel-preset-latest
{"presets": ["latest"// es2015 ~ es2017の内容がすべて入る(async/await含む)   ]}

sleep 関数は自作しても良かったのですがnpmに便利なモジュールがあるのでそちらを使いました: wait-promise)。

import{ sleep } from 'wait-promise';
import{ map as pluck } from 'lodash';

......


async fetchRepliesByItemIds(ids) {let result = [];

    for (let id of pluck(templateItems, 'id')) {const replies = await fetchRepliesByItemId(id);
        result = [...result, ...replies];
        await sleep(1000);
    }return result;
}

for ループの中でfetchしていることに注目してください。

HTTPリクエストにはaxiosを用いました。デフォルトでPromiseを返すので async/awaitを使いたいときには大変便利です。ブラウザ上でも動くので、モダン環境でのAjaxにも良さげです。SlackへのWebhookも同じくaxiosで投げています。

import axios from 'axios';
import{ get } from 'lodash';

......


sendToSlack(itemsToSend) {const attachments = itemsToSend.map(item => {const url  = BASE_URL + get(item, ['id'], '');
        const text = get(item, ['text'], '');

        return{"fallback": `${text} ${url}`,
            "text": `${text} ${url}`,
            "image_url": get(item, ['image', 'url'], ''),
            "unfurl_links": true,
            "unfurl_media": true}});

    return axios.post(SLACK_WEBHOOK_URL, {
        attachments: attachments
    });
}

投げ終わったら、送信済みの作品として、IDをデータベースに入れます。

実際の運用

上のbotを10分に1回のペースで実行しています。これでもなかなかに臨場感があり、チームメンバーも魅力的な投稿イラストを目にする機会が増えました。

弊社Slackの #_sensei_odaiチャンネルができて以降、ユーザがどのように使っているかを知りやすくなりました。「このユーザーさんよく投稿してくれるな」とか「こういうテーマはみんな自分の推しカプにやらせたくなるのだな」といったことが肌感で分かるようになりました。

senseiチームでは翌月のお題を考えるブレストミーティングを月に1回行っているのですが、こうした形でユーザー像ををチーム内で持てるようになった結果、お題を考える際に「これはみんな好きそう」「これはみんな描く気にならないのではないか」といった判断がしやすくなりました(もちろんこれは感覚値ですが)。絵描きの気持ちになるということは、お絵描き学習サービスをやるにあたってとても重要な事だと改めて思います。

まとめ

ピクシブ株式会社では、クリエイターの立場に立って技術力を奮ってくれるエンジニアを募集しています。明日は @uchienneoが、GitLabを用いたプロジェクト運営についてのお話をしてくれます! それでは、明日からも引き続きピクシブ株式会社 Advent Calendar 2016をお楽しみください。

*1:実は「お題投稿機能」は厳密には前者の「講座お題」の投稿機能のみを指します。日替わりでお題を出して描いてもらう企画は、この機能のリリースより前からpixiv Sketch上で実験的に行っていましたし、sensei側の機能自体とは独立です。

*2:並列での実行であれば jQuery.when や Promise.all を用いて簡単に行なえますが、今回はそういうわけにいきません

*3:一応 async/await を使わなくても頑張ればPromiseの入った配列を Array#reduce する方法などで実現できますが、ちょっと冗長です

GitLabの運用方法をドーンと公開!!

$
0
0

ピクシブ株式会社 Advent Calendar 2016の時間です。今回はピクシブ株式会社でエンジニアをしている @catatsuyが担当します。今回は意外と書いてなかったのでGitLabを社内でどう運用しているかの話を書こうと思います。

GitLabとGitHub

ピクシブ社内では以下の2つの方法でソースコードを管理しています。

  • 自社でホストしているGitLab
  • GitHub Organization

それぞれ以下の特徴があります。

  • GitLabのメリット
    • 自社でホストしているため、アメリカにサーバーがあるGitHubよりもgit cloneでリポジトリをダウンロードする場合などは速い
    • オープンソースのプロジェクトのため、社内のサーバーにインストールするだけで使える
      • ソースコードを読めば内部でやっていることが分かる
    • ユーザー数やリポジトリ数などで料金がかからないため、気軽に使える
  • GitLabのデメリット
    • バージョンアップは自分たちでやる必要がある
      • 停止する必要がある
  • GitHub Organizationのメリット
    • GitHubと全く同じ使い方ができる
    • 外部サービスの連携が充実している
    • 外部の会社とのやり取りに使うこともできる
  • GitHub Organizationのデメリット
    • ユーザー数やリポジトリ数などで料金がかかる
    • 日本からだと回線が遅いので、容量の大きいリポジトリだと扱いにくい

ピクシブ株式会社では社内でpixiv.gitと呼ばれている最も大きなリポジトリでGitLabを使用しています。今回は弊社でのGitLabの使い方や運用方法について紹介します。

GitLab上のユーザー権限について

GitLab上のユーザー権限は複数存在します。

Permissions - GitLab Documentation

現在社内では社員全員Developer権限にしています。そうするとリポジトリの作成権限などもなくなるので、Owner権限をもつアカウントを用意して必要なときだけそのアカウントを使用しています。

またGitLabにはprotected branches機能があります。この機能を使うことでmasterブランチなど特定のブランチに対してforce pushを禁止することが出来ます。現在ではGitHubでも使える機能ですが、GitLabには以前から存在した機能の一つです。

pixiv.gitでは基本的に作った本人がmasterブランチにmergeして、本人が本番にデプロイするルールで運用しています。しかしprotected branches機能を使うとDeveloper権限のユーザーでは通常のpushすらできませんでした。そのため以前はDeveloper権限のユーザーでもpushできるようにGitLab側にパッチを当てていました。現在ではDeveloper権限のユーザーにもpushする権限を与えることができるので、現在ではパッチを当てていません。

会社によってはGitLabにパッチを当てて運用していると聞きます。GitLabはオープンソースで開発されていて、主観ですがソースコードも読みやすいと思います。なのでパッチを当てることが容易なところもGitLabの魅力です。以前は上記のパッチを当てていましたが、現在ではGitLabにパッチを当てずに運用しています。

GitLabの認証

pixivの開発サーバーはLDAPでユーザーを管理しています。なのでpixivの開発に関わる人は全員LDAPのアカウントを所有しています。そこでGitLabの認証もLDAPを使用しています。LDAP経由で初めてログインを行った際にユーザーが作成されるので、その後にOwner権限を持っているユーザーでGroupにDeveloper権限で所属させることでアカウントを作成しています。LDAPでの設定は以下のURLを参照してください。

LDAP - GitLab Documentation

加えて、GitLabではRubyで広く使われている omniauth/omniauthで使える認証を使用できます。

社内でGoogle Appsを使用している場合はそのGoogle Appsのアカウントを使用して認証することなども可能です。各社で最適な認証方法で提供できることもGitLabの魅力です。

詳細は以下のドキュメントを参照してください。

OmniAuth - GitLab Documentation

外からも使いたい

社内のGitLabは社内のネットワークからのみ参照できるようにしていました。しかしこれだと社外からアクセスする場合にVPNを使う必要があります。VPNを使うのは手間ですし、ネットワーク機器上の制約もあります。そこでGitLabをもっと社外から気軽に見られるようにしたいという需要が社内からありました。そういった場合に社内ではgateを使ってGoogle認証で提供していました。それについては以前に私が記事を書きました。

inside.pixiv.net

inside.pixiv.net

ただしgateは最近ほとんどメンテナンスされていません。またgateは単純なプロクシサーバーとして機能するわけではないため、GitLabのような様々なオープンソースのツールに対応するのは難しい部分があります。そこで最近では bitly/oauth2_proxyを使って、下の記事の方法で前段にGoogle認証を挟むことで外部からでも手軽にセキュアにアクセスできるようにしています。

lamanotrama.hateblo.jp

GitLabで前段にGoogle認証を挟む場合はいくつか注意すべき点がありました。それを紹介します。

GitLabの前段にGoogle認証を挟む

pixivの開発は専用の開発サーバー上で行われています。そのサーバーは自社データセンター内に存在するため、GitLab上のリポジトリへのアクセスはデータセンター内のサーバーで使えるプライベートなホスト名を使ってsshでアクセスしています。

しかしGitLabでは外部から見れるドメインと、ssh経由でリポジトリを持ってくる際のドメインは同じであることが前提になっているため、指定できるhost名は1つだけです。またGitLab上のソースコードは機密情報でもあるため、HTTPS通信にしたいです。そうなると当然外部から解決できるドメイン名で提供する必要があります。

つまり以下のような設定にしたいです。

  • ブラウザからは外部から解決できるドメイン名でHTTPS通信で見れる
  • git cloneはsshからのみ行う
    • sshのURLのドメインはデータセンター内のプライベートなホスト名

GitLabの設定はgitlab.ymlにYAMLで記述します。HTTPSで提供する場合はgitlab:以下でhttps: trueと記述します。gitlab:以下のhost:にブラウザ上で見れるドメイン名を記述します。

これではリポジトリのsshのURLでhostに指定したもので表示されてしまいます。そこでgitlab.ymlgitlab_shell:の下に ssh_path_prefix: "git@private.domain:"という設定を行います。そうすることでリポジトリのURLだけを変えることが出来ます。

GitLabではリポジトリのURLとしてデフォルトではhttpとsshの両方を提供していますが、Google認証を通した時点でhttp経由ではgit cloneができなくなります。GitLab上の設定でEnabled Git access protocolsというものがあるため、ここをsshのみにすることでGitLab上ではsshのURLしか表示されなくなります。

実は設定のssh_path_prefixはGitLabにパッチを当てるつもりでソースコードを読んでいたときに見つけました。ドキュメントにも載って無さそうだったので隠しオプションかもしれません。使用する際はソースコードを読んで使えるか確認した方がよいでしょう。

バージョンアップについて

GitLabの開発は非常に活発なため、定期的にバージョンアップをしたいです。基本的にはドキュメント通りに行えば問題ありませんが、実際に社内で行っているバージョンアップ方法を紹介します。

GitLabでは全てのバージョン間のバージョンアップ方法のドキュメントが用意されています。現在使用しているバージョンから使いたいバージョン間のドキュメントは必ず全て目を通しましょう。

gitlabhq/doc/update at master · gitlabhq/gitlabhq · GitHub

ドキュメントはほとんどが毎回コピペされているだけですが、バージョンによってはコピペではない文章が書かれていることがあります。その文章を手元にコピーするなどして忘れないようにしましょう。バージョンにもよると思いますが、基本的には複数のバージョンアップを同時にやっても問題ありません。ただし先程紹介したコピペではない文章に注意する必要があります。

バージョンアップのドキュメントでは必ず最初にバックアップを行っています。GitLabのバックアップではMySQLで使用している場合はmysqldumpと、gitリポジトリのバックアップをしています。停止時間はできる限り小さくしたいところですが、MySQLのデータが増えるとmysqldumpだけでもかなりの時間を使います。

バージョンアップの作業では基本的にgitリポジトリをいじることはないため、実はmysqlを停止してからdata_dir以下をバックアップするだけで十分なことがほとんどです。ただし今後のアップデートで変わる可能性があるのでバックアップをどの程度行う必要があるかは各自で確認してください。

またGitLabのリポジトリ上に.ruby-versionがあります。Rubyのコンパイルにはそれなりに時間がかかるので、次に使いたいGitLabのバージョンで使うRubyは予めインストールしておきましょう。rbenvであれば複数のバージョンをインストールする事が可能です。新しいバージョンのRubyをインストールする際は、そのRubyのバージョンでbundlerのインストールをすることを忘れないようにしましょう。

まとめ

紹介したように、GitLabは認証について柔軟な設定ができます。またGitHubが障害になっても業務が行えたり、自社でホストしているためリポジトリのダウンロードが高速に行えるメリットがあります。ソースコードが読みやすいOSSなので、必要に応じてパッチを当てる事も比較的容易です。バージョンアップで気を付けるべき点などもありますが、皆さんの会社でも社内のリポジトリ管理にGitLabを使ってみてはいかがでしょうか。

次回は弊社が誇るおもしろアルバイターのhakatashiがおもしろエントリーを公開してくれます。hakatashi の検索結果 - pixiv insideで是非予習してからご覧ください!

pixivのイラストで機械学習したらどうなるのっと ~pix2pixで自動彩色編~

$
0
0

この記事は、ピクシブ株式会社 Advent Calendar 2016の17日目の記事です。

qiita.com

おはようございます。プログラマーのhakatashiです。普段はpixivコミックの開発をひっそりと手伝っていますが、今回もそれとは全く関係ない話をします。

前回の記事では、pixivに投稿された小説のデータを使用して機械学習を行いました。今回は引き続いて、最近発表されたpix2pixというライブラリで軽く遊んでみたので、簡単に紹介します。

pix2pixとは

pix2pixとは、11月下旬に発表された論文「Image-to-Image Translation with Conditional Adversarial Networks」で提案された機械学習のモデル、および論文と同時に発表された実装の名称です。論文を発表した Phillip Isola らはUCバークレーの人工知能研究所の研究者であり、彼らの研究所は10月にファーウェイとの産学連携を発表したばかりで、AI研究の分野においてなにやら急進の勢いを見せています。

さて、pix2pixは、名前の通り画像から画像への変換を行うアルゴリズムです。画像から画像への変換というと、これまでは画像認識のCNNを応用した(今となっては単純な)モデルによる生成が主でしたが、今回発表されたpix2pixでは、GANを応用させたcGAN(conditional GAN)というモデルを新たに導入しています。cGANの仕組みはGANを理解していれば容易に理解できるものなのでここでは説明を省略しますが、このpix2pixの恐るべき点はとにかくその驚異的な汎化能力にあります。論文で挙げられている例では、

  • 線画から写真の復元
  • 航空写真から地図画像の生成
  • 地図画像から航空写真の復元
  • 昼の風景から夜の風景への変換
  • モノクロ画像からカラー画像への変換

など、大掛かりなチューニングを行うことなく同じモデルで種々様々な画像変換タスクに応用できることが示されています。

f:id:hakatashi:20161215170936p:plain
“Image-to-Image Translation with Conditional Adversarial Nets”より引用

見ての通りどれも変換結果が優れており、しかも少ないデータセットでもかなり高い性能を発揮することが知られています。CNNへのGANの応用はつい昨年末に提案されたばかりだというのに、この分野の成長の速さには驚く一方です。

また、簡単に使える実装もGitHubで同時に公開されており、しかもこれが適当に画像を整形して突っ込むだけでいい感じに学習してHTMLで結果を表示してくれるなど、機械学習がよくわからなくても手軽に試せてしまうとてもありがたい設計になっています。

これはまさしく画像を取り扱うpixivで試してみない手はない、名前もなんだかpixivに似ているし⋯⋯ということで、筆者も機械学習がよくわからないなりに個人的に適当に遊んでみました。

試してみる

さて、pixivにはこんなユーザー企画があります。

塗り方を練習してみよう! by コット(レタス)今は、まよ on pixiv

2009年に投稿され現在でも脈々とイメージレスポンスが投稿されている有名企画の一つです。同じ1つの「元線画」に対して「厚塗り」「ギャルゲ塗り」「黒一色」「アニメ塗り」「水彩塗り」という5つの塗り方を練習する、という内容なのですが、こうした企画は入出力のフォーマットが統一されているので、データセットとして使用するにはまさしくうってつけです。公開されているレスポンス画像も800件近くあり、通常のCNNだとやや心もとないですがpix2pixなら行けるのではと思い突っ走ることにしました。

ちなみに、筆者のオススメはこんな感じです。

塗り練習 by usak on pixiv

イメージレスポンスしてみたかった by 豚ポタージュ@Twitter on pixiv

究極! by にゃー on pixiv

あんぱんまんで塗り練習 by :へ) わなげ on pixiv

by Du @ ドー on pixiv

こういったイメージレスポンスの中から規定の大きさ(1200x800)を満たしている約600件をフィルタリングし、train:validation:test=8:1:1の割合で分類して、これに上下左右の反転を加えてデータセットを4倍にしました。こうして1976件の学習データを作成し、「元線画」から5つの塗りそれぞれへの変換をpix2pixを用いて学習しました。パラメーターは時間の都合により標準で200epochとなっているところを10epochにした以外はすべてデフォルトのままです。

使用したマシンはいつも通り AWS EC2 のg2.2xlargeです。かかった時間は10epoch回すのに約2時間半、テストも含めて6個のモデルを学習したのでのべ約15時間でした。

結果

さて、さっそく結果を見てみましょう。学習したモデルをpixivに投稿されていない線画に適用してみました。生成された画像はすべて元画像と同じライセンスが適用されます。

注意: ここから先にはいささか心臓に悪い画像が含まれている可能性があります。

その1

ミク誕! by まほら licensed under CC BY-NC-SA 2.5

元画像

f:id:hakatashi:20161215181741p:plain

厚塗り

f:id:hakatashi:20161215194825p:plain

ギャルゲ塗り

f:id:hakatashi:20161215182047p:plain

黒一色

f:id:hakatashi:20161215182143p:plain

アニメ塗り

f:id:hakatashi:20161215182156p:plain

水彩塗り

f:id:hakatashi:20161215182207p:plain

その2

蘭丸ちゃん線画 by Risse licensed under CC BY 2.5

元画像

f:id:hakatashi:20161215182402p:plain

厚塗り

f:id:hakatashi:20161215194819p:plain

ギャルゲ塗り

f:id:hakatashi:20161215182414p:plain

黒一色

f:id:hakatashi:20161215182421p:plain

アニメ塗り

f:id:hakatashi:20161215182444p:plain

水彩塗り

f:id:hakatashi:20161215182451p:plain

その3

シーザー線画 by Risse licensed under CC BY 2.5

元画像

f:id:hakatashi:20161215182538p:plain

厚塗り

f:id:hakatashi:20161215194806p:plain

ギャルゲ塗り

f:id:hakatashi:20161215182545p:plain

黒一色

f:id:hakatashi:20161215182554p:plain

アニメ塗り

f:id:hakatashi:20161215182559p:plain

水彩塗り

f:id:hakatashi:20161215182607p:plain

その4

線画 by Motokazu Sekine licensed under CC BY-NC-SA 2.0

元画像

f:id:hakatashi:20161215182815p:plain

厚塗り

f:id:hakatashi:20161215194757p:plain

ギャルゲ塗り

f:id:hakatashi:20161215182902p:plain

黒一色

f:id:hakatashi:20161215182903p:plain

アニメ塗り

f:id:hakatashi:20161215182904p:plain

水彩塗り

f:id:hakatashi:20161215182905p:plain

感想

標準200epochのところを10epochしか回さなかったせいか、完璧な彩色!とはいきませんでした。ただ、髪・肌・目の塗り分けはおおよそできており、それぞれの塗り方の特徴も何となくつかめているように見えます。また時間があったらちゃんと200epoch回した結果を見てみたいところです。

最後に

ピクシブ株式会社では、5つのEC2サーバーにログインしてcGANの出力結果を収集して一つのブログ記事にまとめるめんどくさい作業が嫌いな、怠惰なプログラマーを募集しています。いつか機械学習で仕事を駆逐しましょう。アルバイトインターンもあるんだよ。

2017年はGitLabだけで開発のタスク管理を完結するのも夢じゃない

$
0
0

ピクシブ株式会社 Advent Calendar 2016、18日目の記事です。

こんにちは、エンジニアの@uchienneoです。2015年に小説PDF化機能の記事を書いた時にはpixiv本体の機能開発をメインにやっていたのですが、2016年夏に弊社メディアサイトwww.pixivision.netをリニューアルしてからはそちらの開発に携わっています。

pixivやpixivisionのソースコードの管理には自社ホストのGitLabが使われています。(どのようにGitLabが運用されているのかの詳細は16日の@catatsuyさんの記事をご覧ください)

inside.pixiv.net

GitLabにはタスク管理の機能があります。こちらの機能もどんどんアップデートを続けていて、そろそろ来年くらいにはGitLabだけでタスク管理するのも夢じゃないのでは…と思っています。

今回は実際のプロジェクトにおけるGitLab上でのタスク管理の運用とあわせてその展望を紹介します。

タスク管理ツールとしてのGitLab - 長所と短所

ピクシブでは基本的にはチーム単位で開発をおこなっていて、タスク管理の方法もチームごとの裁量に任されています。 よく使われているものとしては以下のものがあります。

  • Trello
  • GitHub
  • GitLab
  • 物理ホワイトボードにスイムレーンを書き、付箋を貼って管理

タスク管理ツールとしてはGItLabを見た時の以下のような部分が特徴的です。(一部GItHubと重なる部分もあります)

長所

  • コードとissue、MR(Merge Request = Pull Request)、Milestoneを一元管理できる
    • MR, issue, コミットメッセージにリンクを書くとそのまま互いの関連付けができる。 fixed / closed #123のようにコミットメッセージを書いてそのままissueを閉じることもできるし、
  • 開発が活発で、欲しい機能はたいてい実装されているか、今後実装予定がある
  • 2016年現在ではあまり意識する必要がないことかもしれないが、Markdown記法が使える(ピクシブでは過去にRedmine*1とMoinMoinを併用していた時代があったが、Textile記法やMoin記法をMarkdownとは別に覚える必要があった)

短所

  • issueの優先順位や順序づけなど複雑な関係性の設定や、issueとスケジュールの関連づけなどには弱い(こういった概念づけは基本的にすべてタグで補うという思想)
  • 基本的な機能以外を使う際のUIが複雑でわかりづらい
    • 例えばmilestoneは複数のプロジェクトを跨いで配置することができるため、GitLabのmilestoneの詳細ページにはすべてのプロジェクトを通してmilestoneの状況が見られるページと、1つのプロジェクトに限ったmilestoneの状況が見られるページがある。この2つは一見ほとんど同じ見た目をしているが微妙に機能が異なり、紛らわしい

pixivisionでは開発のタスク管理は現在ほぼすべてGitLab上でやっています。

pixivisionでの実際の運用

基本運用スタイルは以下のような感じです。

  • 1週間ごとにイテレーションを切り、GitLab上でMilestoneを作成する
  • それぞれのタスクにストーリーポイントをつけて、イテレーションごとにどれだけのストーリーポイントをこなせるのかを計測し、それを元にこなすタスクの量を決めていく
  • タスクの進行状況はGitLabのスイムレーン上で管理し、基本的にはそこを見れば自分のやるタスクがすべてわかるようになっている

GitLabでタスクをTrelloのようにスイムレーン形式で管理する方法としてはそのままMilestone詳細ページを見るやり方の他に比較的最近実装されたBoard機能を使うやり方があります。

f:id:uchien:20161217215341p:plain

2つの機能を比較すると、Board機能はまだ新しいためなのかissueの管理という部分であまりパワーがなく、比較的タスクの規模の小さいpixivisionチームではMilestone詳細ページのレーン数でもあまり不便がないため、issueの状態を直接操作することを優先してMilestoneページの方を利用しています。

現状の運用上の問題点 - ベロシティとレビューの管理が難しい

現在の運用で開発上のタスク管理はほとんどGitLabを見れば大丈夫という感じになっているのですが、まだいくつか難しい問題もあります。

まず、イテレーションの開発ベロシティの管理について、GitLabにはストーリーポイントをつけるという機能がないため、ストーリーポイントの管理は現状、タイトルにストーリーポイントと期日を入れておき、イテレーションの終わりごとに手でポイントを足し合わせて計算するというアナログな方法でやっています。

また、現在pixivの開発では1つのコードを必ず2人以上がレビューするという方針を取っているのですが、 GitLabではMRやissueに担当者を1人しかアサインすることができません。このため、どうしてもレビュー依頼はチャットやほかの手段を通じて出さなくてはならず、自分が今どのレビューを依頼されているのかわからなくなるということがしばしばあります。

これらの機能についてはGitLabの開発コミュニティで現在実装が進んでいるようで、リリースを心待ちにしています。

まとめ

  • GitLab上だけでイテレーション単位の開発タスクのやりとりはほとんどできる
  • バーンダウンチャートと複数人アサインが実装されたら、GitLab上ですべて完結させるのも夢じゃない

明日のAdvent Calendarはpixivの決済システムの番人、 @ik-fibさんが担当してくれます。

*1:現在はMarkdown記法が使えるようになっているらしい

Viewing all 111 articles
Browse latest View live