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

ククログ

«前月 最新
タグ:

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で行い、実装内容は伝統的なパッチやMozReviewといった専用ツールで送付する必要があります。

そこでこの記事では、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上で全てのテストを実行する事ができます。)

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

作業用ブランチ上で複数回コミットしていた場合、変更内容を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に添付します。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にインストールしておいて下さい。

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

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

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

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

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

チェックインの依頼

パッチのステータスが「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へのパッチ投稿方法

はじめに

前回の記事では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の概要(一行)]

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

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上で「whiteboard」欄に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アドオンでの感覚に近い開発を支援する軽量ライブラリは他にもいくつかあります。以下の解説も併せてご覧下さい。

つづき: 2018-07-10
タグ: 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

«前月 最新
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|
タグ: