Misocaサービスの機能利用率見える化をしました

こんにちは。弥生Misocaチームでマーケティングを担当しているnezurikaです。

今回は、Misocaサービス各機能の利用率見える化をしたお話をしたいと思います。

プロジェクトの立ち上げ

この取り組みは「顧客分析をしたい」という一言から始まりました。

Misocaでは、Redashを利用してサービスに関する様々な数値をダッシュボードで見れるようにしています。

そのダッシュボードでは、新規登録者数の推移や、MAUの推移、総ユーザー数など「過去+現在」の数字が見れるようになっていました。
もちろんこれらの数字からも改善のヒントを得ることはできていたのですが、「いま」明確にどんなユーザーがMisocaに価値を感じてくれているのか分からない、などの課題を解決することはできませんでした。
そこから、最終的にMisocaの優良顧客が何に価値を感じているかを可視化することをゴールに顧客分析プロジェクトがはじまりました。

ただ、ゴールは決めたものの「現状知らない・決まっていない」ことが多かったため、このスコープではいま利用してくれているMisocaユーザーの各機能の利用率を可視化することになりました。

Misocaサービスを知る

「Misocaのユーザーは何に価値を感じてくれているのか?」を調べることになりましたが、今回のプロジェクトメンバーは社内でも社歴の浅いメンバーだったので、そもそもMisoca自体がどんな価値を提供したくて立ち上がったサービスなのかをきちんと把握できていませんでした。

そのため、まずはMisocaの歴史を知ろう、と思い、創業者が残していたMisoca開発ストーリーを読み解いて、当初のターゲット・提供したいと考えていた価値をまとめました。
そして、それらと比較して今はどんな価値を提供できているのか、ということもまとめました。

f:id:nezurika:20200728170318p:plain

これにより、Misocaが今までどんなサービスで、どのように変化して今はどんな価値を提供しようとしているのかがわかりやすくなりました。

Misocaの場合は

  • 作成
  • 発送(発行)
  • 管理
  • 取引先

というカテゴリができていることがわかったので、それぞれのカテゴリで、Misocaでユーザーに提供できている機能を洗い出しました。

一例

f:id:nezurika:20200728170439p:plain

機能を定義する

「請求書の作成」や「請求書の複製」という機能を洗い出したものの、このワードだけでは人によって解釈が異なってしまう恐れがありました。

また、この機能が使われている、と判断するには一体どんな動作をしたら「使った」と言えるのかも定義しておかないと実際の計測ができないため、さらにここから「どうしたら」使ったと言えるのかもすべて書き出しました。

一例

f:id:nezurika:20200728170503p:plain

Misocaの機能は、「取引先の登録」のように、何度も使う可能性のある機能と、「自社情報の登録」のように一度設定したらそのあとはめったに変更することのない機能があります。

それらの機能についてもそれぞれ記載しました。

機能ごとの利用ユーザー数をだす

どうやって計測するか決める

機能を洗い出す前に、機能の利用率をどうやって測るかということも決めていました。

前提として、今回は「現在のユーザーが何の機能に価値を感じているかを知る」ために行います。
それを踏まえて、利用率を出して実際どのように使うかを考えました。

  • 過去(指定した期間)の数字や、今の数字を知りたいときに調べる・見る
  • それを元に、未来(施策実施後)どのような数字になるかの目安がわかるようになる
  • 施策後に数値に変化がでたのか確認できるようになる

という3つがあがりました。

そこからもっと具体的な利用イメージを考えて、「◯月〜◯月の間にログインしたユーザーのうち、〇〇の機能を利用したユーザーの割合」というものを想定することができたので、

  • 集計期間を絞る
  • その期間のうちに機能を利用したユニークユーザー数をだす

という2点を集計で出せるようにしました。

利用率の前に、どれぐらいのユーザーがその機能を利用しているか知る必要があったので、各機能の利用者数と、分母とするアクティブユーザー数(今回の場合は一度でもログインしたユーザーを指しています)をRedash(SQL)とGoogle Analyticsでだしました。

基本的にはSQLでの結果を見ていくつもりではいましたが、計測したい数字すべてを出せるわけではなく、「これはGAで取ったほうが楽なのでは?」「SQLでは取れないけどGAでは取れる」という数字もあったため、両方の数字を見ることにしました。

ただ、SQLとGAでは若干数字に誤差がでる懸念がありました。
そのため、

  • 分母と分子はそれぞれ同じ条件(SQLならSQL、GAならGA)で計測したものにする
  • GAとSQLの「数字」同士での比較はしない

という決まり事をつくりました。

数字の抽出をする

SQLは、エンジニアにチェックや助言をもらいながらもデザイナーがガンガン書いていました。すごい。

GAは、スプレッドシートにGoogle Analyticsのアドオンを追加し、レポートを作成してそのあとは日付を変えたら自動で取得するようなものを作りました。

最終的にはRedashで出した数字もスプレッドシートにまとめるようにし、一覧で利用率がわかるようにしました。

一覧の一部

f:id:nezurika:20200728171404p:plain

もちろん自分たちが知りたい、というところからはじめたプロジェクトですが、自分たち以外の人も簡単に使えるようにならないと今後の業務に活かせられないので、簡単で分かりやすいものができるようにする点は意識しました。

利用率の見える化ができた

最終的には、このプロジェクトで作ったものを社内のメンバーに共有をして一旦は一区切りです。

最後の利用率の一覧もですが、プロジェクトの最初に作ったMisocaの価値についての表や、機能の定義なども共有をしました。

このプロジェクトの当初の目的には入ってはいませんでしたが、改めて「機能」を定義しまとめることができたり、過去提供していたものから、数年が経ちどのように今は変化しているかを把握できたこともこのプロジェクトで良かった点だと思っています。

今後

ダッシュボードなどの数字見える化プロジェクトは、出しただけではなく運用にのせることが一番大変だと思っています。

また、今回はあくまでも定量データに関する取り組みを行いました。

ユーザーに価値を与えられているか、機能がちゃんと使われているかというのは定量データだけではなく定性データとも組み合わせることでようやく把握できるものだと思っているので、今後Misocaでは定性データへの取り組みもすすめていく予定です。

最後に

Misocaではわたしたちと一緒に顧客分析もすすめてくれるデザイナーを募集しています。

松江オフィスの思い出

こんにちは、弥生の日高です。

スタンディングデスクにしたら体調が良くなった気がしなくもないです。

松江オフィスを閉鎖しました

2020年6月末をもって松江オフィス (島根県松江市) を閉鎖しました。

閉鎖に至った理由は、世界的にリモートワークが浸透する昨今の状況と、それ以前から約半数以上のメンバーが自宅等でフルリモートワークをしていた社としての状況も踏まえ、オフィスとして拠点を構えるあり方を再検討したことによるものです。

今回は、そんな松江オフィスの歴史を振り返りつつ、みんなの思い出を綴っていきたいと思います。

歴史

松江オフィスは、2016年10月に株式会社Misoca の島根県松江市の開発拠点として立ち上げました。

今でこそ、たくさんのメンバーがフルリモートワークをしていますが、当時は島根県松江市の私だけでした。その縁もあって、2016年6月ごろに松江市様の支援のもと島根合宿を行いました。

合宿でお世話になった旅館です。

f:id:hidakatsuya:20160622174502j:plain

旅館の眼前にはこんな綺麗な海が広がっていました。

f:id:hidakatsuya:20160623124639j:plain

そして、この合宿をきっかけに、2016年10月に松江オフィスを立ち上げました。入社してちょうど1年後のことでしたが、このスピード感に驚いたことを覚えています。

立ち上げ当初のオフィスはこんな感じでした。何もない...このソファが私の作業場でした。

f:id:hidakatsuya:20161107151105j:plain

地元松江で開催される RubyWorld Conference に合わせてミートアップなんかもやりました。

tech.misoca.jp

その後、採用活動などを通して徐々に仲間が増え、オフィスも充実していきます。

最初の写真と見比べると同じオフィスとは思えないですね。実際、とても快適なオフィスでした。この辺りはこまたつがガンガン整備してくれたおかげです。

f:id:hidakatsuya:20190213150116j:plain

f:id:hidakatsuya:20190213105537j:plain

こまたつがボードゲーム好きということもあり、メンバーでボードゲームで遊んだのも良い思い出です。

f:id:hidakatsuya:20200716164918j:plain

Android エンジニアのこまたつによる Android ハンズオンも開催しました。

misoca.doorkeeper.jp

f:id:hidakatsuya:20200716171847j:plain

そして、2020年6月19日に松江オフィスを撤去。最後は段ボールの上に zoom 端末を置いて朝会に参加しました。

f:id:hidakatsuya:20200716170202j:plain

ビル入り口の Misoca の案内標識で記念撮影。

f:id:hidakatsuya:20200717102244j:plain

その後 2020年6月26日にオフィスの引き渡しが完了し、約4年の松江オフィスの幕を閉じました。

思い出

続いて、他のメンバーのそれぞれの思い出を綴ってもらいます。

こまたつ

実家の近くにオフィスあるじゃん!と思ったのがMisocaに興味を持ったきっかけなので感慨深いですね〜。 今でこそ猫も杓子もリモートワークをしていますが、当時はリモートワークに対応している企業は少なく不安があったのですが「最悪オフィスいけばいいか」と思えたおかげで踏ん切りがつきました。

初めてオフィスに来たとき、ドアをあけたらすぐに執務スペースでめちゃくちゃビックリしたのを覚えています(受付があるんだろうな...と思っていた) そんなオフィスを日高さんたちと一緒に快適な空間へ改善していくのもひとつの楽しみだったなあ〜と感じます。

オフィスに入り浸っていたわけではないですが、天候不良でなかなかたどり着けない同僚がいたり、電気が使い放題になったり、ポストが全然あけられなかったり、集まってボードゲームをしたり楽しい思い出がたくさんあります。

閉鎖は残念ですが人がいなくなるわけではないので、これからはより広い地域のよりたくさんの同僚と一緒に楽しくがんばっていこうと思います。

はらだ

入社してから5回ぐらいしかオフィスに出社しなかったので思い入れがそんなに無いのですが、オフィスビルの裏にある「たかの屋」のランチは美味しかったのでオススメです。

田上

私は現在フリーランスで活動しており、独立して初めての業務委託元がMisocaでした。最終面接をおこなったのも松江オフィスを利用してのリモート面談で、その後もボードゲーム会を開催するなど、楽しい思い出が残っています。

最後に

松江オフィスは閉鎖しましたが、メンバーはリモートワークという形で引き続き松江近隣で活動していきます。今後も、地域のイベントや勉強会等でお会いする機会もあると思います。また、私たち自身も引き続き勉強会等を開催していければと思っています。その際はどうぞよろしくお願いします!

最後に、松江オフィスの立ち上げをサポートしてくださった皆様、立ち上げのお祝いを下さった皆様、松江センタービルの他の企業の皆様、オフィスに遊びに来ていただいた皆様、松江オフィスに関わった全ての皆様にこの場を借りて感謝申し上げます。

また、これまで松江オフィスを一緒に支え盛り上げてくれた松江メンバーと Misoca のメンバー、そしてオフィス運営を支えてくれた弊社にも、立ち上げメンバーの一人としてこの場を借りてお礼を言いたいと思います。

皆様本当にありがとうございました。また、これからも弥生と Misoca と松江メンバーを引き続きよろしくお願いします。

🔦「お気持ち会」で暗闇を払う

こんにちは、@mugi_uno です。気付いたら弥生社員になってました!!

プロジェクトの立ち上げはむずかしい

Misocaチームで何かしらの課題に取り組む場合、基本的にはプロジェクト化して進めていきます。

その際、まずはインセプションデッキを作成して「目的やゴールは何か」「何をして、何をしないか」といったことを明文化し、メンバーで認識を揃える作業をします。

ですが、現実的にはそれ自体が難しいケースが存在します。

何から手を付ければいいのかわかりません!

たとえば

  • 多種多様な立場の人が参加するプロジェクトを始めるが、メンバー個々人が何を重要視しているかを互いに知らない
  • ○○について効率化したいけど、具体的に何が課題で次に何をすべきかが誰もハッキリとは見えていない
  • 膨大なタスクが存在していて、どういった判断軸で優先順位をつけていけばいいのかがわからない

みたいな経験はないでしょうか。

このような状態だと、いきなり関係者でインセプションデッキを書こうにも、「一体何から話せばいいのか」「そもそもゴールとは?」となってしまい、議論が発散して収集がつきません。

何かミーティングをすれば解決する?

インセプションデッキ作成も、言ってしまえばミーティングの一種です。

困ったことがあると、つい「とりあえず集まってミーティングだ!」となりがちですが、内容が曖昧なミーティングをして、結局何も決まらずに空振りするという話も良く聞きます。(自戒)

対して、次のようなプラクティスが存在します。

  • アジェンダをきちんと用意する
  • ミーティングのゴールを定める
  • 次のアクションを出す

