株式会社クリアコード > ククログ

ククログ


apitraceを使ったfirefoxのWebGLのデバッグ例

はじめに

これまでにも何度か紹介してきましたが、クリアコードではGecko(Firefox)を組み込み機器向けに移植する取り組みを行っています。

当プロジェクトでは移植コストを少しでも低減するために、Firefoxの延長サポート版(ESR)のみを対象としています。これまではESR45やESR52をベースにハードウェアアクセラレーションに対応させるための作業を行ってきました。 現在はESR60に対応し、そのバグを修正する作業を進めています。

この作業に関連するOpenGL ESのデバッグのちょうど良い実例を解説します。 今回のデバッグにはapitraceというOpenGLに関するAPIの呼び出しを取得できるツールを用いています。

実例

apitraceを用いてのデバッグ方法は apitraceを使ったOpenGL ESが絡むFirefoxのデバッグ方法 にて解説しました。

今回はこれを元に、実際にRZ/G1M上でのWebGLの不正な挙動を直すまでに至った修正作業を説明します。

WebGLはFirefox ESR52のときは動作していました。 ESR60へのバージョンアップ時にWebGLのContextが作成されるが、キャンバス描画の更新がうまく行かず、真っ黒となってしまう問題が発生しました。

ESR52のときのEGLのAPIの呼ばれ方とESR60のときのAPIの呼ばれ方を比較してみます。

ESR52の時
...
1127 glBindTexture(target = GL_TEXTURE_2D, texture = 70001)
1128 glTexParameteri(target = GL_TEXTURE_2D, pname = GL_TEXTURE_MIN_FILTER, param = GL_LINEAR)
1129 glTexParameteri(target = GL_TEXTURE_2D, pname = GL_TEXTURE_MAG_FILTER, param = GL_LINEAR)
1130 glTexParameteri(target = GL_TEXTURE_2D, pname = GL_TEXTURE_WRAP_S, param = GL_CLAMP_TO_EDGE)
1131 glTexParameteri(target = GL_TEXTURE_2D, pname = GL_TEXTURE_WRAP_T, param = GL_CLAMP_TO_EDGE)
1132 glTexImage2D(target = GL_TEXTURE_2D, level = 0, internalformat = GL_RGBA, width = 16, height = 16, border = 0, format = GL_RGBA, type = GL_UNSIGNED_BYTE, pixels = NULL)
1133 glBindTexture(target = GL_TEXTURE_2D, texture = 0)
1136 eglGetCurrentContext() = 0x9dc973a0
1137 glGenFramebuffers(n = 1, framebuffers = &70001)
1138 glGetIntegerv(pname = GL_DRAW_FRAMEBUFFER_BINDING, params = &0)
1139 glBindFramebuffer(target = GL_FRAMEBUFFER, framebuffer = 70001)
1140 glFramebufferTexture2D(target = GL_FRAMEBUFFER, attachment = GL_COLOR_ATTACHMENT0, textarget = GL_TEXTURE_2D, texture = 70001, level = 0)
1141 glCheckFramebufferStatus(target = GL_FRAMEBUFFER) = GL_FRAMEBUFFER_COMPLETE
1142 glBindFramebuffer(target = GL_FRAMEBUFFER, framebuffer = 0)
1143 glGetIntegerv(pname = GL_DRAW_FRAMEBUFFER_BINDING, params = &0)
1145 glGenRenderbuffers(n = 1, renderbuffers = &70001)
1146 glGetIntegerv(pname = GL_RENDERBUFFER_BINDING, params = &0)
1147 glBindRenderbuffer(target = GL_RENDERBUFFER, renderbuffer = 70001)
1148 glRenderbufferStorage(target = GL_RENDERBUFFER, internalformat = GL_DEPTH_COMPONENT24, width = 16, height = 16)
1149 glBindRenderbuffer(target = GL_RENDERBUFFER, renderbuffer = 0)
1150 glGenFramebuffers(n = 1, framebuffers = &140002)
1151 glGetIntegerv(pname = GL_DRAW_FRAMEBUFFER_BINDING, params = &0)
1152 glBindFramebuffer(target = GL_FRAMEBUFFER, framebuffer = 140002)
1153 glFramebufferTexture2D(target = GL_FRAMEBUFFER, attachment = GL_COLOR_ATTACHMENT0, textarget = GL_TEXTURE_2D, texture = 70001, level = 0)
1154 glFramebufferRenderbuffer(target = GL_FRAMEBUFFER, attachment = GL_DEPTH_ATTACHMENT, renderbuffertarget = GL_RENDERBUFFER, renderbuffer = 70001)
1155 glBindFramebuffer(target = GL_FRAMEBUFFER, framebuffer = 0)
...

どのAPIも正常に終了していることがわかります。ここで、 glCheckFramebufferStatus(target = GL_FRAMEBUFFER) の戻り値を見てみましょう。

GL_FRAMEBUFFER_COMPLETE とあるので、FrameBufferの状態は正常です。 このように、EGLのAPIの呼ばれ方、終了時の戻り値に着目することでWebGLの動作が正常かどうかの判断をapitraceのダンプから解析できることがわかりました。

ESR60の時

続いて、ESR60についてもダンプデータを解析してみます。

...
57913 glGetFloatv(pname = GL_ALIASED_LINE_WIDTH_RANGE, params = {1, 16})
57914 glGetFloatv(pname = GL_ALIASED_POINT_SIZE_RANGE, params = {1, 511})
57917 eglGetCurrentContext() = 0x830079d0
57919 glGenRenderbuffers(n = 1, renderbuffers = &70001)
57920 glGetIntegerv(pname = GL_RENDERBUFFER_BINDING, params = &0)
57921 glBindRenderbuffer(target = GL_RENDERBUFFER, renderbuffer = 70001)
57922 glRenderbufferStorageMultisampleANGLE(target = GL_RENDERBUFFER, samples = 4, internalformat = GL_RGBA8, width = 300, height = 150)
57923 glBindRenderbuffer(target = GL_RENDERBUFFER, renderbuffer = 0)
57926 glGenFramebuffers(n = 1, framebuffers = &210003)
57927 glGenRenderbuffers(n = 1, renderbuffers = &140002)
57928 glGenRenderbuffers(n = 1, renderbuffers = &210003)
57929 glBindFramebuffer(target = GL_FRAMEBUFFER, framebuffer = 210003)
57930 glFramebufferRenderbuffer(target = GL_FRAMEBUFFER, attachment = GL_COLOR_ATTACHMENT0, renderbuffertarget = GL_RENDERBUFFER, renderbuffer = 70001)
57932 glGetIntegerv(pname = GL_RENDERBUFFER_BINDING, params = &0)
57933 glBindRenderbuffer(target = GL_RENDERBUFFER, renderbuffer = 140002)
57934 glRenderbufferStorageMultisampleANGLE(target = GL_RENDERBUFFER, samples = 4, internalformat = GL_DEPTH24_STENCIL8, width = 300, height = 150)
57935 glBindRenderbuffer(target = GL_RENDERBUFFER, renderbuffer = 0)
57938 glFramebufferRenderbuffer(target = GL_FRAMEBUFFER, attachment = GL_DEPTH_ATTACHMENT, renderbuffertarget = GL_RENDERBUFFER, renderbuffer = 140002)
57939 glFramebufferRenderbuffer(target = GL_FRAMEBUFFER, attachment = GL_STENCIL_ATTACHMENT, renderbuffertarget = GL_RENDERBUFFER, renderbuffer = 140002)
57940 glCheckFramebufferStatus(target = GL_FRAMEBUFFER) = GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT
...

glRenderbufferStorage の代わりに glRenderbufferStorageMultisampleANGLE が呼ばれています。また、ESR52のダンプの解析と同様に、glCheckFramebufferStatus(target = GL_FRAMEBUFFER) の戻り値を確認します。今度はGL_FRAMEBUFFER_INCOMPLETE_ATTACHMENTとなっているようです。

解析結果から修正方法の検討

  • ESR52の時には glRenderbufferStorage が呼ばれているときは正常に動作していた
  • ESR60の時には glRenderbufferStorageMultisampleANGLE が呼ばれ、glCheckFramebufferStatusのチェックに引っかかってしまった

このことから、WebGLの動作を復元するには glRenderbufferStorageMultisampleANGLE が呼ばれている箇所で glRenderbufferStorage を呼ぶようにすると良さそうです。

glRenderbufferStorageMultisampleANGLE関数は以下の箇所で読み込まれています: https://dxr.mozilla.org/mozilla-esr60/source/gfx/gl/GLContext.cpp#840

Mozillaのコードベースでは、EGLの拡張機能がドライバがサポートしていてもドライバのバグなどにより正常動作を望めない場合に備えて手動でEGLの拡張機能を無効にする方法が提供されています。

void MarkExtensionUnsupported(GLExtensions aKnownExtension) 関数に無効化したい種類の拡張機能のシンボルを与えてやることにより、その拡張機能を無効化できます。

今回無効化したい拡張機能を識別するシンボルは GLFeature::framebuffer_multisample です。

このシンボルにglRenderbufferStorageMultisampleANGLE関数が紐づけられています。

そのため、以下のパッチにより、RZ/G1M上でのWebGLの不正な動作を修正することができました。

diff --git a/gfx/gl/GLContext.cpp b/gfx/gl/GLContext.cpp
index f60ebcaed82e..1186d5efea20 100644
--- a/gfx/gl/GLContext.cpp
+++ b/gfx/gl/GLContext.cpp
@@ -743,6 +743,15 @@ GLContext::InitWithPrefixImpl(const char* prefix, bool trygl)
             MarkUnsupported(GLFeature::framebuffer_multisample);
         }
 
+#ifdef MOZ_WAYLAND
+        if (Vendor() == GLVendor::Imagination &&
+            Renderer() == GLRenderer::SGX544MP) {
+            // PowerVR SGX544MP's EGL does not have valid
+            // glRenderbufferStorageMultisampleANGLE implementation, which breaks WebGL.
+            MarkUnsupported(GLFeature::framebuffer_multisample);
+        }
+#endif
+
 #ifdef XP_MACOSX
         // The Mac Nvidia driver, for versions up to and including 10.8,
// don't seem to properly support this. See 814839

まとめ

Firefox ESR60へのバージョンアップに伴うWebGLのEGLのAPIの呼ばれ方の変更により、WebGLのキャンバスが黒くなったままの状態から更新されなくなってしまった不具合を修正するに至ったデバッグの実例を解説しました。

タグ: Mozilla
2018-07-02

Firefoxへのフィードバックの仕方:Windows編

近年はOSS・フリーソフトウェアのリポジトリを公開する場所としてGitHubが選ばれる事が多くなりましたので、フィードバックをする際も、GitHub上のIssuesで障害を報告したり、Pull Requestという形で具体的なソースコードの変更を提案したりといった形を取る事が多くなりました。同様のフィードバック方法が、BitBucketやGitLabなどの競合サービスにおいても可能です。

その一方で、歴史の長いプロジェクトではフィードバックの受け付けがメーリングリストに限られていたり、ソースコードがGitHub等での公開ではないため実装の提案をプルリクエストの形では行えなかったりという事があります。Firefoxもそういった例の1つで、不具合の報告や機能の追加要望はBugzillaで行い、実装内容は伝統的なパッチやPhabricatorといった専用ツールで送付する必要があります。

そこでこの記事では、Windows上でFirefoxに対してパッチを含むフィードバックを行う際の流れをご紹介します。基本的にはHow to submit a patchという記事日本語訳)に書かれている内容ですが、本記事では周辺情報も併せてまとめています。Firefoxに直接的なフィードバックをしてみたいという方は、ぜひ参考にしてみて下さい。

まずは、ソースコードからFirefoxをビルドできるようにする

パッチを送付するようなフィードバックを行うためには、まずソースコードからFirefoxをビルドできる環境を整えておきます。1行だけの変更なのに大袈裟な……と思うかもしれませんが、Firefoxほどの規模のソフトウェアでは、些細な変更が予想もしない所に影響を及ぼす事があります。ファイルの内容に変更を行う際は必ずビルドと自動テストを行い、後退バグ(regression)が発生していない事を確認する必要があります。

基本的には、Windows上でのビルド環境一式がセットになっている「MozillaBuild」というツールを使う事になります。これにはMSYSベースのUnix系コマンド群やPython、Mercurialなどが含まれています。また、以下の手順の中に含まれていますが、それ以外の必要なツール類も半自動でインストールされるので、準備は非常に簡単です。

以下は、2018年7月3日時点でのビルド環境整備手順の要約となります。

  1. MozillaBuildの最新版リリースをダウンロードし、インストールする。一般的にはc:\mozilla-buildにインストールする。
  2. C:\mozilla-sourceの位置にフォルダを作る。Firefoxのソースコード一式はこの配下に置く事になる。
  3. c:\mozilla-build\start-shell.batを実行して、MozillaBuildのシェル(Bash)を起動する。
  4. export PATH=$PATH:~/.cargo/bin という行を、~/.bash_profile~/.bash_login、または~/.profileのいずれかに追記する。echo 'export PATH=$PATH:~/.cargo/bin' >> ~/.bash_profileのようにするとよい。
  5. MozillaBuildのシェルを一旦終了し、c:\mozilla-build\start-shell.batで起動し直す。
  6. cd /c/mozilla-sourceして、パッチを書く対象となるFirefoxのMercurialリポジトリをcloneする。
    • 通常は、最新の開発版のリポジトリであるmozilla-centralをcloneする。hg clone https://hg.mozilla.org/mozilla-centralでcloneできる。以下の説明はこちらのケースを前提とする。
    • ベータフェーズにあるバージョンへのパッチを作りたい場合は、releases/mozilla-betaをcloneする。hg clone https://hg.mozilla.org/releases/mozilla-betaでcloneできる。
    • 既にリリースされたバージョンへのパッチを作りたい場合は、releases/mozilla-releaseをcloneする。hg clone https://hg.mozilla.org/releases/mozilla-releaseでcloneできる。
  7. cd mozilla-centralしてリポジトリに入り、./mach bootstrapを実行する。これにより、Visual StudioやRustなどのビルドに必要なソフトウェア群が自動インストールされる。

必要なソフトウェア群が揃ったら、./mach buildでFirefoxをビルドして、ビルドが完了後に./mach runでFirefoxを起動してみましょう。無事に起動すれば、準備は完了です。

ビルド時間を短縮するには

Firefoxのビルドに要する時間のほとんどは、C++で実装された基盤部分のバイナリのビルド時間です。フィードバックしたい変更がGUI部分のJavaScriptの実装に関わる部分だけであるという場合、ビルド済みバイナリを使ってビルド時間を短縮する事もできます。これはartifact buildと呼ばれます。

artifact buildを使うためには、リポジトリ直下に置く.mozconfigというファイルを使ってFirefoxのビルドオプションを指定する必要があります。具体的には以下の要領です。

cd /c/mozilla-build/mozilla-central
echo 'ac_add_options --enable-artifact-builds' >> ./.mozconfig
echo 'mk_add_options MOZ_OBJDIR=./objdir-frontend' >> ./.mozconfig

この状態で./mach buildを実行すると、C++のコンポーネントをビルドする代わりにビルド済みバイナリが自動でダウンロードされてきて、それと組み合わせる形でFirefoxがビルドされるようになります。

筆者はJavaScript部分だけのフィードバックとC++部分に関わるフィードバックのどちらも行う場合があるため、/c/mozilla-source/mozilla-central-artifact/c/mozilla-source/mozilla-central-fullという具合にリポジトリ自体を2つcloneしておき、片方をartifact build専用にして使っています。

「自分の名前」を設定しておく

作業を始める前に、Mercurialリポジトリへのコミットに表示される作業者名を設定しましょう。これは、MozillaBuildのシェルから見た時に~/.hgrcの位置にあるファイルで指定します。具体的には、以下のように書きます。

[ui]
username = YUKI "Piro" Hiroshi <yuki@clear-code.com>

パッチを書けそうなBugを報告する、または見付ける

Firefoxでは、不具合の報告や新機能の提案はすべて「Bug」としてbugzilla.mozilla.orgのバグトラッカーで管理されています。

Bugを登録する場合は、まずアカウントを作成してログインします。ログイン済みの状態だと、ページ上部に鉛筆型のアイコンの「File a new bug」というリンクが現れ、そこから新しいBugを報告・登録することができます。Bugの登録時にはまずそれがどの部分(プロダクト)の話なのかを指定する必要がありますが、基本的には、FirefoxというWebブラウザ固有のUIや機能に関わる話題は「Firefox」プロダクト、Thunderbird等と共通の基盤部分のうちJavaScriptで実装されている物は「toolkit」プロダクト、C++で実装された低レベルの基盤技術に関わる物は「core」と覚えておくとよいでしょう。また、プロダクトの選択後はさらに細分化された「コンポーネント」を選択する必要がありますが、リストの中からそれらしいものを選んでおけば大丈夫です。もしプロダクトやコンポーネントを間違えたとしても、他の開発者の人がBugの内容に合わせた適切なプロダクト・コンポーネントを再設定して誘導してくれます。その後、「summary(要約)」と、コメントとして「steps to reproduce(詳細な再現手順)」「expected result(期待される結果)」「actual result(実際の結果)」を入力して投稿すれば、Bugの報告は完了です。

