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

ククログ

タグ:

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

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

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

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の概要(一行)]

以下、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上で「whiteboard」欄にcheckin-neededのキーワードを付加して、パッチをチェックインしてもらいます。

まとめ

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

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

2018-07-05

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

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

apitraceを使ったOpenGL ESが絡むFirefoxのデバッグ方法

はじめに

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

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

この作業に関連するOpenGL ESのデバッグをする必要が生じたのでその方法を解説します。 今回のデバッグにはapitraceというOpenGLに関するAPIの呼び出しを取得できるツールを使いました。このツールを元に今回のデバッグ方法を解説します。

OpenGLのAPIの呼び出しをトレースするには

apitraceを用いてOpenGLのAPIの呼び出しをトレースするには、以下のようにして行います。

$ apitrace trace --api=egl --output=/tmp/dump.trace [トレースするプログラム]

例えば、FirefoxのOpenGL ESのAPIの呼び出しのトレースを取得するには以下のようにすると取得できます。

$ apitrace trace --api=egl --output=/tmp/dump.trace firefox

OpenGLのAPIの呼び出しのダンブを解析するには

apitraceには採取したダンプを解析する機能もあります。

$ apitrace dump /tmp/dump.trace

とすると、OpenGL ESのAPIの呼び出しを記録したダンプの解析を行えます。 例えば、Firefoxを用いてOpenGL ESのAPIの呼び出し結果を取得し、その結果を解析すると以下のような出力が得られます。

// process.name = "/usr/lib/firefox/firefox"
0 eglGetDisplay(display_id = 0xb6921120) = 0x1
1 eglInitialize(dpy = 0x1, major = NULL, minor = NULL) = EGL_TRUE
9 eglChooseConfig(dpy = 0x1, attrib_list = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_NONE}, configs = {0x2, 0x1, 0x3}, config_size = 64, num_config = &3) = EGL_TRUE
14 eglCreateWindowSurface(dpy = 0x1, config = 0x2, win = 0x9d1e7af0, attrib_list = {}) = 0x9ce7d000
15 eglBindAPI(api = EGL_OPENGL_ES_API) = EGL_TRUE
16 eglCreateContext(dpy = 0x1, config = 0x2, share_context = NULL, attrib_list = {EGL_CONTEXT_MAJOR_VERSION, 2, EGL_NONE}) = 0x9d1638b0
140 eglGetCurrentContext() = NULL
141 eglMakeCurrent(dpy = 0x1, draw = 0x9ce7d000, read = 0x9ce7d000, ctx = 0x9d1638b0) = EGL_TRUE
# ...
27660 glDeleteProgram(program = 1050015)
27661 eglGetCurrentContext() = 0x9d1638b0
27662 glDeleteProgram(program = 840012)
27663 glBindFramebuffer(target = GL_FRAMEBUFFER, framebuffer = 0)
27664 glDeleteBuffers(n = 1, buffers = &140002)
27665 glDeleteBuffers(n = 1, buffers = &70001)
27666 eglGetCurrentContext() = 0x9d1638b0
27667 eglGetCurrentContext() = 0x9d1638b0
27668 eglDestroyContext(dpy = 0x1, ctx = 0x9d1638b0) = EGL_TRUE
27669 eglMakeCurrent(dpy = 0x1, draw = NULL, read = NULL, ctx = NULL) = EGL_TRUE
27670 eglDestroySurface(dpy = 0x1, surface = 0x9ce7d000) = EGL_TRUE

この結果を見ると、FirefoxがEGLを使用する際には、EGLDisplayを取得し、EGLのコンフィグを元にしてEGL WindowSurfaceを作成してからようやくEGLのコンテキストが動き出していることが記録されていることがわかります。

反対に、終了時には動いているEGLのプログラムを片付けてから現在のEGLコンテキストの破棄、EGL WindowSurfaceの破棄を行なっていることが読み取れます。

apitraceを用いるとOpenGL ESのAPIの呼び出しが成功しているのか、失敗しているのかがある程度判別できます。

14 eglCreateWindowSurface(dpy = 0x1, config = 0x2, win = 0x9d1e7af0, attrib_list = {}) = 0x9ce7d000

の箇所を例にとると、上記の呼び出しでは正常にWindowSurfaceが作成されていることが読み取れますが、

14 eglCreateWindowSurface(dpy = 0x1, config = 0x2, win = NULL, attrib_list = {}) // incomplete

となってしまっている場合は eglCreateWindowSurface の呼び出し直後に正常の動作ではない状態になっています。

実際に、このような状況になってしまった時はfirefoxがSEGVしました。

まとめ

FirefoxのOpenGL ESのAPIの呼び出しのダンプを取ることを例にしてapitraceの基本的な使用方法を解説しました。 OpenGL (ES)はハードウェアが絡むためログを仕込むような伝統的なデバッグ手法によるデバッグが難しく、このようなツールに頼ることで別の視点からの情報が得られることが分かりました。 グラフィックに関するデバッグで困っている場合はこのようなツールの助けを借りることも検討してみてはいかかでしょうか。

つづき: 2018-07-02
タグ: Mozilla
2018-06-15

WebExtensionsによるFirefox用の拡張機能で設定の読み書きを容易にするライブラリ:Configs.js

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

XULアドオンでは、設定の保存や読み書きにはpreferencesという仕組みを使うのが一般的でした。これはFirefoxの基本的な設定データベースとなっているkey-value storeで、保持されている設定の一覧はabout:configで閲覧することができます。

一方、WebExtensionsベースの拡張機能の場合はこのような「設定を保存するための仕組み」は特に用意されていません。Indexed DB、Cookie、Web Storage APIなどのWebページ用の一般的な仕組みや、あるいはWebExtensionsのstorage APIのように、データを永続的に保存する様々な仕組みの中から好みの物を選んで使えます。とはいえ、選択肢が多すぎると却って判断に迷うもので、何らかの指針や一般的な方法があればそれに従っておきたい所でしょう。

一般的には、WebExtensionsベースの拡張機能では設定の保存先としてstorage APIが使われるケースが多いようです。storage APIはさらにstorage.localstorage.syncstorage.managedの3種類が存在し、Firefox Syncとの連携を考慮する場合はstorage.sync、そうでない場合はstorage.localが使われるという具合の使い分けがなされます。ただ、これらのAPIは非常に低レベルのAPIと言う事ができ、設定画面・バックグラウンドスクリプト・コンテントスクリプトの間での値の同期のような場面まで考慮すると、上手く整合性を保つのはなかなか大変です。

そこで、preferencesの代替として使いやすいAPIを備えた、設定の読み書きに特化した軽量ライブラリとして、Configs.jsという物を開発しました。

基本的な使い方

必要な権限

このライブラリは設定値をstorage APIで保存するため、使用にあたってはmanifest.jsonpermissionsの宣言にstorageを加える必要があります。

{
  ...
  "permissions": [
    "storage", 
    ...
  ],
  ...
}
設定オブジェクトの作成と読み込み

このライブラリは単一のファイルConfigs.jsのみで構成されており、読み込むと、その名前空間でConfigsという名前のクラスを参照できるようになります。実際に設定を読み書きするためには、これを使って設定オブジェクトConfigsクラスのインスタンス)を作る必要があります。具体的には以下の要領です。

var configs = new Configs({
  enabled: true,
  count:   0,
  url:     'http://example.com/',
  items:   ['open', 'close', 'edit']
});

設定のキーと既定値はこの例の通り、Configsクラスの引数に渡すオブジェクトで定義します。オブジェクトのプロパティ名が設定のキー、値が既定値になり、値はJSON形式が許容する物であれば何でも保持できます。

これをcommon.jsのような名前で保存し、以下のようにConfigs.jsと併せて読み込むようにします。