これらはMisocaチームでも積極的に実践されており、日常的にミーティングを効率的に終わらせるよう工夫していますが、先に挙げたような状態では、そもそもアジェンダを作る時点から「アジェンダ...?」となります。

整理された議論に意識が引っ張られる

ある程度キッチリとしたミーティングでは、何か意見を言うときも「ある程度自分のなかで整理された意見を言おう」「議論が前に進むような発言をしよう」という方向に意識が向きます。

それ自体はとても良いことですが、テーマが漠然としている場合には、一人の頭のなかで考えるには重たすぎたり、整理された意見というより「気持ち」に近い部分の意見は発言するタイミングが難しかったりもします。

こういう場合は、人が集まったわりには沈黙が多かったりします。

「お気持ち会」という文化

先に挙げた状態の場合、Misocaチームでは 「お気持ち会」 という30分-1時間程度の会が開催されることがあります。

お気持ち会の特徴

  • 30分〜1時間程度
  • ハッキリしたアジェンダやゴールは無い
  • 朝会などで全体に告知されて、プロジェクト外のメンバーからも参加を募ることが多い
  • 明確なゴールがあらかじめ設定されることは少なく、わりとゆるく開催される

だいたいは次のような流れで行われます。

  1. お気持ち会のテーマに関して各自が「思っていること」「考えていること」を Trelloに自由に吐き出す
  2. 掘り下げて話したいものにラベルをつけて議論する

お気持ち会の具体例「脱jQueryプロジェクト」

Misocaのコードベースにはフロントエンド部分でツラい箇所がまだまだ残っており、それをなんとかしよう!と「脱jQueryプロジェクト」をフロントエンドチームで始めることにしました。

が、いきなりスタートしようにも

  • え、対象多すぎでは..?
  • そもそも何のためにこれやるんだっけ
  • どこから手をつければいいの

といった心境になります。そこで、「脱jQueryお気持ち会」をチームで開催しました。

実際に出てきたカード

f:id:mugi1:20200703105057p:plain:w200

ざっくりと会話をしたことで「どうやらjQuery自体が問題というわけではないかもしれない」「修正対象がどの程度の規模感なのか誰も知らないよね」といったことが見えてきました。

最終的には、お気持ち会で出た議論をもとに、次のような方向性が定まり、実際に動き始めました。

  • プロダクトのロードマップなどを確認して、何を優先すべきか検討しよう
  • 対象のコードや影響範囲がどの程度のものなのか整理して把握しよう

お気持ち会のメリット

お気持ち会は、その名の通り「お気持ち」を表明するだけでも歓迎されます。お気持ちですので、ふわっとした意見でも問題ありません。

「正直よくわからんけど、難しそうだなって思いました!!」

とかでもオッケーです。具体的な方針や対策を書く必要もありません。

f:id:mugi1:20200706151726p:plain:w300
ふわっとしたカードの例

そのぐらいハードルを下げることで、多くの人からバラエティ豊かな意見やアイデアを集めることができます。

また、参加する側としても「この意見はこの場では言うべきではないかもしれない」というような不安を払拭でき、より心理的に安全な形で参加することができます。

お気持ち会によって得られるもの

お気持ち会を開催することで

  • 不安な箇所がわかる
  • 「知らないこと」が何かわかる
  • メンバーがどういった方向を向いているかを相互に確認できる

といったことを得られます。

輪郭がハッキリしないテーマで何かを始めるのは、暗闇の中にいる状態からスタートするようなものかと思います。

まずはどこに向けて進むか決める必要がありますが、お気持ち会という場で情報を集めることで、暗闇を払って最初の1歩を進むための手がかりになっているのかな〜と感じています。

考察 / お気持ち会の正体

お気持ち会という名前で会が開催されてはいますが、結局のところコレはブレインストーミングをMisocaチームなりに形にしたものかな、と思います。

ブレインストーミング - Wikipedia

とはいえ「お気持ち会」という名前にしていることで、ブレインストーミングの原則である「判断・結論を出さない」「粗野な考えを歓迎する」などと言った部分が説明せずとも理解されているのかもしれませんね。

まとめ

というわけで今回はMisocaで実施されている「お気持ち会」のご紹介でした。それほど準備コストもかけずに気軽にはじめられるので、興味があれば試してみてはいかがでしょうか。

Misoca開発者ブログは「弥生開発者ブログ by Misocaチーム」に生まれ変わります

こんにちは、Misoca開発チームの黒曜(@kokuyouwind)です。

最近ウィングスパンを買ったので、ボドゲ会を開きたい今日このごろです。

🏢 弥生との合併を受けて

株式会社Misocaは2020年7月1日付けで弥生と合併し、Misocaのメンバーは弥生の社員になりました。

とはいえ業務上は大きく変わらないのですが、ひとつ問題が。

このブログ、「Misoca開発者ブログ」という名前なんですよね。

実は弥生にも元々弥生開発者ブログがあるのですが、結構な数ある過去記事のお引越しは大変そうだし、デザインが違うので崩れも怖い… 突貫作業ではやりたくない…

というわけで、このブログは「弥生開発者ブログ by Misocaチーム」として生まれ変わります!

具体的には、ブログタイトルとロゴの2点が生まれ変わりました!

ヘッダーにも出てますが、こんな感じです。

f:id:kokuyouwind:20200702170424p:plain

当面は今まで通り、Misocaのメンバーが様々な記事を投稿する予定です。

Misoca開発者ブログあらため、弥生開発者ブログ by Misocaチームを今後ともよろしくお願いします!

📢 宣伝

Misocaチームでは弥生と一緒にプロダクトを作りたいエンジニアを募集しています!

SectionReport フォーマットを実装した拡張版 Thinreports の公開

こんにちは、Misoca の日高(@hidakatsuya)です。

Fit Boxing は何度始めても3日坊主になります。

拡張版 Thinreports の公開

Thinreports は、オープンソース のPDF 生成ツールです。Thinreports では、Editor と呼ばれる帳票デザインツールで作成したテンプレートファイルを、RubyGems である Generator を使って読み込み、値の埋め込みなどを行って PDF を作成することができます。

Misoca でも以前から一部の PDF 生成機能で利用していましたが、昨年より、本格的に利用を進めるにあたって、不足した機能の拡張という形で SectionReport フォーマットの実装を進めてきました。

Editor 及び Generator それぞれの実装は下記リポジトリの section-report ブランチで公開し、現在も少しづつ開発を進めています。

github.com

github.com

SectionReport フォーマット

SectionReport フォーマットとは、コミュニティで提案されている Thinreports の新しいレイアウトフォーマット です。公開した拡張版 Thinreports は、この提案に基づいて実装した形です。

このレイアウトフォーマットは、

レイアウト定義時点でページの制約をなくし、出力した結果によってページを決めるレイアウトフォーマット

と、提案の中でも説明されているように、従来のフォーマットのような、ページ単位の固定なレイアウトではなく、レイアウトを「セクション」と呼ばれる単位で分割して定義し、PDF生成処理の中で、それらのセクションを組み合わせたり、カスタマイズすることによって、柔軟なレイアウトを可能にします。

f:id:hidakatsuya:20200629164435p:plain
Example: Basic Usage のレイアウト定義

主な特徴を紹介します。

セクションの高さの自動伸縮

描画した結果、テキストの内容がセクションの高さに収まらないとき、それに応じてセクション自体の高さを自動的に拡張させることができます。また、後述する StackView による自動縮小もサポートしています。詳細は Example: Section Auto Stretch を参照してください。

StackView ツール

図形やテキストを配置できる複数の領域(row)をもつ、Misoca 独自の新しいツールです。

StackView は他の図形同様、セクション内に配置することができます。その row は、動的に非表示にすることができ、その下に続く row は上に詰めて描画されます。また、row はセクション同様、自動伸縮をサポートします。StackView を使うことによって、セクション内のレイアウトを動的かつ柔軟なものにすることができます。詳細は、Example: StackView を参照してください。

その他の機能や実装状況

その他の機能や実装状況など、詳細は下記 README に記載しています。また、StackView ツールなどの Misoca 独自機能についても説明しています。

使い方

それぞれの README を参照してください。

また、いくつかの example も用意しています。各 example には、テンプレートファイルと Ruby のコード、その出力結果PDFが含まれています。こちらも参考にしてください。

なお、Editor は現時点では SectionReport フォーマット専用です。従来のフォーマットを編集することはできません。

今後について

いずれの実装もまだまだ発展途上です。Misoca としても開発を継続しつつ、近く Thinreports コミュニティに pull request を送る形で提案していきます。 そして、活発な議論のもと、より良い形での機能の提案ができればと思っています。その際は、ぜひレビューやコメントをしていただけると嬉しいです。

最後に

拡張版 Thinreports の概要と公開のお知らせを中心にご紹介しました。 今後も、この拡張版 Thinreports について、詳しい使い方や Misoca での具体的な利用例などを引き続きご紹介していきたいと思います。

宣伝

Misocaでは OSS に貢献したいエンジニアを募集しています。

www.wantedly.com

急なレスポンスタイム悪化から、オープンソースプロジェクトにPull Requestを送るまで

こんにちは、Misoca開発チームの黒曜(@kokuyouwind)です。

最近はシャニマスのイベントシナリオ感想記事をnoteにまとめたりしています。

😨 急に本番のレスポンスタイムが悪化した話

Webエンジニアにとって、「本番障害」という4文字ほど見たくないものはないでしょう。

本番障害ほどではないにしても、「急なレスポンスタイム悪化」もあまり見たくない文字列ですね。まぁ、見たくなくても向こうからやってくるんですが…

というわけで、今回は本番レスポンスが急に悪化したときの話です。いろいろ調べた結果、利用しているオープンソースプロジェクトが原因だったことがわかりPull Requestを送ったので、その流れをまとめてみたいと思います。

❗️ レスポンスタイム悪化の検知

Misocaでは監視ツールとしてMackerelを、APMツールとしてSkylightを利用しています。

本番レスポンスタイムはMackerelでアラートを設定しており、平常値から大きく悪化した場合はSlackに通知されるようになっています。

その日はリリース作業を行った直後、レスポンスタイム悪化のアラートが飛んできました。気になってSkylightを確認したところ、驚きの事態が。*1

f:id:kokuyouwind:20200609181504p:plain

リリース以降、95パーセンタイルがめちゃくちゃ悪化しとるやん!*2*3

🩹 応急処置

幸いアプリケーションが使えなくなるほどの悪化ではなかったため、落ち着いて応急処置に臨みます。

Skylightで表示する期間をリリース前とリリース後で切り替えて比べた結果、どうやらPDF生成処理にかかる時間が伸びているらしいとわかりました。

リリース内容を精査したところ、ちょうどそのタイミングでPDF生成処理に関係しそうなTTFunk gemの更新が入っていることが判明。*4

おそらくこれだろうとアタリをつけ、該当コミットを巻き戻してリリース。結果、見事にレスポンスタイムが回復しました。

f:id:kokuyouwind:20200609182842p:plain

🕵️‍♀️ 原因調査

ひとまず本番の問題は落ち着いたため、なにが原因だったのかを調査していきます。

まずはローカル環境での再現確認。以下のようなコードでベンチマークを取り、gem更新前と更新後とでPDF生成にかかる時間が増えるかを確認します。

10回繰り返しているのはActiveRecordアクセスなどPDF生成以外の要因を相対的に小さくするためです。

require 'benchmark'

Benchmark.bmbm do |x|
  x.report('generate') {
    10.times { Misoca::PDF.generate(Invoice.first) }
  }
end

結果、ttfunk 1.5.1のときと比べて、ttfunk 1.6.2.1では概ね4倍ほど遅くなることが確認できました。

本番でしか起きない問題だと調査が難しいのですが、ローカルでも再現することがわかったため調査がしやすくなりましたね。

続けてStackprofを使ってプロファイルを取り、どのような処理に時間がかかっているかを調べていきます。

require 'stackprof'

StackProf.run(mode: :cpu, out: 'tmp/stackprof-cpu-myapp.dump', raw: true) do
  10.times { Misoca::PDF.generate(Invoice.first) }
end

stackprof tmp/stackprof-cpu-myapp.dump で確認した結果、以下のようにActiveSupport::CompareWithRange#cover?が50%以上の時間を使っていることがわかりました。

==================================
  Mode: cpu(1000)
  Samples: 13617 (0.71% miss rate)
  GC: 1461 (10.73%)
==================================
     TOTAL    (pct)     SAMPLES    (pct)     FRAME
      7765  (57.0%)        7765  (57.0%)     ActiveSupport::CompareWithRange#cover?
      1138   (8.4%)        1138   (8.4%)     (marking)
      8609  (63.2%)         844   (6.2%)     TTFunk::Table::OS2.group_original_code_points_by_bit
       322   (2.4%)         322   (2.4%)     String#unpack
       322   (2.4%)         322   (2.4%)     (sweeping)
...

TTFunk::Table::OS2.group_original_code_points_by_bitもTOTALでは多くの時間を使っており、SAMPLESは少なくなっています。 このことから、TTFunk::Table::OS2.group_original_code_points_by_bitからActiveSupport::CompareWithRange#cover?を呼び出している箇所が重いのだろうと推測することができました。

実際にTTFunk::Table::OS2.group_original_code_points_by_bitのコードを確認すると、以下のようにr.cover?を繰り返し呼び出しています。

os2.file.cmap.unicode.first.code_map.each_key do |code_point|
  # find corresponding bit
  range = UNICODE_RANGES.find { |r| r.cover?(code_point) }

この処理は1.6.0で追加されたものでした。ここにパフォーマンス上の問題があるようですね。

📝 Issueを立てる

問題が把握できたため既存のIssueやPull Requestを確認しましたが、どうやらまだ報告されていないようでした。

新しいIssueを立てたいところですが、このためには最小限の再現コードを作り、ttfunk自体の問題である確証を得ておきたいところです。

PDF生成の内部処理からどのようにttfunkが呼び出されているかわからなかったため、stackprof --graphviz tmp/stackprof-cpu-myapp.dump > tmp/stackprof.dotでdotファイルを作成し、dot -T pdf -o tmp/stackprof.pdf tmp/stackprof.dotでPDF化して呼び出しグラフを見てみました。*5

f:id:kokuyouwind:20200610134938p:plain:h300

どうやら、TTFunk::Subset::Base#encodeが外部とのインターフェイスのようです。

specファイルを参考にしつつ、この処理を呼び出す再現コードを書いてみます。

require 'ttfunk'
require 'benchmark'

# English Font
file = TTFunk::File.open("DejaVuSans.ttf")
# Japanese Font
# file = TTFunk::File.open("GenShinGothic-Normal.ttf")

subset = TTFunk::Subset.for(file, :unicode)
Benchmark.bmbm { |x| x.report('encode') { 10.times { subset.encode } } }

日本語などのマルチバイト文字でしか再現しない問題かもしれないので、フォントを英字フォントと日本語フォントの2種類で確認できるようにしています。

このコードを用いて1.5.1と1.6.2.1でベンチマークを取ったところ、英字フォント・日本語フォントを問わず実に30倍程度遅くなっていることがわかりました。やはり日本語フォントのほうが長時間かかるため、英字フォントを扱う海外ではパフォーマンスの問題に気づかなかったのかもしれません。

無事ttfunkのみで再現コードを作り計測もできたため、Issueとして報告することができました。

🛠 修正Pull requestを送る

あまりに難しそうな問題であれば手出しできませんが、今回は比較的読めそうなコードだったため、自分で直せないか検討してみることにします。

問題の箇所ではos2.file.cmap.unicode.first.code_mapの各キーについてeachを呼び出し、UNICODE_RANGESの中からr.cover?(code_point)を満たすものを探し出しています。

それぞれの要素数をN,M とすると、この処理は Ο(N * M)の計算量になります。Mは200弱の固定値ですが、それでも効率の悪い計算に見えますね。

素直に思いつくのはUNICODE_RANGESを探索木にすることです。ただしUNICODE_RANGESRangeの配列のため通常の探索木ではなく区間木を使うことになります。Ruby標準でサポートされないデータ構造のため依存gemを増やすか自力で実装するかになりますが、いずれも少々大きな変更になります。オープンソースプロジェクトへのPull Requestとしては、できれば避けたいところです。

ここで発想を変えて、「UNICODE_RANGESごとに、その範囲に含まれるcode_mapキーをまとめて見つけ出す」としたらどうでしょうか。code_mapが整列していた場合、同じUNICODE_RANGESに含まれるキーは必ず固まって並ぶため、効率良く処理できそうです。またUNICODE_RANGESは互いに重複しない範囲であるため、他のUNICODE_RANGESに含まれていたものを以降の判定から除外することができます。

この方針で実装してみたところうまく動いたため、Pull Requestを出すことにしました。計算量はΟ(max(N, M))まで改善しており、ベンチマークも8倍ほど早くなっています。

💬 感想

今回は本番でパフォーマンス悪化に気づいてから原因を調査し、オープンソースプロジェクトにIssueを立ててPull Requestを送るまでの流れをまとめてみました。

定番ですがStackprofを用いたプロファイル分析は大切ですね。またみんなのコンピュータサイエンス読書会で読んだばかりの計算量やアルゴリズムの話がさっそく活かせたのも良かったです。

tech.misoca.jp

今後も利用しているオープンソースプロジェクトの問題を見つけたときは、Issueで報告して直せるものはPull Requestを出していきたいと思います。

📢 宣伝

Misocaではパフォーマンスの問題を改善したいエンジニアを募集しています。

*1:話を簡単にするために省略しましたが、実は月初の負荷にまぎれてしまい、当日はアクセス量の問題だと思っていました。翌日も数値が回復しなかったため怪しんで調査し、コード起因の問題であったことがわかりました。

*2:95パーセンタイルは、レスポンスタイムすべてを短い順に並べて95%目の値を取り出したものです。SkylightのTypical responseは中央値(50パーセンタイル)を、Problem Responseは95パーセンタイルを表示しています。

*3:グラフの縦軸は具体的なパフォーマンスの情報になってしまうため、意図的に伏せています。

*4:話を簡単にするために省略しましたが、実際には別のgemの更新に伴い、依存していたttfunkも更新されていました。原因調査の際はここの切り分けでかなり苦労しました。

*5:flamegraphでも同じようなコールスタックが見えますが、スクリーンショットで貼ったときに細かすぎて見づらいため、今回はgraphvizを使っています。

みんなのコンピュータサイエンス読書会

こんにちは、Misoca開発チームの黒曜(@kokuyouwind)です。

最近はVTuberのシャニマス実況を見て無限に時間を溶かしています。だいたい委員長のせいです。

📕 みんなのコンピュータサイエンス読書会

Misocaでは有志で集まりみんなのコンピュータサイエンスの読書会を開催しています。

みんなのコンピュータサイエンス

みんなのコンピュータサイエンス

読書会を始めたのは「単に自分が読みたかったから」という理由が大きいのですが、コンピュータサイエンス(以下CS)の知識のベースラインを揃える取り組みをしたかったという面もあります。

チームメンバーの来歴はCSの専門課程を経た人から独学でプログラミングを学んだ人まで様々です。

例えば「MySQLのクエリはインデックスが効かないと遅くなる」という知識ひとつをとっても、CSの知識があれば「インデックスが探索木になっているから、Ο(log N) と Ο(N) の差があるんだな」とイメージを持てるのに対して、計算量や探索木の知識がない場合は「とにかくインデックスが効かないとだめなのかな」という理解になってしまいかねません。このように理解の齟齬がある状況では、「どの程度のサイズのテーブルで、どのカラムにインデックスを足すべきか」といった議論がまともに成り立たなくなってしまうでしょう。

こういった問題を防ぐために、CSの基礎知識をチームで揃えるのは重要だと個人的に考えています。その点で、CSの基礎を広く平易に扱ったこの本はちょうどよい題材になったと思います。*1

🏃‍♂️ 進め方

読書会では、節ごとに誰かが音読してから掘り下げて議論する流れを取っています。これは読む速度で取り残される人が出ないようにするとともに、内容をしっかり掘り下げられるようにするためです。

掘り下げ議論では、詳細を追って参加者全員の理解を確認することに加えて、現実での応用例や自社サービスで使える箇所がないかなどもよく話しています。例えば貪欲法の節では並列テストのグループ振り分けに利用している話や、探索木の節ではMySQL InnoDBのインデックスがB+木である話などをしていました。

掘り下げの際にMiroを使ってグラフや図を描くこともあります。以下は動的計画法のボトムアップについて、グラフを描いて確認したものです。

f:id:kokuyouwind:20200526173251p:plain:w480

ツールをろくに使わずフリーハンドで描いているため汚いですが、こういった図を描きながら話すことでより理解しやすくなりました。もう少し使いこなせるようになりたいですね。

📈 進捗

年初から始めて、現在5.3節までを読み終わったところです。本全体の半分ちょっとくらいでしょうか。

かなりゆっくりしたペースですが、これはかなり掘り下げて理解を確認しながら進めていることと、参加者の都合が合わず中止になる日も多いためです。

特に基礎となる論理・確率や計算量などは理解しないまま進めてしまうと後で困りそうだったので、1章で大きく時間を割いていました。

残りもこのペースでじっくり読み進められればと思います。

📢 宣伝

MisocaではCSの基礎を固めて業務に活かしたいエンジニアを募集しています!

*1:ベースラインを揃える目的で、基本情報技術者試験などの取得を推奨する企業も多いと思います。そちらも有効な手段だと思いますがやや堅苦しくなってしまうため、個人的には読書会くらいのライトさが好みです。