既ににあるBugの中からパッチを書けそうな物を探す場合は、good-first-bugというキーワードで検索すると、比較的難易度が低いと考えられているBugを一覧表示することができます。興味のある話題のBugを見付けたら、これまでの経緯を読んで、どのようなパッチが求められているのかを読み取ってみましょう。

パッチを書くBugを登録あるいは検索結果から見付けたら、他の人が同時に作業を始めないように、自分がこれからパッチを書いてみますという事を宣言しておくとよいでしょう。これは、単純に「Now I'm trying to write a patch for this bug.」のように書いてもいいですし、「Assignee(担当者)」という欄に自分のアカウント名を設定しても良いです(前者のようにだけしていても、他の人が気を利かせてあなたの名前をAssigneeに設定してくれる事もあります)。

パッチを実際に作成する

準備ができましたので、早速パッチを作ってみる事にします。

クリーンな環境に戻す

以前にパッチを作るために変更した結果が残っていると、作成したパッチが期待と異なる内容になってしまいます。Mozilla公式のリポジトリの内容に対するパッチを作るために、まず以下のようにコマンドを実行し、リポジトリの状態をMozilla公式の物と同じに揃えておきましょう。

hg update -C default # 変更を全て取り消す
hg pull -u # Mozillaのリポジトリから変更点を複製し、手元のコピーに反映する
作業用のブランチを作る

次に、作業用の一時的なブランチを作成します。hg branch fix-bug-xxxxx-workingのように実行して、新しいブランチの作成を予約しましょう(gitではgit checkout -b branch-nameのようにするとその瞬間にブランチが作られますが、Mercurialではこのように「ブランチの作成を予約」した後で何かコミットした段階で初めてブランチが作られます)。

このブランチそのものは、後でパッチを作るための素材として使うだけなので、名前には「working」を付けて作業用である事を示しています。(用事が終わったら、このブランチはhg branch -C xxxxxコマンドで消去してしまって問題ありません。)

コードを改変する

問題を修正するための変更(新機能を追加するための変更)を行います。変更はhg commit path/to/fileでコミットすることができ、新しいファイルを追加する場合はhg add path/to/fileでリポジトリに登録できます。作業経過を失わないように、こまめに「Bug XXXXX - 変更の概要」といった要領のコミットメッセージを付けてコミットするようにしましょう。コミット時にはEmacsが起動しますが、普通にコミットメッセージを入力して、上書き保存して終了すればOKです。

方針を間違えたなどの理由でコミットを取り消したくなった場合は、hg log -Gでコミットツリーを表示して、取り消したいコミットの番号(XXXX:YYYYYYYという形式の、数字と文字列の組)を調べた上で、調べたコミットの番号の「:」より手前の数字を指定してhg strip XXXXと言うコマンドを実行します。こうすると、指定したコミットとその後に続くコミットが無かったことになります。

また、コミット単位ではなく作業そのものを最初からやり直したいという場合には、ブランチそのものを破棄して作り直すのが手っ取り早いです。その場合はhg update -C defaultでデフォルトブランチに戻した上で、hg branch -f fix-bug-xxxxx-working-fオプションを付けて同名のブランチ作成を予約すれば、ブランチ作成の時点から作業をやり直す事ができます。

自動テストを書く

不具合の修正でも新機能の追加でも、対応する自動テストの追加は原則として必要です。変更対象のファイルの近傍に自動テストのファイル群が配置されている事が多いはずなので、それらを参考にして、関連していそうなテストファイルにテストケースを追加したり、あるいはテストのファイルそのものを新たに追加したりしましょう。

新しいテストファイルを追加する場合は、hg addでのリポジトリへの登録だけでは不十分で、同じディレクトリのiniファイルにもテストファイル名を追記する必要があります。

自動テストを実行する

自分で追加した自動テスト(テストケースを追加したファイルや、自分で追加したテストファイル)は、./mach test path/to/test/dir/or/fileとすると適切なフレームワークで実行する事ができます。テストが確実に通る事を確認しておきましょう。また、ディレクトリを指定すると複数のテストを一括実行できますので、関連する他の機能に意図しない影響を及ぼしていないか(他のテストが失敗するようになっていないか)も確かめておきましょう。

(なお、何度もパッチを投稿していると、tryserverという自動テスト専用のサーバーの使用権を貰えることがあります。tryserverを使うとWindows以外のプラットフォームでも自動テストを実行できますし、手元での実行ではないのでテストの実行中も他の作業を並行して行えるので、非常に有用です。tryserverの使用権を持っている場合は、./mach try -b o -p linux,macosx64,win64 -u all -t none --no-artifactというコマンド列を実行するだけで、現在作業中のブランチの内容を反映した状態でtryserver上で全てのテストを実行する事ができます。)

コーディングスタイルを揃える(2019年4月12日追記)

実装も自動テストも両方揃ったら、コーディングスタイルがMozillaのルールに則っているかも確認しておきましょう。せっかくパッチを提出しても、コーディングスタイルがMozillaのルールに則っていないと受け付けてもらえません。

コーディングスタイルのチェックは、文法チェックの一環として./mach lint (チェックしたいファイルのパス)で行えます。例えば以下の要領です。

./mach lint browser/components/extensions/test/browser/browser_ext_sessions_restoreTab.js

文法チェックでエラーが出なくなったら、パッチを提出できるクオリティに達したと判断して次のステップに進みます。

変更の完了とパッチ作成の準備

作業用ブランチ上で複数回コミットしていた場合、変更内容を1つにまとめたコミットを作成してパッチにします。これは以下の手順で行うことができます。

hg diff -r default > ../working.diff # 変更内容を差分ファイルに出力する
hg update -C default # デフォルトブランチに切り替える
hg pull -u # 念のためデフォルトブランチの状態を最新にする
hg branch -f fix-bug-xxxxx # ブランチを作り直す
patch -p 1 < ../working.diff # 差分ファイルに出力した内容を書き戻す
hg add path/to/added/files # ファイルを追加していた場合は、この時点で手動で追加し直す。
hg commit

この時のコミットメッセージは「Bug XXXXX - 変更の概要」のようにします。このコミットメッセージはパッチに含まれる事になります。

複数回のコミットがあった状態のままでパッチを作成すると、1コミットが1パッチとして分割される事になります。大規模な変更を行う場合はパッチも複数に分けて段階的に反映する場合がありますが、そのような大規模な変更を行う機会は一般の外部コントリビューターはまず行う機会がありませんので、基本的には上記の手順で1コミットにまとめるようにしましょう。

なお、自分が作業を始めた時点のデフォルトブランチの状態とhg pull -uで更新したデフォルトブランチの状態が異なっていた場合(自分が作業中に他の人の変更が反映された場合)、パッチの適用に失敗する事があります。このような場合、衝突箇所を修正して(自動テストも再実行して)、「最新のデフォルトブランチに対して行った変更」という体裁のパッチに手直しする必要があります。

パッチの提出

準備が整ったら、このブランチの変更内容をパッチとしてBugに添付します。

2019年4月12日現在、パッチはPhabricatorというレビューツールを使って提出するようになっています。Phabricatorによるパッチの提出手順は別の記事で詳しく説明していますので、そちらを併せてご覧下さい。以下は、何らかの理由でPhabricatorを使用できない場合に使う、伝統的な差分ファイル形式でのパッチの提出手順の解説となります。

hg bzexport -eというコマンドを実行すると、Emacsが起動してコメントの入力を求められます。対応するBugの番号が最初の行に「Bug XXXXX」と表示されていれば大丈夫なので、そのまま保存して終了して下さい(「No changes made; continue with current values (y/n)?」と訊かれるので、yと入力して下さい)。コミットメッセージにBugの番号を入れ忘れていた場合は、対応するBugの番号を「Bug XXXXX」のように記入した上で、変更を保存してEmacsを終了します。すると、パッチがBugに自動的に添付されます。
(なお、同じBugに複数回パッチを送信すると、前のパッチは自動的に「Obsolete」扱いになります。)

Mercurialの拡張機能がまだインストールされていないと、hg bzexport -eはエラーになります。その場合は、先に./mach mercurial-setupを実行して必要なプラグインをMercurialにインストールしておくか、またはhg export > ./bugXXXXX.patchで手動でパッチを作成して手動で当該Bugにファイルを添付します。この場合、古いパッチを適宜手動で「obsolete」にする必要があります。

Mozillaでは、全てのパッチはレビュアーによるレビューを経てからマージされる運用になっています。このパッチをFirefoxに取り込んでもらうためには、レビューを依頼しなくてはいけません。

パッチがBugに添付されたら、Bugのページをブラウザで開いて、添付されたパッチの「Details」のリンクをクリックします(hg bzexportを使わない場合は、パッチファイルの添付時のフォームでそのまま操作します)。「review」という欄があるので「?」を選択し、その隣の入力欄にレビュアーを指定してレビューを依頼します。基本的にはそのモジュールの担当者を設定するのですが、誰に依頼したらいいか分からない場合は、suggested reviewersという所をクリックすると、お薦めレビュアーが出てきます(モジュールから自動的に検索された結果が一覧表示されます)。基本的には、レビューのキューの数が少ない人を設定するのがお薦めです。ただし、レビューの件数が多くても猛烈なスピードで消化する人や、レビューの件数が少なくてもなかなかキューが減らない人もいるので、できれば類似のBugを見て活動がアクティブな人を設定する方が望ましいです。あるいは、変更対象のファイルの変更履歴を確認し、最近投入されたパッチのレビューを担当した人を設定するのも有効です。

その後、指定した人物によりレビューが行われます。レビューを無事通過できれば、パッチのメタ情報に「r+」と表示されるようになります。

パッチの内容に不具合や考慮不足、コーディングルールに反している部分などがあると、レビューが却下され、パッチのメタ情報のステータスが「r-」になります。その場合は、指摘された問題点を修正してパッチを再提出しましょう。

なお、このように伝統的な差分ファイル形式のパッチを使ったパッチ提出のやり方の他に、2019年4月12日現在ではPhabricatorという専用の仕組みを使ったレビュー運用も行われています。こちらについては別の記事で詳しく解説します。

チェックインの依頼

パッチのステータスが「r+」になったら、いよいよチェックインです。Bugの「whiteboard」欄にcheckin-neededと記入してBugを更新しておくと、担当者の人がそれを見付けて、だいたいその日の中に「inbound」というリポジトリにパッチをチェックインしてくれます。このinboundの自動ビルドと自動テストで何もエラーが検出されなければ、同じ内容が自動的にmozilla-centralなどのリポジトリにチェックインされます。

もしinboundのビルドや自動テストが失敗した場合は、その旨のコメントがBugに書かれますので、適切に対応してパッチを再提出する事になります。もし自分の行った変更が原因で他の自動テストが失敗したようなら、それらのテストに悪影響を与えた原因を修正したり、あるいは、もし他の部分の方に問題がある場合(例えば、やっつけで実装されていたテストが、自分の行った変更の影響で失敗するようになった場合など)はそちら側を修正したりします。

また、inboundへのチェックイン自体に失敗したという事でパッチが差し戻される事もあります。これは、自分が作業した時のデフォルトブランチの状態と、担当者がinboundにパッチをチェックインしようとした時の最新の状態とがずれていて、パッチが衝突してしまったという場合に起こります。その場合、複数コミットを1つのコミットにまとめる時と同じ要領で、最新のデフォルトブランチを対象にしてパッチを作り直しましょう。他の変更と衝突はしていたものの、パッチは全体的に以前の物から代わっていないと言える場合は、再レビュー依頼を省略できます。再提出したパッチに対して「r?」を設定する代わりに「r+」を自分で付けて、コメントには「r+ is carried-over from the previous patch」のように書いておきましょう。

Beta版のBugを直したい場合は

次期リリースのBeta版を使っていて見付けた問題でも、まずはmozilla-centralに対して修正を行います。以下は、変更が既にmozilla-centralに反映されたという前提での話になります。

まず、releases/mozilla-betaのリポジトリをローカルにcloneして、mozilla-centralにチェックインされたパッチをpatch -p 1 < path/to/patch.diffで反映します。もし衝突が発生してパッチを反映できなかった場合は、衝突箇所を修正してコミットし、新しいパッチを作り直して改めて提出します。内容的に変更がなければ、ここでもレビューを省略して「r+」を自分で付け、前のパッチからレビュー済みの状態を引き継いだ旨を書いておきます。

元のパッチがそのままBetaに反映できる状態、またはBeta向けにパッチを再提出し終えた状態で、そのパッチをreleases/mozilla-betaに取り込んでもらうよう依頼する事を、uplift申請と言います。Beta版またはリリース版に対しては原則としてこのuplift申請を経由してパッチが取り込まれるようになっています。

uplift申請をするには、当該パッチの詳細情報の画面で「approval-mozilla-beta」欄に「?」を設定します。すると、コメント入力欄にuplift申請のテンプレートが自動入力されますので、「ベータフェーズでクオリティを高める段階にある今、新たな問題を引き起こすリスクを押してでもこの変更を反映するべき理由」「この変更を反映しても問題は起こらないと言える理由」を説明するよう各欄を埋めて投稿します。実際の申請例(Firefox 60betaに対して、セーフモード無効化のポリシー設定を導入するパッチのuplift申請を行っている物)も参照して下さい。申請が受理されれば、パッチはチェックインされます。

充分な理由を示せていなかった場合、申請は却下される場合もあります。その場合、改めてその修正の重要性を説明し直したり、味方に付いてくれる人(Mozillaの中の人)にコメントを求めたりという形で、説得・交渉を行う事になります。

まとめ

以上、Windows上での作業を前提とした、Firefoxへのパッチ提出の流れを解説しました。

「Firefoxのような大規模プロジェクト、しかもGitHubではない独自のやり方でソースコードや問題を管理している所にコントリビュートする」というのは、GitHub上でフィードバックした事がある人でも、心理的に高いハードルがあるかもしれません。しかし、根本的な部分ではそう大きな違いはなく、むしろ、Bugzillaのような高機能のバグトラッキングシステムやinboundのような規定は、GitHub上でのプロジェクト運営では「運用でカバー」されているものをシステムとしてきちんと体系化した物と言う事ができるでしょう。皆さんも必要以上に恐れずフィードバックしてみて下さい。

2018-07-03

MozReviewでのFirefoxへのパッチ投稿方法

(2019年4月12日追記)
この記事で解説しているMozReviewは既に廃止されており、代わりにPhabricatorというツールを使うようになっています。この記事自体は歴史的文書として残していますが、パッチを新たに投稿したいという場合にはPhabricatorの解説を参照して下さい

はじめに

前回の記事ではFirefoxへのフィードバックの仕方を紹介しました。この記事で紹介したように、Firefoxへのパッチ投稿は、以前はBugzillaにパッチファイルを手動で添付する形で行われていました。その方法は今でも通用するのですが、最近ではMozReviewというReview Boardベースのコードレビューシステムも導入されています。GitHubと同様にソースコードにインラインでコメントを付けられますし、パッチをリモートリポジトリでバージョン管理できるので、現在ではこちらを使用する方がおすすめです。しかし最初に少し設定が必要ですので、使い始めるまでに心理的障壁があるのも事実かと思います。そこで今回はMozReviewの使用方法について紹介します。

セットアップ

本記事で紹介するセットアップ方法はMozReview User Guideを元にしています。詳細はそちらを参照して下さい。

用意するもの
  • BMO(bugzilla.mozilla.org)のアカウント
    • まだBMOアカウントを取得していない場合は https://bugzilla.mozilla.org/createaccount.cgi で作成します。
    • Real nameにはYour Name [:ircnick]のような形で末尾にニックネームを付けておきましょう。
      • Review Board上ではこのニックネームがユーザー名として表示されます。
      • ただし、既に存在するニックネームは使用できません(代わりにメールアドレスのアカウント名+任意の数字になります)。
  • mozilla-centralのワーキングコピー
    • 前回の記事等を参考に、hg clone https://hg.mozilla.org/mozilla-centralで取得して下さい。
    • 本記事ではバージョン管理システムとしてMercurialを使用する場合のみを対象とします
      • MozReviewはgitでも使用できるようですが、本記事では対象としていません。
Review Boardへのログイン確認

MozillaのReview Boardは https://reviewboard.mozilla.org でアクセスできます。Bugzillaアカウントの登録が終わったら、Review Boardにもログインできることを確認してみましょう。アカウントはBugzillaと連携されているため、Review Boardのログインページにアクセスすると、自動的にログインすることができます。

Review Boardにログインできたら、画面右上のユーザー名を確認して、先ほど設定したニックネームを使用出来ていることを確認すると良いでしょう。

MozReviewユーザー名

APIキーの生成

自分がソースコードに対して行った変更をMercurialでreviewboard.mozilla.orgにpushするためには、BugzillaのAPIキーが必要になります。この後のMercurialのセットアップの際に必要になりますので、事前に生成しておきましょう。APIキーの作成はBugzillaアカウント設定画面のAPI Keysページで行うことができます。

APIキーリスト

Mercurialの追加セットアップ