HTMLファイルから読み込む場合:

<script type="application/javascript" src="./Configs.js"></script>
<script type="application/javascript" src="./common.js"></script>

manifest.jsonで指定する場合:

{
  ...
  "background": {
    "scripts": [
      "./Configs.js",
      "./common.js",
      ...
    ]
  },
  ...
  "content_scripts": [
    {
      "matches": [
        "*://*.example.com/*"
      ],
      "js": [
        "./Configs.js",
        "./common.js",
        ...
      ]
    }
  ],
  ...
}

manifest.jsonの記述例から分かる通り、設定を読み書きしたい名前空間のそれぞれで個別にConfigs.jsと設定オブジェクトの作成用スクリプトを読み込む必要があります。

なお、storage APIを使う都合上、このライブラリはコンテントスクリプトのみで使う事はできません(コンテントスクリプトからはstorage APIにアクセスできません)。サイドバーやパネルなどを含まずコンテントスクリプトだけで動作する拡張機能である場合は必ず、設定オブジェクトのインスタンスを作成するだけのバックグラウンドページを読み込んでおいて下さい。こうする事で、コンテントスクリプト内で行われた設定の変更はバックグラウンドページ経由で保存され、逆に、保存されていた値はバックグラウンドページを経由してコンテントスクリプトに読み込まれる事になります。

保存された設定値の読み込み

各スクリプトを読み込んだそれぞれの名前空間では、設定オブジェクトのインスタンスが作成されると同時に、保存された設定値が自動的に読み込まれます。設定オブジェクトは値がPromiseであるプロパティ $loadedを持っており、このPromiseは設定値の読み込みが完了した時点で解決されます。例えば保存された設定値を使ってページの初期化処理を行いたい場合は、以下のようにする事になります。

window.addEventListener('DOMContentLoaded', async () => {
  await configs.$loaded;
  // ...
  // 読み込まれた設定値を使った初期化処理
  // ...
}, { once: true });
設定値の参照と変更

設定オブジェクトは、インスタンス作成時に指定された各設定のキーと同名のプロパティを持っており、プロパティの値が設定値となっています。$loadedのPromiseの解決後に各プロパティを参照すると、読み込まれたユーザー設定値または設定オブジェクト作成時の既定値が返されます。

console.log(configs.enabled); // => true
console.log(configs.count);   // => 0

また、設定値を変更するには、設定オブジェクトの各プロパティに値を代入します。

configs.enabled = false;
configs.count   = 1;

設定値は型情報を持ちません。初期値と異なる型の値を設定した場合、値は初期値と同じ型に変換されるのではなく、設定値の型のまま保存されます。例えば真偽値だった設定のconfigs.enabledに数値として0を代入した場合、次に取得した時に返される値はfalseではなく0となります。

値がObjectArrayである場合、以下の例のように必ず、元のオブジェクトの複製を作り、そちらを書き換えて、新しい値としてconfigsのプロパティに設定する必要があります。

var newEntries = JSON.parse(JSON.stringify(configs.entries)); // deep clone
entries.push('added item');
configs.entries = newEntries; // 設定値の変更

var newCache = JSON.parse(JSON.stringify(configs.cache)); // deep clone
newCache.addedItem = true;
configs.cache = newCache; // 設定値の変更

言い換えると、configs.entries.push('added item')configs.cache.addedItem = trueのように値のオブジェクトそのものを変更する方法では、変更結果は保存されませんvar entries = configs.entriesのように「値を参照した時に取得したオブジェクト」そのものを扱う場面では、値の変更前に必ずJSON.parse(JSON.stringify(entries))などの方法でディープコピーしてから変更するように気をつけて下さい。

また、未知のプロパティに値を設定した場合、その値は保存されません。設定のキーを増やしたい場合は、必ず設定オブジェクト作成時に既定値とセットで定義する必要があります。

設定値の変更の監視

設定値の変更は、ライブラリ自身によって各名前空間の間で暗黙的に通知・共有されます。if (configs.enabled) { ... }のように処理の過程で設定値を参照している場合、参照する時点で最新の設定値が返されますので、特に何かする必要はありません。

一方、「設定値が変わったらボタンのバッジを変更する」といった風に、設定値の変更を検知して何らかの処理を実行したい場合、設定オブジェクトの$addObserver()メソッドでオブザーバーを登録することができます。オブザーバーには関数を指定でき、第1引数として変更が行われた設定のキーが文字列として渡されます。以下は、関数(アロー関数)をオブザーバーとして登録する例です。

configs.$addObserver(aKey => {
  const newValue = configs[aKey];
  switch (aKey) {
    case 'enabled':
      ...
    case 'count':
      ...
  }
});
Firefox Syncで同期する設定、同期しない設定

初期状態では、設定オブジェクト作成時に定義した各設定はFirefox Syncでは同期されません。同期の対象にするには、「同期したい設定のキーの一覧」あるいは「同期させたくない設定のキーの一覧」を設定オブジェクト作成時にオプションで指定する必要があります。

Configsクラスは第2引数として各種オプションの指定のためのオブジェクトを受け取ります。基本的には設定を同期せず、一部の設定のみを同期したいという場合、同期したい設定のキーの配列をsyncKeysオプションとして指定します。

var configs = new Configs({ /* 既定値の指定 */ }, {
  syncKeys: [
    'enabled',
    'url',
    'items',
  ]
});

このように指定すると、syncKeysに列挙された設定のみFirefox Syncで同期されるようになります。

また、基本的には設定を同期し、一部の設定のみを同期対象外にしたいという場合、同期したくない設定のキーの配列をlocakKeysオプションとして指定します。

var configs = new Configs({ /* 既定値の指定 */ }, {
  localKeys: [
    'count',
  ]
});

このように指定すると、localKeysに列挙されなかったすべての設定がFirefox Syncで同期されるようになります。

Firefox Syncで同期された設定は、その実行環境で保持されたユーザー設定値よりも優先的に反映されます。 ただし、システム管理者によって定義された値がある設定は、それが最も優先されます。

システム管理者が設定を指定できるようにするには

企業等の組織でアドオンを使用する場合、システム管理者が設定を指定し固定したい場合があります。普通のアドオンでそのような事をしたい場合には、アドオンの中に書き込まれた既定値を書き換えた改造版を作る必要がある場合が結構ありますが、Configs.jsを使って設定を管理しているアドオンでは、そのような改造をせずとも、システム管理者が任意の設定値を指定できます。

システム管理者が設定値を指定するには、Managed Storageマニフェストという特殊なマニフェストファイルを作成し、Windowsではさらにレジストリに情報を登録する必要があります。Windows以外のプラットフォームでは、特定の位置にファイルを配置するだけですConfigs.jsを使用しているアドオンの一つであるIE View WEを例として、設定の手順を説明します。

まず、以下のような内容でJSON形式のファイルを作成します。

{
  "name": "ieview-we@clear-code.com",
  "description": "Managed Storage for IE View WE",
  "type": "storage",
  "data": {
    "forceielist"      : "http://www.example.com/*",
    "disableForce"     : false,
    "closeReloadPage"  : true,
    "contextMenu"      : true,
    "onlyMainFrame"    : true,
    "ignoreQueryString": false,
    "sitesOpenedBySelf": "",
    "disableException" : false,
    "logging"          : true,
    "debug"            : false
  }
}

nameにはアドオンの識別子(WebExtensionsでの内部IDではなく、Mozilla Add-onsに登録する際に必要となるIDの方)を、descriptionには何か知らの説明文を、typeにはstorageを記入します。設定値として使用する情報はdataに記述し、記述の仕方はnew Configs()の第1引数に指定する既定値と同様の形式です(JavaScriptではなくJSONなので、キーは明示的に文字列として書く必要がある点に注意して下さい)。

内容を準備できたら、ファイル名を(アドオンの識別子).jsonとして保存します。この例であればieview-we@clear-code.com.jsonとなります。

次に、Windowsではレジストリの HKEY_LOCAL_MACHINE\SOFTWARE\Mozilla\ManagedStorage\(アドオンの識別子)またはHKEY_CURRENT_USER\SOFTWARE\Mozilla\ManagedStorage\(アドオンの識別子)の位置にキーを作り、「標準」の値として先のJSONファイルのフルパスを文字列型のデータとして保存します。キーの位置は32bit版Firefoxでも64bit版Firefoxでも同一である(WOW6432Node配下ではない)という事に注意して下さい。 LinuxやmacOSでは、MDNに記載があるパスのディレクトリ配下にJSONファイルを置くだけで充分です。

当然ですが、管理者でないユーザーがファイルを書き換えて設定を変更してしまう事がないように、このJSONファイルは一般ユーザーでは読み取り専用・ファイルの書き込みを禁止するようにアクセス権を設定しておく必要があります。

このようにして管理者が設定値を定めた項目は、Configs.jsで参照する時は「ロックされた」状態になり、値を変更できなくなります*1

まとめ

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

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

*1 値を設定しても特に例外等は発生しませんが、変更の内容は無視されます。

タグ: Mozilla
2018-06-12

Firefox 62で修正されたFirefox 61での後退バグに見る、不要なコードを削除する事の大切さ

一般的に、プログラムの継続的な開発においては「機能の追加」や「不具合の修正」が行われる事は多いですが、「機能の削除」が明示的に行われる事はそう多くないのではないでしょうか*1。この度、使われなくなっていた機能が残っていた事により意図しない所で不具合が発生し、機能を削除することで不具合が解消された事例がありましたので、使われなくなった機能を削除する事の意義を示す一時例としてご紹介いたします。

発生していた現象

現在のFirefoxでは、インストールするアドオンは必ずMozillaによる電子署名が施されていなくてはならず、未署名のファイルからはアドオンをインストールできないようになっています。ただ、それではアドオンの開発そのものができないため、開発者向けの機能であるabout:debuggingから、開発中のアドオンを「一時的なアドオン」として読み込んで動作させられるようになっています。

この機能について、Firefox 61で「条件によっては、アドオンを一時的なアドオンとして読み込めない」「条件によっては、一時的なアドオンの読み込みに非常に時間がかかる」という不具合が発生していましたが、Firefox 62で修正される事になりました*2

現象が起こり始めたきっかけと、原因の調査

開発中のアドオンを一時的なアドオンとして読み込めないという現象について、現象発生のタイミングを二分探索で絞り込むツールであるmozregressionを使って調査した所、1452827 - XPIInstall has a bunch of cruft that needs to be cleaned upでの変更が投入されて以降発生するようになっていた事が分かりました。

また、この現象が発生していた場面では、WSL*3で作成したシンボリックリンクがリポジトリ内に含まれている場合に、開発者用のコンソールにwinLastError: 1920という情報が出力されるという状況でした。これはWindowsが返すファイル関連のエラーコードのひとつであるERROR_CANT_ACCESS_FILEを意味する物で、Bugzilla上において、上記変更の中に含まれているファイルの情報を取得する処理がWSLのシンボリックリンクに対してこのエラーを報告している、という情報を頂く事ができました。

実際の修正

ファイルの情報を調べようとして何らかのエラーが発生するファイルというものは、WSLのシンボリックリンク以外にも存在し得ます。また、一般的に、規模が大きな変更は予想外の影響をもたらすリスクがあり、変更は可能な限り最小限に留める事が望ましいとされます。これらの理由から、当初は「ファイルの情報を取得しようとした時のエラーを適切にハンドルすることで、特殊なファイルがあっても問題が起こらないようにする」という方向でパッチを作成し提出しました。

しかしながら、このパッチはレビューの結果却下され、そもそもこの「ファイルの情報を取得する処理」自体が不要だからそれを削除する方がよいのではないかという指摘を受けました。そこで改めて調査したところ、以下の状況である事を確認できました。

  • この処理は、アドオンの総ファイルサイズを計算する過程で呼ばれている。
  • しかしながら、現行のFirefoxではアドオンのファイルサイズは画面上に表示される事が無く、また、ファイルサイズの情報が有効に使われる部分も存在していない。
    • 過去には使われていた機能のようだが、機能の改廃が進んだ結果、現在は「ファイルサイズの計算が正しいかどうか」を検証する自動テストの中で参照されるだけの機能になっていた。
    • 類似の情報として、自動更新を通じてダウンロードされたアドオンのインストールパッケージのファイルサイズという物もある。
      • こちらは現在も使われているが、今回問題となっている「アドオンの総ファイルサイズ」とは別の物である。
  • よって、アドオンの総ファイルサイズという情報自体が現在では無用となっている。

既にある機能や情報の削除は、その機能を使っている箇所が1つでも残っていれば不可能なため、影響範囲の調査は特に念入りに行う必要があります。今回の事例では、

  • Mozillaのスタッフの人から、使われていない機能を削除する方向での対応が望ましい旨のコメントが出ていた。
  • 実際に調査した限りでは、確かに目に見える部分や他の機能からは参照されていないという事の確認が取れた。

という2つの根拠があったため、思い切って、アドオンの総ファイルサイズの計算に関わるコードと、サイズ情報を参照している箇所*4を削除するという内容でパッチを再作成しました。その結果、パッチは無事に取り込まれ、Firefox 62ではこの問題が修正される事になりました。

また、このパッチが取り込まれた結果、「条件によって一時的なアドオンの読み込みに非常に時間がかかる」という別のBugにも影響が及んでいた事が後になって分かりました。こちらのBugは、開発中のアドオンのフォルダー配下にnode_modulesのようなフォルダーがあると*5、その配下の大量のファイルをスキャンするために時間がかかるようになってしまった、という物です。アドオンの総ファイルサイズの計算自体を行わなくした事により、棚ぼた的にこちらのBugも解消されたという状況でした。

ただ、実はその後、「WSLのシンボリックリンクのせいでエラーが発生してアドオンを読み込めない」という状況自体が今となってはほぼ発生しなくなっているという事も分かりました。具体的には、Bugの報告時にエラーの原因になっていたシンボリックリンクはWindows 10 Creators UpdateやWindows 10 Fall Creators UpdateのWSLで作成された物でしたが、その後Windows 10 April 2018 UpdateのWSLなどではNTFSの妥当なシンボリックリンクが作成されるようWSLが改良されたため、この問題の影響を受けるのは「古いWSLで(npm installを実行するなどして間接的に作成される場合も含めて)シンボリックリンクを作って、それをその後もずっと使い続けている場合」だけという事になっています。一般ユーザーに影響が及ばないアドオン開発者向けの機能であるという事に加え、プラットフォーム側の改善により今では問題自体が発生しなくなっている(新たにシンボリックリンクを作り直すだけでよい)という事も相まって、このパッチはFirefox 61には取り込まれないという判断がなされています。

まとめ

以上、Firefox 62で取り込まれたパッチを題材に、既に不要となっていた機能が問題を引き起こしていた事例についてご紹介しました。

Firefox 56およびそれ以前のバージョンのFirefoxにおいて使用できていた従来型アドオンは、今回削除されたコードのような「Firefox内部では既に使われなくなった機能」を使用している事が度々ありました。そのためFirefox全体として、アドオンが動作しなくなる事を警戒して古いコードを大量に抱え込まざるを得ない状態が長く続いていました。Firefox57での「従来型アドオン(XULアドオン)廃止」という大きな変化には、このような状態を是正するためという意味合いもあったと言えるでしょう。これを反面教師として、不要になった機能や実装はその都度速やかに削除してコードを簡潔に保っていくという事の重要さを実感していただければ幸いです。