MercurialでMozReviewと連携できるようにするためには、Mercurialに追加の設定が必要になります。この設定は前回の記事でも紹介した./mach bootstrapの中で対話的に行うことができます。ただし、前回はMozReviewについての説明は省略したため、それに関するセットアップは飛ばしてしまっているかもしれませんね。Mercurialを最初から設定し直したい場合は、既存の~/.hgrcを退避させて、./mach mercurial-setupで実行することができます。

$ cd /path/to/mozilla-central
$ mv ~/.hgrc ~/.hgrc.bak
$ ./mach mercurial-setup

後はコンソールに表示される指示に従って必要な項目(自分のフルネーム、メールアドレス、ニックネーム等)をセットアップして行けば良いです。Yes/Noで答えるものについては、基本的には全てYesで良いでしょう。

以下の項目については、一度MozReviewを設定してしまえは他の方法でパッチを投稿することは無いでしょうから、1で良いでしょう。

Commits to Mozilla projects are typically sent to MozReview. This is the
preferred code review tool at Mozilla.

Some still practice a legacy code review workflow that uploads patches
to Bugzilla.

1. MozReview only (preferred)
2. Both MozReview and Bugzilla
3. Bugzilla only

Which code review tools will you be submitting code to?  

以下の項目では、先ほどBugzillaで生成したAPIキーをコピペして入力します。

Bugzilla API Keys can only be obtained through the Bugzilla web interface.

Please perform the following steps:

  1) Open https://bugzilla.mozilla.org/userprefs.cgi?tab=apikey
  2) Generate a new API Key
  3) Copy the generated key and paste it here

./mach mercurial-setupが正常に終了したら、~/.hgrcを確認して必要な項目が適切に設定されていることを確認します。

[ui]
username = Your Name <your.name@example.com>

[extensions]
reviewboard = /path/to/home/.mozbuild/version-control-tools/hgext/reviewboard/client.py

[paths]
review = https://reviewboard-hg.mozilla.org/autoreview

[mozilla]
ircnick = nick

[bugzilla]
username = your.name@example.com
apikey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

レビューリクエストの作成

前回の記事で紹介したように、Firefoxで何かパッチを投稿したい場合は、全てBugzilla上の該当Bugを起点に作業を行います。何か新機能を追加したり、不具合修正を行いたい場合は、まず該当するBugが既に存在するかどうかを確認し、無い場合は新たに自分で新しいBugをfileします。

該当Bugでソースコードに対して何か変更を行って、いざMozReviewでパッチを投稿したいとなった場合、まずはMercurialで手元のリポジトリに変更をコミットします。

$ hg commit

このとき、コミットメッセージの形式に注意しましょう。具体的には以下のような形式にする必要があります。

Bug [Bug番号] - [Bugの概要(一行)]

以下、Bugの詳細についての記述...

Mercurialでリモートリポジトリにpushする際、上記のコミットメッセージのBug番号から自動的にBugzillaの該当Bugにパッチが投稿されます。

また、末尾にr?ircnickという形式でレビュアーを指定すると、push後に自動的に該当レビュアーにレビューリクエストを投げることもできます。このレビュアーの指定はpushした後にReview BoardのWeb UIから行うこともできますので、必ずしもコミットメッセージに含める必要はありません。

以下に、筆者が実際にパッチを投稿した際のコミットメッセージを示します。

Bug 1306529 - OmxDataDecoder: Fix a stall issue on shutting down r?jya

Because the shutdown closure awaits finishing itself by
TaskQueue::AwaitShutdownAndIdle(), the function blocks infinitely.

The code is wrongly introduced at the following commit:

  * https://bugzilla.mozilla.org/show_bug.cgi?id=1319987
    * https://hg.mozilla.org/mozilla-central/rev/b2171e3e8b69

This patch calls it on mTaskQueue intead of mOmxTaskQueue to
avoid the issue.

詳細な議論はBug番号から辿ることができるため、コミットメッセージには必ずしも詳細な記述は必要ないようです。有った方が好ましいとは思いますが、慣れていない場合には、まずはBug番号と一行サマリを適切に記載することに注力すると良いでしょう。

ローカルリポジトリへのコミットが完了したら、リモートリポジトリにpushします。

$ hg push review

pushが完了すると、以下のようにReview Board上のURLが表示されますので、後の操作はWeb UI上で行うことができます。

...
submitting 1 changesets for review

changeset:  424886:1e4bfbffe0de
summary:    Bug 1451816 - [Wayland] Avoid closing popup window on button-press on Weston r?stransky
review:     https://reviewboard.mozilla.org/r/254690 (draft)

review id:  bz://1451816/takuro
review url: https://reviewboard.mozilla.org/r/254688 (draft)

この状態では、レビューリクエストはまだ公開されておらず、レビュアーにも通知は届いていません。
最後に以下のようにレビューリクエストを公開するかどうかを尋ねられますので、

publish these review requests now (Yn)?  

ここでyを入力すると、レビューリクエストが公開され、レビュアーに通知されます。
ここではいったんnとしておいて、Web UI上でレビュアーを指定した上でレビューリクエストを公開することも可能です。この場合は下記画面のReviewersでレビュアーを指定した後、Publishを押してレビューリクエストを公開します。

Review Board

これでレビューリクエストは完了ですが、Bugzillaの該当Bugの方も確認して、Review Board側のレビューリクエストと正しく紐付けられていることを確認しておくと良いでしょう。

なお、先ほどコミットした内容をhg exportで確認してみると、以下のようにMozReview-Commit-IDという行が追加されていることに気が付きます。

# HG changeset patch
# User Your Name <your.name@example.com>
# Date 1530688340 -32400
#      Wed Jul 04 16:12:20 2018 +0900
# Node ID 1e4bfbffe0de2870b7539a8ed42b9b6d6721fc87
# Parent  987ea0d6a000b95cf93928b25a74a7fb1dfe37b2
Bug 1451816 - [Wayland] Avoid closing popup window on button-press on Weston r?stransky

MozReview-Commit-ID: 2khX59CotQK

...

これは後にレビュー結果を受けてパッチを修正する際に必要になります。

パッチの修正

レビュアーによってパッチがレビューされ、Review Board上で修正箇所を指摘されたら、パッチを修正して再度Review Boardにpushすることになります。この際、同一のレビューリクエストに対する修正であることを指定するために、先ほどと同じMozReview-Commit-IDをコミットメッセージに含めてhg commitし、hg push reviewします。

Mercurialでのパッチ管理方法は本記事のスコープ外のため割愛しますが、パッチ(コミット)が1つのみで、ローカルリポジトリに過去のバージョンが不要である場合、もっとも簡単な修正方法はhg commit --amendで前回のコミットをやり直す方法でしょう。この方法の場合、コミットメッセージは特に修正しなければ前回のままとなりますので、MozReview-Commit-IDも前回と同じものが使用されます。ローカルリポジトリの修正は上書きされてしまいますが、リモートリポジトリ上では過去のバージョンも管理され、その差分を確認することもできます。

Review Board Diff

修正をpushしたら、Review Board上でレビュアーのコメントに返信をします。この際も、最後にPublishボタンを押すことを忘れないで下さい。なお、Review Board上の会話は自動的にBugzillaの該当Bugの方にも同じ内容が投稿されます。

レビューが通ったら

レビュアーによってパッチが問題ないと判断された場合、r?あるいはr-となっていた部分がr+に変更されます。この状態になったら、前回の記事と同様に、Bugzilla上で「Keywords」欄にcheckin-neededのキーワードを付加して、パッチをチェックインしてもらいます。

まとめ

MozReviewでのFirefoxへのパッチ投稿方法について紹介しました。

なお、記事中で例として紹介したBug 1451816はGNU/LinuxのWestonという環境でポップアップウィンドウのボタンを操作できないという問題です。本質的な修正は他のBugで行う必要があり、投稿しているパッチは必ずしも本体に入れるつもりの無いad-hocなパッチです。このようなパッチでも、アップストリームに報告しておけば同じ問題で困っている人の助けになるかもしれませんし、本質的な修正のヒントになる可能性もあります。自分のパッチに価値があるかないかは出してみないと分かりませんので、皆さんも臆することなくどんどんパッチを提出してみてはいかがでしょうか。

2018-07-05

IBus変換エンジンでXfce4でもシンボルをステータスに表示できるようにするには

はじめに

IBus変換エンジンの実装によっては、GNOME ShellとXfce4というようにデスクトップ環境によって違いがでることがあります。 今回は IBusProperty のシンボルを設定していても、Xfce4では表示されない場合の対処方法を紹介します。

問題の詳細

以前、IBusエンジンでのメニューはどのように構成されているのかについて、IBus変換エンジンでネストしたメニューを正しく表示できるようにするにはという記事で簡単に解説しました。 IBusProperty はメニューの個々の項目を作成するときに使うものです。

参考までに、前回紹介したコードの一部を抜粋します。

IBusProperty *menu = ibus_property_new("InputMode",
                                       PROP_TYPE_MENU,
                                       label,
                                       NULL,
                                       NULL,
                                       TRUE,
                                       TRUE,
                                       PROP_STATE_UNCHECKED,
                                       submenulist);

IBusPropertyibus_property_new を使って作成します。 では、メニューを選択したときにステータスの表示を変更するにはどうすればよいでしょうか。

その場合には、 ibus_property_set_symbol を使ってプロパティのシンボルを設定します。 シンボルは IBusText に設定できるものであればよいので、「あ」とか「A」とか好きなものを設定できます。

しかし、実際にGNOME ShellとXfce4それぞれで試すとどうなるでしょうか。

  • GNOME Shellでサンプルプログラムを実行した場合

GNOME Shellの場合

GNOME Shellの場合には、スクリーンショットのようにシンボルを変更するとステータスの表示もかわります。

  • Xfceでサンプルプログラムを実行した場合

Xfceの場合

Xfceの場合、シンボルを変更してもデフォルトの歯車アイコンのままステータスの表示はかわりません。

シンボルをステータスに表示できるようにするには

解決策は、IBus変換エンジンのコンポーネントの設定ファイルに <icon_prop_key>InputMode</icon_prop_key> を追記します。

diff --git a/change-inputmode-symbol/changeinputmodesymbol.xml b/change-inputmode-symbol/changeinputmodesymbol.xml                         
index a476008..b083fa1 100644
--- a/change-inputmode-symbol/changeinputmodesymbol.xml
+++ b/change-inputmode-symbol/changeinputmodesymbol.xml
@@ -17,6 +17,7 @@
       <author>Kentaro Hayashi</author>
       <icon></icon>
       <layout>default</layout>
+      <icon_prop_key>InputMode</icon_prop_key>
       <longname>ChangeInputModeSymbol</longname>
       <description>Change Input Mode Symbol (Japanese Input Method)</description>                                                         
       <rank>50</rank>

icon_prop_keyの効果

これでXfce4でも期待通りにステータスにシンボルが表示されるようになります。

まとめ

今回はデスクトップ環境によらずシンボルをステータスに表示できるようにする方法を紹介しました。

2018-07-06

WebExtensionsによるFirefox用の拡張機能で設定画面の提供を容易にするライブラリ:Options.js

(この記事は、Firefoxの従来型アドオン(XULアドオン)の開発経験がある人向けに、WebExtensionsでの拡張機能開発でのノウハウを紹介する物です。)

XULアドオンでは、設定UIを提供するのに便利な仕組みが用意されていました。文字入力欄やチェックボックス、ラジオボタンといった入力フィールドに対して、真偽型・整数型・文字列型それぞれの設定を関連付ける事により、設定UIの状態と設定値とが自動的に同期されるため、難しい事を考えなくても良いのが利点でした。標準的なUI要素でできる事の幅は限られていましたが、その制約が同時にガイドラインとなり、ユーザー視点においても、混乱することなく各アドオンの設定を編集できていたと言えるでしょう。

一方、WebExtensionsでは設定UIを提供する標準的な仕組みは用意されておらず、HTMLとJavaScriptの組み合わせで自力でUIを提供しなくてはなりません。そもそも設定値をどのように保存するのかすらも標準的なやり方が定まっておらず、採用した保存先によって設定の変更の監視方法も異なるため、設定値とUIの状態を同期するのも悩み所が多いです。そのためアドオンによって設定画面の作りはまちまちです。XULアドオンのように、何らかのガイドラインに従ってページ(HTML)を記述すれば設定値と状態が適切に同期されてくれる、というような仕組みが欲しくなる人も多いのではないでしょうか。

そこで、Configs.jsを使って表現された設定と併用する事を前提に、一定のガイドラインに則って記述されたHTMLのページと設定値の状態を適切に同期して、「設定UI」としてのページを提供する事を支援する軽量ライブラリとして、Options.jsという物を開発しました。 このライブラリを使うと、以下のような設定画面を比較的簡単に作成できます。

IE View WEでの使用例 Multiple Tab Handlerでの使用例

基本的な使い方

必要な権限

このライブラリを使う事自体には、特別な権限は必要ありません。ただし、設定の読み書きにConfigs.jsを使うため、間接的にstorageの権限が必要となります。

{
  ...
  "permissions": [
    "storage", 
    ...
  ],
  ...
}
読み込みと初期化

このライブラリを使うためには、設定画面を提供するページからファイルを読み込みます。依存ライブラリであるConfigs.jsと併せて読み込む場合は以下のようになります。

<script type="application/javascript" src="path/to/Configs.js"></script>
<script type="application/javascript" src="path/to/Options.js"></script>
<script type="application/javascript" src="init.js"></script>

Options.jsを読み込むと、その名前空間でOptionsというクラスが使えるようになります。初期化処理は、単にOptionsの引数にConfigs.jsのインスタンスを渡してnew Options()とするだけです。例えば以下の要領です。

// init.jsの内容
var configs = new Configs({
  enabled:  true,                  // チェックボックスにする
  urls:     'http://example.com/', // 複数行の文字入力にする
  position: 2,                     // 0, 1, 2のいずれか。ドロップダウンリストで選択する
  theme:    'default'              // 'default', 'light', 'dark'のいずれかをラジオボタンで選択する
});

var options = new Options(configs);

これだけで、設定の読み込みが完了し次第、そのページに書かれたフォーム要素と設定値とが同期するようになります。フォーム要素の状態(チェック状態、入力内容など)を変更すると、その結果は即座にconfigsで定義された設定に反映されます。

設定画面で使えるフォーム要素

Options.jsは、以下の種類のフォーム要素に対応しています。

  • チェックボックス(<input type="checkbox">
  • ラジオボタン(<input type="radio">
  • 文字入力(<input type="text">
  • パスワード入力<input type="password">
  • 数値入力(<input type="number">
  • 非表示(<input type="hidden">
  • ドロップダウンリストによる選択(<select><option>
  • 複数行の文字入力(<textarea>

Options.js自体は基本的にフォーム要素を自動生成する事はなく、利用者が好みの方法で記述したフォーム要素に対して使えるようになっています。基本的には、「configsConfigs.jsのインスタンス)のプロパティ名」と「それと同じidを持つフォーム要素」(ラジオボタンだけは「configsのプロパティ名と同じnameを持つ項目群」)が自動認識され、設定値とフォーム要素のvalueプロパティの値(チェックボックスはcheckedの値、ラジオボタンはvalueが設定値と一致する項目のチェック状態)が同期されます。前述の例のinit.jsで定義している設定に対応するフォーム要素の例を以下に示します。

真偽値の設定に対応するチェックボックスは、以下の要領です。

<p><label><input id="enabled" type="checkbox">機能を有効にする</label></p>

文字列型の設定に対応する入力欄は、以下の要領です。

<p><label>URL:<input type="text" id="urls"></label></p>

<!-- 複数行の入力欄にする場合:
<p><label>URL:<textarea id="urls"></textarea></label></p>
-->

値があらかじめ決められたいくつかの値の中のいずれかになるという選択式の設定に対しては、以下のようにして選択肢を提供できます。

<p><label>表示位置:
     <select id="position">
       <option value="0"></option>
       <option value="1">中央</option>
       <option value="2"></option>
     </select>
   </label></p>
<p><label>テーマ:
     <input name="theme" type="radio" value="default">既定</option>
     <input name="theme" type="radio" value="light">明るい</option>
     <input name="theme" type="radio" value="dark">暗い</option>
   </label></p>

設定の型は既定値の型と一致するように自動的に変換されるため、上記のドロップダウンリストのような例で値が"0"のような文字列型になってしまうという事はありません。

なお、Managed Storageで管理者が設定を指定している場合、対応する設定項目は読み取り専用の状態になります。

about:configに相当する機能を付ける

XULアドオンでは、いわゆる隠し設定を定義しておき、about:configで値を変更して細かい挙動を制御するという事ができました。しかしWebExtensionsベースのアドオンでは統一的な設定の保存先が無く、またstorage APIで保存された情報には基本的にはアドオンの中からしかアクセスできません。about:debuggingからデバッガを起動すればアドオンの名前空間で任意のスクリプトを実行して設定を変えられますが、設定変更のためだけにデバッガを起動するのは億劫なものです。

このような問題への回避策として、Options.jsは初期化時に渡されたconfigsに定義されている全ての設定を一覧し変更できるようにする機能が含まれています。UIは以下のような要領です。

自動生成された設定一覧

全設定の一覧は、Optionsのインスタンスが持つbuildUIForAllConfigs()メソッドを実行するとページの末尾に挿入されます。

options.buildUIForAllConfigs();

また、メソッドの引数に任意のコンテナ要素を渡すと、その最後の子として全設定の一覧が挿入されます。

options.buildUIForAllConfigs(document.querySelector('#debug-configs'));

全設定の一覧の表示・非表示を切り替えるという機能は特にありません。ユーザーには見えないようにしておきたい場合、configs.debugのような設定を定義しておき、その変更を監視して以下の要領でコンテナ要素の表示・非表示を切り替えるといった形を取ると良いでしょう。

configs.$addObserver(aKey => {
  if (aKey == 'debug') {
    const container = document.querySelector('#debug-configs');
    container.style.display = configs[aKey] ? 'block' : 'none';
  }
});

まとめ

以上、Firefox用のWebExtensionsベースのアドオンにおける設定画面の提供を容易にするライブラリであるOptions.jsの使い方を解説しました。

XULアドオンでの感覚に近い開発を支援する軽量ライブラリは他にもいくつかあります。以下の解説も併せてご覧下さい。

タグ: Mozilla
2018-07-09

WebExtensionsによるFirefox用の拡張機能で組み込みのページのローカライズを容易にするライブラリ:l10n.js

(この記事は、Firefoxの従来型アドオン(XULアドオン)の開発経験がある人向けに、WebExtensionsでの拡張機能開発でのノウハウを紹介する物です。)

XULアドオンでは、表示文字列のローカライズには「DTDファイルで<!ENTITY menu.open.label "開く">といった形式でロケールを定義し、XULファイルの中に<label value="&menu.open.label;">のように書いておくと自動的に適切なロケールの内容に置き換わる」「propertiesファイルでmenuLabelOpen=開くといった形式でロケールを定義し、JavaScriptからstringbundle.getString('menuLabelOpen')といった形で参照する(String Bundle)」という2つの方法がありました。

WebExtensionsの国際化対応の仕組みはそれよりももっと単純です。ロケールはJSON形式のみで定義し、browser.i18n.getMessage()でキーを指定すると適切な言語のロケールの内容が文字列として取得できるという物で、XULアドオンでのString Bundleに近い形式です。

この方法はテンプレートエンジンやVirtual DOMなどでUIを構築する場合は特に支障にならないのですが、静的なHTMLファイルで設定画面のページなどを作成する場合には、「参照されるべきロケールのキーを書いておくだけで表示時に適切なロケールの内容が反映される」というXULファイルで使っていた方法のような仕組みが欲しくなる所です。実際、manifest.jsonの中では__MSG_menuLabelOpen__のように書くだけでFirefoxが表示時に自動的に適切なロケールの内容を反映してくれるので、これと同じ事がHTMLファイルではできないというのはもどかしいです。

そこで、静的なHTMLファイルの中にロケールを埋め込む使い方を可能する軽量ライブラリとして、l10n.jsという物を開発しました*1

基本的な使い方

読み込みと初期化

このライブラリを使う事自体には、特別な権限は必要ありません。最も単純な使い方では、国際化するページからファイルを読み込むだけで機能します。

<script type="application/javascript" src="path/to/l10n.js"></script>

このライブラリを使う時は、HTMLのページ中のテキストや属性値に__MSG_(ロケールのキー)__と書いておきます。例えば以下の要領です。

<p title="__MSG_config_enabled_tooltip__">
  <label><input type="checkbox">
         __MSG_config_enabled_label__</label></p>
<p title="__MSG_config_advanced_tooltip__">
  <label><input type="checkbox">
         __MSG_config_advanced__</label></p>
<p title="__MSG_config_attributes_tooltip__">
  <label>__MSG_config_attributes_label_before__
         <input type="text">
         __MSG_config_attributes_label_after__</label></p>

これだけで、ページの読み込み時に自動的に各部分が対応するロケールの内容で置き換わります。

英語での表示 日本語での表示

任意のタイミングでの反映

このライブラリは、動的に挿入されたDOM要素の内容テキストや属性値に対しては作用しません。ページの読み込み完了後に追加された内容に対してもロケールの反映を行いたい場合は、それらがDOMツリー内に組み込まれた後のタイミングでl10n.updateDocument()を実行して下さい。例えば以下の要領です。

var fragment = range.createContextualFragment(`
  <p>__MSG_errorDescription__
     <label><input type="checkbox">__MSG_errorCheckLabel__</label></p>
`);
document.body.appendChild(fragment);
l10n.updateDocument();
明示的に空文字列を使う場合の注意点

「See(リンク)」と「(リンク)を参照して下さい」のように、言語によって要素の前や後に何もテキストを設けない方が自然になる場合があります。このようなケースでは、あらかじめ要素の前後にテキストを埋め込めるようにしておき、言語によってその内容を変えるというやり方が使われる事があります。

<p>__MSG_before_link__
   <a href="...">__MSG_link_text__</a>
   __MSG_after_link__</p>
{ // 英語
  { "before_link": { "message": "For more details, see " } },
  { "link_text":   { "message": "the API document." } },
  { "after_link":  { "message": "" } },
}

{ // 日本語
  { "before_link": { "message": "" } },
  { "link_text":   { "message": "APIドキュメント" } },
  { "after_link":  { "message": "に詳しい情報があります。" } }
}

XULではこのような場合空文字列は空文字列として埋め込まれていましたが、l10n.jsでは対応するロケールが空だった場合は__MSG_after_link__という参照のための文字列がそのままUI上に残ります。これは、browser.i18nにおいて「参照したロケールが未定義だった場合」と「参照したロケールの値が明示的に空文字に設定されていた場合」を区別できないことから、ミスの検出を容易にするために敢えてそのような仕様としているためです。

このようなケースでは、明示的に空にしたい部分には\u200b(ゼロ幅スペース)と書いて下さい。上記の例であれば、訂正後は以下のようになります。

{ // 英語
  { "before_link": { "message": "For more details, see " } },
  { "link_text":   { "message": "the API document." } },
  { "after_link":  { "message": "\u200b" } },
}

{ // 日本語
  { "before_link": { "message": "\u200b" } },
  { "link_text":   { "message": "APIドキュメント" } },
  { "after_link":  { "message": "に詳しい情報があります。" } }
}

まとめ

以上、Firefox用のWebExtensionsベースのアドオンにおける静的なHTMLファイルのローカライズを容易にするライブラリであるl10n.jsの使い方を解説しました。

XULアドオンでの感覚に近い開発を支援する軽量ライブラリは他にもいくつかあります。以下の解説も併せてご覧下さい。

*1 「l10n」は「localization(ローカライズ、地域化)」の略としてよく使われる、先頭と末尾の文字、およびその間の文字数を組み合わせた表現です。ちなみに、同様に「i18n」は「internationalization(国際化)」の略です。

タグ: Mozilla
2018-07-10

2018年から2,3年かけて立ち上げたい新規事業:データ処理ツールの開発

クリアコードの新規事業の立ち上げに取り組むことが多い須藤です。

どういう事業を立ち上げたいかは社内だけで共有することもできますが、クリアコードのポリシーは「デフォルトで公開して共有」なので、ここにまとめています。私が取り組むときは1人で少しずつ進めていたのですが、今回はだれかと一緒に取り組めるといいなぁと思っています。興味があるクリアコードのメンバーは今月中に一度取り組み方を相談しましょう。興味があるクリアコードメンバー以外の人には選択肢が2つあります。1つはクリアコードに入社して一緒に取り組む方法です。もう1つはビジネスパートーナーとして一緒に取り組む方法です。

クリアコードの新規事業立ち上げのパターン

クリアコードは今月から第13期目に入っています。創業時からフリーソフトウェアの推進と稼ぐことの両立を大事にしていますが、それの実現方法はずっと同じだったわけではありません。創業時から変わらずやっていることもあれば新しく始めたこともあります。新しく始めるときはあまり意識せずに取り組んでいたのですが、ふりかえってみるといつものパターンがあることに気づきました。このパターンを知っておくことは今回の取り組みでも役に立つはずなので、まずはそれをまとめます。

新しく立ち上がった事業で大きなものは以下の事業です。

  • milter関連
  • Groonga関連
  • Fluentd関連
  • 組み込みGecko関連

どれも最初に主要なソフトウェアの開発に参加し、徐々に事業の幅を広げていき、結果的に大きな事業になりました。

milter関連はmilter managerを新規に開発しましたし、Groonga・Fluentd・組み込みGecko関連はGroonga・Fluentd・Firefox本体の開発に参加しています。このうち、組み込みGecko関連以外は開発自体でお金を稼げていました。

徐々に事業の幅を広げていくやり方は次の通りです。

  • お金をもらって開発する範囲を広げる
  • 複数のお客さんから仕事がくるようにする

最初のやり方の実現方法は、初期の開発にお金を払っていたお客さん(だいたいは「主要なソフトウェア」の開発元)がもっとクリアコードに開発をして欲しいと思えるようにする、です。具体的には、継続的に成果を出し続けたり、要所要所で魅力的な今後の開発の提案をしたりします。

2つ目のやり方は、初期の開発にお金を払っていたお客さん以外からも仕事がくるようにする、ということです。複数のお客さんと仕事ができると、金額が増えますし内容の幅も広がります。

2つ目のやり方の実現方法は、クリアコードが開発した成果・持っている知見を公開し広く共有する、です。クリアコードの営業力では一緒に仕事できそうなお客さんを見つけることはできないので、お客さんにクリアコードを見つけてもらえるようにするための方法です。有用な情報を提供しているとお客さんが見つけてくれることがあります。

ただ、どんな情報が有用かはわからないので、情報を選別せずにできるだけ多く提供します。あるお客さんには「本体の開発に参加している」という情報が有用かもしれませんし、他のお客さんには「○○する方法」という情報が有用かもしれません。そもそもクリアコードののポリシーは「デフォルトで公開して共有」なので、選別せずにできるだけ多く共有する方が私たちにあっています。より多くの情報を共有した方がフリーソフトウェアの推進につながりますし、お客さんに見つけてもらうことにもつなが(ることがあ)ります。

クリアコードはできるだけ開発した成果・持っている知見を公開できるような契約で受注します。たとえば、開発した成果はユーザが自由に使えるライセンス(開発対象のソフトウェア周辺でよく使われているライセンスを選択することが多い)で公開する、という契約です。この方針にマッチしないお客さんもいますが、クリアコードに問い合わせてくれるお客さんの場合はだいたいマッチします。たとえば、Groonga・Fluentd本体を開発した場合、開発成果を隠すよりも、より多くのユーザーが自由に使えるようにした方がGroonga・Fluentdの普及につながるので、お客さんのメリットとマッチします。

開発した成果は対象ソフトウェアの通常のリリースに組み込んで公開し広く共有します。

持っている知見は、公式ドキュメントに反映したり、クリアコードのブログ(ここのこと)にまとめたり、イベントで発表して広く共有します。

これまでの経験では、このような取り組みを継続的に続けて2,3年くらい経つと徐々にお客さんに見つけてもらえるようになります。2,3年ずっとこの取り組みをフルタイムでやっていると売上が足りないので、他の仕事をしながら少しずつ継続的に続けます。一時的に集中して取り組むのではなく、細く長く取り組みます。

いままでの私たちの新規事業の立ち上げのパターンはこんな感じです。

これから立ち上げたい新規事業

それではここ数年をかけて立ち上げたい新規事業について説明します。

データを処理するツール群を開発する事業を立ち上げたいです。理由は私がやりたいからです。(どーん)

この4,5年、データ関連のことに少しずつ関わってきてデータを処理するツール群を開発したいなぁと思うようになりました。どんな感じで関わったかというと、自然言語データを扱うGroongaを開発していたり、ログデータを扱うFluentdを使ったりプラグインを書いたり、Red Data ToolsというRubyでデータを処理するツール群を開発するプロジェクトをはじめたりしていました。データを分析したいのかも?と思っていた時期もありましたが、データを分析するよりもデータを分析するツールを作る方がやりたいと気づきました。

データを分析するビジネスや分析してビジネスに活かしている組織はたくさんあるので、そのような組織を支援することで、データを処理するツールの開発を仕事にできないかなぁと考えています。(新規事業を立ち上げるときに考えている「こんな仕事になるといいかも!?」はだいたい外れてきたので、これも外れる可能性の方が高そう。)

Red Data Toolsでは「Ruby用」にフォーカスしていますが、Rubyに限らずいろんな言語で使えるツールに取り組めるといいなぁと思っています。たとえば、ここ数年私が開発に参加しているApache Arrowもいろんな言語で使えることを大事にしています。

私が考えている進め方は次の通りです。必ずしもこの進め方でやりたいというわけではなくて、私が持っているただの1案です。一緒に取り組む人と随時相談しながら進め方を考えていけるといいなぁと思っています。

まずはApache Arrowの開発に参加します。理由は、Apache Arrowはデータ処理の文脈で重要なソフトウェアになる可能性が高いからです。これは、Rubyにとってもそうですし、PythonやJavaなどRuby以外の言語にとってもそうです。

参加の仕方はいろいろありますが、プロジェクトメンテナンスまわりに取り組むのはどうかなぁと思っています。今、Apache Arrowはプロジェクトメンテナンス面でリソース不足なので、そのあたりで価値を発揮できるとApache Arrowの開発が捗りそうだからです。ただ、いきなりプロジェクトのメンテナンスはできないので、私に聞きながらドキュメントの不足部分を追加したり他の人のpull requestをレビューしたりしつつ中身に詳しくなっていくのはどうかなぁと思っています。中身に詳しくなっていけば徐々にプロジェクトメンテナスまわりもできるようになってくるはず。

開発に参加するときの時間の使い方は、一気に集中的に取り組むよりも少しずつ継続的に取り組む方が向いています。Apache Arrowの開発はタイムゾーンがバラバラの世界中の人達が関わっているので、他の開発者の人たちとのやりとりが進むのは半日後や1日後になるからです。

Apache Arrowの開発に参加することと並行して、私に聞きながらApache Arrow関連の情報を発信していきます。イベントで発表したり、クリアコードのブログ(ここのこと)やApache Arrowのブログに記事を書きます。英語で情報がまとまっていないものはApache Arrowの公式の情報として公式サイトに載せます。英語ではまとまっているものは、日本語で情報をまとめます。日本語でまとめるときは英語の情報のサマリーや英語の情報を理解するための前提知識の説明をするにとどめて、詳細は英語の情報を参照してもらうようにするのがいいんじゃないかと思っています。理由は、日本からも開発に参加する人が増えるといいなぁと思うからです。(日本だけでなく世界中から開発に参加する人が増えるといいと思っています。)日本から開発に参加するときの最初の一歩のために日本語情報があるのは価値があると思います。ただ、日本語だけで閉じて世界中の開発者とやりとりをしなくなるのはアレなので、英語の情報も参照するパスがあった方がいいんじゃないかと思っています。

Apache Arrow関連のことに慣れてきたらApache Arrow周辺のことも同様に進めていくのがよいのではないかと思っています。そうやって、開発に参加しつつ得られた知見を共有していくことで、徐々に「クリアコードと協力してデータ処理ツールの開発を進めるといいかも!?」と思ってくれるお客さんが増えてくれるといいな!

まとめ

クリアコードの新規事業としてデータ処理ツールを開発する事業を立ち上げたいという話をまとめました。また、前提知識としてこれまでの新規事業の立ち上げパターンもまとめました。

興味があるクリアコードのメンバーは今月中に一度取り組み方を相談しましょう。クリアコードメンバー以外で興味がある人は、クリアコードに入社して一緒に取り組むビジネスパートーナーとして一緒に取り組むかしましょう。

タグ: 会社
2018-07-11

mozregressionを使って、いつFirefoxの機能が壊れたのかを調べる

見つけた不具合をFirefoxにフィードバックする時には、それが後退バグである場合、いつの時点から不具合が発生するようになったのかという情報を書き添えておく事が大事です。この記事では、Firefoxの後退バグの発生時期を割り出す事を支援するツールであるmozregressionの使い方を解説します。

後退バグとは?

後退バグ(regression)とは、今まで正常に動いていた機能が、別の箇所の変更の影響を受けて、意図せず壊れてしまった、というケースを言い表す言葉です。

規模の大きなソフトウェアや複雑なソフトウェアでは、気をつけていても後退バグがどうしても発生してしまいがちです。後退バグが発生した直後にすぐに気付ければいいのですが、普段あまり使わない機能だと、いつからかは分からないが気がついたらその機能がずっと壊れたままだった、という事も起こりえます。

このような場面でよく使われるのが、二分探索という手法です。履歴上の「確実に正常に動作していた時点」と「現在」との中間にあたる時点のコードを調べて、その時点でまだ後退バグが発生していなければそこから「現在」との間の中間を調べ直し、その時点でもう機能が壊れていればそこから「確実に正常に動作していた時点」との中間にあたる時点のコードを調べ直す……という要領で範囲を絞り込んでいく事で、当てずっぽうで調べたり虱潰しに調べたりするよりも遙かに効率よく後退バグの発生時点を割り出す事ができます。

Gitでバージョン管理されているプログラムであればgit bisectというコマンドを使ってそれを行えますし、Mercurialにも同様にhg bisectというコマンドがあります。ただ、Firefoxのように大規模なソフトウェアでは、二分探索でその都度ビルドするというのは現実的ではありませんし、「特定の時点のNightlyのビルド済みバイナリをダウンロードしてきて展開して起動して……」という事を繰り返すのも大変です。そこで登場するのがmozregressionなのです。

mozregression-guiの使い方

Quick Startのページに英語音声の動画での解説がありますが、ここではアドオンのサイドバーパネル上のツールチップの表示がおかしくなる不具合を例に、後退バグの発生時点を割り出してみる事にします*1

mozregressionの配布ページから辿ってGUI版のWindows用ビルドをダウンロードすると、「mozregression-gui.exe」というファイルを入手できます。これはインストーラで、ダブルクリックして実行すると自動的にC:\Program Files (x86)\mozregression-guiへインストールされます。起動用のショートカットは自動的には作成されないため、インストール先フォルダを開いて「mozregression-gui.exe」のショートカットをデスクトップなどに作成しておくと良いでしょう。

二分探索の開始

mozregression-guiを起動すると、3ペインのウィンドウが開かれます。

mozregression-guiのメイン画面

メニューバーの「File」をクリックし、「Run a new bisection(新しく二分探索を始める)」を選択すると、「Bisection wizard」というタイトルのウィザードが開かれて、どのような内容で二分探索を始めるかの設定が始まります。

基本設定の画面

今回は以下のように設定しました。

  • Application(アプリケーションの種類): firefox(この他に「fennec(Android版Firefox)」「Thunderbird」も選択できます)
  • Bits(バイナリの種別): 64(Windows版のバイナリは現在は64bit版が主流なので。32bit版特有の不具合であれば「32」を選択します)
  • Build Type(ビルドの種別):opt(この他に「debug」(詳細なデバッグログを出力できるビルド)と「pgo」(最適化ビルド)も選択できます)
  • Repository(リポジトリ):mozilla-central(この他に「mozilla-beta」などのブランチや「comm-release」などのThunderbird用リポジトリも選択できます)

「Next」ボタンをクリックすると、テスト時のFirefoxの状態を設定する画面になります。

プロファイル設定の画面

特定のプロファイルで起動したときだけ後退バグが再現するという場合、再現に必要なプロファイルのパスを「Profile」欄に入力します。「Profile persistence(プロファイルの永続性)」は初期状態では「clone」になっており、テスト実行のたびに元のプロファイルを複製した物を使い捨てする事になります。「reuse」を選択すると指定したプロファイルをそのまま使う事になります。「clone-first」は両者の中間で、最初のテスト実行時に元のプロファイルを複製した後、以後のテストではそれを再使用します。ただ、新規のプロファイルでも現象を再現できる場合は、「Profile」欄は空にしておくことをお勧めします。

「Custom preferences」には、テスト実行時にあらかじめ設定しておく設定値を記述します。Bug 1474784の当初の報告内容は「extensions.webextensions.remotefalseの時に再現する」という物ですので、そのように設定する事にします。「Add preference」をクリックすると行が追加されますので、「name」欄にextensions.webextensions.remote、「value」欄には"false"と引用符でくくらずにそのままfalseと入力しておきます。

「Custom addons」には、テスト実行時にあらかじめインストールしておくアドオンを登録します。Bug 1474784の当初の報告内容はツリー型タブを使用したときという説明になっているため、アドオンの配布ページの「Firefoxへ追加」ボタンを右クリックしてリンク先のファイル(アドオンのインストールパッケージ)をダウンロードし、mozregression-guiのウィザードの「Add addon」ボタンをクリックしてファイルを選択しておきます。

さらに「Next」ボタンをクリックすると、二分探索を行う範囲を指定する画面になります。

二分探索の範囲の設定

初期状態では、起動した日とその1年前の日の範囲で二分探索を行うように入力されています。「Last known good build」欄には最後に正常に動いていたと確認できているビルドの日付を、「First known bad build」欄には最初に異常に気づいたビルドの日付を入力します。この探索範囲は、狭ければそれだけ効率よく絞り込みを行えます。Bug 1474784は7月の1週目までは起こっていない問題だったので、ここでは2018年7月6日から2018年7月11日までを範囲として入力しました。

探索の範囲を入力したら、準備は完了です。「Finish」ボタンをクリックするとウィザードが終了し、二分探索が始まります。

二分探索が始まると、検証用として先ほど指定した範囲の日付の中からいずれかの日のNightlyビルドがダウンロードされ、新規プロファイルで起動されます。この時には、ウィザードで設定した設定やアドオンが反映された状態になっていますので、後退バグが発生しているかどうかを実際に確かめてみることができます。

二分探索が始まり、後退バグの発生を確認した状態

この例では、このビルドでは後退バグが発生している事を確認できています(スクリーンショット内のNightlyのウィンドウにおいて、サイドバー部分のツールチップが適切にスタイル付けされていない事が見て取れます)。検証用のFirefoxを終了した後、mozregression-guiのメインウィンドウ左上の領域にある「Testing (ブランチ名) build: (日付)」の項目の「good」「bad」の二つのボタンのうち「bad」の方をクリックしましょう。すると再び別のビルドのダウンロードが始まり、ダウンロードが完了するとまたそのビルドが新規プロファイルで起動します。

二分探索中に、後退バグが発生していないビルドに遭遇した状態

最初のビルドに続き2番目のビルドも後退バグが発生していましたが、3番目のビルドでは後退バグは発生していませんでした(スクリーンショット内のNightlyのウィンドウにおいて、サイドバー部分のツールチップが適切にスタイル付けされている事が見て取れます)。このような場合は、mozregression-guiのメインウィンドウ左上の項目の「good」「bad」の二つのボタンのうち「good」の方をクリックしましょう。

このようにして「good」と「bad」を振り分けていくと、やがて、次のビルドが起動されない状態になります。

二分探索が終了した状態

この状態になると、二分探索は終了ということになります。mozregression-guiのメインウィンドウの左上の領域に表示される項目のうち最も下にある緑色の行が「最後の正常ビルド(last good build)」、最も下にある赤色の行が「最初の異常ビルド(first bad build)」を表しており、行をクリックすると右上の領域にそのビルドの詳細が表示されます。この例では、「最後の正常ビルド」は以下の通りでした。

app_name: firefox
build_date: 2018-07-09 14:02:55.353000
build_file: C:\Users\clearcode\.mozilla\mozregression\persist\140937d55bd0--mozilla-inbound--target.zip
build_type: inbound
build_url: https://queue.taskcluster.net/v1/task/eyRSVJsJT4WGMysouGUC_w/runs/0/artifacts/public%2Fbuild%2Ftarget.zip
changeset: 140937d55bd0babaaaebabd11e171d2682a8ae01
pushlog_url: https://hg.mozilla.org/integration/mozilla-inbound/pushloghtml?fromchange=140937d55bd0babaaaebabd11e171d2682a8ae01&tochange=e711420b85f70b765c7c69c80a478250bc886229
repo_name: mozilla-inbound
repo_url: https://hg.mozilla.org/integration/mozilla-inbound
task_id: eyRSVJsJT4WGMysouGUC_w

また、「最初の異常ビルド」は以下の通りでした。

app_name: firefox
build_date: 2018-07-09
build_file: C:\Users\clearcode\.mozilla\mozregression\persist\2018-07-09--mozilla-central--firefox-63.0a1.en-US.win64.zip
build_type: nightly
build_url: https://archive.mozilla.org/pub/firefox/nightly/2018/07/2018-07-09-22-12-47-mozilla-central/firefox-63.0a1.en-US.win64.zip
changeset: 19edc7c22303a37b7b5fea326171288eba17d788
pushlog_url: https://hg.mozilla.org/mozilla-central/pushloghtml?fromchange=ffb7b5015fc331bdc4c5e6ab52b9de669faa8864&tochange=19edc7c22303a37b7b5fea326171288eba17d788
repo_name: mozilla-central
repo_url: https://hg.mozilla.org/mozilla-central

これらの情報をbugの報告に書き添えておくと、実際に修正を行おうとする人の調査の手間が大幅に軽減されます。

なお、最初の異常ビルドの「pushlog_url」欄に現れているURLを開くと、前のビルドからそのビルドまでの間に行われた変更の一覧が現れます。この例では40以上のコミットが一度にマージされた時点から後退バグが発生したという事が読み取れ、後はこの中のどの変更が原因だったかを割り出すという事になります。運がよければ、最初の異常ビルドでの変更が1コミットだけに絞り込める場合もあり、その場合は調査範囲が一気に限定されます。

まとめ

以上、mozregressionを使ってFirefoxの後退バグの発生時点を割り出す手順をご紹介しました。

後退バグの修正にあたっては、いつの時点から機能が壊れていたのか、どの変更の影響で機能が壊れたのか、という事を特定する事が重要です。どの変更でおかしくなったのかが分かれば、原因箇所を特定する大きな手がかりになります。また、新たな別の後退バグを生み出さないためには、後退バグの発生原因となった変更の意図を踏まえつつ対応策を検討する事が有効です。後退バグを報告する場合は、できる限り「どの変更から問題が発生するようになったか」を調べてから報告することが望ましいです。

ただ、このように二分探索で後退バグの発生時点を割り出すためには、各コミット段階で「問題が発生するかどうかを確実に確認できる事」が必須条件となります。ミスが原因で「そもそも起動すらしない」状態のコミットが途中に存在していると、動作を確認できるのは「正常に起動するコミット」の間だけになってしまい、二分探索を有効に行えません。GitHub Flowのようにmasterを常にいつでもリリースできる状態に保ったり、プルリクエストのマージには必ずCIが通る事を条件にしたりといった運用をとる事もセットで行う必要があります。後退バグが発生しても原因をすぐに特定しやすいよう、健全なプロジェクト運営を心がけたいものですね。

*1 このBug自体は実際には既に報告済みの他のBugと同じ原因であったことが分かったため、既にあった方のBugで続きをトラッキングするよう誘導されて閉じられています。

2018-07-18

リーダブルなコードを目指して:コードへのコメント(3)

1週間に1回ずつコメントできるといいなぁと思っていたけど3週間に1回のペースになっている須藤です。

リーダブルなコードを目指して:コードへのコメント(2)の続きです。前回はメイン関数の全体を読んでコメントしました。これに対し、その後、「シングルトンパターンはどういうときに使うのがよいだろう」というのを一緒に考えていました。

リポジトリー: https://github.com/yu-chan/Mario

今回のコメントに関するやりとりをするissue: https://github.com/yu-chan/Mario/issues/3

メインループ

それではメインループの中を読んでいきましょう。

	while(!InputInterface::isOn(KEY_INPUT_Q)) { //Qを押したら終了
		if(ProcessMessage() != 0) {
			break;
		}
		InputInterface::updateKey();
		Framerate::instance()->update();
		ClearDrawScreen();

		//ゲーム開始
		Sequence::Parent::instance()->update();

		ScreenFlip();
		Framerate::instance()->wait();
	}

短くまとまっています。このくらいの粒度で抽象化されていると、全体の流れがわかりやすくて読みやすいですね。このコードからは1回のループでは次の処理をしていることがすぐにわかります。

  • メッセージを処理(メッセージがなにかはわからないけど)
  • キー入力を処理
  • フレームレートを更新(「フレームレートを更新」というのがどういうことかわからいけど)
  • オフスクリーンの描画領域をクリアー
  • ゲーム開始(ループ毎に「ゲーム開始」というのがどういうことかわからないけど)
  • オフスクリーンの描画領域を使用
  • フレームレートを調整

処理の中身がイメージできないものもありますが、全体像はわかりました。

それでは、順に見ていきましょう。

InputInterface

以下の2つの処理が関連していそうなのでまとめて見ていきます。

	while(!InputInterface::isOn(KEY_INPUT_Q)) { //Qを押したら終了
		InputInterface::updateKey();

なぜ関連していそうかと思ったかというと同じクラスに属しているからです。関連しているものをまとめるためにクラスを使うのは読みやすくていいですね。

ただ、InputInterfaceという名前は大げさかなぁと思いました。というのは、現状、このクラスはキー入力しか扱っていません。キーだけでなく、マウスやタッチスクリーンなどいろんな入力も扱っているならInputInterfaceでもいいかもしれませんが、そうではないので、KeyboardInterfaceくらいの方がよさそうに思います。私ならInterfaceは冗長だと思うのでKeyboardにするかなぁ。

InputInterface.hは次のようになっています。

#ifndef INCLUDED_INPUTINTERFACE_H
#define INCLUDED_INPUTINTERFACE_H

class InputInterface {
public:
	static int key[];
	static bool isOn(int id);
	static void updateKey();
};

#endif

気になるのは次の2点です。

  • #ifndef INCLUDED_INPUTINTERFACE_H
  • すべてのメンバー関数が静的メンバー関数

最初の#ifndef ...からいきます。

これはヘッダーファイルを重複して読み込まないようにするための伝統的なガード方法なのですが、今どきはこれを実現するには次のようにします。

#pragma once

class InputInterface {
public:
	static int key[];
	static bool isOn(int id);
	static void updateKey();
};

#ifndef ...に指定する識別子を考えなくてもいいし、最後の#endifも書かなくていいのでかなりスッキリします。

「最近のコンパイラーでしか使えないんじゃないの。。。?互換性を考えると#ifndef ...の方がいいんじゃ。。。」と思うかもしれませんが、今どきのC/C++のコンパイラーなら全部使えるので気にしなくてよいです。Wikipediaのpragma onceのページによると使えないのはCray C and C++くらいのようです。(ドキュメントには使えると書いていない。)

次にすべてのメンバー関数が静的メンバー関数なことについてです。

すべて静的メンバー関数であればインスタンスは必要ありません。他のところではシングルトンパターンを使っているので、ここでも同じようにシングルトンパターンを使った方がよいでしょう。(私はインスタンスを作るようにする方が好みですが。)

同じことを実現するのに違うやり方を使っていると「どうしてここは違うやり方なんだろう?実は違うことを実現したいのかも?」と読んだ人が詰まってしまいます。同じプロジェクト内では、同じことを実現するときは同じやり方を使いましょう。

それでは順番に関数を見ていきましょう。

まずはInputInterface::isOn()です。boolを返す関数の名前にisをつけるのは読みやすくなっていいですね。boolを返す関数にisをつけるのはよく見る書き方なので、そういう書き方を知っている人は読みやすいです。isXXX以外にも真偽値を返す関数によく使われる名前があります。たとえばis_XXXXXX?(SchemeやRuby)やXXXp(Lisp)などです。それぞれの言語でよく使われている書き方を踏襲すると読みやすくなります。

それでは実装を見ていきましょう。

int InputInterface::key[256];
bool InputInterface::isOn(int id) {
	bool flag = false;
	//updateKey();
	if(key[id]) {
		flag = true;
	}
	return flag;
}

おそらく、InputInterface::keyにはキーが押されたら0でない値が入っているのでしょう。0以外の値が入っていたらflagtrueになるようになっています。

ここで気になることはflagという名前です。オン・オフを表しているのでフラグなのですが、一般的過ぎるので使わなくていいなら使わない方がよい名前だと私は思っています。私ならどう書くかというとこう書きます。

int InputInterface::key[256];
bool InputInterface::isOn(int id) {
	return key[id] != 0;
}

真偽値を返すならそもそもkey[id] != 0の結果を直接使えるので、ローカル変数は必要ないのです。

言語を問わず次のようなコードをちょいちょい見ます。

if(condition) {
	return true;
} else {
	return false;
}

こういうコードは次のようにしましょう。↑のようにifしてからtruefalsereturnしていると、読むときに「なにか特別なことをしているのかも?」とちゃんと確認しなければいけません。↓のように不必要なifを使わないことで「あぁ、この条件の結果を返すんだな。特別なことはしていないな。」と読むことができます。

return condition;

なお、コメントアウトして使っていない次のコードも削除しておいた方がよいです。コードをバージョン管理して必要ないコードは削除してしまいましょう。残っていると読むときに「どうしてコメントアウトしているんだろう?」と考えなければなりません。バージョン管理しておけば後から取り出すことができるので、思い切って消しましょう。

	//updateKey();

次はInputInterface::updateKey()を見てみましょう。

void InputInterface::updateKey() {
	char stateKey[256];
	GetHitKeyStateAll(stateKey);
	for(int i = 0; i < 256; i++) {
		if(stateKey[i] != 0) {
			key[i]++;
		} else {
			key[i] = 0;
		}
	}
}

DXライブラリのGetHitKeyStateAll()関数で押されているキーの情報を取得してInputInterface::keyの値を更新しています。押されていればインクリメントして押されていなければ0にしています。

stateKeykeyStatesの方がいいんじゃないかと思いました。複数形にすることで複数のキーの状態を保持していることがわかるからです。

同様にInputInterface::keykeysと複数形にした方がいいと思います。ただ、keysだとなにが入っているのか不明瞭なので、ちょっと長いですがkeyPressedCountsとかそういう方がいいんじゃないかと思います。

余談ですが、私は「何回」というのを表すときはn_XXXという名前を使っています。たとえばn_pressedです。英語の「the number of XXX」を略してn_XXXです。これはGLibで使われている名前のつけ方ですが、GLib以外でもよく見ます。ただ「何回」が複数個のときには使えないんですよねぇ。XXXの部分が名詞だと複数形になるからです。たとえば「要素数」ならn_elements(「the number of elements」)です。複数形をさらに複数形にできないので、n_elements_setとかn_elements_listとかにするんですが微妙だなぁと思っています。余計な情報が入ってしまうからです。setだと重複を許さないような気がするし、listだとリストで実現していそうな気がします。なので、keyPressedCountsかなぁと思いました。

ところで、keyの値はインクリメントする必要がないかもしれません。次のように単にboolを入れておけば十分な気がします。

key[i] = (stateKey[i] == 1);

この値を使っているところを見てみると、Game/Character.cppに次のようなコードがあるのですが、ここは押されたかどうかの情報だけでも十分なように見えます。

	if(InputInterface::key[keyIndex] == 1 && !isJump) {

あとはバッファーサイズは定数にしておきたいところです。

void InputInterface::updateKey() {
	const size_t nStates = 256;
	char stateKey[nStates];
	// ...
	for(size_t i = 0; i < nStates; i++) {
		// ...
	}
}

Visual C++では配列のサイズに変数を使えなかったような気がするので、こう書かないといけないかもしれません。

void InputInterface::updateKey() {
	char stateKey[256];
	const size_t nStates = sizeof(stateKey) / sizeof(*stateKey);
	// ...
	for(size_t i = 0; i < nStates; i++) {
		// ...
	}
}

ただ、これは少しわかりにくいのが難点です。こう書くときはだいたい次のようにマクロを使ってわかりやすくします。

#define ARRAY_SIZE(x) (sizeof(x) / sizeof(*x))
void InputInterface::updateKey() {
	char stateKey[256];
	const size_t nStates = ARRAY_SIZE(stateKey);
	// ...
	for(size_t i = 0; i < nStates; i++) {
		// ...
	}
}

C++なので同じことをテンプレートでも実現できるのですが、テンプレートでの実装がわかりやすいかというと、うーん、という感じなので今回はやめておきます。(テンプレートを使うとARRAY_SIZEintの配列ではなくintのポインターを渡したときにエラーにできるという利点があります。)

まとめ

リーダブルコードの解説を読んで「自分が書いたコードにコメントして欲しい」という連絡があったのでコメントしています。今回はメインループ内で使っているInputInterfaceを読んでコメントしました。次回はメインループの違う処理を読んでいきます。

「リーダブルなコードはどんなコードか」を一緒に考えていきたい人はぜひ一緒にコメントして考えていきましょう。なお、コメントするときは「悪いところ探しではない」、「自分お考えを押し付けることは大事ではない」点に注意しましょう。詳細はリーダブルなコードを目指して:コードへのコメント(1)を参照してください。

2018-07-20

IBus の最新版を使って問題の切り分けを行うには

はじめに

ソフトウェアを使っていると、意図しない挙動に遭遇することがあります。 ソフトウェアによっては、ディストリビューションで配布されているものが最新とは限りません。 そのため、場合によってはアップストリームではすでに修正済みということもあります。*1 せっかく報告しても最新版ですでに直っていたりすると、悲しいですね。そんなことのないように最新版でも確認するのがおすすめです。

そこで今回は IBus を例に、不可解な挙動に遭遇したときに最新版の IBusを使っても再現するかどうか確認する方法を紹介します。

問題の詳細について

Ubuntu 18.04上のibus-mozcを使っているときに、プロパティパネルから文字種を切り替えると、文字種の変更が追従しないことがあることに気づきました。 Xfce4のパネル1に表示されるアイコンをクリックしたときに表示されるメニューから文字種を変更したときにはそのような挙動はありません。*2

Xfce4のパネル1に表示される IBus のメニューとプロパティパネルに表示されるメニューはどちらも同じ IBusProperty で実装されているので基本的には挙動に違いはないはずです。そこで IBus の最新版で挙動を確認することにしました。

IBus の最新版をビルドするには

まずは IBus が依存しているパッケージをインストールします。build-depを使うと依存関係をまるごとインストールできるので便利です。

$ sudo apt build-dep ibus

次に、 IBus が必要としているファイルをあらかじめダウンロードして配置します。*3

$ sudo mkdir -p /usr/share/unicode/ucd
$ wget https://www.unicode.org/Public/UNIDATA/NamesList.txt
$ wget https://www.unicode.org/Public/UNIDATA/Blocks.txt
$ sudo cp NamesList.txt Blocks.txt /usr/share/unicode/ucd

次に、 IBus のソースコードを入手して、./autogen.shを実行します。

$ git clone https://github.com/ibus/ibus.git
$ cd ibus
$ ./autogen.sh

最後に、make を実行した後にインストールします。

$ make
$ sudo make install

これで /usr/local 以下へと IBus の最新版がインストールされました。

最新版のIBusを動かす手順

パッケージで IBus をすでに導入している場合、古いライブラリーを見に行ってしまうことがあります。 そこであらかじめ、ライブラリーのパスを環境変数にて指定しておきます。

$ export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBLARY_PATH

これで準備ができたので最新版の IBus を試せるようになりました。

次のコマンドを実行すると、ibus-daemon を起動できます。

$ /usr/local/bin/ibus-daemon -x -r

今回追試したいのはパネル1上に表示されるメニューの挙動です。 それを有効にするには、次のコマンドを実行します。

$ /usr/local/libexec/ibus-ui-gtk3

これでパネル1に表示されるメニューの挙動を確認できるようになりました。 実際に試したところ、最新版でも同様の問題を抱えていたため、アップストリームに報告しておきました。

まとめ

今回は IBus の最新版を使って問題の切り分けを行う方法を紹介しました。

*1 細かいことをいうと、ディストリビューションが配布しているバージョンで特別にパッチを当てることで対応している場合もあります。

*2 実装が異なるibus-anthyやibus-skkにはこの問題はないようです。

*3 この作業を忘れるとautogen.shを実行したときにconfigureでエラーになります。

2018-07-25

サポートエンジニアNight vol.3

「サポートエンジニア」と聞いて、皆さんはどのような職務内容を想像するでしょうか。ついたてで仕切られたブースに大勢の人が並んで座っている? 開発経験の浅い、あるいは経験が無い人が、マニュアルに従って問い合わせを右から左にさばいている? 突っ込んだ事を質問されたら「仕様です」で煙に巻く? エンドユーザー向けの無料の電話サポートだとそのような形態のサポートもあるようですが、BtoB(法人向け)のテクニカルサポートは、それとはだいぶ状況が異なります。

去る2018年7月18日、そのようなサポート業務に関わる人達の情報交換を目的としたイベントであるサポートエンジニアNight vol.3が開催されました。発起人の方による背景説明では、BtoBのテクニカルサポートが技術的な問題解決力を要求される物であり、ビジネス上も非常に重要な役割となっている事が語られています。

当社は自社Webサービスは運営しておらず、またパッケージ製品の販売も行っていませんが、WebブラウザのFirefoxやE-mailクライアントのThunderbird、全文検索エンジンのGroonga、自社開発したmilter managerのほか、ご相談に応じて様々なフリーソフトウェアのテクニカルサポートを法人向けに提供しています。その過程で必要となる「調査の仕方」や「開発元へのフィードバックの仕方」などはフリーソフトウェア開発一般の知見と言う事ができ、ノウハウも広く知られていますが、それらをお客様にサービスとして提供しビジネスとして成立させる部分については、我流でどうにか回してきたというのが正直なところです。ノウハウとして確立しているとは言い難く、新たに採用したメンバーにサポート業務に関わってもらう際には特に、知見の伝達に苦労しています。この記事では、サポートエンジニアNight vol.3で発表されていた知見について、当社のサポートビジネスでの知見も交えつつまとめてみます。

(なお、会場入り時間の関係でTreasure Dataとはてなの事例については発表を聞けておりません。)

Sider, Inc.の事例

専任のサポート担当者をいつ置くべきかという問題

GitHub上でのプルリクエストに対して自動コードレビューを行うサービスであるSiderでは、元々専任のサポート担当者は置かれておらず、開発チームの人員がサポートを兼任するという体制だったそうです。その後、サポートにかかる負荷の増大を受けて、段階的にサポート体制を改善していったという事が述べられていました。

  • 専任のサポート担当者がいない時期には、「気付いた人が対応する」というモデルから始まった。
    • 問い合わせがあると、その情報が社内のSlackに流れてくるので、それに気がついた人が対応する。
    • このモデルだと、重い開発タスクが発生しているときにサポートに手が回らない事があった。
  • 週替わりで担当者をローテーションする方式(ラウンドロビン方式)にしてみた。
    • このモデルでも、担当者が不在の場合は結局元と同様に「誰か気付いた人が代わりに対応」する必要があった。
  • 専任のサポートエンジニアという役割を設ける事にした。
    • 重めの開発タスクは振られなくなった。
    • ドキュメントのメンテナンスは職責外だが(おそらくサポートの過程で発覚した不備は)対応している。
    • 開発タスクも、期限が切られていない(重くない)物については対応している。
    • サービスのバックエンドとして様々なOSSを使用しているため、サポートの中でそのOSS製品にある不具合に遭遇した場合には、アップストリームへのフィードバック(コントリビュート)も行う。
    • サービスの性質上、サポート業務上でも様々なコードに触れる必要がある。

具体的な数字として、直近7.5ヶ月での対応件数は291件だったそうです。1ヶ月の営業日を仮に20日と仮定すると、291÷(20×7.5)≒4.2で1営業日あたり4件強のサポートをこなしている計算になるでしょうか。テクニカルサポートでコンスタントに1日4件の問い合わせを1人でさばくというのは、当社の場合は相当忙しいという印象*1です。

サポートをやりやすくするための工夫

サポートを効率よくこなすための方法としては、以下のような事が語られていました。

  • ユーザー側から見えるステータスとは別に、内部的にはエラーのステータスがさらに細かく分かれており、それを見ればだいたいの見当がつくようになっている。
  • Inspectlet(行動トラッキングツール)を採用しており、UIに関する問い合わせにおいて、ユーザーが実際に行った操作がどのようなものであったかを調べられるようにしている。
  • 様々な手法を駆使し、なるべくユーザーの手を煩わせないで問題を解決できるようにしている。

エラー情報をリッチな物にしておく事は、ソフトウェア開発において一般的に大事な事です。というのも、最初に伝えられるエラーに十分な情報が出力されていれば、そこからの原因調査が楽になるからです。エラー情報が不十分だと、まず同じ現象を再現させる所から取りかからなくてはならず、再現できなけれは調査は大幅に困難なものとなります。当社のFirefox/Thunderbirdサポートにおいても、Firefox自体が持つエラーコンソールの内容や、より低レベルのログの収集を依頼する事が多々あります。

サポート品質をどう定めるべきかという問題

現状の問題点としては、以下のような事が述べられていました。

  • サポート品質の指標が定まっていないため、「なるべく早く返す」「なるべく詳しく説明する」といった事について、担当者個人の裁量任せとなってしまっている。
  • 現状ではまだ担当者が一人だけのため、不在時にどうすればよいかについては未解決のままになっている。
  • サービスは世界中で使われるため、問い合わせの10〜20%は海外から寄せられる物であるが、タイムゾーンが異なるため、問い合わせが日本での業務時間外にくる事がある。
    • そのような問い合わせに即時対応しようとすると、労働基準法に抵触する事になるため、即時対応ではなく翌日対応としている。
    • どうしてもという場合には、労働基準法が適用されない管理職の人に対応してもらうという方法がある。

当社のサポートサービスの場合、約款によって「サポートの回答期限はN営業日以内」「サポートの受付時間を何時から何時までと定め、その時間外に発生した問い合わせについては翌営業日を起算日とする」という事を明記しています。基本的には可能な限り早く対応していますが、問い合わせが同時多発的に発生した場合にはそのすべてに1日以内で対応しきれない場合もありますし、担当者が不在の場合もあります。そのため、「諸々の条件を考慮して確実に提供可能なサービスレベル」を先に見積もっておき、そのサービスレベルの範囲内での対応をお約束するという形で、あらかじめお客様に了解を頂いている*2という事になります。

株式会社レトリバの事例

音声認識のソフトウェアパッケージを提供しているレトリバでは、「研究部門」で生み出された技術を「製品開発部門」で製品に落とし込み、「営業技術部門」で販売やサポートを行っているそうです。このため、テクニカルサポートは営業技術部門の担当範囲となっていて、他の業務との兼務でサポートを行われているとの事でした。

サポートへの問い合わせが減るようにするという事

こちらの会社では、サポートにあたっては以下の事を大事にされているそうです。

  • お客様が困っている問題を迅速に解決する事
  • お客様が困っている課題を製品にフィードバックし改善して、お客様の課題を解決する事
  • お客様にスキルを伝達し、なるべくサポートに問い合わせなくても良いようにする事

一見してユニークなのは3番目の点です。近視眼的に考えれば、お客様からのお問い合わせを受けてさばくのがサポートの仕事である以上、問い合わせ件数を減らすのは自分で自分の仕事を減らして、自分の存在意義を減じる事に思えるかもしれません。ですがより大きな視点で考えると、問い合わせが減る事には以下のようなメリットがあります。

  • 簡単なトラブルはお客様側で自己解決してもらえるようになると、お客様のビジネスがより円滑に進むようになる。
  • 問い合わせが減るとサポート業務が減り、その分他の事に時間を割けるようになる。
  • 簡単で退屈な作業が減って、困難な少数の仕事にじっくり取り組めるようになると、純粋な技術者視点においてやりがいが増す。
  • 問い合わせに多く答える事が直接的な収益増につながらない契約形態なのであれば、問い合わせ対応に要する工数と収益には負の相関関係が成立する。端的に言うと、問い合わせが発生しない方が助かる。

パッケージの販売や製品の使用権のサブスクリプション契約を伴わない、純粋なサポート契約のみを行っている当社の場合でも、これらの点はメリットになっています。というのも、当社のサポート契約はインシデントサポート*3なので、上記の4番目の点と同じ事が言えるからです*4

ただ、どの程度まで「お客様を育てる」かは場合によりけりであるとのことでした。サポート提供側の狙い通りに自己解決できるようになってくれるお客様ばかりではなく、中には、これについて教えてくれるのであればあれもこれも教えてほしい、と本来のサポート範囲以上の事を求めてくるタイプのお客様もいるそうで、そのようなケースではある程度のところで対応を打ち切る必要もあるようです。当社のサポートにおいても、障害の発生原因を切り分けて対応責任の有無を明確にする事を心がけています。

サポート提供者もお客様も、どちらも「完璧」ではないという事を前提にした運用

また、それ以外のトピックとしては、過去のサポート対応事例はその都度チケットに記録を残しておき、同じお客様から同じ問い合わせがあった時に時間をかけずに回答したり、何月何日に回答済みと返したりできるようにしておく、という事も行っているとのお話がありました。お客様側も人間である以上、忘れるという事はありますし、また担当の方が変わった時に十分な情報が引き継がれていなかったり、複数の担当の方がいる場合に情報が随時共有されていなかったりという事は起こりえます。またサポート担当者が複数名いる場合には、サポート提供者の側でも情報を共有しておく必要があります。

当社でも、サポートのお問い合わせはすべてRedmineのチケットで管理しており、基本的な運用は以下のような形を取る事が多いです。

  • 新しい問い合わせが発生した場合、問い合わせ内容を説明文として、新しくチケットを作成する。
  • その問い合わせに対して回答を返し終えたら、回答文を注記として加える形で、ステータスを「解決」に設定する。
  • 回答に対し再度質問があった場合や、回答の中で「この点を調査した結果をお知らせください」と記載した部分について情報をご連絡いただいた場合、その問い合わせや情報を注記やチケットへの添付ファイルとした上で、ステータスを「フィードバック」に設定する。
  • ステータスが「解決」または「終了」でないチケットは、すべて「こちらがボールを持っており、こちらから連絡(回答)しなければならない」状態として取り扱う。

ただし、当社では、お客様からのお問い合わせのメールを元にRedmineに起票して、それ以後のやりとりをそのチケット上で管理するという場合と、お客様自身の手でRedmineに直接起票を頂く場合の両方をお客様ごとに切り替えています。メールベースでのサポートの場合、メールの本文がすべてRedmineのチケットの情報として記録される形になっており、過去に回答済みの情報はRedmineの検索機能を使って見つける事になります。

Herokuの事例

チケットベースでのサポート運用

Herokuでは、サブスクリプションモデルである事からユーザーの離脱率を下げる事が重要視されているようで、単なる「問い合わせ窓口」にとどまらず、サポートの質を高める事を強く意識しているという事が述べられていました。

  • サポートの受付はすべてチケットの起票を以て行う運用をとっている。
    • チケットが起票されると、サポートエンジニアが閲覧しているSlackに通知が流れるようになっている。
  • 第1担当者から第3担当者までの3人1ユニットを「その週のサポート担当ユニット」として、週替わりでローテーションしている。
    • その週の担当ユニットの中では原則として第1担当者が新規チケットを拾い上げるが、第1担当者が不在であれば第2担当者が、第2担当者も不在であれば第3担当者が拾い上げる。
    • サポートエンジニアは世界中の各地に在住しており、ユニット間・チーム間の連絡はインターネット越しに英語で行っている。
  • 日本語での問い合わせチケットは、日本語の問い合わせであることを示す専用の分類になっている。
    • 日本語で問い合わせがあった場合、それを英訳して、通常のサポートチケットとして起票し直す。
    • 回答が出たら、それを適宜日本語に翻訳し直して、元の問い合わせに対する回答とする。

当社のサポートでお客様に直接Redmineに起票を頂くケースは、お客様自身が開発者やサーバーの運用担当者である場合に取る事が多く、ユーザー層がITエンジニアであり要領が掴めている事が期待されるHerokuと事情が似通っていると言えそうです。

日本語の表現の問題

日本語でのサポートがシステム上は特別な扱いになっているというのは、英語アレルギーの人が多いという日本固有の事情でしょうか。問い合わせ内容を日本語から英語へ・回答を英語から日本語へ翻訳する際には、以下の点が難しいとの事を述べておられました。

  • 直訳すればよいという物ではなく、ケースバイケースで時には大幅な「意訳」も求められる。
    • 書かれていない事を補ったり、書いてある内容を言い換えたりしなくてはならない。
  • サポートの回答に特有の表現などがあるためか、ビジネス日本語・敬語の取り扱いに苦慮している。
    • 「これを読んでからメールを書けば失礼がない」というような参考書が求められている。

当社のサポートサービスは日本国内のお客様が対象なので、敬語については同様の悩みを持っています。当社では特にビジネスメール研修のような事は実施していないため、サポート経験が浅い担当者が書いた回答文は手直しが必要な場合もあり、ある程度慣れるまではベテランサポート担当者が回答をレビューするという体制をとっています。教科書のようなテキストとして使える参考書籍があれば、当社でも是非導入したいところです。

サービスへのフィードバック

問い合わせに対する回答以外での顧客満足度に寄与する取り組みについても紹介がありました。

  • サービスのステータス(負荷の状況、障害の発生状況や復旧状況)は積極的・自発的に公表するようにしている。
    • 障害が発生しない安定したサービス、であるかのように装おうとして障害の発生を隠そうとしても、ユーザーのリテラシーが高いためすぐにボロが出る。
    • それよりも、正確な情報を公開しておく事の方が信頼に繋がる。
  • 同じ問い合わせが何度も発生する状況は、サービスの異常や不備の兆候と考えて、サービス運用に適宜フィードバックする。
  • FAQ(よくある質問と回答)というレベルに至っていなくても、受けた問い合わせに基づく特定の事例でも、ナレッジとして文書にまとめて公開する。
    • ナレッジからサービス・製品にフィードバックする場合もある。

先のレトリバ社の事例でも同様の話があったと思われますが、同じ問い合わせが度々発生するというのは不調や不備の兆候と考えるのが妥当で、FAQを用意して終わりと片付けて良いものではありません。皆が躓く石があるのであれば、まずはそれを取り除くのがベストで、避け方をノウハウとして周知するのはあくまで次善の策です。サービス品質やユーザー体験の向上のための情報が集まる窓口として、サポートが大事な役割を担っているという事がよく分かる事例ですね。

当社の場合も、自社開発製品でないフリーソフトウェアのサポートにおいて未知の不具合に遭遇する事は度々あり、原則としてそれらはアップストリームにもフィードバックするよう努めています。また、Redmineにお客様自身で起票を頂くケースでは、ナレッジ蓄積・共有の場としてRedmineに付属のWikiをお客様自身がお使いになっている場合もあります。

かなり割り切った評価システム

サポートそのものの品質に関わる話として、得られたサポートに対する顧客からの評価・フィードバックという物があります。満足度を5段階評価で入力するなど、この種のフィードバックの収集方法には様々なやり方があり得ますが、詳細なアンケートをとろうとすればするほど顧客側の負担が増えてしまうため、どこまで質問するかというのは非常に難しい問題です。

Herokuでは(一旦10段階評価を導入したものの意味が無かったので改めた、というような経緯があったわけではなく、)当初から単純な「良かった or 悪かった」の2値入力にしているとの事でした。これについて、サポートエンジニアの方々は元気がない時に「良かった」評価を眺めて元気を出すというような「使い方」もされているそうです。製品開発での「リリース」のような分かりやすい成果が見えにくいサポート職において、従事する人自身のモチベーションを高める一つの方法として参考になる事例だと言えるでしょう。

密なコミュニケーションを心がける事

会社のメンバーが各国に離れていることから、エンジニア間でのコミュニケーションには、メール、Slack、Google Hangoutでのビデオチャットなど様々な方法を併用しているそうです。中でも、特に文字情報でのコミュニケーションにおいては、「思っていて表情には出ているが、文字には現れない」というような、その通信手段上では伝達されない情報からくる微妙な齟齬をなくすために、大げさなくらいに感情や状況を報告するという、いわゆるオーバーコミュニケーションを心がけているという事が語られていました。

それでも全く対面した事がないままでコミュニケーションを続けていると伝わらない部分が出てきてしまうそうで、年に何回かというペースで全社的に一カ所に実際に集まり、お互いに顔を見て対面しながら話す機会を設けて、感謝の言葉などのやりとりをしているそうです。

サポートにおける、スピードと品質のトレードオフについて

質疑応答の際には、Herokuの有償サポートの「1時間や30分といった時間の中での回答を保証」というプランについて、「回答の速度と正確性のどちらを優先しているのか?」という質問が寄せられていました。これについては明確に「正確性を優先している」とのことで、回答内容に確信を持てない時などは、時間がかかる旨を伝えた上できちんと調べてから回答をしているそうです。そのように時間がかかるとお客様からの評価が下がるのでは? と心配する所ですが、実際の所は、回答の質が高ければユーザーからの評価が悪くなるという事は無いという感触があるそうです。

当社の場合、調査に非常に時間がかかるようなお問い合わせを受けた時には、まず1時間から数時間という一定時間の範囲内で判明した事実を元に、「現時点ではここまでは分かっていますが、ここから先は未調査領域のため不明です。N時間の範囲内で継続調査を実施しますか?」とその都度判断を仰ぐという運用をとっています。これは、お客様によって、あるいは同じお客様でも状況によって、回答のスピードと正確性・詳細さのどちらを優先するかが変わるためです*5

Microsoftの事例

Microsoft社の事例紹介では、リクルーティング用の資料を引用しながら、キャリアとしてのサポートエンジニア*6という点に焦点を当てて話されていました。

Microsoft社内でのサポートエンジニアの位置付け

Microsoft社内では、サポートエンジニアが属するセクションは開発やセールスなどの他のセクションとは別の、独立性の高い組織として存在しているそうです。

  • 職務上、開発部門にコンタクトをとるための正式な権限や、ソースコードのリポジトリを閲覧するための権限などが与えられている。技術の最後の砦、ゴールキーパーと言えるような位置づけである。
  • ミッションを一言で言うと「Microsoftのファンを作る仕事」。
  • 国ごとのチームの結びつきが強い他部署に比べると、サポートエンジニアは「全世界のサポートエンジニアが属する1つの集団」という性質が強い。
    • サポートエンジニア全員が書き込めるOneNoteを使用して情報共有・集積を図っている。

Microsoftのサポートエンジニアが本当にこれらの権限を持っているという事は、過去の対応事例からも分かります。過去、当社のお客様がMicrosoftとサポート契約を結んでいたことから、Windows上でFirefoxを使用した際に発生する希な現象について調査を行った事がありましたが、Firefoxのソースコードを調査した結果を踏まえた当社の回答に対し、Microsoft側もソースコードレベルでの調査結果を出してきてくれて、責任範囲が明確になったという事が実際にありました。

国際的なコミュニケーションという点では、発表中では特にアジア圏の各国にまたがったコミュニケーションについて紹介されていました。アジア各国のスタッフは、英語ネイティブでない人ばかり(基本的に第二言語として英語を使っている人ばかり)なので、音声での会話よりも文字での読み書きの方が得意という人が多いそうです。そのため、メール、チャット等、連絡事項は基本的に文章にするという文化があるとの事でした。

社内でサポートエンジニアをどう評価するか

Herokuの事例でも触れられていましたが、Microsoftでもサポートエンジニアの評価の仕組みは独特な物になっているそうです。

  • 回答の数が多いからといって優秀と評価されるわけではない。
  • 車輪の再発名は評価されない。既にある物は、極力そのまま使う事が評価される。
  • 一人で抱え込むよりも、他の人に助けを求めている方が評価が高くなるようになっている。
    • 他者を巻き込んで、他者の成功に貢献する事が評価に繋がる。
    • 「誰々さんのおかげで回答できた」「誰々さんが書いてくれたドキュメントに助けられた」といったフィードバックを行う社内ツール*7があり、そのデータが評価の指標になる。

当社の場合、会社自体の人数規模が非常に小さい事もあって、厳密なデータに基づく評価制度という物はまだありません。しかし、ある程度以上の規模になり「お互いの存在を知らない」スタッフ同士という関係があり得る状態になってくると、公正な運用のためにも数値に基づく評価制度は必要になってくるでしょう。

サポートエンジニアのキャリアに求められる物、得られる物

以上のような職務内容から、Microsoftにおけるサポートエンジニアには以下の能力が求められ、また、職務を通じてこれらの能力が高まるという事が語られていました。

  • 技術力(調査力、問題解決力)
  • コミュニケーション力(交渉力、チームワーク力)
  • 英語力

今回発表を見る事ができたいくつかの企業においても、また当社においても、これらは同様の事が言えます。テクニカルサポートの業務で行う事は、既にあるプロダクトのバグ報告や質問に対応する際に行う調査・検証の作業と、かなりの部分が共通しています。一般的に言って、バグ修正の作業の大部分は調査が占めており、調査の正否が修正の正否を決めると言っても過言ではないでしょう。また、新機能の開発などにおいてエンドユーザーや顧客の要望を的確に吸い上げるためにはコミュニケーション力が求められますし、スケジュール調整や、その機能を次のリリースに含めるかどうかなど、意見や利害の調整・交渉が必要になる場面もあります。最新情報を調査するためには、翻訳を待っていては遅すぎるため英語で元資料を参照する事が欠かせませんし、トラブルの類似事例調査についても、日本語だけで調査するよりも英語で公表されている事例も調査範囲に含められた方が、より有用な情報に辿り着きやすいです。

こうして考えると、テクニカルサポートは開発に特化したスキルセットとはまた異なる、技術とコミュニケーションの総合力が求められる業務であると言えるのではないでしょうか。

まとめ

以上、サポートエンジニアNight vol.3にて聞く事ができた他社でのテクニカルサポートの事例を、当社で提供しているテクニカルサポートの運用事例を交えつつご紹介しました。

具体的なプロダクトやサービスという成果が目に見える形で世に発表される「開発」や、売り上げが数字として出る「営業」に比べると、サービスの安定した稼働を支える「運用」や、トラブル発生時の問い合わせに対応する「サポート」という分野は、成果が可視化されにくい性質があります。職務の性質上公に語る事ができない情報も多いので、どういう人がどういう働き方をしているのかという情報もあまり表に出てこず、また、メディアに大々的に取り上げられてモデルケースになるような人物もそうそういません。学生さんなどにとっては「イメージしにくい」という印象だけでもあればまだいい方で、そもそも存在すら意識されないという事になってしまうのも致し方ないでしょう*8

実際のサポートエンジニアは、業務を遂行する上でITエンジニアとしての総合力が求められる仕事と言えます。突き抜けた開発力には自信がないが、技術もコミュニケーションもそつなくこなせる、というジェネラリスト型の人は、自分の特性を活かせる職としてサポートエンジニアにも目を向けてみてはいかがでしょうか?

*1 当社では各メンバーがサポート専任ではなく開発案件等と兼任している事が多い事と、対象製品のソースコードを深く調査するようなケースでは集中力の消費が激しい事から、実際に1日で対応可能な上限はこのくらいになる場合が多いです。

*2 このような取り決めを一般的に「Service Level Agreement(SLA)」と言います。

*3 平たく言うと、回数券のような契約形態。問い合わせ1件につきいくらという都度支払い/従量制とは異なり、あらかじめ決まった回数/時間の問い合わせが可能な権利を販売する、という形をとります。

*4 ただ、サポート契約を単独で提供している当社の場合には、この点を推し進めると「これだけ製品が安定していて問い合わせも日常的には発生しなくなったなら、もうサポート契約は不要なのでは?」とお客様が判断され、サポート契約が継続されなくなる、という可能性は出てきます。そのような状況においてのサポート契約は「いざ何かトラブルが起こったときのための保険」としての性質が強まってくるため、常日頃からどんなトラブルでもきちんと解決できるという信頼を積み重ねていく事がなおさら重要だと言えるでしょう。

*5 回答のスピードだけを気にして、未調査の事についてまで思い込みで不正確な回答をしてしまうと、その回答を信じて運用したお客様に不利益を与えてしまい、ひいては自社の信用も損ないます。また、もし知っていたら教えて欲しいという程度のつもりだった質問に対して、勝手な忖度を行って何十時間と先走った調査をして工数を消費してしまうと、それをインシデントとして消費されればお客様の不利益になりますし、かといってインシデント消費なしでの無償対応とすればやはり自社の不利益になります。最初に簡単な範囲で一次回答を返して以後の判断をゆだねるというのは、自社にとってもお客様にとっても最も不利益が発生しにくい方法ではないか、というのが当社の見解です。

*6 Microsoft社内では「Customer Service and Support」という呼び方になるそうです。

*7 残念ながら、これ自体は公開されておらず、製品としても販売されていないとのこと。

*8 そういう理由から「あこがれの職業」にもなりにくく、多くの会社でサポートエンジニアの採用や育成は難しい課題となっているようです。

2018-07-26

Windows Subsystem for LinuxでプレゼンツールのRabbitを動かす

皆さんはRabbitというプレゼンテーションツールをご存じでしょうか。2018年のRubyKaigiでのMatz氏によるKeynoteでも使われており、「名前は知らないが見た事はある」という方もいらっしゃるかもしれません。

RabbitはRubyで開発されており、Git等でバージョン管理しやすいMarkdown形式やRD形式などのプレーンテキスト形式でスライドを記述できたり、「スライドの進行割合」を表すウサギと「時間の経過度合い」を表すカメのアイコンでスライドの進行状況を把握できたりと、痒い所に手が届くツールです。

RabbitはWindowsでも動作するのですが、開発は主にLinux上で行われているため、Windows上では動作が不安定だったり表示が崩れたりと、期待通りの動作結果にならない場合があります。このような場面に出くわした場合、Windows上での動作を改善するためのフィードバックやパッチの作成に挑戦してみるいい機会なのですが、発表の日程が差し迫っているためそこに時間をかけられない、というような場合もあるでしょう。

一般的に、有志の個人開発者によって開発・メンテナンスされているフリーソフトウェアは、開発者自身が日常的に使っている環境に近い環境で最も安定して動作します。そういう意味では、仮想マシンを用意したり、使用していないPCを用意したりしてLinuxディストリビューションをインストールしたりという形をとるのが常道なのですが、この変形として、Windows 10であればWSL(Windows Subsystem for Linux)とWindows用のXサーバーを組み合わせるという方法が使えます。

以下、本記事での解説は、WSLのLinuxディストリビューションとしてUbuntuを使用している状況を前提とする事にします。

WSLでRabbitが動く理由

WSLは、Windowsのカーネルに対して、Linuxカーネルのように振る舞うための層を被せることで、UbuntuやFedoraなどのディストリビューションで提供されているビルド済みバイナリをそのまま動作させられるようにする技術です(動作イメージ、導入手順はまんがでわかるWSLなどをご覧ください)。

画面描画の基盤技術はWindowsとLinuxで顕著に異なる部分であるため、Windows向けにRabbitやその依存ライブラリ群を完全対応するのは非常に大変な作業です。しかし、WSLであれば基盤部分はLinux用の物がそのまま使われるため、Rabbitのように主にLinux上で開発されているGUIアプリは、「Windows用の移植版」を動かすよりも「WSL上でLinux用のオリジナル版」を動かした方が良い結果を得られる場合がままあります。

ただし、そのために1つだけ欠かせない物があります。それがWindows用のXサーバーです。

Windows Subsystem for Linuxで使えるXサーバー

X(X Window System)は、現在多くのLinuxディストリビューションで一般的に使われている、GUIを実現するための最も重要な基盤技術です。「物理的な画面をキャンバスとして管理し、各ウィンドウを描画する」「ユーザーのクリック操作やキー入力などの操作を一元的に受け付けて、情報を各アプリケーションに引き渡す」といった事を行う物なのですが、現在の所WSL上ではXサーバーは動作しません。何故かというと、WindowsはWindowsでXとは別の画面描画の仕組みを持っており、両方が同時に動作するとリソースの奪い合いになってしまう(マウスやキーの入力をWindowsが受け取ればXが受け取れなくなるし、Xがそれらを受け取ればWindowsが受け取れなくなる)からです。

そこで登場するのがWindows用のXサーバーです。これはWindowsアプリケーションの1つとして振る舞いつつ、一般的なXサーバーと同じ機能を提供する(アプリケーションがXに対して指示した位置にWindowsのウィンドウを開き、そのウィンドウに対して操作が行われた場合はXが操作を受け付けたものとしてアプリケーションに情報を引き渡す、という働きをする)物です。VcXsrv(※リンク先はダウンロードページ)はその代表的な例で、VcXsrvを起動しておき、WSL上のアプリケーションに対してVcXsrvをXサーバーとして使うよう指示する事により、WSL上のGUIアプリケーションがWindows上でも動作するようになります。

という事で、まずはリンク先のページのダウンロード用ボタンからVcXsvrのインストーラをダウンロードして、インストールしましょう。

VcXsrvを初めて起動すると、「Display settings」というタイトルのウィザードが表示されます。ここではとりあえず以下のように設定して下さい。

  1. 「Multiple Windows」を選んで、「次へ」を押す。
  2. 「Start no client」を選んで、「次へ」を押す。
  3. オプションは変えず、「次へ」を押す。
  4. 「完了」を押す。
  5. WindowsのファイアウォールがVcXsvrによるネットワーク接続に対して警告を出すので、接続を許可する。

初期設定を終えると、VcXsrvのアイコンがタスクトレイに表示されます。VcXsvrを使わないときは、このタスクトレイ上のアイコンから終了させておくと良いでしょう。

VcXsvrのアイコンがタスクトレイに表れている様子

Windows上のXサーバーを使うように、WSLの環境を設定する

Windows上でXサーバーが動作しているだけでは、WSL上のGUIアプリケーションは動作しません。WSL上のGUIアプリケーションに対して、Windows上のXサーバーを使うように指示する必要があります。これは以下のようにすれば実現できます。

  1. WSLのUbuntuが提供するBashを起動する。
  2. echo 'export DISPLAY=localhost:0.0' >> ~/.bashrc と実行する。
  3. exitでWSL上のBashを終了し、再度WSL上のBashを起動する。

この操作により、Bash起動時に読み込まれる初期化用の設定ファイルに、入出力用の画面としてWindows上のXサーバーが提供する仮想的な画面を使うように指示するための指定が追記され、それが反映された状態でBashが起動します。

Rabbitのインストール

Xサーバーの準備ができたら、いよいよRabbitのインストールです。Rabbitは、プラットフォームのパッケージ群と、RubyGemsのパッケージ群の2段階に分けてインストールする必要があります。

  1. Rabbitの実行に必要なパッケージ群をインストールする。 sudo apt install rubygems ruby-dev build-essential fonts-ipafont で一通りインストールされる。
    (フォントはfonts-notoなど他の選択肢もありますが、行の高さの違いが原因で表示が崩れてしまう事があるので、安全のためにはfonts-ipafontをインストールする事をお勧めします。)
  2. Rabbitをインストールする。 sudo gem install rake rabbit で必要なGemパッケージ群が一通りインストールされる。
    • この時、Windowsのファイアウォールがgemコマンドによるネットワークへの接続に対して警告を出すので、明示的に許可を与える。
    • 2019年4月11日追記:新しい環境で試行した際に、何故か依存パッケージがインストールされず、Markdown形式のファイルを開こうとすると[警告] サポートしていない形式です。(サポートしている形式: [Wiki, PDF, image, RD])というエラーが表示される結果となる事がありました。RabbitでMarkdown形式を取り扱うためにはkramdownkramdown-parser-gfmの両方が必要なので、どちらかがインストールされていないとこのエラーが表示されますので、パッケージの有無を確認し、無ければ追加でインストールして下さい。

以上でRabbitのインストールは完了です。

スライドの作成と実行

Rabbit用のスライドは、前述したとおりRDやMarkdownなどの形式で作成できます。試しに、以下のような内容でファイルを作成し、Windowsのデスクトップ上にsample.mdのような名前で保存して下さい。文字エンコーディングや改行コードは自動判別されますが、安全のためにはLinux上で一般的な「UTF-8(BOM無し)、改行コードLF」を使用する事をお勧めします。

# Rabbitでプレゼン

subtitle
:   WSL上のRabbitで表示

author
:   自分の名前

institution
:   所属会社

allotted_time
:   45m

# 準備

 * Xサーバーのインストール
 * Rabbitのインストール

# おわり

ご静聴ありがとうございます

Markdownファイルができたら、ファイルを置いたディレクトリーに移動し、ファイル名を引数に指定してrabbitコマンドを実行しましょう。RabbitのウィンドウがWindows上で開かれ、スライドの1ページ目が表示されます。

$ cd /mnt/c/Users/(ログオン中のユーザーアカウント名)/Desktop
$ rabbit ./sample.md

WSL上のRabbitのウィンドウがWindowsのウィンドウとして表示されている様子

Rabbitのスライドは、Enterキーで次のページに進み、BackSpaceキーで前のページに戻ります。サンプルを見ると分かる通り、最大レベルの見出しがそのままスライドの各ページのタイトルになります。詳しくはMarkdown形式でのスライドの書き方を参照してください。

ウィンドウの最下部のウサギは現在スライドの何ページ目が表示されているかを示しています。allotted_timeで時間を指定してある場合(この例では「45分」という意味になります)、その時間に合わせてウィンドウの最下部をカメが進んでいくようになります。カメが先行していれば進行が遅れ気味、ウサギが先行していれば逆に走り気味という事になります。

Rabbitのスライド下部に表示されているウサギとカメ

まとめ

以上、Windows上でWSL経由でRabbitを動作させる手順をご紹介しました。

Rabbitのサイトでは、Rabbitで作成された様々なスライドの例が公開されています。どんなスライドを作れるのか、ぜひ参考にしてみて下さい。

タグ: Ruby
2018-07-27

Go言語でつくるインタプリタ

まともにGoでプログラムを書いたことはない須藤です。

2018年6月にオライリー・ジャパンから「Go言語でつくるインタプリタ」というプログラミング言語のインタプリタをGo言語で実装するという内容の本が出版されました。

Go言語でつくるインタプリタ
Thorsten Ball/設樂 洋爾
オライリージャパン
¥ 3,672

内容

書名からはわかりにくいですが、「インタプリタを作りながらGo言語を学べる本」ではなくて「Go言語を使ってインタプリタの作り方を学ぶ本」です。

必要なGoの知識

Go言語自体の説明はとくにありません。なにかしらプログラミング言語を知っていたら見ればわかるよね、という感じです。結構前にA Tour of Goをやったことがあるくらいの私はなんとなくわかりました。ただ、プログラミング言語が初めてな人はわからないと思うので、「これからプログラミングをはじめよう!Goというのがよさそうだからこの本で勉強しよう!」という人には向きません。

私がわからなかったGoの書き方は以下の通りです。300ページ弱でこれだけだったのでGoを知らない他の人も大体わかると思います。

6ページ:

func New(input string) *Lexer {
    l := &Lexer{input: input}
    return l;
}

*Lexerとか&Lexer{...}ってなんだ!?(後で調べてGoにもポインターがあったことを知った。)

11ページ:

if tok, ok := keywords[ident]; ok {
    return tok;
}

ifの条件部に代入文(文でいいの?)と式を書けるってこと?(私はうまく構文解析できなかった。)

126ページ:

switch node := node.(type) {
// ...
}

node.(type)ってなんだ!?(後で調べてtype switchという書き方と知った。気持ちはわからなくもないけど同じ変数に代入しなくてもいいんじゃないかなぁ。)

137ページ:

leftVal := left.(*object.Integer).Value

キャストってこんな風に書くの!?(後で調べてtype assertionという書き方と知った。)

142ページ:

integer, ok := tt.expected.(int)

キャストできるかどうかってこんな風に書くの!?(後で調べてtype assertionの別の書き方だと知った。)

224ページ:

t.Fatalf("Eval didn't return Hash. got=%T (%+v)", evaluated, evaluated)

%T%+vってどういう表示方法の指定なんだ!?(後で調べて%Tは型名を表示する指定で%+vはフィールド名付きで構造体を表示する指定だと知った。)

実装するインタプリタの機能

この本では{...}でコードブロックを表現するプログラミング言語のインタプリタを実装します。このプログラミング言語には四則演算機能や(...)での評価順の変更機能、変数、関数、配列・ハッシュテーブル(コンテナー)など一通りの機能が揃っています。クロージャーも実装しています。また、付録ではマクロも実装します。

クロージャーやマクロをどうやれば実現できるかの雰囲気を知れるので、普段使っている機能はどうやって動いているかが気になる人には面白いでしょう。

字句解析から始まるので、プログラミング言語に限らずパーサーを実装する機会がある人には参考になると思います。たとえば、CSSのパーサーを実装する機会がある人とか。

雰囲気

文章はテンションが高いです。TDDの本みたい。

そうそう、テストを書きながら実装を進めていくのは他の本ではあまりやっていないスタイルだと思います。ただ、いろいろテスティングフレームワークを実装している私からすると野暮ったいテストだなぁという気持ちになります。Goのtestingパッケージはエラーメッセージのフォーマットを自分でやる方針なので私と好みが違うだけなんですが。。。

日本語はすごく読みやすいです。もともと日本語で書かれた文章みたいというほどではなく、翻訳された日本語という感じです。ただ、「ひとつの文がすごく長い」とか「日本語として意味がわからない」とか「そもそも原文ではなにを言いたかったんだ?」となることはありません。リーダブルコードの解説は翻訳された日本語風に書いたんですが、そんな感じです。ダジャレはありませんでした。(気づきませんでした。)

まとめ

プログラミング言語の処理系の実装の勉強をしたいとかGoの雰囲気を知りたいという人は楽しめると思います。私は手を動かさずに読んだだけなんですが、手を動かしながら読むともっと理解が進むと思います。(私は移動時間で本を読むことが多くて、パソコンの前にいるときに読むことはあまりないんです。。。)

2018-07-30

不具合をフィードバックする時に、不具合の様子を動画で説明する

一般的に、バグ報告は「再現手順」「期待される結果」「実際の結果」をセットで説明する事が望ましいと言われます。報告をする側にとっては自明の事でも、報告を受ける側にとってはそうではないという事は多々あり、そういった情報を過不足なく伝えるための指針と言えるでしょう。

しかしながら、特定のタイミングで操作した場合にのみ再現するというような不具合だと、言葉で説明しきるのは難しい場合があります。また、日本人が英語でフィードバックするというように自分の得意でない言語を使う場面では、説明自体がそもそも困難な場合もあります。そのような場合に便利なのが「スクリーンキャスト(screencast)」です。

スクリーンキャストとは、自分が操作しているPCの画面全体またはその一部の様子を動画にした物の事です。ゲームの実況配信などもスクリーンキャストの一種と言えます。今回、Firefoxにおいてドラッグ操作と同時にタブの復元が行われると、以後のドラッグ操作のイベントが発行されなくなる、という不具合の報告を行うにあたり、現象の再現のための操作のタイミングがシビアだったため、念のためスクリーンキャストの形式でも現象発生時の様子を報告することにしました。同様の形でフィードバックをしたい人は参考にしてみてください。

スクリーンキャストの録画方法

以下は、OS標準の機能や標準添付のアプリケーションなどによる手軽なスクリーンキャスト撮影の方法をWindows、macOS、Linuxの各プラットフォームごとに例示します。文字を入れるなどの高度な編集を行いたい場合は、動画編集ツールを別途組み合わせて使ったり、より高機能なスクリーンキャスト撮影用アプリを使ったりする事をおすすめします。

Windowsの場合

Windows 10 Creators Update(バージョン1703)以降では、「ゲームバー」という機能を標準状態で使えます。これはその名の通りゲームアプリのための機能で、前述の例のようなゲーム実況を行えるよう、アプリケーションのウィンドウ単位でのスクリーンキャスト機能を含んでいます。1つのウィンドウの中で完結する現象であれば、これを使って説明用のスクリーンキャストを作成できます。

スクリーンキャストを撮影したいウィンドウにフォーカスがある状態で、キーボードのWindowsキーを押しながら「G」キーを押すと、以下のような画面が表示されます。

ゲームバーが表示された様子

ゲームバー上部の「Allow gaming features」のチェックボックスにチェックを入れると一旦ゲームバーが消えますので、もう一度ゲームバーを開きなおします。すると、先ほどまでは使用できなかったゲームアプリ向け機能を使えるようになっています。左上の「Game capturing」という領域にあるボタンがスクリーンキャスト用の機能です。

有効化されたスクリーンキャスト機能

中央の「●」アイコンのボタンをクリックすると、即座に録画が始まります。録画中は画面の右上の方に以下のような小さなツールバーが表示され、「■」アイコンのボタンをクリックすればその時点で録画が終了します。

スクリーンキャスト録画中に表示されるツールバー

録画が終了すると、撮影された動画が「ビデオ」内の「キャプチャ」フォルダ(C:\Users\(ユーザー名)\Videos\Capturesの位置)に保存されます。

保存されたスクリーンキャスト

macOSの場合

macOSでは、標準状態で添付されているアプリの「QuickTime Player」にスクリーンキャスト撮影機能が含まれています。スクリーンキャストを撮影する準備が整ったら、QuickTime Playerを起動します。

アプリケーション一覧からQuickTime Playerを選択している様子

QuickTime Playerを起動したら、メニューの「ファイル」から「新規画面収録」を選択すればスクリーンキャスト録画用のウィンドウが開かれます。

メニューの「新規画面収録」を選択している様子 画面収録の操作用ウィンドウ

赤い「●」アイコンのボタンの横にある「⌄」マークをクリックすると、カーソルを含めるかどうかなどのオプションを設定できます。準備ができたら「●」アイコンのボタンをクリックします。すると、画面上のどの領域の様子を動画として録画するかを訪ねるメッセージが表示されます。

録画する領域を訪ねるメッセージ

デスクトップ全体を録画すると動画が大きくなりすぎますので、現象を説明するのに必要最小限の領域を設定するのがおすすめです。画面上をドラッグすると、矩形選択の要領で録画対象の領域を設定できます。

録画領域が設定された状態

録画領域が決まったら、「収録を開始」ボタンをクリックすればすぐに録画が始まります。録画中は画面上部のメニューバーに「■」アイコンのボタンが表示されており、これをクリックすると録画が終了します。

録画を終了するボタン

録画された動画のプレビューが表示されますので、後は任意のファイル名で保存すれば*1スクリーンキャストは完成です。

Linuxの場合

Linuxディストリビューションのデスクトップ環境は、標準で録画機能を備えている例は今のところ無いようですが、スクリーンキャスト録画用のアプリケーションは簡単に導入できます。ここではその例としてUbuntu 16.04LTSでKazamを使う例を紹介します。

sudo apt install kazamでパッケージをインストールして、端末上でkazamコマンドを実行するかメニューから「Kazam」を選択すると、Kazamが起動します。

Kazamが起動した様子

Kazamは画面全体の録画、アプリケーションのウィンドウ単位での録画、指定した領域の録画のそれぞれに対応しています。「Window」をクリックすると、録画対象にするウィンドウをクリックで選択する画面に切り替わります。「Area」をクリックすると、録画領域を矩形で選択する画面に切り替わります。矩形選択の場合は、範囲を選択した状態でEnterキーを押すと選択が確定されます。

録画領域を設定している様子

録画対象が決まったら、メインウィンドウの「Capture」ボタンをクリックすると録画が始まります*2。Kazamが起動している間はデスクトップ上部のバーにカメラのアイコンが表示され、これをクリックしてメニューから「Finish recording」を選択すると録画を終了できます。

録画を終了する様子

録画を終了すると、動画の取り扱いを選択するダイアログが表示されます。

録画した動画の取り扱いの選択

ここから動画編集アプリを起動して加工工程に移る事もできますが、録画した物を無加工でアップロードするだけであれば、「Save for later」を選択して動画ファイルを保存します。

スクリーンキャストの公開

FirefoxのBugzilla(bugzilla.mozilla.org)ではbugに任意のファイルを添付できますので、録画したスクリーンキャストをそのまま添付・アップロードする事も可能です。

メーリングリストへの投稿の場合のように、動画そのものを添付できない*3ケースでは、別途YouTubeのような動画共有サイトに投稿してそのURLを記載するという方法をとると良いでしょう。その場合、動画の共有範囲を「公開」とするか、またはURLを知っている人なら誰でも閲覧できるように設定しておく必要があります。

まとめ

以上、各プラットフォームでのスクリーンキャストの録画手順を簡単に紹介しました。

ところで、キーボード入力に関連する報告では「ここでこのキーを押した」という情報もスクリーンキャストに含めたくなるでしょう。そのような場合には、Linuxのデスクトップ環境であればscreenkeyというツールを使ってキー入力の様子を可視化できます

障害報告のフィードバックは分かりやすい言葉で書かれているに超した事はありませんが、「言葉で説明しなければならない物」ではありません。スクリーンキャストや、あるいはスクリーンショットのように、「言葉で説明するより見た方が早い」物については、むしろ積極的に画像や動画を活用して障害発生時の様子を伝えた方が良いでしょう。皆さんも是非、様々な手段を駆使してより分かりやすいフィードバックを行うように工夫してみてください。

*1 初期状態では「.mov」形式になります。

*2 初期状態では5秒間のカウントダウン後に録画が始まります。

*3 メールに動画を添付すると、そのファイルが全受信者に複製されて届き、各受信者の通信帯域やローカルディスクを圧迫する事になるため、メールへの大きなファイルの添付は避ける事が望ましいです。

2018-07-31

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|