*1 ただし、機能一式の刷新にあたって「新しい実装に持ち越されなかった機能」が結果的に「旧バージョンから削除された機能」として見える、という事はあります。

*2 一般ユーザーへの影響は小さいと判断された結果、Firefox 61への修正の反映は見送られています。

*3 Windows Subsystem for Linux

*4 大半は自動テスト内で、UI上に現れる物もFirefoxでは使われていないThunderbird用に残されたコードに1箇所あるだけでした。

*5 静的な文法チェックを行うためにeslintを使っている場合などに、このような状況が発生し得ます。

2018-06-07

Gecko Embedded ESR60

はじめに

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

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

今回は、この作業の現在のステータスを紹介します。詳細なビルド方法については、Gecko Embedded 次期ESR対応 を参照してください。

現在のステータス

現在はFirefox ESR60の開発を進めています。ビルドは通るようになり、ソフトウェアレンダリングでの描画もある程度安定して動作するようになっています。Firefox 60からはWayland対応のコードも本体に入っているため、ソフトウェアレンダリングであればほぼ無修正で動作させることができています。 また、60ESRには入りませんでしたが、当プロジェクトが作成したEGLおよびOpenMAX対応のパッチも無事Firefox本体に取り込まれています。

ESR52で実現出来ていたハードウェア対応の移植作業については、2018年5月30日現在、以下のような状況です。

  • レイヤーアクセラレーション: 移植済み(RZ/G1M、R-Car M3、Raspberry Pi3 Model Bで検証)
  • OpenMAX(H.264デコード): 移植済み(RZ/G1Mで検証)
  • WebGL: 移植済み(R-Car M3で動作)
  • WebRTC: レイヤーアクセラレーションとの組み合わせでは動作せず(調査中)
  • Canvas 2D Contextのアクセラレーション: e10sオフでは動作

また、ESR52ではなかった要素として、Rustで書かれたコードが追加されてきている点が挙げられます。細かく動作検証はできていませんが、少なくともStylo(Quantum CSS)は実機(Renesas RZ/G1M)で動作することを確認できています。

前回からの改善点は以下の通りです。

  • EGL有効化時に表示されるUIと操作するときに反応する座標がずれる不具合が解消した
  • UIのタブのドラッグ&ドロップが動作するようになった
  • コピー&ペーストが動作するようになった
  • WebGLがR-Car M3で動作することを確認

現状では、以下の制限があります。

  • 動画を再生&一旦停止し、別の動画を再生した時にサウンドデバイスが解放されず、音が出力されない
  • SoCのGPUがOpenGL ES 2.0のプロファイルを持ちrobustness拡張をサポートしない場合、WebGLのコンテキストが不完全となるか、コンテキスト作成に失敗する
  • Yocto 2.4ベースのBSPではStyloのビルドが確認できていない
  • Renesas RZ/G1MのPowerVR SGX 544MPを使用する場合、コンポジターでハードウェア支援を有効にするとウィンドウリサイズ時に高確率でSEGVすることがある
  • e10s有効化時にEGLを有効化するとContentプロセスがSEGVする

動作確認を行ったハードウェア

現時点ではRenesas RZ/G1M、R-Car M3、また、コミュニティサポートのレベルですが、StyloのビルドサポートなしでRaspberry Pi3 Model Bにも移植しました。

まとめ

GeckoEmbeddedプロジェクトのESR60の状況とビルド方法について紹介しました。ESR60対応がひと段落し、対象ボードを拡充する対応は少しづつ進んでいるものの、まだまだ手が足りていない状況です。興味を持たれた方は、ぜひ当プロジェクトに参加して頂ければ幸いです。

2018-05-30

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|
タグ: